mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
v0.21.0
1829 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d2caab7edf | 0.21.0 v0.21.0 | ||
|
|
31b01a2fe8 |
docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (#2355)
* docs(adr): record the #2278 coupling audit and re-trace entry-to-platform hops (ADR 0022) * docs(adr): re-trace hop tables with spine side-calls excluded from hop counts * docs(adr): renumber R74 references to R76 after the main merge |
||
|
|
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) |
||
|
|
257b3e10c2 | test(capture): shrink the 3x crop fixture to avoid coverage timeouts (#2399) | ||
|
|
07c4e404b2 |
perf(ios): settle the first interaction on a deadline, not a fixed sleep (#2395)
The runner guarantees a 250 ms gap between activating a target and the first interaction on it. It implemented that as a 250 ms sleep taken *at* the interaction, so the gap was charged again however much of it had already passed — and in an agent-driven flow the client round trip alone is 190-260 ms, so almost all of it had. Record when the window closes on the monotonic uptime clock at each of the three activation points, and wait only for what is left of it. A caller that already spent the window waits for nothing; a tight loop still gets the whole guard. The guarantee is unchanged, and nothing downstream re-pays the wait. Refs #2381. |
||
|
|
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. |
||
|
|
be622491f6 | fix(conformance): isolate post-tap settling from app launch (#2390) | ||
|
|
13a45b9632 |
test(remote): assert the proxied snapshot's whole wire conversation (#2387)
#2198 required proof that "the bridge adds no network round trip and transfers only the published response". That was read off the RTT benchmark — identical response bytes, unchanged wall-clock slope — which is inference, not proof, and the raw-result schema carries no round-trip count. Assert it instead. Record every request the proxy forwards upstream while one `snapshot -i` runs, and pin the whole conversation rather than just the RPC, so a new call of any kind fails. The bridge contributes nothing to it: no helper, admin or acquisition route appears. Then read what crossed rather than sizing it, at every level of the payload — envelope, result, published data — each against its declared key set. Comparing the wire's result against the client's response proves nothing on its own, since the client publishes whatever the result holds and both sides move together. Refs #2198. |
||
|
|
bfec821dab |
fix(daemon): stop an attested tenant from downgrading its own isolation (#2386)
When the auth hook attests a tenant, isolation is the server's answer. The RPC handler was defaulting to `'tenant'` rather than deciding it, so a client that sent `sessionIsolation: 'none'` kept it: `scopeRequestSession` then returned early, the session never got its `<tenant>:` prefix, and the request landed in the `cwd:<hash>:` namespace instead — one the client names and another tenant can name too. An attested tenant now always gets tenant isolation, on both carriers, so a downstream consumer that reads the flag cannot see an isolation the meta just overrode. Stacked on #2382, which threads the attested/declared distinction this uses. |
||
|
|
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.evidence/ios-snapshot/6a03688d80 |
||
|
|
527a56a6e7 |
refactor(move): move the batch runner and batch policy into @agent-device/command-registry (#2388)
* refactor(command-registry): move the batch runner and batch policy into @agent-device/command-registry * chore(gates): re-point the sdk-batch chunk groups and fallow baseline at the command-registry batch module |
||
|
|
367e795ee7 |
fix(remote): let a plain-session client read its own failure record (#2382)
`GET /sessions/<session>/requests/<requestId>/diagnostics` applied the `<tenant>:` prefix rule to every caller carrying a tenant, but `scopeRequestSession` only writes that prefix under tenant isolation — which the daemon forces exactly when the auth hook ATTESTS the tenant. A client whose tenant is only declared (the `x-agent-device-tenant` header on a daemon with no auth hook) therefore ran in a plain session such as `default` or `cwd:<hash>:default` and was then refused 401 reading the record its own failed command wrote, directly and through `agent-device proxy`. The addressability rule now lives beside the naming rule in `session-tenant-scope.ts`, which exists so the two cannot disagree. `isTenantAddressableSessionName` takes the caller's session namespace and applies the prefix rule only where the namespace is actually partitioned; `resolveTrustedTenant` now reports whether the tenant was attested, and `authorizeAuxiliaryHttpRequest` hands that namespace to the route. The attested case is unchanged: an attested tenant is still refused any session outside its own prefix, with the same typed UNAUTHORIZED error. |
||
|
|
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> |
||
|
|
65ff27000b | perf(cli): keep scroll vocabulary off gesture runtime (#2378) | ||
|
|
e90206d64e |
chore: drop the stale planned-operations closure approval (#2373)
The APPROVED_OVER_CEILING row for packages/command-registry/src/planned-operations.ts was consumed by #2329. Now that main carries the entry, the no-growth rule governs it and the row can no longer change any verdict, so the "no row is stale" probe fails on main and on every PR based on it. Remove the row, as the probe instructs. |
||
|
|
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> |
||
|
|
8299d5b4a7 |
fix(ios): honor the startup budget through a cold Simulator boot (#2325)
* fix(ios): honor the startup budget through a cold Simulator boot A never-booted Simulator runs Apple's first-boot migration, which can take minutes, but the boot wait was capped at a fixed 120 seconds that neither `prepare --timeout` nor `open` could reach (#2324). - The boot wait takes an absolute deadline. `prepare --timeout` now covers the boot and the runner preparation as one budget; `open --timeout` is new and bounds the boot. Expiry fails with `boot_timeout` and leaves the Simulator booting, so a retry finds it further along. - The client envelope for open/prepare keeps the 30s margin over the budget so the daemon's structured timeout wins the race against the client's reset. - `close --shutdown` no longer trusts the session device's selection-time `booted: false`; it always asks simctl. A session opened on a cold Simulator otherwise reported a shutdown that never happened. Supersedes the original implementation of #2325 by @PrinceD96 (head 8bdb85b7a3), which found the bug, the shutdown shortcut, and the validation recipe. Closes #2324 Co-authored-by: PrinceD96 <53633741+PrinceD96@users.noreply.github.com> * fix(ios): keep the confirming boot listing inside the startup budget After bootstatus, the listing that confirms the Booted state ran on its own 15-second timeout and the success path never re-checked the deadline, so a bootstatus that used nearly the whole budget could still return success past it. The listing now gets the remaining budget (capped at its own 15s), and a confirmation that lands after the deadline is reported as boot_timeout. --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
e7a5b8b622 | fix(capture): export durable resource record type (#2365) | ||
|
|
cc9fd725a1 |
test(android-e2e): read the logcat tail for rotation evidence (#2359)
The first CI failure with the evidence hook (PR #2356, run 34029660070) lost its logcat section: dumping the emulator's whole 2MB buffer took longer than the 5s per-probe bound on the loaded host. The probe now reads the last 4000 lines, which holds the rotation decisions of the last minutes and returns well inside the bound. |
||
|
|
64b7cc45d4 |
fix(android): return from orientation once the display reports the rotation (#2356)
`orientation` wrote accelerometer_rotation and user_rotation and returned at once, while the display rotated some time later. On the loaded CI emulator that takes seconds, and accessibility reads hang meanwhile: the Android smoke's `wait text landscape` right after `orientation landscape-left` got a helper request timeout and then no readable capture for its whole 10s budget, with the failed-step snapshot taken afterwards already in landscape (PR #2344, run 34025424834). The command now polls `dumpsys display` for mCurrentOrientation to match the requested rotation before returning, each probe bounded by what is left of the 15s settle budget so a stuck probe ends the settle as a failure. A display that never gets there fails the command with the observed rotation instead of reporting success; a display that reports no rotation at all is left to the setting as before. The provider scenario scripts the display read against the last user_rotation write. |
||
|
|
7bcbf1350b |
feat(remote): proxy parity for Simulator observation (#2198 slice B) (#2351)
* feat(remote): give a proxied client the daemon's own failure envelope, cancellation, and version check #2198 slice B. Direct-daemon and proxy execution over the same deterministic Simulator fixture now publish the same responses, and the three places where they did not are closed: - A client that disconnects mid-request behind the proxy now cancels the daemon request. The proxy's upstream fetch is bound to its client's connection, so the daemon's own disconnect cancellation (`markRequestCanceled`) fires exactly as it does for a direct client. - The proxy forwards `GET /sessions/<session>/requests/<id>/diagnostics` (#1801), so a remote client behind it localizes a failure's diagnostics record instead of reporting `logPathUnavailable: HTTP 404`. GET only; the route still enumerates nothing. - The client's ADR 0006 health check reads the `upstream` link a proxy's /health already nests: a proxy whose daemon speaks another RPC protocol fails at health, before the command RPC. The provider scenario harness exposes its request boundary so a scenario daemon can sit behind a real HTTP server and proxy; the new parity suite runs one script direct and proxied and compares the published responses with transport identity removed, and proves two proxied clients contending for one device fail at claim admission before any lifecycle call. * test(remote): a proxied lease that expires tears its session down and a reacquired lease starts clean #2198 acceptance: lease expiry, session cleanup, and device reacquisition do not reuse prior capture or comparison state. The parity world takes a clock-driven LeaseRegistry; a leased session behind the proxy captures a diff baseline, its lease lapses past the proxy TTL, the next request is refused as UNAUTHORIZED/LEASE_NOT_FOUND with the session torn down, and a freshly allocated lease reopens and reports baselineInitialized on its first diff. * test(remote): a proxied lease heartbeat renews the lease and keeps the session's comparison state #2198 acceptance: lease heartbeat through the proxy. A leased session behind the proxy captures a diff baseline; an explicit lease_heartbeat RPC just before the lease's reported expiry moves the expiry forward; a request past the old expiry but inside the renewed window still finds the session and reports the baseline. Red without the renewing heartbeat (LEASE_NOT_FOUND). * test(remote): follow the daemon client and session artifact path moves |
||
|
|
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. |
||
|
|
5ba4ac707e | test(android): reveal smoke canaries by visibility (#2369) | ||
|
|
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> |
||
|
|
89c7536850 |
feat(ios): support explicit iOS simulator keychain reset (#2345)
* feat(ios): support explicit iOS simulator keychain reset `settings clear-app-state` never touched keychain-backed credentials (e.g. Firebase auth), so a customer's fresh-install reset via the CLI left an app signed in when their in-app reset button did not (#2282). simctl exposes no per-app keychain reset, only a whole-simulator one (`simctl keychain <device> reset`), so this ships as a separate, explicit `settings reset-keychain clear` command rather than folding it into `clear-app-state` — callers opt in knowing the scope is the whole simulator, not just the app under test. Split the pre-existing `apps.test.ts` and `snapshot-handler.test.ts` suites along the `app-settings.ts`/`snapshot-settings.ts` modules they actually mirror, since both were already over the test-file-size tripwire and could not grow further. * fix(ios): reject extra reset-keychain arguments and add live-tested keychain fixture settings reset-keychain clear <extra-arg> silently dropped the extra argument in both the CLI reader and the direct-daemon parser, so a caller expecting per-app scoping could get a whole-simulator wipe without any signal something was off. Reject it instead in both places, with tests proving no settings mutation happens. Also add a small keychain-backed "auth" fixture to the test-app's automation lab (expo-secure-store) so the settings reset-keychain guarantee has a real regression surface: authenticate, verify the credential survives clear-app-state and a plain relaunch, then verify reset-keychain actually clears it. Validated live against a disposable iOS simulator. * fix(ci): stop a bare gradle.properties append from corrupting the last line expo prebuild's generated android/gradle.properties has no trailing newline, so `echo "org.gradle.jvmargs=-Xmx4g" >> gradle.properties` appended directly onto its last line instead of a new one, producing expo.inlineModules.watchedDirectories=[]org.gradle.jvmargs=-Xmx4g. Gradle's JSON.parse of that property then fails at configure time, before any real compilation runs -- the exact "Process 'command 'node'' finished with non-zero exit value 1" failure this branch hit on Android Release and the Smoke Tests fixture-app fallback build. This was a dormant bug: the Android build-cache job only runs on a fingerprint miss, and no PR had changed the test-app's native dependencies in a while. Adding expo-secure-store (#2282's keychain fixture) was enough to trigger it. Reproduced locally against a clean install with the exact CI script, confirmed the corrupted property, and confirmed the printf-based fix builds cleanly (870/870 tasks). |
||
|
|
a78b6bb9a3 |
feat(wait): carry a per-poll timeline in timeout failures (#2344)
* feat(wait): carry a per-poll timeline in timeout failures A wait timeout said `reason`, `readableCaptures`, and `waitedMs`, so a failure could not say where its budget went: the runs behind #2343 spent a 10s budget on one poll (5.8s runner findText on a fresh app, 3.4s of target discovery, a fallback cancelled at the deadline) and reported the same `wait_capture_stalled` as a dead runner. The failure details now carry `captures` and `polls[]`, one entry per poll with `startedMs` on the wait's own clock, `durationMs`, and a typed outcome (readable, unreadable, deadline, runner-restart), next to the unchanged reason and the request-log link. Long waits keep the first five and last twenty-five polls so the response stays compact. * docs(wait): name the polling timeout paths that carry the poll timeline Review follow-up: wait --stable uses its own error builder and a never-readable strict absence preserves its predicate failure, so the timeline is documented for the polling timeout paths that emit it. * feat(wait): carry the poll evidence on the replay landmark-mismatch refusal Review follow-up. A replayed selector wait refused for a recorded landmark mismatch threw without the captures/polls evidence, and when its final poll ended in a runner restart the refusal hid that outcome. The refusal now carries the same failure evidence a timeout does, next to its mismatch details; two regressions cover a mismatch followed by a deadline-cancelled capture and by a runner restart. Docs and changelog name the refusal alongside the polling timeout paths. |
||
|
|
ff59309415 |
test(android-e2e): record rotation state and logcat rotation decisions on a failed step (#2350)
* test(android-e2e): record rotation state and logcat rotation decisions on a failed step The Android smoke has failed on the post-alert canary since 2026-09-03, and the failed-step screenshot from run 34021894996 shows why the reads miss: the device is in landscape at that point, with the canary below the fold, although `orientation portrait` had taken effect (the fixture confirmed it and every tap before the alert landed at x=540). Nothing we keep says what rotated it. A failed step now also writes failed-step-N-device.txt with the two rotation settings, the display's rotation lines, and WindowManager's rotation decisions from logcat, read through adb so they stand even when the CLI path failed. * test(android-e2e): keep the rotation evidence to WindowManager decisions and display rotation fields * test(e2e): own failed-step evidence in one collector, bound the device probes, test it Review follow-up on the rotation evidence. The collectors move out of the harness closure into failed-step-evidence.ts (fallow complexity), where the platform hook runs alongside the screenshot and snapshot and is bounded as a group (15s) so it can never delay them; a hook that throws, answers nothing, or never answers records nothing for the device file and leaves the CLI evidence in place. The Android probes get a 5s per-command bound, and logcat lines are capped in count and length. Deterministic tests cover the file contents, the hook failure and timeout cases, and the harness naming every evidence file, device file included, in failed-step.txt. |
||
|
|
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> |
||
|
|
835af32577 |
fix(ios): stop charging every capture for a slow Simulator app discovery (#2331)
* fix(ios): stop charging every capture for a slow Simulator app discovery Since #2279 every eligible Simulator capture resolves its AX-bridge target first, and a cache miss spawns `simctl launchctl list` through xcrun with a 3s timeout on the capture's own critical path. On a loaded CI host that spawn takes longer than 3s, the timeout is not remembered, and the next capture pays it again. A `wait` issued right after `open` lost its whole 10s budget that way (runner findText on the fresh app, then the probe timeout, then a fallback capture cancelled at the deadline) and reported `wait_capture_stalled` with the app already on screen: four iOS Smoke failures on main and PRs since 2026-09-05 21:00, none before. Discovery is now single-flight and detached from the capture that starts it: a capture waits a bounded 1.5s for it, then takes the XCTest fallback while the probe keeps running with its own 15s budget; later captures join the in-flight probe or reuse its result. A responsive host still reaches the bridge on the first capture. The resolver's error names its reason so the route diagnostic says why the fallback ran. * fix(ios): one deadline per discovery, exact claims, route-level regression Review follow-up. The 15s budget was per subprocess (two simctl probes, then a 3s ps read); discovery now shares one deadline across all three. The comments and changelog no longer claim "once per app generation": single-flight removes duplicate discoveries, each capture still waits up to its 1.5s slice for the in-flight one, and a failed discovery is forgotten so the next capture starts another. A route-level test runs the production resolver over a simctl that answers only when released: the first capture falls back within the slice, the released discovery then serves the bridge without a second spawn. * fix(ios): keep the discovery slot until both probes settle Review follow-up. Promise.all rejected as soon as either probe failed and the finally then released the single-flight entry while the other probe was still running, so a failed `simctl list devices` next to a slow `launchctl list` let every later capture start its own launch-job probe. Both probes now settle before the discovery does; the regression test runs three captures against exactly that partial failure and sees one spawn, then the settled discovery reports the runtime failure and frees the slot. |
||
|
|
6768a045a2 |
refactor(commands): move commands-side rendering out of src/daemon and retire the doctor progress flag (#2349)
* refactor(core): move doctor line formatting and status markers below their callers `formatDoctorCheckSummaryLine`, `formatDoctorCheckDetailLines`, and `formatCliStatusMarker` are shared by the daemon doctor handlers, the CLI replay-test reporter, and the doctor CLI output formatter. Owning them in `src/daemon/handlers/` forced `src/commands` to reach up into `src/daemon` through a dynamic import; `src/core` sits below every caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eddu5VC45qAXUkXNZJyruZ * refactor(commands): move result serialization to the command surface The `serialize*` helpers render command results for the CLI and MCP surfaces, not for the daemon; only the two deployment target resolvers were shared with daemon handlers. The serializers move to `src/commands/output/` beside the formatters that call them, and the resolvers move to `src/core` below both owners, so `src/commands` and `src/mcp` no longer reach into `src/daemon` for them. The MCP edge becomes a static import; the `src/commands` edges stay function-scoped because `src/cli.ts` eagerly evaluates both output modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eddu5VC45qAXUkXNZJyruZ * refactor(cli): render command progress in the command surface, retiring the doctor progress flag Streamed `command` progress was rendered to stderr by the daemon client, which recorded "doctor progress was rendered" in a process-global flag that `doctorCliOutput` then consumed to suppress its final summary. The rendering was commands-side, so it moves there: the CLI installs `createStderrCommandProgressSink` as the request's `RequestProgressSink` for every non-`test`, non-`--json` command, and hands its `CommandProgressState` to the output formatters with the result. The transport now only forwards events to the sink the caller installed. Behavior is unchanged in the three modes the tests pin: progress streamed to stderr suppresses doctor's check lines, no progress prints them, and a caller that installs its own sink (SDK, MCP) writes nothing to this process's stderr and is not suppressed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eddu5VC45qAXUkXNZJyruZ * test(cli): own the doctor progress cases in their own file The end-to-end doctor progress cases landed in `cli-network.test.ts`, which is already past the 1,000-line tripwire, so the test-file-size ratchet rejected the growth. They are doctor-output cases rather than network ones: they move to `cli-doctor-progress.test.ts`, and `cli-network.test.ts` returns to its merge-base size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eddu5VC45qAXUkXNZJyruZ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
27a97ee619 | fix(ios): confirm alerts without repeating activation (#2326) | ||
|
|
ed5e9736ec |
fix(runtime): resolve each lazily imported platform module exactly once (#2327)
The application-tools ports each opened their own `import(...)` of the same specifier — seven of them for `@agent-device/platform-apple/runner/operations` alone. In production those duplicates are equivalent, because the loader caches. Under Vitest they are not: while a `vi.mock` factory is still awaiting `importOriginal()`, a second dynamic import of that id resolves to the UNMOCKED module. The open path makes that overlap routine — it deliberately leaves the iOS runner prewarm unawaited — so a unit test that mocks the runner could still reach the real one. `session-open-runtime.test.ts` did: its two iOS-simulator cases scheduled two prewarms, the second one bypassed the mock, and the real local XCTest runner started for device `sim-1`. Its stale-process cleanup then spawned `pkill -f xcodebuild...session-sim-1-[0-9]` against the developer's own process table, which the hermetic-signal guard refused and reported against whichever test happened to be running when it landed — the unrelated `open --metro-port alone stays host-ambiguous on a physical Android device`. Each specifier now has one memoized loader, and the ports reach their module only through it, so the second resolution the escape needs no longer exists. Fixes #2314 |
||
|
|
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. |
||
|
|
0bebbe8dc2 |
feat: admit managed requests within lease authority (#2319)
* feat(daemon): admit managed requests within lease authority * test: verify managed request authority and activation boundaries * test: run managed request admission in provider integration |
||
|
|
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 |
||
|
|
07e25087e1 |
fix: contain managed ADB transport to its lease target (#2311)
* fix: contain managed ADB transport to its lease target * fix: block managed ADB server administration and inherited sockets |
||
|
|
6b0e5d6c1c |
feat: add fenced managed lease admission (#2308)
* feat(daemon): add fenced managed lease admission * refactor: derive managed horizons from canonical budgets * fix: carry managed admission through operation dispatch |
||
|
|
9b5b2452e7 |
refactor: share daemon command and teardown budgets (#2307)
* refactor(daemon): share command and teardown budgets * fix(daemon): load shared request timing on demand * refactor: reuse core request timeout policy * test(session): isolate runtime prewarm side effect --------- Co-authored-by: AdzeB <59929333+AdzeB@users.noreply.github.com> |
||
|
|
c4dc5621a3 |
fix(ios): avoid replaying alert mutations (#2323)
* fix(ios): avoid replaying alert mutations * docs(alert): state the single-send rule once, without a backend qualifier --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
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. |
||
|
|
f30328d086 |
fix(android): preserve editable-field metadata in snapshots (#2290)
* fix(android): preserve editable-field observation metadata * fix(android): carry field facts through the attrs digest and selection offsets past editability - `get attrs --level digest` kept only the pre-#2288 semantic fields, so `editable`/`password`/`hintShowing`/`selectionStart`/`selectionEnd` vanished on the token-cheap route. The digest now keeps them, with a regression covering explicit false/zero/empty and omission when unavailable. - The helper emitted selection offsets only inside `isEditable()`, but read-only selectable text exposes a selection too. Each nonnegative offset is now emitted independently; -1 stays absent. Parser-to-snapshot regression for a non-editable selectable node. - Docs: the field-metadata notes get their own section instead of leading the efficiency tips. - Dropped the test-isolation commit: main already mocks notifyIosRunnerAppRelaunched, and the lifecycle test passes without it. --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
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) |