mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
v0.21.0
366 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
006f2d9f60 |
chore(gates): layering baselines ratchet against merge-base (#2299)
* refactor(layering): ratchet R6, R9 and R10 against the merge-base tree R6 type-spine inversions, R9's largest type cycle and R10's R7 ownership pressure now compare the working tree with the same measurement taken over the merge-base with origin/main, read through the shared committed-tree reader (one git ls-tree, one git cat-file --batch, no second checkout). Growth still fails with the same message shape, a shrink needs no edit, and no change can bank headroom by leaving a number above the tree. R9's per-zone check gains membership from the reference, so the overflow message names the file that joined instead of listing the whole zone. * chore(gates): delete the R6, R9 and R10 pins the merge-base now supplies TYPE_INVERSION_BASELINE, LARGEST_TYPE_CYCLE_ZONE_CEILINGS, TYPE_CYCLE_BASELINE and DAEMON_MODULARITY_BASELINE.sessionState were the hand-edited references these three ratchets compared against. The merge-base measurement replaces them, so there is no number left to leave above the tree and no entry to raise. externalDaemonTypesImporters stays: it names files, not a count. |
||
|
|
a04b587993 |
chore(layering): derive the contracts export inventory from package.json (#2297)
* chore(gates): derive the contracts export inventory from package.json R11's package-boundaries test pinned every packages/contracts export subpath by hand (~115 entries, compared with deepEqual), so every merge that touched packages/contracts/package.json also had to hand-edit the pin. Replace it with a structural check derived from the manifest itself: every exports target must resolve to an existing, tracked source file, and the number of resolved targets must equal Object.keys(manifest.exports).length. The must-not-resolve negative list (Node resolution enforces the exports map at runtime) is untouched. * chore(gates): restore exact-membership for the contracts export surface assertExportTargetsMatchManifest's two checks (targets resolve to tracked files, count equals manifest.exports keys) both read from the same manifest.exports object, so a subpath added or removed moves both sides together and the equality holds regardless -- the widen/shrink guarantee the deleted ~120-line CONTRACT_EXPORTS pin gave was silently gone (#2297 review). Add scripts/layering/contracts-exports.snapshot.json, an independently committed baseline regenerated by the new generate-contracts-exports-snapshot.ts, and deepEqual the live export specifiers against it. A subpath change now fails until the contributor reruns the generator and reviews the snapshot diff -- "run a script and commit its output" instead of hand-retyping an alphabetized array, so the original maintenance-burden goal holds too. Planted red: widened packages/contracts/package.json#exports with a throwaway "./planted-red-widen" subpath, then separately deleted the "./wait" subpath; `node --test --experimental-strip-types scripts/layering/package-boundaries.test.ts` failed both times on the new deepEqual with the regenerate-and-review message. Reverted before committing. |
||
|
|
35362fe517 |
refactor(cli): let help resolve command aliases itself and retire R12 (#2293)
* refactor(cli): let cli-help resolve the --help alias itself bin.ts's --help fast path composed buildCommandUsageText(normalizeCliCommandAlias(helpTarget)) inline, which let a future edit call buildCommandUsageText raw without anyone noticing until an alias's help silently dropped back to a full CLI bootstrap (the regression #1641 fixed). Move the composition into cli-schema/cli-help.ts as resolveHelpTargetUsageText, so bin.ts just calls one function that owns its own alias normalization; bin.ts no longer imports the alias registry at all. Retargets cli-help-alias-fast-path.test.ts at the new function (same three cases) and adds a process-level smoke test asserting `tap --help`/`launch --help` stdout is byte-identical to `press --help`/`open --help`. Seen red by temporarily removing the `tap` alias from CLI_COMMAND_ALIASES (both fast and slow paths lose the alias, producing an "Unknown command: tap" mismatch); green again after restoring it. Verified manually: `node --experimental-strip-types src/bin.ts tap --help` stays byte-identical to `press --help`, and `launch --help` to `open --help`; `rotate --help` still falls through to the retired-command error. * chore(gates): retire R12 now that cli-help owns its own alias resolution bin.ts can no longer compose buildCommandUsageText and normalizeCliCommandAlias incorrectly because it doesn't hold either import any more — resolveHelpTargetUsageText in cli-schema/cli-help.ts is the only call site, and cli-help-alias-fast-path.test.ts plus the new smoke-cli process test pin it. The static R12 checker existed only to prove that composition from source text; delete it along with its rule wiring in check.ts (rule function, import, LAYERING_RULE_IDS/LAYERING_RULES entries, header comment, summary string). Drops scripts/layering/bin-alias-fast-path.ts (352 lines) and its test (311 lines). Updates the two stale references left behind: record-runtime-mechanics-policy.ts's comparison to R12's "delegate to your single owner" shape, and check-wiring.test.ts's header, which named bin-alias-fast-path.test.ts as the seam it protects. rule-ids.ts discovers rule ids by scanning source text rather than a hand-maintained list, so no entry there needed updating. Verified: pnpm check:layering green (175/175), including check-wiring.test.ts and rule-ids.test.ts; pnpm check:quick (lint + typecheck) clean; scripts/__tests__/eager-closure-budgets.test.ts (418/418) unaffected, since neither bin.ts nor cli-help.ts sits in any HUB_ENTRY_FILES or facade closure — both files reach cli-help.ts only through a dynamic import. * test(cli): pin the alias help fast path with a coverage-based oracle The byte-identical stdout test cannot fail when the fast path is bypassed: src/cli.ts's slow path resolves the same alias and writes the identical string, so a reintroduced hand-written table in bin.ts (the exact shape of #1641) would still pass it. Add a second process-level test that runs `tap`/`launch --help` and `rotate --help` with NODE_V8_COVERAGE set and reads the subprocess's own coverage report for src/cli/process-entry.ts, the one module runCli's slow path loads and the fast path never does. Seen red: forcing the fast path to always fall through to runCli (simulating the reintroduced-table bug) failed this test (bootstrappedFullCli true where false was expected) while the byte-identical test stayed green; reverted and confirmed both green. * test(cli): restore an independent oracle for alias help parity The canonical side of "alias help output matches its canonical command" also called resolveHelpTargetUsageText, so the assertion became self-consistency: a degenerate normalizer that maps every input to one canonical command would make aliasHelp and canonicalHelp equal for every case. Compare resolveHelpTargetUsageText(alias) against buildCommandUsageText(canonical) (no alias normalization on the canonical side) instead, restoring the original two-source oracle. Seen red: pointing resolveHelpTargetUsageText at a degenerate `return buildCommandUsageText('press')` failed this test ("launch --help" no longer byte-identical to "open --help"); reverted and confirmed green. * refactor(mcp): route the help tool through resolveHelpTargetUsageText server-guide.ts's help tool composed buildCommandUsageText(normalizeCliCommandAlias(topic)) inline, the same composition bin.ts held before this PR moved it into cli-help.ts. That left a second hand-written call site the R12 gate's own kill criterion said had to be gone before retirement was moot. Call resolveHelpTargetUsageText(topic) instead; behavior is unchanged (manually confirmed tap/press and rotate topics still match) since it's the same composition, and no closure/layering change since server-guide.ts already imports cli-help.ts statically. * style: apply oxfmt * test(cli): prove the help fast path for every registered alias * refactor(cli): make the process entry importable and test it directly bin.ts ran its dispatch at import time, so the only way to prove that an alias --help never loads the full CLI was to spawn the process under NODE_V8_COVERAGE and grep the report for process-entry.ts. That oracle needed a paragraph to justify; the code was wrong, not the comment. The dispatch now lives in src/cli/entry.ts as runEntry(argv, modules, io), with the five lazy imports injected by bin.ts. entry.test.ts drives it with recording loaders and the real help module: every registry alias prints its canonical help with only the help module loaded, an unknown topic falls through to the CLI loader, --version, bare usage, mcp, and startup failures each have one case. The subprocess coverage machinery, the alias table pin, and the multi-line comments are gone; the smoke test keeps one registry-derived byte-identical alias --help check against the real bin.ts. Seen red: hand-routing long-press and relaunch to the CLI loader inside entry.ts failed "every registered alias prints its canonical help without loading the CLI"; restored. |
||
|
|
d1b9914d88 |
refactor(commands): retire the navigation-only type projection (#2294)
* refactor(commands): retire the navigation-only type projection `commands/system/navigation-projection.ts` built the five navigation client methods out of a phantom-typed registry: a `unique symbol` brand carrying Options/Result/required-ness, two conditional types to read them back, and a mapped type keyed on `clientMethod`. Nothing else ever used the concept, so the machinery existed to derive five signatures that fit in five lines. Those five now say what they mean. `BackCommandOptions`, `HomeCommandOptions`, `OrientationCommandOptions`, `AppSwitcherCommandOptions` and `TvRemoteCommandOptions` join their siblings in `packages/contracts/src/client-system.ts`, and `AgentDeviceCommandClient` declares all 14 methods in one object type. `back` keeps the `--settle` triple (#1638), and `orientation`/`tv-remote` keep their required options parameter. The five MCP output schemas move to `mcp/command-output-schemas.ts` beside the other handwritten ones, byte-identical. With the projection gone, `defineExecutableCommand`'s third overload, `ExecutableCommandProjection`, `AnyCommandDefinition.projection`, `ProjectedCommandOutputSchemas`/`projectCommandOutputSchemas` and the family's `clientCommandMethods` table have no users either. Removing the table also removes the `as unknown as` cast the client used to build eight system methods from it; the client now writes all eight out, typed. That closes the `commands/system` -> `client` inversion the client-types header called the one remaining one. Public API: the five method signatures are unchanged (structural comparison of the built `dist/src/index.d.ts` before and after: empty diff). `HomeCommandOptions` is a new published name for the shape `home` already took. Tests seen red before green: - `src/__tests__/client-system-commands.test.ts` (new): wired `home` to the `app-switcher` daemon command, saw it fail, restored. - `src/mcp/__tests__/command-tools.test.ts`: dropped `durationMs` from the inlined `tv-remote` schema, saw the dispatch-shape assertion fail, restored. - `src/commands/system/index.test.ts`: made `home`'s options parameter required, saw `expectTypeOf` fail under `pnpm typecheck`, restored. * test(mcp): pin the closed top-level shape of the navigation output schemas Retiring the projection replaced an identity assert (`schema === projection.outputSchema`) with a deep-equal over properties/required, which no longer rejected an extra top-level key such as a stray `description` or `additionalProperties`. The loop now also asserts the key set is exactly type/properties/required, so the closed shape is pinned by a test again rather than by object identity. Seen red once by giving the `app-switcher` schema a description argument, which adds a top-level `description` key: the new assert failed with `+ "description"`. Green after removing it. The `deriveSettleObservationSchemas` docstring cited that deleted identity assert as the reason for copying. The press/click shared-object half is the real reason and is all that remains. * chore(gates): drop the retired projection from the R6 inversion rationale The R6 baseline numbers are unchanged (5 inversions, commands -> client still 3): retiring the projection removed a client -> commands edge, which the ratchet does not count. What changed is the ARGUMENT next to those numbers. The commands/mcp -> client bullet justified itself with a zone-level cycle (client-types.ts imported ProjectedNavigationCommandClient back out of commands/system/); that cycle no longer exists, so the bullet now rests only on the port argument that was always the second half of it. docs/dependency-graph-findings.md §0/§0b/§1 carried the same claim and the same 'move the navigation-projection types out of commands/' follow-up, now recorded as answered by deletion. The blocked-shapes table in §1 now reads eight-at-the-time / three-still-blocked, matching the struck navigation row directly under it. * test(mcp): split the navigation schema tests out of command-tools.test.ts |
||
|
|
fa06c8c6e9 | fix(daemon): fence managed readiness behind runtime admission (#2280) | ||
|
|
5bb3ea3b2a |
feat(ios): productionize Simulator AX snapshot bridge (#2277)
* feat(ios): productionize Simulator AX snapshot bridge * fix: address Simulator AX bridge review comments * docs: refresh Simulator AX evidence * fix: address new Simulator AX bridge review comments * docs: record public snapshot source timings * fix: preserve size report helper on base checkout * fix: allow base packages without snapshot bridge * fix: close simulator snapshot source ownership gaps * docs: explain simulator bridge language choice |
||
|
|
17b6ca36f8 |
test(coverage): rename-only hunks owe no changed-line coverage (#2248)
* test(coverage): rename-only hunks owe no changed-line coverage Pass --find-renames=90% to the changed-line diff so rename detection no longer depends on the host diff.renames setting: a 100%-similarity move contributes no changed lines and an edited move contributes only the hunks that differ from its source. Threshold unchanged. * docs(agents): pure moves carry their tests unchanged Drops the stale src/daemon/handlers/session.ts over-budget bullet (242 lines on main) to stay under the AGENTS.md byte budget. * style: format coverage-changed run.ts * docs(agents): restore the session.ts over-budget rule |
||
|
|
172ee149cf |
feat(screenshot): add --crop-on to crop captures to a selector frame (#2276)
* feat(screenshot): add crop-on geometry core and cropTarget selector rows
* feat(screenshot): declare crop-on flag, script round-trip, and snapshot runtime plan
* feat(screenshot): run the crop leaf after the platform write and before scale
* feat(screenshot): expose --crop-on in the CLI and surface crop warnings
* chore(gates): declare crop-on capture-kit subpaths and scope the crop scenario exemption
* refactor(screenshot): split crop target/policy module and trim redundant coverage
Address review comments at
|
||
|
|
658f822c40 |
fix: encode the mcp subcommand in server.json package arguments (#2275)
* fix: encode the mcp subcommand in server.json package arguments A registry-format launcher (e.g. one consuming /.well-known/mcp.json or the MCP registry entry) starts the server from the package descriptor only; without the positional "mcp" argument it runs the bare CLI instead of the stdio MCP server (bin.ts only starts the MCP server for the mcp subcommand). Enforce the argument in scripts/sync-mcp-metadata.mjs so sync and the CI/prepack checks (check:mcp-metadata) keep server.json correct, and regenerate server.json. * test: own the registry launch-argument invariant; add changelog entry - scripts/__tests__/mcp-metadata.test.ts asserts the checked-in server.json's agent-device npm package entry declares the exact fixed positional mcp argument (and stays stdio-only), so a missing or wrong argument fails the unit lane in both directions. Wired into the unit-core project include list. - Changelog: user-visible release fix under Unreleased. |
||
|
|
33084c7748 |
perf(ios): decide Simulator AX bridge viability (GO, Node-direct guest reader) (#2237)
* test(ios): add guest simulator AX bridge evidence * test(ios): make alert cleanup selector unique * test(ios): admit recovered alert cleanup surface * test(ios): narrow AX spike to guest evidence path * docs(ios): record guest AX bridge decision * chore(ios): remove unused spike import * docs(ios): correct simulator bridge verdict * test(ios): drive the guest Simulator AX bridge directly from Node Replace the idb companion + Python reader in the #2192 spike with a Node client for idb v1.5.2's in-Simulator SimulatorFrameworkBridge: one private guest per session spawned through simctl, 4-byte length-prefixed JSON over a UNIX socket, single-fetch traversal with automation mode asserted per request, nested trees flattened to parent-linked raw nodes with XCTest type names, and typed crash/timeout/cancel/stale-generation failures. The targeted harness now observes app readiness with a throwaway probe instead of admitting on pid presence, relaunches the app per bootstrap sample, records host load per sample, and runs recovery probes through the adapter. Hard tiers follow the corrected #2192 contract (warm 300/500 ms, relaunch 500 ms); the former 75/150 ms and 250 ms values are reported as stretch findings. Preboot preference edits are optional and unused by the guest path. The prototype's targeted artifact is preserved under a -python-prototype name; its bootstrap and recovery samples measured the packaging, not the mechanism. * test(ios): narrow Simulator bridge decision evidence * docs: publish Simulator bridge evidence out of tree * fix: tighten iOS bridge evidence gates * docs: publish corrected bridge evidence * docs: point to post-rebase bridge evidence |
||
|
|
941ca0e7e0 | ci: enforce the image-size parser mitigation through a test-app gate (#2269) | ||
|
|
e882cf9723 |
feat(runtime): add managed-local ownership and the exact-only managed runtime (#2258)
* docs: trim the CONTEXT.md glossary within the guidance byte budget
CONTEXT.md sat at 11,992 of its 12,000-byte guidance budget, so no new domain term could be added
without first paying for it.
- Condense eighteen definitions that had grown past one line (platform leaf, command surface,
runtime use, runner command traits, interactor, coordinate-first resolved element activation,
parent-owned touch point, guarantee cell, delegation-on-error, ref frame, snapshot producer,
snapshot policy facet, capture hint, regular presented-depth frontier, clip fold,
AX-unavailable target invalidation, Maestro program, Maestro observation generation). The
definitions keep their meaning; only the elaboration is gone.
- Move the five test-harness terms of 'Providers and tests' (provider-backed integration
scenario, provider transcript, scenario transcript, in-process provider scenario harness, HTTP
contract test) to docs/agents/domain.md, which AGENTS.md already routes to for domain
vocabulary. None of them names a concept a command or a wire shape carries, and none appears in
a test name.
CONTEXT.md is 10,517 bytes after this pass.
* feat(runtime): add the managed-local owner kind, device-claim rule, and managed binding fence
ADR 0021 foundations, unit 1. Nothing registers a managed local owner yet, so every arm below is
reached from tests only; the point of the unit is that the arms exist and fail closed.
- `RuntimeOwnerRef` gains `{ kind: 'managed-local'; instance }` with `managedLocalRuntimeOwner`:
one owner per allocator instance, family-agnostic because the device carries its family. Every
owner-kind discrimination becomes an exhaustive switch, so a fourth kind is a type error at each
site: the owner key, the unavailable-facts provider mode, the durable envelope decode, and the
gateway's provider-mode acceptance and exact-owner selection.
- `deviceClaimRuleForOwner` ('ordinary' | 'allocator-held' | 'none') in the new leaf
src/daemon/device-claim-rule.ts replaces the boolean `isLocalDeviceClaimTarget`. Both claim
gates switch on it, and the admission gate now evaluates it under every device-claim policy: the
`transient-exclusive` condition moved inside the ordinary arm, so a managed owner is verified
where an ordinary owner would never have touched the store.
- `requireAllocatorHeldDeviceClaim` (src/daemon/device-claim-allocator.ts) is the one read-only
verifier both gates consult. It never acquires, never locks and never clears; in this unit it
can only answer `binding-invalid`, `missing`, or `conflict`, because no allocator-held claim
kind exists until unit 2. `allocatorHeldAdmissionError` answers each outcome with its own
refusal through an exhaustive switch, so an outcome the verifier learns to produce is a
compile error until it is answered.
- A missing allocator-held claim refuses with COMMAND_FAILED / `allocator-claim-missing`,
`retriable: false`. It is deliberately not a `DeviceClaimConflictReason`: replay retries every
conflict reason as infrastructure, and a managed identity no allocator activated is permanent.
- `managedBindingFence` / `decodeManagedBindingFence` encode `[requesterId, identityIncarnationId]`
as the fence token and the request generation as its generation, so two requesters on one
identity incarnation never share a fence. The ids are fenced verbatim, and the decoder accepts a
token only if it re-encodes to itself.
- Claim admission now receives the binding intent the gateway bound, so an exact-owner fence
reaches the gate unchanged. Session open still binds ordinarily and passes an ordinary intent:
a managed local owner is therefore refused there structurally, and the Host open route replaces
that intent when it lands.
- CONTEXT.md: managed local owner, device-claim rule, managed binding fence, request generation,
identity incarnation.
* fix(daemon): decide allocator-held admission totally instead of by an optional error
`allocatorHeldAdmissionError` returned `AppError | undefined`, so its switch without a default
was never exhaustiveness-checked: TS2366 fires only when the return type excludes `undefined`,
`noImplicitReturns` is off, and oxlint has no exhaustiveness rule. A verifier outcome nobody
answered would therefore fall out as `undefined`, which both gates read as an admission — claim
admission throws nothing and session open proceeds to open the session on a device it never
verified.
Replace it with `decideAllocatorHeldAdmission`, returning
`{ admitted: true } | { admitted: false; error }`. The return type excludes `undefined`, so
dropping an arm is now a compile error at the switch, and a gate asks whether the outcome was
admitted rather than whether an error happened to come back. `buildAllocatorHeldRefusal` and the
admission gate are projections of that one decision.
* docs: restore the meaning five CONTEXT.md definitions lost in the trim
The condensing pass shortened these five past the point where they still said what they meant:
- Capture hint said 'presented depth' where the term is 'regular presented depth', which is what
Regular presented-depth frontier is measured against; the short form read as a different axis.
- Clip fold lost both that the interpreter runs inside presentation for every backend and that a
platform difference may not enter as a backend exception. Those are the whole rule.
- Snapshot policy facet lost the process boundary that makes it host-side at all: runner-side
Swift presentation stays separate.
- Runner command traits lost 'independently of the public command surface', which is what
distinguishes them from the command surface.
- Delegation-on-error said 'settles', and Settled observation makes 'settle' a term of its own.
CONTEXT.md is 11,674 of its 12,000-byte budget.
* docs(daemon): correct the claim-gate and managed-owner comments
- The claim-gate docstring claimed there is no other way to obtain device operations. That is
true of command handlers, but two daemon-owned recovery paths bind outside the seam:
application-lifecycle-recovery.ts (ordinary intent, daemon shutdown) and
durable-capture-runtime-recovery.ts (exact-owner intent read back from a durable envelope,
which this unit makes able to carry a managed local owner). Name them instead of claiming
coverage the seam does not have.
- The open path's comment described a session executing under an allocator-held claim, a state
this route cannot produce. Say what the `{ kind: 'ordinary' }` literal actually is: the truth
of a route that binds ordinarily, which the Host open route replaces with the request's exact
intent when it lands.
- Name U3 as the unit that fills the exact-owner selection arm, rather than the whole ADR.
* fix(runtime): accept transport-composed facts for a managed owner
providerModeMatchesOwner's managed-local arm accepted mode === 'local' only, but
selectExactOwner's managed-local arm loads the device's local family owner through the same
loadLocal a local-family owner uses, so it inherits that owner's provider modes verbatim. A
managed binding over a transport-composed local device (e.g. a remote ADB or web-provider
transport) would fail bindingContractFailure's facts check and be rejected as an owner/facts
mismatch. Accept the same local-family modes the local-family arm already does; still
unreachable until U3 registers the exact-only owner, which is where the binding regression
test that pins this lives.
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port (#2259)
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port
ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.
- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
arm never see it, and `providerModules` pairs one provider-runtime owner with one
`ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
rather than by a check. A duplicate instance is refused at composition.
- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
under an exact-owner intent naming itself, loads the device's own family owner through the
gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
owner — and republishes the binding under the managed owner. It does not read the fence: what a
managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.
- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
`finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
never reattach because the family runtime stamps envelopes with its own local owner. The
operations are then filtered by those facts, so an operation cannot outlive its own fact.
- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
status, removal acknowledgement, and the typed environment projection. Types only, named to match
the allocator's published contract so the two sides cannot drift, with no dependency on any
allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.
- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.
* fix(runtime): withhold the deployment cells from a managed binding and trim the allocator port
Review findings on the managed local owner.
- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
`install` on a managed binding would have booted the allocator's device with nothing to refuse
it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.
- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
That is the same class as the pre-binding readiness path, and closing it is a family-runtime
change.
- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
fixes, and it made the scripted fake carry a real parser whose only test passed with every
production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
`LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
first turns a grant into a device.
- CONTEXT.md drops an operation enumeration that was already incomplete.
* fix(runtime): withhold the lazily-booting Apple system and screenshot cells
Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.
Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.
* chore: retrigger CI (stale synchronize event after rebase)
* fix(runtime): lazy-load the managed owner wrapper to satisfy the eager-closure no-growth gate
Main's eager-closure budget gate (the merge-base ratchet) replaced the hand-tracked
HUB_BUDGETS map with an automatic no-growth-vs-merge-base check: src/platform-runtime.ts
is a hub with no growth allowed at all, not a number bumped by hand with a justifying
comment. The static import of createManagedLocalRuntimeOwner in platform-runtime-gateway.ts
added one module to that hub's closure (47 -> 48), which now fails
scripts/__tests__/eager-closure-budgets.test.ts outright rather than needing a manual bump.
Move the value import into loadManaged's dynamic `await import`, matching how the rest of
this file's owner loaders defer their leaf modules. Only the managed-local arm reaches this
path, so an ordinary bind never pays for it, same as before -- the wrapper module itself was
simply the wrong side of the eager/lazy line.
|
||
|
|
e2ce98556b |
chore(gates): eager-closure budgets ratchet against merge-base with per-category ceilings (#2257)
* refactor(closure): walk a source tree through a reader seam The eager-import-closure walker read the working tree directly through fs, so every consumer could only ask about the checkout in front of it. Closure computation now takes a SourceTreeReader; the working tree stays the default, and a committed git tree answers the same four questions for any tree-ish without checking it out -- one `git ls-tree` for the tracked set and one long-lived `git cat-file --batch` for the sources the walker can reach. Per-tree memoization of package directories and direct edges, plus a content-keyed parse cache, keep a second tree paying only for what differs. * chore(gates): eager-closure budgets ratchet against merge-base with per-category ceilings The 202 façade and 6 hub numeric pins are gone. The six platform façades stay exact at one module, every other existing entry may evaluate no more than the same file evaluated at the merge-base with origin/main (renames followed), and an entry that did not exist there fits a per-category ceiling derived from its path, or carries an APPROVED_OVER_CEILING row naming issue, reason and owner. Shrinking now needs no gate edit, and a stale approval fails. The standing denial -- a façade closure never reaches a concrete platform implementation before discovery or binding selects an owner -- is unchanged. * chore(gates): scope stale approvals to introduced entries and keep readers in sync Address review findings on the eager-closure merge-base ratchet. - docs/agents/testing.md: drop the new bullet. The file was 386 bytes over the 10,000-byte focused-doc budget, and the gate module's header already owns the invariant, so the prose was duplication the ownership rule forbids. - The closure walker's relative resolver no longer tries a .tsx suffix. The repo defines a production source as .ts (tracked-sources.ts pathspecs and isProductionSourceFile), so the committed-tree reader never loads .tsx content; resolving one produced an edge that reader could not read, crashing the ratchet instead of failing it. - The APPROVED_OVER_CEILING staleness check now looks only at entries still first-introduced. Once the merge-base carries an entry, the no-growth rule governs it and nothing reads its row again, so the row is stale for the same reason a shrunk entry's row is. |
||
|
|
e3c44ea4c3 |
test(vitest): retire the subprocess-stub kill-criterion experiment (#2255)
* test(vitest): record the subprocess-stub kill-criterion outcome
#1823's kill criterion was met (~64 consecutive genuine Coverage-job
completions since
|
||
|
|
fbf914b907 |
chore(bench): move iOS snapshot benchmark evidence to the evidence/ios-snapshot branch (#2251)
* chore(bench): move iOS snapshot benchmark evidence to the evidence/ios-snapshot branch The three hash-named raw results under scripts/ios-snapshot-benchmark/evidence/ are measurement output, not fixtures. They now live on the orphan branch evidence/ios-snapshot (commit |
||
|
|
a4f625c774 |
feat: add strict wait absent polling (#2236) (#2264)
* feat: add strict wait absent polling * fix: keep wait absent coverage gates green * fix: preserve wait absent restart diagnostics |
||
|
|
a5a7f6dfa1 |
docs(layering): kill criteria on every rule module (#2244)
Adds a four-line Catches/Evidence/Cost/Kill-criterion header to every layering rule module for R2, R4-R7, R9-R14, R16, R18, R19, R65-R73, and the rule-id uniqueness gate, so each structural check states what it catches, why no other gate sees it, its LOC cost, and the concrete condition under which it gets deleted. No behavior change. |
||
|
|
2371ba9bff |
feat: add strict native absence assertion (#2245)
* feat: add strict native absence assertion * fix: address absence assertion review feedback |
||
|
|
6a24dc1b2d |
chore(depgraph): stop re-deriving the layering inversion baseline (#2241)
* chore(depgraph): stop re-deriving the layering inversion baseline The report's typeInversionsByPair and the gate's checkTypeInversions run the same loop over the same resolveImportEdges output, so asserting that the report reproduces TYPE_INVERSION_BASELINE over the real tree checked one code path against itself. Replace the tree-wide cross-check with a synthetic test of the report's own counting rule (raw edges, once per file pair). * chore(gates): retitle the depgraph gate as the report's model tests The Layering Guard step no longer claims to agree the report with the gate; it runs the depgraph model and blast-radius tests, which the gate manifest requires a registered check to own. * docs: clarify inversion ratchet ownership |
||
|
|
5eebba5fcd |
refactor(layering): one retired-paths rule for src/utils and src/replay (#2240)
R14 (src-utils-retirement) and R71 (replay-ownership) were the same path-prefix denylist instantiated in two modules. Fold both into scripts/layering/retired-paths-policy.ts, driven by a table keyed by rule id; ids, messages, inputs (tracked src/utils paths vs production sources) and check.ts wiring are unchanged. |
||
|
|
fbf6097700 |
chore(gates): drop the test-file size pin map, keep the merge-base ratchet (#2238)
The exact-length pin map duplicated what the merge-base already records and made every shrink a two-file edit. The gate now has one rule: a test file over the 1,000-line tripwire may be no longer than at the merge-base with origin/main, and no new test file may cross the tripwire. |
||
|
|
2c7fb93cfc |
fix(android): apply settings airplane through the connectivity service (#2234)
* fix(android): apply settings airplane through the connectivity service settings airplane wrote airplane_mode_on and then broadcast android.intent.action.AIRPLANE_MODE, which Android refuses for non-system callers. The write landed, the broadcast failed, and the device reported airplane mode with the radios still up. The connectivity service now owns the change: it is read to prove the build supports airplane mode before anything is written, driven with cmd connectivity airplane-mode enable|disable, and read again so the response reports the mode connectivity holds rather than the one requested. Builds without that command are refused unmutated with UNSUPPORTED_OPERATION. Closes #2223 * test(android): pin the mechanics eager closure at 178 modules Splitting the airplane owner out of settings.ts adds one module to the mechanics facet, which is implementation-eager by design. The row moves to the measured number in the PR that grows it. * fix(android): report only capability absence as unsupported airplane mode An unrecognized nonzero probe — a permission denial, a connectivity-service error — was answered with "requires Android 11; use a newer device". Only the prose adb prints when a build ships no shell implementation for the command now selects UNSUPPORTED_OPERATION; every other failed read stays COMMAND_FAILED with its classified hint, and the write is unreachable from both. The predicate that reads that prose already existed for the clipboard service and is now named for the question it answers, so airplane mode reuses it instead of adding a second message sniff. |
||
|
|
7ee1a5ded7 |
refactor(ios): carry provider acquisitions through one presentation owner (#2233)
* refactor(ios): centralize provider snapshot presentation * fix(ios): close provider snapshot ownership gaps * fix(ios): enforce provider snapshot ownership boundary * fix(capture-kit): preserve snapshot engine lazy closure |
||
|
|
1f0eedff89 |
refactor(daemon): move shared snapshot execution out of handlers (#2232)
* refactor(daemon): move shared snapshot execution out of handlers * fix: remove retired snapshot health baseline |
||
|
|
6c8c0508d9 |
refactor(ios): converge Limrun snapshots through engine (#2222)
* refactor(ios): converge Limrun snapshots through engine * fix(limrun): defer snapshot engine loading * fix(limrun): harden snapshot viewport evidence * fix(limrun): preserve snapshot engine evidence * fix(limrun): preserve unknown snapshot truncation * refactor(ios): reuse private presentation evidence seam * test(ios): remove stale presentation assertion binding * test(ios): extract snapshot truncation regressions * test: ratchet snapshot suite size pins * test(snapshot): cover provider presentation ownership * test(snapshot): type Limrun composition fixture * test(snapshot): exercise public Limrun runtime composition |