mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
main
391 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6db1a4270f |
fix(android): report the clip an Android recording really captured (#2566)
* fix(android): report the clip an Android recording really captured Android `screenrecord` encodes a frame only when the screen changes, so a window that ends on an unchanged screen returns a video far shorter than the requested duration, and `record stop` had nothing to say about it: the reported `durationMs` is host wall clock from `record start` until the export finished, which is not the length of the file that was just pulled. `record stop` now measures the pulled MP4 timelines and reports them as `capturedDurationMs`, and warns with the clip length against the window when the video is two or more seconds short. The window is measured on the device's own elapsed clock, read before the stop signal and at launch, because host wall clock drifts against the clock the encoder timestamps frames with; an unreadable clock or a chunk that answers no duration costs the caller the claim, never the recording. Measuring the timeline needed an ISO-BMFF box walk, which now lives in `@agent-device/capture-kit/recording-mp4-duration` and replaces the private top-level atom scan that MP4 container detection was doing. Stop replay through daemon recovery carries the field too, so a completion read back from the session resource reports the same numbers it did live. * chore(gates): enumerate the capture-kit MP4 subpaths the layering scan holds `@agent-device/capture-kit/recording-mp4-duration` and its fixture sibling are new declared package subpaths, so the boundary enumeration that holds every exported workspace subpath has to name them for the layering scan to accept the Android recorder's read of a pulled clip's timeline. * fix(capture-kit): evaluate the MP4 box scan only when a file is validated The Coverage job's ADR-0019 eager-closure probe failed: `recording/video.ts` evaluated 25 modules on import where the merge-base evaluated 24, because the MP4 container gate statically imported the box walk it now shares with the clip-duration read, and `recording/overlay.ts` grew by the same module. An entry the merge-base already carries gets no growth budget, so the edge moves behind a function-scoped `await import`: the scan is something recording completion asks for, and importing this module for `waitForStableFile` or WebM detection should not evaluate a box walker. The alternative the probe offered -- hosting the walker in a module both growing entries already evaluate -- would have put an ISO-BMFF walk in `swift-cache.ts` or `video-webm.ts`, or made the duration read import the Swift validator machinery that sits behind `video.ts`. * refactor(android): bracket the recording window with the host clock Human review of #2566: the device-clock read defended against host-vs-encoder drift that does not matter at this threshold. Quartz drifts by tens of ppm, so a 30-minute chunked recording moves the window under 100 ms against a 2s warning threshold, while the read cost a transport operation, its own probe budget, and two adb round trips per recording. The window is now the host elapsed time between `Date.now()` immediately before the recorder launches and `Date.now()` immediately before the stop signal, so the contract change and the extra device I/O are gone, and the one case where the clocks genuinely diverge -- a host that sleeps mid-recording -- reports a shorter window and misses the warning rather than inventing one. The surviving clock arithmetic is one subtraction, so it lives in the window module that already owns that concern instead of a module of its own. A stop recovered through daemon recovery now passes the manifest's own start instant, which is the first host timestamp the recording ever had, so a recovered stop gets the same comparison a live stop gets. |
||
|
|
1e50f9672a |
fix(daemon): take a foreign device claim the device's own reboot invalidated (#2570)
* fix(daemon): take a foreign device claim the device's own reboot invalidated An open that found a claim belonging to another session gave up even when the device had rebooted since that claim was taken, leaving the surface unreachable for every session. A reboot already took the app and the runner away, so the claim guarded nothing. Ask the device when its current boot began and release a foreign claim whose stamp predates it. The stamp is the last instant the owner vouched for the device, renewed by every open that reaches it, including the one that boots the device on the way in, so an owner that boots the device for its own work keeps it and only an owner that never came back loses it. Co-Authored-By: Claude <noreply@anthropic.com> * chore(gates): classify the daemon edge that asks a device when it booted Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
15644b6f0d | feat(devices): answer when a device's current boot began (#2575) | ||
|
|
d77eff452d |
refactor(daemon): cut the last edges into the CLI schema layer (#2543) (#2562)
* refactor(command-registry): own command defaults; cut the daemon's CLI-schema edges The daemon resolved routes through the command registry but still reached two symbols across the layer boundary — recordedFlagKeys and applyCommandDefaults — both imported from src/cli-schema/command-schema.ts, and the apps filter default was declared a second time in the apps facet. The registry is now the single source of command defaults: a COMMAND_DEFAULTS table typed on CommandFlags plus applyCommandDefaults sit in registry.ts, where every other command default (the defaultValue descriptors) is already declared. The CLI parser and the daemon request scope both import it from @agent-device/command-registry/registry, the one module already in both callers' eager closure. Registry and CLI closure are unchanged: the table adds a literal, not a new import, so no entry grows. The apps facet default and CommandSchema.defaults go away; DEFAULT_APPS_FILTER collapses into resolveAppsFilter's provider fallback and stops being a public export. session-action-recorder reads the recorded-flag vocabulary straight from @agent-device/command-registry/flag-registry, and removing that import also drops command-schema's now-unconsumed recordedFlagKeys re-export. Part of #2545 / #2543. * chore(gates): forbid the daemon importing the CLI schema layer R10 (scripts/layering/daemon-modularity) now rejects any daemon import of src/cli-schema/, value or type. The last two value edges into command-schema.ts are gone, so a total boundary is available, and an explicit rule beats a rank that would let the next daemon module reach the CLI schema layer again. Part of #2545 / #2543. |
||
|
|
4f5a87b6b3 |
refactor(command-registry): move CLI flag grammar, text and command aliases down (#2561)
Move the vocabulary that both the CLI and commands read but no command's runtime depends on into the package below both: flag types, registry, groups and the four flag-definitions files, command-text, and cli-command-aliases. These are pure moves; only their import specifiers change. No compat re-export at the old paths — every consumer switches to the owning subpath. The per-command defaults stay where they are for now (the daemon's edge into the facet resolver is the harder cut and belongs with the daemon-closure work). Part of #2545 / #2543. |
||
|
|
e3880f10c8 |
fix(android-helper): build with the pinned build-tools version, fail on CI when unset (#2568)
build-android-helper.sh compiled with whichever build-tools directory was newest on the image while every lane that builds a helper installs exactly build-tools;36.0.0. That selection feeds d8 and aapt2, so a newer package on the runner changed the helper's bytecode and resources rather than just its packaging, and nothing said so. The script now takes the version as an input, its last positional or AGENT_DEVICE_ANDROID_BUILD_TOOLS, resolves it under $SDK_ROOT/build-tools, and checks the four tools the build actually runs instead of aapt2 alone. An unpinned build is a hard failure on CI; locally the newest-installed fallback stays and is announced on stderr. Each lane that builds a helper declares the version it installs and interpolates that same value into its sdkmanager line and the build environment, so an install and a build inside one lane cannot drift. setup-android-replay-host exports it to the packaging gate and names it in both helper cache keys, which are keyed on APK bytes; size.yml and release-android-snapshot-helper.yml declare it at job level. The fixture-repack cluster keeps its own pin: a different command family that ships no helper APK. Closes #2527 |
||
|
|
ab3d11e069 |
test(layering): classify daemon edges that reach platform mechanics through a hub (#2557)
R76 keyed its inventory on the target filename, so a daemon import of a root module that imports the platform-runtime family itself was invisible: the daemon could gain or widen an edge to a root hub without any gate noticing. Dynamic edges were invisible in the same way, and the ranked spine (R4, R5, R6) cannot see a dynamic import's direction at all. The target predicate is now computed from the tree: the platform-runtime family plus every module outside the daemon zone that reaches it, over static and dynamic edges alike. That made exactly three real edges visible, and all three are classified rather than allowlisted: the two provider-runtime hubs the daemon runtime composes, and the dynamic interactor lookup in the snapshot capture, which is a leak and now carries the rationale and the deepening issue (#2555) that a filename pattern never would have asked for. Part of #2542 |
||
|
|
b2b084d2e1 |
docs: fix phantom specifiers, the duplicate ADR 0019, and the Node floor (#2533)
AGENTS.md routed request cancellation/progress and diagnostics to `@agent-device/capture-kit` subpaths that no package exports; both live in `@agent-device/host-kit/request` and `@agent-device/host-kit/diagnostics`. It also named `@agent-device/contracts` as an importable seam although that package publishes no root export, and claimed `src/daemon/handlers/session.ts` was over budget after that extraction already landed at 242 lines. Two ADRs carried number 0019. The hop trace has its own claims to make, so it now numbers 0023, joins the index, and keeps the links from ADR 0019 and ADR 0022. The Node floor split was undocumented: `engines.node` stays at 22.12 because CI installs the published tarball on that floor, while contributors need 22.13 for the pinned pnpm. CONTRIBUTING now says so, and installation.md names the 22.12 floor and the web backend's Node 24 requirement. Extend the agent-guidance contract to resolve every `@agent-device/*` specifier AGENTS.md names against the owning package's `exports`, root included, so neither a phantom subpath nor a phantom package root can route an agent to a module that does not exist. |
||
|
|
9aa6465768 |
perf(scripts): add a device-free PNG crop benchmark (#2505)
* perf(scripts): add a device-free PNG crop benchmark `pnpm bench:png-crop` runs the whole-image pipeline and the shipped region crop over the same bytes in one process, so the comparison holds the capture content, the deflate stream, and the machine fixed. The corpus is generated, which keeps a run at seconds with no device; real captures join the same table via `--file`, and each corpus entry prints its compressed size so an unrealistic corpus is visible. The README records what the measurements said, including the case the encoder policy loses: `None` on every scanline is faster everywhere but writes about 1.7x more bytes than a filtered encoding on smooth low-frequency content. * chore(gates): run the PNG crop benchmark's model tests in unit-core Registers scripts/png-crop-benchmark/*.test.ts so the timing summary that the report is built from stays covered without a device lane. |
||
|
|
0feb4e26a0 |
feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438) (#2448)
* 0.21.1 * feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438) iOS apps that sign in via ASWebAuthenticationSession present the identity provider in com.apple.SafariViewService, out of the app's process. Two facts, both verified live on the iOS 26.2 Simulator, made these flows unautomatable: activating or launching the host cancels the auth session, and the host AX bridge cannot see the sheet because the app stays the AX primaryApp. Serve and drive the sheet in place. A closed registry names the host (shared by the TypeScript and Swift sides under a parity test); the runner reads and drives it without activation and never adopts it as the session target; and the Simulator route detects a running host with a cheap device-scoped ps probe and takes the runner path, since the bridge would serve the occluded app tree as if healthy. open refuses to launch a registered host, and captures carry a system-surface disclosure. Presence is foreground state, not tree content: a torn-down host serves a richer tree than a live one, so content heuristics cannot tell them apart. The never-activate guard is what keeps the foreground predicate sound, which also makes the stale-tree failure mode unrepresentable for this flow. Closes #2438 * chore(gates): register contracts/ios-system-surface in the export snapshot * fix(ios): close the system-surface correctness gaps from review Presence probe: absence and probe failure are no longer reported as "no surface". The probe returns present/absent/unknown and the route takes the runner for anything but a proven absent, so a sheet opened between two captures, or a probe that cannot answer, can no longer fall through to a bridge capture that would answer confidently from the occluded app tree. Only a positive observation is memoized. The probe now matches with pgrep and reads only a matched pid's environment, which is ~3x cheaper than the previous full process-environment dump and stops copying every process's environment. Open guard: the refusal moved to every resolved-host launch and terminate, so the URL, deep-link and launch-args branches that returned before the old check can no longer launch the host. Terminating a host is refused too, since that cancels the presented session just as launching it does. Comparison: the surface identity now reaches SnapshotState, and tap-failure corroboration refuses outright when a baseline and a post-action capture disagree about it, instead of letting app and sheet captures meet in legacy same-presentation matching. Selector routes disclose an iOS system surface through the shared disclosure seam rather than reading only the Android field. The contracts import in the launch path is deferred so the app-lifecycle facade's eager closure stays flat, and the runner's comment prose is trimmed because apple/runner ships to npm as uncompiled source. * fix(ios): route the system-surface probe through the Apple tool provider The probe shelled out with runCmd, so every eligible capture spawned a real process even in provider-backed tests that stub the Apple tool seam — 17 real spawns in one scenario file, which is both wasted work and added latency on timing-sensitive settle paths. It now goes through runAppleToolCommand like the sibling ps probe, so a stubbed provider answers instead of spawning. * fix(ios): disclose a skipped bridge when the surface probe cannot answer Routing an unprovable probe to the runner is right, but the early return also skipped runFallback, so the response lost its warning and kept an identity that could still be compared against a bridge publication. An unknown probe now falls back through the same disclosed path as a bridge failure, with its own reason. * fix(ios): keep surface identity through comparison, find, and probe scope A ps read that carries no SIMULATOR_UDID at all was reported as absence, so an unreadable or truncated environment could route a live sheet to the occluded app tree. Only a scope naming a different device is a real negative now; a missing one stays unknown. The shared post-gesture comparison token used comparisonKey or the backend alone, so an app capture and a sheet capture — both XCTest — compared equal and a sheet appearing or dismissing read as a stable surface. The token now carries the surface, which covers stabilization, verify and settle through the one path they share. Mutating find rebuilt its capture without iosSystemSurfaceBundleId, so the shared disclosure helper could not report the sheet on either outcome. It is preserved now. Each fix has a regression that fails without it. * fix(ios): keep surface identity in verify and settle comparisons `--verify` compared node digests and `--settle` diffed node-only baselines, so an app baseline and an in-place system-surface capture (a web sign-in sheet) were treated as one presentation: a meaningless changed verdict, and a whole-surface replacement presented as an in-surface diff with refs. The pre-action baseline now travels with the surface its capture described, from the resolution and the session frame through to the settled capture, and one module owns the comparison for both routes. Across a surface change no same-surface claim is made: evidence reports the transition instead of a digest comparison, the settled diff and its refs are withheld, and both payloads disclose the transition. * refactor(test): move the cross-surface settle tests onto their source mirror The #2438 cross-surface cases were appended to `settle.test.ts`, taking it over the test-file size ratchet (2528 lines, 2359 at the merge-base). They assert the comparison `post-action-surface.ts` owns, so they move to that module's mirror test file, and the device double plus the trees both files drive move to a sibling fixtures module under `__tests__/` rather than being duplicated. Pure move: every test and every assertion is unchanged, and `settle.test.ts` is back under its merge-base length. * test(daemon): cover the cross-surface settle refusal on the generic route `scroll --settle` and `back --settle` plumb the baseline's surface identity through `baselineSurfaceBundleId`, but nothing asserted it: the generic route had zero coverage of the #2438 refusal, so a regression there would have been silent while the element-targeted route stayed green. Assert the same contract the targeted route guarantees, in both directions and for both commands: no diff is attached across an app/sheet boundary — therefore no tail and no `refsGeneration` — the transition is disclosed, and the settle observation still reports its own verdict alongside that disclosure. Each direction falsifies a different half of the plumbing, so both are needed: dropping the baseline's surface identity fails only the sheet-to-app tests (an app baseline has no surface id to lose), and dropping the settled capture's fails only the app-to-sheet tests. No production change: the plumbing was correct, only untested. * refactor(ios): inline the single-caller surface disclosure wrapper iosSystemSurfaceDisclosure() only mapped provenance-or-nothing onto the shared constant for one caller, so the caller now reads the constant directly and the wrapper is gone. Its test becomes a test of the transition disclosure, which is the function that still earns its place (the "sheet is gone" sentence). readAppleSnapshotResult also called readSystemSurfaceProvenance twice inside one spread; it is bound to a local and read once. * docs(adr): state that a presented surface outranks a requested bundle id prepareActiveCommandContext checks for a presented system surface before it resolves or activates command.appBundleId, so a command naming a different app is still served the sheet. That is intended, but the code does not read that way; the amendment now says it plainly. * refactor(ios): carry the system surface in the capture's comparison lineage A capture of an in-place system surface (a web sign-in sheet) describes a different presentation than a capture of the app, so it must never compare equal to one. The `present` branch of the iOS snapshot route returned a bare fallback, so that capture carried no comparison identity at all, and two comparison sites hand-rolled the distinction from `iosSystemSurfaceBundleId` instead. The probe now reports which host it matched, and the `present` branch goes through `runFallback` like the `unknown` branch beside it, lineaged to `<device>:<host bundle>`. The comparison key then differs from an app capture's by construction, so the surface branch in `hasMatchingPresentation` and the surface concatenation in `snapshotComparisonKey` are gone: both sites are plain key equality again, and neither knows that system surfaces exist. Two captures of the same surface still share a lineage, so they stay comparable with each other. A presented surface is not a bridge failure, so it gets its own warning wording: the bridge is inapplicable here, not unavailable. * refactor(interaction): carry the pre-action baseline as one surface-scoped value The same pre-action tree travelled as a flattened nodes/surface pair at every boundary, and each boundary rebuilt it with a conditional spread. Carry SurfaceScopedNodes itself instead: - ResolvedInteractionTarget gets preAction?: SurfaceScopedNodes, replacing the preActionNodes/preActionSurfaceBundleId pair and the PreActionBaselineFields intersection on all three arms of the union. - SettleObservationCommandOptions gets baseline: SurfaceScopedNodes, replacing baselineNodes/baselineSurfaceBundleId. - RefResolution carries tree: SurfaceScopedNodes instead of nodes plus a loose surfaceBundleId. That retires preActionBaselineFields(), preActionBaseline(), evidenceBaseline(), the local SettleBaseline type, the split-then-reassemble in settleObservationCommand, and the 'preActionNodes' in resolved narrowing tests. SurfaceScopedNodes moves to contracts, where ResolvedInteractionTarget can name it; only two sites now mint one from a SnapshotState. Behaviour is unchanged: the cross-surface guarantees keep their existing tests. * fix(ios): identify a surface capture by what the runner served The `present` path stamped the capture's comparison lineage from the host-side presence probe. That probe answers about a host PROCESS and deliberately stays positive while a dismissed host lingers, so during that window the runner truthfully returned APP content while the route lineaged it to the HOST: the sheet capture before the dismissal and the app capture after it compared equal, and a post-gesture poll could read the transition as a stable surface. Derive the identity from the returned capture's `systemSurface` instead - the runner stamps the surface it actually served - and say which of the two the capture holds in the warning. The probe's host is now evidence only: it names the matched host in a route diagnostic so a lingering window is legible in the daemon log. Other reasons keep their lineage and wording byte for byte. Captures that bypass the route's planning (a pinned backend, a custom-actions read) also reach the runner, and the runner serves the sheet there too. They carried no comparison key at all, so a sheet and app content fell through to legacy presentation matching as one presentation and could corroborate a tap across the two. The capture owner now gives those a surface-scoped identity as well, with no fallback-source residue: nothing fell back. An app capture off the route is untouched. * fix(ios): derive a served surface identity at the one stamping point A runner fallback's comparison identity was decided per call site. The `present` path and the off-route path read the runner's `systemSurface` stamp, but the plain `runFallback` path did not: it stamped the app lineage the route had planned, whatever the runner returned. The probe and the capture are separate observations, so a sheet can appear in the gap between them. With the bridge circuit already disabled for the generation, an app capture and a later sheet capture both received the same app-generation key, so tap corroboration could treat two different surfaces as comparable. `stampFallback` now owns the decision for every runner fallback: the surface the runner served outranks the app lineage the route planned. The reason the bridge was skipped survives either way, and app-generation evidence leaves with the app lineage it describes, so two captures of the same sheet still compare equal. `runSurfaceFallback` keeps only the reason, which is the one thing that path decides. |
||
|
|
df0a0f7fd2 |
perf(package): strip comments from the Apple runner source the npm package ships (#2467)
* perf(package): strip comments from the Apple runner source the npm package ships The packager copies apple/runner/** into dist/ as Swift source, removing only its AGENT_DEVICE_RUNNER_UNIT_TESTS blocks, so doc comments and design notes were downloaded on every install: 71.9 kB of 441.2 kB of packaged runner Swift. Add a lexical scanner for the removal. A regex cannot do this: `//` and `/*` open a comment only in code position, raw literals move their own delimiter and escape with the `#` count, interpolation segments hold code and further literals, and Swift block comments nest. A construct the scanner cannot account for throws at packaging time instead of shipping Swift that does not compile. * fix(package): keep Swift regex literals out of the comment scanner `#/foo//bar/#` is a valid extended regex literal with no comment in it, but the scanner only knew the `#"` raw-string family, so it read the literal's `//` as a line comment and shipped `let pattern = #/foo` — Swift that does not compile. Add `#/…/#` and `##/…/##` as a literal context: matching `#` counts, the single- and multi-line forms, Swift's own-line rule for a multi-line closing delimiter, and the `\/` escape that keeps one from closing early. Bare `/…/` literals stay unresolvable, because the same `/` opens a comment, divides, and starts a regex literal, and only the parse separates them. Where one could begin — an expression position whose `/` is not followed by a space, a tab or `)` — packaging throws by file and line instead of rewriting bytes it cannot prove are code. Divisions (`width/2`, `Double(3)/Double(4)`), the recording scripts' shebang and `(/)` keep flowing through. * fix(package): keep the packaged runner source on the checkout's line numbers `dist/apple/runner/**` is the Swift a user's `xcodebuild` and the runner name a file and line in (it lands in runner.log), so those numbers are only worth reading if they point at the same line of `apple/runner/**`. Both rewriting passes now empty the lines they remove instead of deleting them: comment removal (889 lines, 889 B) and the pre-existing unit-test `#if` block strip, which was moving everything below a block by up to 883 lines (3,737 lines, 3,737 B). `dist/apple/runner/` 555,907 B -> 488,635 B (-67,272 B, -12.1%); its Swift alone 441,196 B -> 373,924 B (-15.2%). Parity costs 4,626 B of the 71,898 B the previous head saved. Nothing in the repo compiles the packaged source, so a mis-lex that failed to throw would ship Swift that does not build and no gate would see it. Add `pnpm check:packaged-runner-swift`: it packages into a throwaway root and asserts line-count parity plus the line of every declaration each packaged file still carries, then runs `swiftc -parse` over all 44 files. The parse half reports itself skipped where no Swift toolchain exists, so the gate is declared on the macOS lane, where both halves run. |
||
|
|
220bab08ba |
chore(gates): name the added modules and import paths when an eager closure grows (#2471)
* chore(gates): name the added modules and their import paths when an eager closure grows The no-growth diagnostic in scripts/__tests__/eager-closure-budgets.ts only named the FIRST newly evaluated module and always advised a dynamic import. On #2423 that sent five reviewers toward the wrong fix when the growth was a small new module that belonged in a module every affected entry already evaluated -- the dynamic-import advice was never coherent for a brand-new module with no old edge to defer. - describeClosureGrowth now lists every added module (bounded to 10), each with the shortest static import route from the entry to it. - describeSharedGrowthHomes runs once after every entry is evaluated: when two or more entries grew by the same added module, it names the modules they already evaluate at the merge-base under that module's own package -- candidate homes, not a verdict. - classifyGrowth's closing advice now states the two common causes (a new static edge, or something that used to load lazily) and the two remedies (give the symbol a home in a module already in the closure, or make the new edge lazy) instead of prescribing one fix. The verdict logic (when an entry is flagged as having grown) is unchanged. * chore(gates): split the shared-growth-homes diagnostic into small helpers * chore(gates): aggregate only net growth and keep shared homes per added module The cross-entry shared-homes note took every entry with a newly evaluated module, which is not the condition the per-entry rule applies: a closure that swaps one module for another, or shrinks while adding one, has added modules and no growth. `classifyGrowth` passes it, so the aggregate must too -- entries now carry their head closure size and the grouping keeps only the ones whose closure actually grew. Candidate homes are no longer unioned across added modules. Each added module shared by two or more grown entries gets its own block naming those entries with how much each grew and the merge-base modules exactly those entries evaluate, so the label no longer claims a home is common to every failing entry when two independent groups are in play. |
||
|
|
fea7ca8a43 |
chore(layering): runner modules reach host-kit only through the runner host port (#2470)
R77 apple-runner-host-port bans a direct @agent-device/host-kit/* value import from packages/platform-apple/src/runner/**; the port at runner/host.ts, bound in core/runner-host.ts, is the only door. runner/** sits in the eager closure of seven Apple facade entries eager-closure-budgets.ts holds at a fixed size, so a direct import grows all seven at once (#2423 measured one candidate import adding 5 modules to runner/index.ts's closure, 13 -> 18, after two review rounds spent rediscovering this). |
||
|
|
6d08de4609 |
feat(scroll): find off-screen targets in one command with --until (#2436)
* refactor(interaction): extract the scroll command runtime out of gestures.ts * feat(scroll): add --until <selector>, report honored travel, fix web amount units * test(scroll): cover --until through the provider-backed integration path * perf(selectors): keep the scroll-until predicate off the eager import path * fix(scroll): refuse an unreadable capture instead of reporting end-of-content * fix(selectors): keep the capture-readability check off the eager import path * test(selectors): use a declared snapshot quality state in the capture fixtures * fix(scroll): read the capture quality verdict under the spelling the backend uses * refactor(scroll): collapse --until onto the one route that runs it * refactor(scroll): drop unexported until types and duplicated guidance prose * test(scroll): fix the climbing fixture and drop duplicated route-level cases * refactor(scroll): delete the dead command-runtime executor and reuse canonical predicates * refactor(interaction): keep requireResolvedPoint local to the gesture runtime |
||
|
|
8dd1f6c51a |
fix(check): honor Vitest worker configuration (#2437)
* fix(check): honor Vitest worker configuration * test(apple): freeze the default readiness budget clock --------- Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com> |
||
|
|
0dfd65f6a2 |
perf(ios): speed up deep snapshots and keep first taps reliable (#2414)
* perf(ios): recover deep snapshots and isolate optional tap probes * chore(gates): enforce snapshot assets and optional probe lifecycle * fix(ios): preserve capture bounds and local probe recovery * chore(gates): validate base package assets with its own policy * chore(gates): verify recovery failures respect launch observation policy * fix(ios): fail closed on unknown snapshot frontier completeness |
||
|
|
342e98cff9 |
refactor(daemon): separate open-target policy from platform mechanics (#2416)
* refactor(daemon): separate open-target policy from Android mechanics Move resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen behind the Android owning seam in packages/platform-android. resolveSessionAppBundleIdForTarget now lazily reaches Android mechanics itself instead of taking an injected resolver function, so open-prepare and selector-dispatch import only the neutral open plan/result surface from platform-runtime-open-target.ts. Reclassifies the two R74 inventory edges to daemon-policy-essential and updates ADR 0022. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * refactor: address adversarial review findings on open-target seam Restore try/catch around the Android-mechanics lazy load so a module load failure still resolves to undefined instead of throwing. Rename the unrelated private resolveAndroidPackageForOpen in app-lifecycle.ts to requireAndroidPackageForOpen to remove the naming collision with the new exported function. Add a planted-violation regression test for reintroducing Android mechanics on the selector-dispatch edge. Tighten ADR/inventory wording that overstated which files consume the neutral resolver. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * fix(platform-android): keep the mechanics facade lazy for the new open-target exports resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen were re-exported statically from mechanics.ts, which eagerly evaluates open-target-resolution.ts on import and tripped the eager-closure-budgets gate (177 -> 178 modules). Wrap them as lazy async functions, matching the existing pattern used for listAndroidAppsWithAdb/captureAndroidLogcatWithAdb in the same file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * fix(android-tools): keep inferOpenedAppBundleId best-effort on a mechanics load failure Loading Android mechanics moved from the near-infallible root platform-runtime-open-target.ts to the real adb-backed mechanics module, but the wrapper call stayed unguarded. A loader failure now throws instead of leaving the app-bundle identity unset, even for a targetless open that never needed the loaded module. Wrap the load and delegate in try/catch so it degrades to the current bundle id, matching the pre-refactor behavior, and add a regression test with the loader rejecting on a targetless open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * perf(android-tools): skip loading Android mechanics when app-bundle identity is known inferOpenedAppBundleId always loaded Android mechanics before delegating, even when currentAppBundleId already made the delegate's own fast-return a no-op. Check it first so the load is skipped entirely once the identity is already known, and add a regression test asserting the loader is never called in that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
9d7d60c5e0 |
test(coverage): declare every public command's coverage judgments once (#2418)
* test(coverage): declare every public command's six platform coverage judgments once One row per public command in test/integration/command-coverage/declarations.ts carries the android-emulator, ios-simulator, macOS, tvOS, web and Linux classifications with the same fields the six per-platform manifests use today. Each platform's Record<PublicCommand, ...> is projected from that table at load time by a small per-platform view module, so no projected record is committed. No judgment is derived from another platform's: all six stay authored per command. * test(coverage): read the projected per-platform coverage view The six coverage smoke tests, live harnesses and coverage reports now import the platform view module that projects the declaration table. Both the depgraph blast-radius query and the device-lane test follow the iOS and macOS paths. * test(coverage): delete the six per-platform coverage manifests Their rows now live once, per command, in the declaration table; each platform's record is projected from it at load time. * fix(check-affected): extend macos-coverage and integration-node ownership to command-coverage/ test/integration/command-coverage/declarations.ts now carries the per-command coverage judgments that used to live directly under test/integration/macos-e2e/. Its nested path wasn't matched by macosCoverageOwnership (top-level or macos-e2e/ only) or isNodeIntegrationPath (no nested segments), so it fell through to vitest-related, which can't actually run its node --test consumers. Extend both rules to also match test/integration/command-coverage/. |
||
|
|
871ce1b755 |
refactor(daemon): normalize lifecycle participation of platform resource owners (#2415)
* refactor(daemon): typed lifecycle participation for platform resource owners daemon-runtime.ts no longer imports the Apple runner owner, the Android snapshot-helper/Web orphan cleanups, or the app-log legacy marker recovery directly. Those are now behind PlatformOwnerLifecycle, a typed startup/shutdown surface owned by the daemon; the root composition module platform-runtime-daemon-lifecycle.ts is the sole place that wires the concrete platform owners into it. Reclassifies the R76 daemon-platform-runtime-inventory edges accordingly: the leaked apple-runner-owner and operation-host edges are removed, and resource-cleanup/daemon-lifecycle stay as composition-essential. Closes #2333 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5 * style: apply oxfmt formatting to R76 inventory test fixtures Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5 * test(daemon): update source-ordering assertion for the renamed call site daemon-runtime.ts now calls platformDaemonLifecycleOwners.recoverLegacyAppLogMarkers instead of the old recoverLegacyAppLogMarkersAfterDaemonLock dynamic import; the text-based ordering guard needs to look for the new call site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5 * docs(adr): record #2333 as landed in ADR 0022 The registry (DAEMON_PLATFORM_RUNTIME_EDGES) already reflects the retired apple-runner-owner/operation-host edges; update the ADR prose to match instead of leaving #2333 listed as outstanding work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5 * fix(daemon): configure the Apple runner owner only after the daemon lock is held configureForDaemonLock ran before acquireDaemonLock, which meant a process that lost the lock race briefly published a global runner-owner state dir and claim-authority probe it didn't own. Move the call inside the post-lock try block (verified nothing reads the runner-owner state before request time — runner-host.ts only captures getter closures) and drop the now-unreachable clear call on the lock-failure branch. Adds a source-order regression test pinning the new sequence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017oNQ8THHYFcK7NX1s78kW5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fef0b12cc5 |
chore: hoist shared snapshot/selector test fixtures into a single canonical location (#2419)
* chore: hoist shared snapshot/selector test fixtures into @agent-device/selectors PR #2397 left two copies of the snapshot-state builder and duplicated geometry/touch-point arbitraries (root's src/__tests__/test-utils/ and the package's internal/__tests__/), because packages cannot import root src/. Move the canonical versions into a new @agent-device/selectors/test-fixtures subpath and have both root and the selectors package import from it, leaving buildNodes and the root-only replay/gesture arbitraries in place. Fixes #2402 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: exempt test-fixtures.ts's test-only arbitraries from dead-code check PROPERTY_RUNS, scrollingContainerTypeArb, distinctRectPairArb, and interactionTouchPointScenarioArb are consumed only by *.test.ts files, which Fallow's --production analysis does not see, matching the existing pattern for other workspace-package symbols reached only from the test tree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: consolidate makeSnapshotState into capture-kit, rename fixtures file An adversarial review of the #2402 fixture-hoisting change found a third copy of makeSnapshotState in packages/capture-kit/src/snapshot-state.fixtures.ts, predating PR #2397. Since @agent-device/selectors already depends on capture-kit, make capture-kit's copy canonical (exported as ./snapshot-state-fixtures) and have the selectors package's fixtures module re-export it instead of duplicating it a third time. Also rename packages/selectors/src/test-fixtures.ts to snapshot-geometry.fixtures.ts (subpath ./snapshot-geometry-fixtures) to match every other test-fixture module's *.fixtures.ts convention in this repo, which lets it fall under .fallowrc.json's existing blanket **/*.fixtures.ts dead-code exemption instead of needing a bespoke per-symbol entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8d5ca680c0 |
refactor(move): move the selector pipeline and interaction targeting into @agent-device/selectors (#2397)
* refactor(move): move the selector pipeline and interaction targeting into @agent-device/selectors The 11 pipeline modules (selector-pipeline, selector-pipeline-policy, interaction-targeting, touch-semantics, interaction-positionals, press-retarget, interaction-touch-point, absence-observation and its errors/resolution companions, and the interaction-error vocabulary) are exposed as per-file subpaths. The two test-utils files the moved tests share with root tests are copied into the package, following the existing package-local test-utility pattern. * chore(gates): point the layering pins at the moved selector pipeline R19's owner constant now names the pipeline in packages/selectors, and the rule additionally refuses in-package relative routes to the engine file so the co-location cannot widen the door. The package-boundaries export/dependency pins and the fallow health baseline key follow the files. * fix: drop two unused exports flagged by fallow * test: point press-retarget comment at the relocated touch-semantics module |
||
|
|
ef5a459294 |
refactor(daemon): isolate Apple session observations (#2405)
* refactor(daemon): consume a semantic Apple session observation port * chore(gates): retire direct daemon observation imports --------- Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com> |
||
|
|
0627190524 |
refactor(move): move lease scope vocabulary into @agent-device/contracts (#2380)
* refactor(contracts): move the lease scope vocabulary into @agent-device/contracts * chore(gates): pin the contracts lease-scope subpath in the exports snapshot * fix(tests): re-point remote-proxy-parity lease-scope import to contracts The test landed on main after this branch cut and still imported the pre-move path src/core/lease-scope.ts; use the @agent-device/contracts/lease-scope subpath like the other consumers. |
||
|
|
c5d9707196 |
refactor(daemon): move the device selection cluster into @agent-device/device-selection (#2396)
* refactor(daemon): move the device selection cluster into a workspace package
git-rename src/core/dispatch-resolve.ts, src/core/device-selection-resolver.ts and src/request/device-inventory-context.ts (plus their tests and fixtures) into a new private package @agent-device/device-selection (deps: contracts, host-kit, kernel). One subpath per module points straight at the moved file; no index.ts, no re-export at the old path. Consumers switch to the owning specifier in the follow-up commit.
* refactor(daemon): install the installed-app probe on the inventory gateways and switch consumers to the device-selection package
The composition root (src/platform-runtime-device-inventory.ts) now attaches an optional findInstalledApp probe to the composed device inventory gateways, lazily importing the Apple simulator app-resolution mechanics. Device selection reads the probe from the request context instead of importing a platform package directly, so absent probes fall back to the ordinary inventory rules. All consumers switch to the @agent-device/device-selection/{dispatch-resolve,device-selection-resolver,device-inventory-context} subpaths; the moved tests carry a package-local inventory test util.
* chore(gates): re-point the layering gates at the device-selection zone
TARGET_DAG_RANK gains the device-selection leaf zone and drops the retired request zone; the back-edge fixture and the capture-kit ALS substrate fixture move to the package path.
* test(daemon): prove the factory-installed app probe narrows simulator selection
The moved selection tests inject their own probe, so nothing covered the
composition root actually attaching findInstalledApp to the gateways:
omitting it would leave those tests green while app-based selection
silently fell back to the generic local rules.
The new case runs two booted simulators through
createComposedDeviceInventoryGateways and the request context, fakes
only the leaf xcrun spawn (core tool-provider), and asserts the
single-app-installed-local selection plus both probe consults.
|
||
|
|
22a46d12d2 |
refactor(move): move the remaining package-ready modules out of src/core (#2401)
* refactor(move): move remaining package-ready modules out of src/core - validation.ts -> @agent-device/kernel/validation - android-system-surface-disclosure.ts -> @agent-device/contracts/android-system-surface-disclosure - project-runtime.ts -> @agent-device/host-kit/project-runtime - runtime-transport-hints(.test).ts -> @agent-device/host-kit/runtime-transport-hints - app-events.ts (+tests) -> src/daemon/app-events.ts - dispatch-payload.ts (+test), payload-input.ts -> src/daemon/ - fill-backend-result.ts (+test) -> src/daemon/ interaction-outcome.ts stays in core: it correlates ResolvedInteractionTarget with error objects across the commands(rank3)/daemon(rank4) boundary, and contracts explicitly refuses mutable interaction-outcome lifecycle (R18). * chore(gates): pin the exports and boundary snapshots for the moved core modules |
||
|
|
d4cf02a889 |
chore(gates): classify #2278 daemon-platform-runtime edges and ratchet handler session authority (#2354)
* chore(gates): classify #2278 daemon-platform-runtime edges (R74) and ratchet handler session authority (R75) * chore(gates): discover the R75 shape target from the SessionState declaration * chore(gates): capture dynamic-import bindings in R74 so symbol drift cannot hide * chore(gates): reject dynamic-import destructure residue in R74 * chore(gates): R74 rejects open-ended dynamic imports beside named bindings * chore(gates): renumber daemon-platform-runtime-inventory to R76 (R74 taken on main) |
||
|
|
d11c8cf9d6 |
feat: support standalone Maestro clearState command (#2366)
* feat: support standalone Maestro clearState command Accept '- clearState' / '- clearState: <appId>' in Maestro YAML flows. Unlike launchApp.clearState (clear-then-open), the standalone form clears app state without relaunching, projecting to 'settings clear-app-state' on the daemon. Covers the Rocket.Chat login-with-deeplink helper, which previously failed with 'Maestro command "clearState" is not supported'. * test(maestro): cover standalone clearState with authored corpus flow Replace the UNVERIFIED_COMMANDS exemption with an authored clear-state flow exercising default and explicit appIds, plus the regenerated upstream parser fixture proving Maestro compatibility. Live iOS Simulator evidence (iPhone 16, com.apple.mobilesafari): - marker files in the data container, then replay '- clearState' (default) and '- clearState: <appId>' (explicit) via 'replay --maestro'; both replay 1/1, wipe the container, and leave MobileSafari not running (no reopen). |
||
|
|
1f9d940bff |
refactor(capture-kit): complete ADR 0019 end state — relocate snapshot and recording zones (#2385)
* refactor(capture-kit): relocate snapshot and recording zones into capture-kit
Move the ADR 0019 end-state capture zones into @agent-device/capture-kit:
- src/snapshot/** -> packages/capture-kit/src/snapshot/** (presentation,
freshness, scroll-edge-state, ios-snapshot-runtime, android occlusion)
- src/recording/** -> packages/capture-kit/src/recording/**
- src/core/snapshot-{chrome,state,tree-ingestion,node-lookup}.ts ->
packages/capture-kit/src/
- src/snapshot-quality/ test -> capture-kit presentation tree (directory
retires with its last file)
Pure renames: import re-pointing and gate updates follow in the next commit.
The snapshot-desktop-surface test parks in src/__tests__/ because it pins
the root eager-import-closure walker.
* refactor(capture-kit): re-point capture and recording consumers to the new subpaths
Rewires every consumer of the relocated snapshot/recording modules to the new @agent-device/capture-kit subpath exports, adds the 23 subpath entries to the capture-kit exports map, fixes the moved recording-scripts test's __dirname-relative paths for the deeper location, and records the completed migration in ADR 0019's end state.
* chore(gates): align layering, mutation, fallow and CI gates with the capture-kit relocation
Moves the executable-policy roots, presentation-owner constant, zone ranks, authority fixture, mutation sharding globs, stryker aliases, fallow baselines and the iOS workflow's android-owned paths-ignore entry onto the new packages/capture-kit paths, and extends the planted-red coverage to the new presentation-owner subpath.
* chore: point capture-domain source-of-truth comments at the relocated capture-kit modules
* test: point shutdown recording mock at capture-kit and cover interactor acquisition presentation
* test(capture-kit): update upstream presentation test imports
* chore(gates): follow relocated snapshot assembly in R74
* test(daemon): freeze prewarm deadline assertion clocks
|
||
|
|
a6cf1b1fd4 |
refactor(ios): delete the unused snapshot plan interface (#2392)
`planIosSnapshot`, `IosSnapshotPlan`, the `plan` member of `IosSnapshotEngine` and `createIosSnapshotEngine` had no production caller: production reaches presentation through `publishIosSnapshot` / `presentIosSnapshot` directly, and the barrel re-export was all that kept the factory alive for fallow. Deleting the plan takes the last reader of most of `IOS_SNAPSHOT_PRODUCER_CAPABILITIES` with it. The table was typed over all four producers while only the two provider producers ever consumed its residue-shaping fields, and it had already drifted: it declared `simulator-ax-bridge` with `hittabilityEvidence: 'available'` while the bridge adapter emits `unavailable-fact: hittability` on every capture. Rather than correct the value, the table is now keyed on `IosProviderAcquisitionProducer`, so a producer that builds its own facts cannot declare one at all. Truncation is the one capability the runner and the bridge still need answered, so it moves to a table of its own over all four producers, read through `iosSnapshotTruncationEvidence`. Both keep `'available'`, which is what the adapter and the runner payload actually prove. |
||
|
|
6a03688d80 |
refactor(ios): prune converged snapshot paths (#2383)
The daemon snapshot assembly no longer presents. `shouldPresentLegacyIosInteractiveSnapshot` fired whenever an xctest capture arrived without a producer, or with a producer whose capability table still named `snapshot-state` as its presentation owner — which `simulator-ax-bridge` still did after routing moved it onto the engine, so a bridge capture with `--interactive-only` ran the iOS semantic presentation twice (#2188 invariant 2). Rather than deleting a runtime guard and hoping, `buildSnapshotState` now takes `SnapshotCaptureProvenance`: a capture either knows nothing about its origin or carries the whole pair, so the producer-less branch does not compile. Requiring the pair broke only test fixtures, which is the proof that production never omitted it. `presentationOwner` had one value left once the bridge was accounted for, so the capability and its type are gone; the truncation verdict that read it now reads `truncationEvidence`, which is the fact it was standing in for and matches it producer for producer. Post-wire scope planning names the channels that still need the pass instead of excluding the ones that do not, which takes iOS out of it. `compactIosInteractiveSnapshot` was a byte-identical alias of `presentIosInteractiveSnapshot` with no production caller. R74 holds it: the assembly and the Simulator bridge producer adapter may not import iOS presentation, and the assembly may not name the iOS channel or a producer. |
||
|
|
a9283fabc7 |
refactor(move): move replay divergence vocabulary into @agent-device/ad-replay (#2384)
* refactor(ad-replay): move the replay divergence vocabulary into @agent-device/ad-replay * chore(gates): pin the ad-replay divergence subpath in the layering boundary |
||
|
|
63711929d8 |
refactor(cli): take the CLI's vocabulary off runtime barrels (#2379)
* refactor(cli): read the cloud provider vocabulary from its own subpath `provider-policy.ts` asks three questions of `@agent-device/provider-webdriver`: the known-provider map, the predicate over it, and one type. All three live in `providers.ts`, a leaf with zero imports. Reaching them through the package barrel loaded 31 modules of WebDriver runtime — session handling, capture, XML — into every CLI invocation to answer "is this string a known provider name". The package predates the subpath-per-file rule and published only `"."`. It now publishes `./providers` as well (in the gates commit, with the recorded surface); this points the sole in-closure consumer at it. `src/cli.ts` eager closure: 325 -> 295 modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz * chore(gates): publish provider-webdriver/providers and record the surface The provider vocabulary needs a door of its own so a consumer asking whether a string names a known cloud provider does not load the WebDriver runtime. `src/providers.ts` has no imports, so the subpath points straight at it and costs the package nothing. R11's recorded export surface for the package grows by exactly that entry; the assertion is sorted so the list stays order-independent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3f022b0730 |
fix(gates): stop an eager-closure approval from turning main red on merge (#2375)
`no APPROVED_OVER_CEILING row is stale` reads the introduced-entry set, which is derived from `git merge-base origin/main HEAD`. On a push to main the merge-base IS the head, so nothing is first-introduced and every approval row reads as stale whatever its real state. That is exactly the shape of the approving PR's own merge commit: #2329 added the `packages/command-registry/src/planned-operations.ts` row to merge, and the merge that followed it called the row dead. Coverage has been red on main since (run 34099687663), and every branch cut from main after it inherits the same failure. - `staleApprovalRows` makes the verdict a named rule and defers it when the merge-base is the head, where no row is readable at all. Enforcement is not lost: a row that outlives its PR is still reported on the first branch whose merge-base could have read it, and the rule is pinned in both directions. - The `planned-operations.ts` row goes, which is what the rule asks for now that main carries the entry: its closure (74) is governed by the no-growth rule from here on, not by the domain-facade ceiling. Claude-Session: https://claude.ai/code/session_01SfQqXj7JKQVgBA8eg9SMVB Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d26b0786fb |
perf(ios): derive runner demand for Simulator opens and stop observation from awaiting the runner (#2329)
* perf(ios): derive runner demand for Simulator opens and stop observation from awaiting the runner Local Simulator opens now decide how much the XCTest runner is needed from the runtime operations declared by the steps still ahead in the same batch: an observation-only plan starts no runner, an unknown plan keeps the speculative prewarm without ever awaiting it, and a plan with an interaction prepares readiness for that step. open --relaunch no longer waits for runner readiness on a Simulator and resets the runner target only when a session is already alive. The Apple find ports report not-proven instead of starting a runner on a Simulator without a live session, so wait and read-only find observe through the canonical AX-bridge tree. Physical devices keep their lifecycle unchanged. The plan travels through the server-private internal request channel, never the wire; the Apple owner maps declared operations to a runner demand through a record complete over the runtime operation union. Refs #2198 * test(fixtures): share one inert audio-probe host across the platform runtime fixtures The Apple and Android runtime fixtures carried identical audio-probe doubles; host-kit now owns the one copy and both fixtures import it. Also folds the two Apple native-find ports onto one admission helper and lifts the Simulator runner prewarm policy out of the open sequence, keeping both under the complexity gate. * fix(ios): answer runner liveness through the runner provider seam The find ports and the relaunch target reset asked the local session registry whether a runner was alive, which misreads scripted and request-scoped runner providers as absent. Liveness is now a provider question: the local provider consults its session registry, a provider without startup cost counts as live, and an awaited prewarm proves liveness without asking. * perf(ios): select plan uses from step input and give young Simulator targets a bounded bridge grace A snapshot, diff, or find step now selects the runtime uses its structured input reaches, the way its handler does, so a plain snapshot no longer counts the custom-actions alternative and an observation-only batch resolves runner demand none. The descriptor declares the selector next to its alternatives; the daemon plan derivation honors it and keeps the union for every other command. Without the runner wait, the first snapshot after an open reached the AX bridge while the app was still becoming the primary foreground owner or registering its accessibility server, and the typed fallback then started the runner the plan had just avoided. A target younger than ten seconds is re-read for a bounded grace measured from the first such failure: five seconds for a missing AX server, one second for an ownership miss so a launch-time system dialog still reaches the fallback quickly. Established targets get no grace. * fix(ios): a registered runner session counts as live only once it has answered A session record exists while xcodebuild is still connecting, so an alive child pid is not a runner that can answer. Treating it as live sent the relaunch target reset into a starting runner, queued behind its connection retries, and the failed reset invalidated the very session the prewarm was building. Liveness now also requires the session's readiness flag, which the first successful runner response sets. * refactor(ios): lift the bridge launch grace out of the snapshot route capture * test(descriptors): pin the snapshot, diff, and find step-use selectors * test: stub runner operations in the replay test-runner suite and keep runner-session tests within the size ratchet A Simulator open schedules a best-effort runner prewarm that outlives its request. The replay test-runner suite opened a Simulator with the real Apple tools, so the prewarm's deferred import resolved after the file finished and spawned into whichever file the worker ran next, where the hermetic signal guard failed an unrelated test. * fix(plan): count only required operations and read find and snapshot steps the way their handlers do Runner demand now counts a command's required operations only: a preferred or conditional operation is a measured fast path the command succeeds without, so get, wait, and read-only find stay observation-only. The step selectors for snapshot, diff, and find live next to the registry and read the daemon step exactly as the handlers do: the daemon flag for custom actions, and find's positionals through the same parser, where a missing action is a click and an unparseable step keeps every declared alternative. The handler and the selector share one action-to-intent map. The batch runner hands each step its remaining steps in handler shape, and the derived operations reach the platform as a typed list on the lifecycle execution instead of an untyped plan on every open. * perf(ios): let open wait for the launched app to become observable, and make runner liveness explicit The snapshot route no longer infers a launch from process start text and retries inside its own capture. Open owns launch timing instead: a local Simulator open asks the AX bridge whether the launched app is observable, bounded by per-code windows measured from the first typed launch-transition failure and never extended, so an ownership miss seen after an AX-server miss shrinks the deadline to the ownership window and a launch-time system dialog still reaches the typed fallback quickly. Any other device, or a bridge that cannot answer, keeps the fixed settle. The open response reports what it learned. Every runner provider now states whether it can answer without a startup wait; a bare executor answers directly by construction and scripted providers say so. The runner prewarm policy and the observation settle move out of the open sequence into their own module, and the native find admission is named for what it admits. * docs(context): keep the runner-demand vocabulary within the guidance budget The enumeration and the no-public-flag rule live on the contract type that owns them; CONTEXT.md keeps the term itself, and two neighbouring entries lose words that carried no meaning. * refactor(contracts): name the runtime operation vocabulary below the operations union The lifecycle execution carries the operations a plan requires, but typing that list with the operations union closed a 36-file type cycle: the operations types depend on the lifecycle types. The vocabulary now lives as a const list below both, proven equal to the union by a type test, so the plan is typed end to end, the Apple host table indexes it without casts, and the daemon narrows descriptor names through a guard instead of a cast. * fix(apple): reach runner liveness through the memoized operations loader Every Apple tool port loads the runner operations through the one memoized loader (#2314): a port that opens its own dynamic import can resolve the unmocked module while a test's mock factory is still loading and let a real local runner escape. The liveness port now uses the loader like its siblings; the facade members consumed only through the loader are declared to fallow, and the plan resolver reads one step per helper to stay under the complexity threshold. * fix(ios): keep bridge-only behavior to iOS Simulators The launch observation, the runner-free find admission, and the relaunch policy apply only where the host AX bridge exists: iOS Simulators. A tvOS Simulator keeps its awaited prewarm and asks for no observation, which the tvOS provider scenario now pins. * bench(ios): add a first-interaction cell to the snapshot convergence harness An open that defers runner readiness moves its cost to the first runner-dependent command. The cell starts each sample like cold, opens the fixture untimed, then times the first press that follows (the deep-link confirmation when the launch URL raises it, otherwise the screen anchor). * bench(ios): read the deep-link confirmation from a snapshot and by node type The open response carries no tree and regular snapshots publish the node type, so the confirmation iOS raises for a launch URL was never seen on this runtime and every deep-linked cell failed its anchor check. * refactor(plan): keep the step-use selectors inside the registry The eager-closure ratchet counts every module the registry loads; the selectors need nothing the registry does not already import, so they live beside find's recording-effect reader instead of adding a module to every entry that loads the registry. * feat(apple): release a speculative runner when the plan is proven observation-only #2198 requires a `none` runner demand to retain no runner, not only to start none. A runner a prewarm started that no command has used yet is speculative: the session records that mark at creation, the first command that is not a readiness probe clears it, and a Simulator open whose plan is proven observation-only asks the runner owner to release a speculative session in the background, so the observation path never waits for a runner to stop either. A runner that has served a command is the session's working runner and stays under the existing idle-stop policy, so a mixed workload does not pay a cold runner start at every observation-only open. The release goes through the runner provider seam: the local provider stops its own speculative session; a provider that never starts speculative work omits the operation and releases nothing. * bench(ios): press an unambiguous target on the catalog and Settings screens The first-interaction cell pressed the screen's anchor text, which on the catalog and iOS Settings screens names two actionable elements (the native tab and the screen title); the CLI refuses that as AMBIGUOUS_MATCH by design, so those two cells could never measure anything. Each such screen now names the element the cell presses. * fix(ios): keep observation on the bridge while app discovery is pending and no runner is live #2331 bounds one capture's wait for the Simulator app discovery and takes the XCTest fallback past it; #2198 stops a Simulator open from awaiting the runner. Together, a `wait` right after a relaunch on a loaded host fell back to XCTest while the runner was still starting, spent its poll budget on that start, and timed out (the iOS smoke lane after the main merge). A capture with no live runner now stays on the single-flight discovery, one wait slice at a time, until the discovery's own deadline or the request signal ends it; a runner that is already live still takes the fallback at once, the cheaper route #2331 chose. * fix(apple): queue a speculative-runner release behind a start that is still in flight A `possible` open's prewarm registers its session only when the start completes, so a `none` open that released in that window found nothing and the runner it meant to release survived as a retained speculative session. The release now takes the runner session lock: it queues behind the in-flight start, sees the registered speculative session, and stops it; a start a command asked for is left alone. Two deferred-start regressions pin both outcomes. |
||
|
|
233a34d138 |
refactor(daemon): extract the session event journal into a workspace package (#2361)
* refactor(daemon): extract the session event journal into a workspace package `src/daemon/session-event-*.ts` (6 modules) and `src/core/keyboard-actions.ts` move as git renames into a new private package `@agent-device/session-journal`. One subpath per moved module points straight at the moved file; no `index.ts`, no re-export at any old path. Every consumer switches to the owning specifier. The journal's request-shaped inputs now name `DaemonRequest`/`DaemonResponse`/ `DaemonResponseData` from `@agent-device/kernel/contracts` instead of the daemon's own `daemon-request.ts`, which the package may not reach (R11) and which carries `internal` with its `SessionState` callbacks and admitted `DeviceLease`. The response types were already re-exports of the kernel ones, so no shape changes; the request type narrows to the four fields the journal reads. A type-level test reads every request-shaped parameter off the real signatures and asserts the reachable type graph declares no `internal` key, holds nothing shaped like a live session record or a `DeviceLease`, and carries no callback. The daemon reaches the journal only by workspace specifier now, so the code-signature walk gets the same pin the descriptor registry got: a walk stopping at the package boundary would report an unchanged signature after an entry-shape or retention-window edit, and a client would keep reusing a daemon writing the superseded journal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUhCzFDpMU92EM8o3tjRhy * chore(gates): rank the session-journal package on the layering spine R6's drift guard requires every production zone to be a deliberate ranked-or-unranked decision. `session-journal` is vocabulary the daemon reads a dispatched request through, so it takes rank 1 beside `command-registry` and `contracts` rather than the unranked kit treatment: its only ranked edges are to same-rank zones, which is not a back-edge. No `APPROVED_OVER_CEILING` row and no fallow baseline edit: rename detection carries all seven moved entries' merge-base closures, so each falls under the no-growth rule, and no baseline entry was keyed on the old paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUhCzFDpMU92EM8o3tjRhy --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51ed6217cc |
refactor(daemon): relocate the daemon client out of src/daemon (#2360)
* refactor(daemon): extract the repair-tombstone reader below store and client `findUnrecoveredRepairCommitFailure` reads session artifacts off disk and is reached from the daemon client, which had to import `session-store.ts` — the daemon's largest server module — for it. Move the tombstone shape, its file reader and the unrecovered-commit scan into `session-repair-tombstone.ts`, a leaf below both, and give the tombstone file name a single owner. No behavior change; both consumers keep their existing tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * refactor(daemon): relocate the daemon client out of src/daemon `src/daemon/client/` is the daemon's client, not the daemon: no daemon file imports it, and its consumers are the CLI, the Node client, the proxy command and the injected dispatch type. Move it to `src/daemon-client/` as renames so `src/daemon` is server code plus the shared kernel the client still needs — `config.ts`, `daemon-process.ts`, `request-progress-protocol.ts`, `daemon-request.ts` and the extracted `session-repair-tombstone.ts`. Zone name and rank are unchanged (`daemon-client`, 5); the zone now falls out of the folder instead of a `src/daemon/client/` prefix. Tests move unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * refactor(daemon): move the session artifact path helpers out of session-store `src/cli.ts` and `src/remote/remote-request-diagnostics.ts` reach into `session-store.ts` for one pure path function, `resolveRemoteRequestDiagnosticsPath`, which made every CLI process eagerly evaluate the daemon's session store and its whole subtree — the script writer, the event log, the action recorder and the replay transaction vocabulary. The four artifact path helpers name files; they hold no store state. Move them to `src/daemon/session-artifact-paths.ts`, a leaf over `session-paths.ts`, and point all ten consumers at it. `src/cli.ts`'s eager closure drops from 379 modules to 365 and no longer contains `session-store.ts`; the store itself is 464 -> 341 lines. AGENTS.md's declaration-site pointer follows. No behavior change: the helpers are unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc * chore(gates): re-key the daemon-client gate paths onto src/daemon-client Path-keyed enforcement follows the relocated files: the fallow health baseline entries, the oxlint per-file override, the wire-compat surface/ledger/mutation paths, and the layering zone derivation (the `src/daemon/client/` prefix is dead now that the folder itself names the zone). R10's external daemon request/session-state importer list gains the five client modules. The edges are unchanged by this PR — the client has always built `DaemonRequest` and read `DaemonResponse`; it sat inside `src/daemon/` and so fell under the prefix skip. Naming the files keeps the dependency enumerated and shrink-only, so a new `src/daemon-client/` module reaching `session-state` still fails. Its size assertion now reads the recorded list instead of a literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZkJjeEhLmyGpGtcwY8pqc --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
bd08e6e0f2 |
refactor(contracts): move single-owner modules out of @agent-device/contracts (#2357)
* refactor(daemon): move root-only contracts vocabulary into its owning zone Six @agent-device/contracts modules had no consumer outside the root zones, so the shared vocabulary package carried types only the daemon and root composition ever read. Each one moves to the zone that owns it and every consumer switches to the owning module; no re-export stays behind at the old contracts path. - perf-runtime-plan, snapshot-timeout-evidence, platform-resource-cleanup -> src/daemon - daemon-owner-cleanup -> src/ - interaction-error -> src/core wait-runtime-plan stays in contracts: @agent-device/command-registry consumes it, so it is not root-only after the registry package landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdXHhx9NkfH1PT4XjYe7tE * refactor(platform): move single-consumer contracts modules into their platform package Four modules in @agent-device/contracts had exactly one consuming package, so the shared vocabulary carried Android- and Apple-specific shapes no other zone could use. Each moves into the package that owns it, with every consumer switched to the owning module and no re-export left at the old contracts path. - android-helper-artifacts -> platform-android/src/helper-artifacts.ts - android-touch-plan -> platform-android/src/touch-plan-lowering.ts, which also retires the package-local touch-plan.ts re-export barrel that existed only to give the contracts module a local name - snapshot-presentation -> platform-android/src/snapshot-presentation-node.ts (renamed to keep the package's existing Android-specific snapshot-presentation.ts distinct) - apple-multitouch-support -> platform-apple/src/multitouch-support.ts APPLE_OS_DISPLAY_NAMES folds into gesture-admission.ts, its one remaining contracts caller, so both gesture refusals still share one copy of the wording without a new contracts subpath for a table its own doc calls non-public. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdXHhx9NkfH1PT4XjYe7tE * refactor(core): move the replay divergence implementation into src/core replay-divergence.ts mixed the wire vocabulary every zone reads with the sanitizing, bounding and reporting implementation only root zones call. The ten value consumers are all root (daemon replay, the session replay coordinator, the daemon client lifecycle, the replay-test reporter, the command error projection, and the MCP tool error), so the implementation moves to src/core/replay-divergence.ts and carries its test unchanged. The types stay in contracts and keep the @agent-device/contracts/divergence subpath, which packages/ad-replay and packages/selectors type-import. ReplayVarScrubEntry follows the implementation: it is the sanitizer's own parameter shape, not part of the divergence wire report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdXHhx9NkfH1PT4XjYe7tE * chore(gates): shrink the contracts export surface by the moved subpaths The nine relocated modules no longer live in @agent-device/contracts, so its exports map drops their subpaths (118 -> 109) and scripts/layering/contracts-exports.snapshot.json is regenerated from the manifest, which is what R11 package-boundaries diffs the live surface against. The two resolution assertions naming the retired snapshot-presentation and snapshot-timeout-evidence subpaths go with them; interaction, snapshot and react-native-overlay still cover both the direct-module and facade shapes the assertions were there to prove. The property tests that needed fast-check left with snapshot-presentation and replay-divergence, so the dependency moves too: contracts drops it and platform-android declares it, as fallow's unused-devDependency check reports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdXHhx9NkfH1PT4XjYe7tE --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2ec4e91b11 |
refactor(core): move the command descriptor registry into its own workspace package (#2348)
* refactor(core): move the command descriptor registry into its own package `src/core/command-descriptor/`, `src/command-catalog.ts`, `src/core/wait-positionals.ts` and `src/core/parse-timeout.ts` move as git renames into a new private package `@agent-device/command-registry` (deps: contracts, selectors). One subpath per module points straight at the moved file; no `index.ts`, no re-export at the old path. Every consumer switches to the owning specifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz * test(host-kit): pin the command-registry package inside the daemon code graph The daemon reaches the registry and its catalog only by workspace specifier. A walk that stopped at the package boundary would report an unchanged signature after a descriptor edit, and the client would keep reusing a daemon running the superseded policy. The manifest is asserted beside the sources because its `exports` map is what chose them. The cache doc comment quoting the old ~800-module graph is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz * chore(gates): point the descriptor-registry gates at the package path R66's `COMMAND_DESCRIPTOR_MODULE`, R16's record-runtime join subject and the Fallow `AssertTrue` totality-guard key follow the registry to its package. The two descriptor hubs leave `HUB_ENTRY_FILES` because the package manifest now publishes them, so the eager-closure gate discovers them as facades and one entry gets one rule; this also flips `denyPlatformImplementations` from false (hub) to true (package entry) for both, which is intentional and stricter. `command-registry` joins the ranked spine at rank 1. No `APPROVED_OVER_CEILING` row: rename detection carries every moved entry's merge-base baseline, so all twelve fall under the no-growth rule rather than a ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
dcd8b65d4c |
refactor(daemon): split src/daemon/types.ts into request types and session state (#2346)
* refactor(daemon): split daemon/types.ts into request and session-state modules `src/daemon/types.ts` served two audiences from one file: the dispatch request shape and the daemon's live session record. It also sat in the only daemon type cycle — it imported `RefFrame` from `ref-frame.ts`, which imported `SessionState` back — so neither file could be read in isolation. Three modules replace it, each importing only downward: - `daemon-request-wire.ts` declares `DaemonWireRequest`: a dispatched request with no `internal` key and no property path to `SessionState` or `DeviceLease`, so a consumer can read a request's command, flags and public metadata without depending on the session record. - `daemon-request.ts` adds the daemon-only half (`DaemonRequestInternal`, which stays unexported) plus the response vocabulary. - `session-state.ts` owns `SessionState` and the shapes only it holds. The cycle is cut by `ref-frame-slot.ts`, declared below both `ref-frame.ts` and `session-state.ts`: it owns the frame VALUE (the class stays unexported, so the type remains nominal and unconstructible from outside), while `ref-frame.ts` keeps every lifetime transition and every `session.refFrame` write. No behavior change: every importer moves to the module owning the symbol it uses, with no re-export shim at the old path. `client-normalizers.ts` takes `SessionRuntimeHints` from `@agent-device/kernel/contracts`, which declares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * test(daemon): assert the wire request shape cannot reach session state A type-level walk over `DaemonWireRequest` fails `tsc` if the shape regains an `internal` key or grows a property path back to `SessionState` or `DeviceLease`. Positive controls over `DaemonRequest` prove the walk finds both when they are there, so a walk that never matches anything cannot pass by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * test(daemon): keep the three over-budget test files at their base length Splitting `daemon/types.ts` turns one combined import into two in every file that used both halves. Three of those test files are already over the 1,000-line tripwire, where the size ratchet allows no growth, so each sheds one line that was carrying nothing: - `snapshot-handler.test.ts` and `find.test.ts` each drop a `toHaveLength` assertion an adjacent `toEqual` on an explicit array literal already makes. - `session-replay-repair-transaction.test.ts` names the filtered close actions instead of wrapping the expression across three lines inside `expect`. No assertion is weakened and no test content is removed. Splitting these files along the modules they mirror is the standing remedy, but none of those modules split here, so it stays out of this change and is tracked in #2353. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y * chore(gates): point the daemon modularity and wire-compat gates at the split modules R7 now locates the `SessionState` declaration by the declaration itself rather than by a recorded path: `sessionStateWritePressure` measures the merge-base tree too, and that tree still declares it in `daemon/types.ts` — a path constant would measure it as zero pressure and bank the headroom. R10's external-importer ratchet covers all three modules that replaced `daemon/types.ts`, so moving a symbol between them cannot reopen the boundary to a new outside zone. The recorded membership is unchanged: `client-normalizers.ts` and `remote/daemon-artifacts.ts` both import `daemon-request.ts` only. The daemon RPC closure gate waives `DaemonRequest`, `DaemonResponse` and `DaemonArtifact` by path, so those three keys follow the declarations to `daemon-request.ts`. `DaemonRequest`'s rationale now says what it is — the server-side narrowing of the kernel declaration that fixes the wire shape — rather than calling it a re-export alias. The `live-state-shape` and session-resource declaration sites move with `SessionState`; the depgraph lookalike fixture takes a new plausible path now that `daemon/session-state.ts` is the real root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b4ebd778cc |
refactor(daemon): move four pure leaves to their kits (#2347)
* refactor(selectors): own the parameterized recorded fill leaf `parameterized-recorded-fill.ts` has no value dependency on the daemon: it reads a `TargetAnnotationV1` type from contracts and calls `selectorContainsValue`, so its whole value graph already sits inside `@agent-device/selectors`. Move it there behind its own subpath and let the two daemon consumers reach it by specifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * refactor(host-kit): own the daemon code signature leaves `code-signature.ts` fingerprints a checkout from `node:crypto`/`fs`/`path` and `findProjectRoot`; `code-signature-cache.ts` adds a stat-validated cache over it through `publishFileSync`. Neither reaches the daemon, and both questions — what does this source tree hash to, and can that hash be replayed from stat alone — are host mechanics. Move both into host-kit behind their own subpaths, carrying `code-signature-cache.test.ts` unchanged apart from its specifiers, and let the launch spec and server lifecycle reach them by specifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * refactor(capture-kit): own the screenshot overlay cluster `screenshot-overlay.ts` decides which snapshot nodes earn a ref and where the ref lands on a screenshot; `screenshot-overlay-draw.ts` paints them. Both read kernel snapshot vocabulary, contracts snapshot predicates, and capture-kit's own PNG and rect-projection mechanics — nothing from the daemon. The two `src/snapshot/screenshot-overlay/` helpers had no other importer, and `react-native-overlay.ts` sits on kernel plus its contracts vocabulary alone. Move the cluster into capture-kit as flat siblings of the PNG and projection modules it already used, exposing `./screenshot-overlay` and `./react-native-overlay`; the draw, rects, and android halves stay package internals with no subpath of their own. The moved tests carry over unchanged apart from their specifiers, over a package-local snapshot-state fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * refactor(capture-kit): own the post-gesture stability loop `post-gesture-stability.ts` polls a caller-supplied snapshot function until a surface settles. It is generic over its snapshot and signature types and reads only host-kit diagnostics and `sleep`, so the loop is capture mechanics with no daemon knowledge; the daemon keeps the pending record, the comparator, and the verdict wiring it hands in. The verdict test stays in `src/daemon` because it composes the loop with the daemon's own `interaction-outcome-policy.ts`; only its specifier changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * chore(gates): pin the four leaf subpaths in the R11 export lists R11 pins every workspace package's exact subpath set, so the four moves need their new specifiers named: `@agent-device/selectors/parameterized-recorded-fill`, `@agent-device/host-kit/code-signature{,-cache}`, and `@agent-device/capture-kit/{screenshot-overlay,react-native-overlay,post-gesture-stability}`. The selectors comment counted its subpaths in prose; it now counts four and says what the fourth is. No eager-closure row is needed: every new entry is a rename the merge-base reader follows, and each closure is unchanged (56/5/19/32/4/8), so all six fall under no-growth rather than the new-entry ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * refactor(capture-kit): drop the needless duplication suppression The `fallow-ignore-next-line code-duplication` on the package-local snapshot-state fixture suppressed nothing: `fallow dupes` reports three clone groups on this tree and the fixture is in none of them, with or without the comment. A suppression that matches no finding is dead weight at best and a stale-suppression failure at worst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK * fix(host-kit): follow workspace subpaths when fingerprinting daemon source `walkDaemonCodeGraph` followed relative specifiers only, so a source checkout's signature covered whatever the daemon still imported by relative path. That was already lossy and the leaf moves made it wrong: the walker itself, the overlay, the recorded-fill and the stability loop all left the graph, so editing them no longer changed the signature a client compares a running daemon against, and the cache's format guard lost the "the walk invalidates every document" property its comment rests on. Measured from `src/daemon.ts`: 619 modules on main with the walker stamped, 611 after the moves with it gone. Resolve a scoped specifier through the owning workspace package's `exports` map and walk into the file it names. The manifest is stamped, not merely probed, so an `exports` retarget invalidates without either endpoint changing; an uninstalled package is recorded as an absent path. Installed dependencies are still not followed — they change on install, not on edit — and the test is structural rather than a name pattern. The graph is now 1459 modules and ~112ms cold, which is what the stat-validated cache exists to absorb. Regression: five of the six new walker tests fail against the previous walker, including one that stamps the real daemon graph and asserts the walker is in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPFjVXwbDrXPqp5W6K2iDK --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7bea29d61e |
fix: repair main after the managed-allocation move landed under stale PRs (#2328)
`main` has been red since #2308. That PR was authored before #2316 moved managed-device allocation into `@agent-device/managed-allocation`, so the daemon files it added still import pre-move sibling paths that no longer exist: src/daemon/managed-device-allocation/lease-admission.ts(19,8): error TS2307: Cannot find module './record-validation.ts' src/daemon/managed-device-allocation/__tests__/lease-admission.fixtures.ts(10,61): error TS2307: Cannot find module './fixtures.ts' Typecheck, Repo Guards, and the two managed provider-integration suites all fail on it, which makes every open PR red. - `lease-admission.ts` now reaches the record validators through the package's `./record` surface, which re-exports them. The `TS2322` at line 70 was a consequence of the unresolved import, not a separate defect: with the module resolved, `isVerbatimId` narrows `identityIncarnationId` again. - The daemon-side grant fixtures come back under `src/daemon`, stated in contract terms only. Both trees keeping their own test data is the shape #2316 already chose for `managed-device-allocator.fixtures.ts`. - The root now consumes `@agent-device/managed-allocation`, so its `ignoreDependencies` entry — whose comment said "no root consumer yet" — goes. Separately, the eager-closure ratchet was failing on a stale approval row: the merge-base now carries `packages/capture-kit/src/durable-capture/index.ts`, so nothing can read its `APPROVED_OVER_CEILING` row and the table's own staleness rule fails it. Removed, exactly as the rule prescribes. |
||
|
|
6e22e266d7 |
refactor(contracts): own the daemon HTTP wire contract so clients stop importing src/daemon (#2322)
* refactor(contracts): own the daemon HTTP wire contract so clients stop importing src/daemon Move the pure wire vocabulary (base path, header names, URL/auth/tenant builders, /health payload) from src/daemon into @agent-device/contracts as the daemon-http subpath, so src/remote and src/cli stop importing daemon server internals. buildDaemonHealthPayload takes the version its caller advertises (R18 keeps host mechanics out of contracts); both callers pass readVersion(). Wire-compat surface, mutation, and ledger references follow the package path. * chore(gates): pin the moved daemon HTTP wire surface and teach the released-baseline check file moves Exports map + snapshot gain the daemon-http subpath. The wire ledger re-keys the eight moved declarations (buildDaemonHealthPayload moves with its new caller-supplied version parameter, acked additive). The released-baseline comparison now classifies a baseline declaration that re-appears unchanged at exactly one new path as a move instead of a removal: a file move is not wire surface a released peer stopped sending. A move that changes shape is a change acked at the destination path, and a name still owned by the baseline stays a removal. |
||
|
|
ebdaa7617e |
feat: delegate reviewed managed automation (#2312)
* feat: delegate reviewed automation through managed lease authority * fix: preserve lazy simulator readiness through scoped authority * fix: admit managed operations at their dispatch boundary * test: move managed automation scenarios to integration lane * chore(gates): declare the private managed readiness scope export |
||
|
|
bcb6c55b7f |
refactor(capture-kit): move durable-capture resource mechanics out of the daemon (#2320)
* refactor(daemon): give durable capture a session-store port and a cleanup report The durable-capture mechanics reached two daemon-owned authorities directly: the concrete `SessionStore` class plus `SessionState`, and the admission ledger, which `recoverFailedAdoption` called to block or clear a replacement start. Both are daemon policy, so neither can travel with the mechanics. Replace them with a two-member `DurableCaptureSessionStore<S>` port and a session type parameter, and let the mechanics report what they observed — `DurableCaptureCleanupOutcome` — while `createDurableCaptureResource` keeps the clear/block decision and the reason text. Recovery takes the session directory resolver from its caller instead of importing `safeSessionName`. Splitting `DurableCaptureRecordDefinition` out of the definition says which half needs a session at all: recovery, finish-recovered, and start preflight terminalize a persisted record with no session in hand. * refactor(capture-kit): move durable-capture resource mechanics out of the daemon The daemon held two halves of one mechanism. capture-kit already owned the durable-resource envelope, JSON, and descriptor codec; the fence, transition, adoption, and recovery mechanics that operate on that envelope still sat in `src/daemon` as eight files. Move them behind the store port and cleanup report the previous commit introduced, exposed through one new `@agent-device/capture-kit/durable-capture` subpath — not the `.` index, which is the eager closure every platform runtime imports. Admission, start preflight, runtime binding, the kind stamps, and the composition root that wires the mechanics to the admission ledger stay daemon policy. The moved tests exercise the mechanics through a resource kind and session type of their own, so what they prove is that the mechanics need neither the daemon's closed kind set nor `SessionState`. The composition root keeps the admission mapping the adoption test used to assert, now in `durable-capture-resource.test.ts` where the ledger lives. * refactor(capture-kit): drop the now-dead durable-envelope decoder re-export The daemon's store and adoption modules were the `.` index's only production consumers of `decodeDurableResourceEnvelope`; both now sit beside the encoder inside capture-kit and import it directly. * chore(gates): approve the durable-capture subpath over the domain-facade ceiling * refactor(daemon): merge the duplicated durable-capture subpath imports * refactor(capture-kit): keep the durable-capture subpath to its consumed surface `tsc -b` cannot name the fixture spy's inferred type across the package boundary, and five re-exported vocabulary types had no consumer. * style(capture-kit): keep package specifiers ahead of relative imports * test(daemon): make the failed-adoption clear mapping effective The confirmed-cleanup test started with an unblocked ledger, so deleting `clearUndurableCleanup` from the relocated composition mapping still left `assertStartAllowed` green. Seed a block first, so the assertion is that the mapping lifted it. |
||
|
|
e0f8c55f6e |
refactor(move): move managed device allocation into its own workspace package (#2316) (#2321)
* refactor(move): move managed device allocation into its own workspace package (#2316) * chore(gates): unrank the managed-allocation zone, declare its durable-json seam, and ignore its unconsumed root dependency (#2316) |
||
|
|
4b7c561d1e |
refactor(commands): author each executable command once in its facet (#2313)
* refactor(commands): let the facet derive its executable definition * refactor(commands): author interaction commands once * refactor(commands): author system commands once * refactor(commands): author capture commands once * refactor(commands): author management commands once * refactor(commands): author observability commands once * refactor(commands): author recording commands once * refactor(commands): author replay commands once * refactor(commands): author debugging commands once * refactor(commands): author react-native commands once * refactor(commands): author metro commands once * refactor(commands): author batch commands once * refactor(commands): author perf commands once * refactor(commands): state the facet type docs in one line each * chore(gates): scan facets, not executables, for provider coverage The integration-progress scanner mapped client calls to command names by finding defineExecutableCommand blocks. Those are gone; a facet's run callback now holds the call, and its name may be a module constant. |
||
|
|
0c8227e9b7 |
refactor(runtime): let platform runtimes list apps and read app state directly (#2295)
* refactor(runtime): let platform runtimes list apps and read app state directly
The root host carried two adapters, appInventory and appState, that only
forwarded a platform call back into that platform's own package. Each platform
runtime now performs its own listApps and appState call through a lazy import
inside its package, keeping the deferred load, the AbortSignal threading, and
the package/bundleId -> id rename. PlatformRuntimeHost loses both keys, so
Android, Apple and Harmony fixtures no longer stub the two platforms they do
not own.
Android is the one platform runtime whose package now reaches adb directly.
The adb host that adb mechanics require is bound by a module side effect that
only the root can perform, so the Android runtime-module registration binds it
before the module loads. loadAndroidMechanics keeps its own binding import for
the root host ports that reach mechanics without binding a runtime; neither
binder subsumes the other.
Android appstate now runs one foreground-focus loop instead of two. The host
shaped readAndroidAppState/AndroidAppStateHost pair is gone: limrun's adapter
already closes over its own adb executor, so it calls the executor variant
directly, and that variant took the per-attempt abort check the host variant
had. AppStateRuntimeCommand and AppStateRuntimeCommandResult described the
deleted host port and go with it.
Tests: the new ordering test in
src/platform-runtime-android-adb-binding.test.ts was seen red by deleting the
binding import from that registration (order came back
["android-runtime", "adb-host"]); the composed-gateway listApps test in the
same file was seen red by reverting the Android runtime's inlined listApps to a
host.appInventory lookup (TypeError reading 'android'); the new abort test in
packages/platform-android/src/app-state.test.ts was seen red by removing both
signal?.throwIfAborted() calls from readAndroidFocusWithExecutor (the second
dumpsys was issued and the call resolved). All green after.
* chore(gates): drop the retired app-inventory/app-state host allowances
The two PLATFORM_RUNTIME_HOST_FILES rows point at host files this change
deletes, and the ./platform-runtime-app-state-host.ts composition allowance has
no importer left.
* refactor(runtime): construct the Android runtime module with its adb host binding
The Android runtime now calls adb from inside its package for listApps and
appState, which needs the process-wide adb host port bound. That dependency
was hidden in a registry wrapper doing a side-effect import, with a paragraph
explaining why it and loadAndroidMechanics did not subsume each other and an
import-order test pinning the ordering. The package now declares the
dependency: createAndroidRuntimeModule({ bindAdbHost }) awaits the binding
before the runtime loads, and the composition root supplies the one binding
implementation (evaluating its adb host module). The wrapper, the paragraph
and the import-order test are gone; the routed listApps test stays and a
routed appState test joins it.
|
||
|
|
ba6c818d81 |
spike(daemon): give the ADR-0014 ref frame private ownership (#2296)
* refactor(daemon): make the ADR 0014 ref frame one owned value
The four `refFrame*` fields on `SessionState` were policed only by the R7
ownership table: any daemon module could write them, and only a full-graph AST
scan could say whose write it was. They are now one `RefFrame` value whose brand
key is private to `src/daemon/ref-frame.ts`, so a module outside that file cannot
construct one and cannot edit the one a session holds; the transitions replace it
whole. Every transition, rejection reason and epoch rule is unchanged.
Readers moved to the accessors ref-frame.ts exports (`refFrameState`,
`refFrameScope`, `refFrameEpoch`, plus a new `refFrameTree` and `refFrame`).
`internal-observation.ts` drops its four-field lineage copy and its field-by-field
comparison: frame identity is now one `===`.
Seen red: with the empty-result early return removed from
`markSessionPartialRefsIssued`, the new frame-identity assertion in
session-snapshot.test.ts fails; restored, it passes. A planted foreign writer
module was rejected by tsc (TS2741 missing brand, TS2540 read-only property)
before deletion.
* docs(depgraph): note the ref frame outgrew its R7 row
* refactor(daemon): make the ref frame nominal, not symbol-branded
A symbol brand on a plain object type stops construction from nothing, but not
`{ ...refFrame(session), state: 'active' }`: object spread copies the symbol key,
so any daemon module could mint an incoherent frame (active state, stale tree)
out of a coherent one and it type-checked. Proven before the fix with a throwaway
module doing exactly that write: tsc reported nothing.
The frame is now a class with `#`-private fields behind getters. That makes the
type nominal, so no object literal is assignable to it — the same probe now fails
with TS2739 (`missing #fields, scope, generation, expired`). Construction stays
inside ref-frame.ts, and the four claim sites (ADR 0014, the SessionState field
doc, and the two in the R7 owner table) now say what the type does and does not
judge: it cannot see a whole frame moved unchanged, which is why the R7 row stays.
Expiry is idempotent by identity again. `expired()` returns THIS frame when the
frame is already expired, rather than an equal copy, which is what the lineage
check in internal-observation.ts compares with `===`. Seen red: with that early
return removed, the tightened ref-frame test fails with "Values have same
structure but are not reference-equal"; green with it.
Also: the ADR 0014 stale-ref help sample seeds its epoch through a real frame
activation again, instead of leaning on the pre-frame snapshotGeneration
fallback, and a find test drops a `?? []` that can no longer be reached.
Behavior is unchanged: same frame contents, same transitions, same admission.
* chore(gates): collapse the four ADR 0014 R7 rows into the owned refFrame value
R7's owner table listed `refFrameState`, `refFrameScope`, `refFrameTree` and
`refFrameGeneration` as four fields that had to be written together by one
module; the code now carries them as one nominal value, so the table carries one
row. R10 follows: 19 writer-owned fields to 16, 22 owner claims to 19.
The row itself stays. The type stops construction, editing and spread-derivation
of a frame outside ref-frame.ts, but it cannot judge a whole frame moved
unchanged — clearing the field, or assigning another session's frame — and the
table can. The comments say that rather than claiming full enforcement.
Seen red: a planted `session.refFrame = undefined` in snapshot-session.ts fails
R7 with "owned by src/daemon/ref-frame.ts"; green once reverted.
* style: apply oxfmt
* refactor(daemon): keep ref-frame expiry module-private
`RefFrame` exposed a public `expired()` method, so any module holding a
frame could derive a new valid one and install it through a reconstructed
session record, past the R7 field scan. Expiry is now a static on the
unexported class, reachable only inside ref-frame.ts; the frame's surface is
four getters. A type-level regression pins that no outside module can
construct, spread, edit, or derive a frame (tsc covers src tests, so a
directive that stops erroring fails typecheck).
* test(daemon): hold the three accessor migrations within the size ratchet
Each file grew by exactly its new ref-frame import; one blank line between
mock blocks goes so the files stay at their merge-base length.
|
||
|
|
96727a0b42 |
fix(apple-runner): never compare an unavailable toolchain probe; name the mismatching cache keys (#2306)
* fix(apple-runner): never compare an unavailable toolchain probe A timed-out or failed `xcodebuild -version` / `xcrun --show-sdk-*` probe used to fall back to the literal `unknown`, which was memoized for the process and then persisted into the rebuilt cache's metadata, so every later daemon on a healthy host mismatched again and paid a full build-for-testing. Unavailability is now a distinct outcome with no comparable value: only successful probes are memoized, an unreadable toolchain fails the cache decision with a retriable typed error naming the probe that could not answer, and the CI metadata writer refuses to persist a probe it could not read. The cache_metadata_mismatch diagnostic now lists the differing keys with expected and actual values instead of only saying the metadata differed. The runner-source fingerprint moves to the module that owns the runner's source roots, keeping the cache-metadata module within its size budget without adding a module to the Apple facades' eager closure. * refactor(apple-runner): home the fingerprint tests and the rebuild-decision glue Tests mirror source topology: the runner-source fingerprint tests move with the function into runner-source.test.ts and call it directly instead of reaching it through resolveExpectedRunnerCacheMetadata. The rebuild diagnostic's mismatch details move next to the cache state that carries them, so runner-artifact.ts — already past the 500-line extract threshold — gains no behavior. * fix(apple-runner): memoize only a parsed toolchain fingerprint runToolchainProbe cached every nonempty zero-exit answer before parseXcodeVersionOutput could classify it, so a transient malformed xcodebuild answer stayed cached and every later cache decision in the process kept failing after the host recovered. The memo now holds the complete parsed fingerprint per SDK, written only after all three probes answered and parsed; a failed round keeps nothing, so the next request re-probes. Tests cover malformed-to-healthy recovery in one process without resetting the memo, and that a partial round is not kept. * style(apple-runner): oxfmt the cache-metadata module and its tests |
||
|
|
7a2d48d160 |
perf: bundle runtime dependencies and report full install size (#2310)
* perf: bundle runtime dependencies and report full install size * refactor: remove unused size report breakdowns |