From d68cf675e2fa88b1ad2941927c855d0357724e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:09:02 +0200 Subject: [PATCH 01/58] docs(adr): identify the derived daemon registry owners Item A. packages/command-registry/src/registry.ts declares the separately typed daemon facet; daemon-command-descriptor.ts owns DaemonCommandDescriptor. packages/daemon/src/core/daemon-command-registry.ts derives the table and exposes predicates. Its tests pin policy and routes. 4648051b09 (#906) introduced the dormant descriptor and parity tests. 47abc8c416 (#907) activated derivation and deleted the hand table. This corrects the brief attribution to #906 alone. Known invariant-3 gap: types.ts CommandDescriptorBase exposes daemon?: DaemonCommandTraits, and src/cli/command-explain.ts describeCommandDaemon reads and publishes route/traits. Preserve the invariant and report the gap; do not change behavior. --- docs/adr/0003-daemon-command-registry.md | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/adr/0003-daemon-command-registry.md b/docs/adr/0003-daemon-command-registry.md index e909deb7e8..341968f139 100644 --- a/docs/adr/0003-daemon-command-registry.md +++ b/docs/adr/0003-daemon-command-registry.md @@ -7,11 +7,12 @@ Accepted ## Rules at a glance - Keep public command identity in `packages/command-registry/src/catalog.ts` and public input/output contracts in `src/commands/**`. -- Keep daemon route ownership and request-policy traits in the daemon command registry. +- Declare daemon route ownership and request-policy traits in the separately typed `daemon` facet of the descriptor registry. +- Derive the daemon registry from that facet. Expose request-policy predicates only through the daemon registry. - Request modules consume named predicates from the registry. They do not recreate command string sets. - Handler modules own execution logic only. They do not export duplicate coverage tables to prove route membership. - The daemon registry must not define CLI grammar, Node.js client options, MCP schemas, user-facing help, or platform capability support. -- A derived daemon registry is permitted only if it preserves all four invariants in the 2026-06 update. +- Preserve all four invariants in the 2026-06 update. The CLI explanation has the known invariant-3 gap described below. ## Context @@ -35,7 +36,9 @@ Duplicate command sets required alignment by convention. Keep public command identity in `packages/command-registry/src/catalog.ts`. Keep public input/output contracts in `src/commands/**`. -Add `packages/daemon/src/core/daemon-command-registry.ts` as the daemon-side source of truth for command route ownership and request-policy traits. +Declare command route ownership and request-policy traits in the `daemon` facet in `packages/command-registry/src/registry.ts`. +`DaemonCommandDescriptor` in `packages/command-registry/src/daemon-command-descriptor.ts` types that separate facet. +`packages/daemon/src/core/daemon-command-registry.ts` derives its table through `deriveDaemonCommandDescriptors(commandDescriptors)`. Request modules consume predicate functions from the registry. They do not recreate command string sets. Handler modules own execution logic only. @@ -64,7 +67,8 @@ The command contract, projection, help, and capability modules retain these resp ## Consequences -When a daemon-handled command is added or moved, its route and request-policy traits require updates in the daemon command registry. +When a daemon-handled command is added or moved, update its `daemon` facet in `packages/command-registry/src/registry.ts`. +The daemon registry derives its table from those declarations. The registry tests pin the trait decisions. Provider-backed integration scenarios verify important request-policy behavior through the real daemon request path. @@ -79,7 +83,7 @@ Thus, future changes do not require readers to infer the rationale from agent in ## Update (2026-06): single-declaration / derivation model -A later proposal, the `CommandDescriptor` direction in [ADR 0008](0008-command-descriptor-registry.md), unifies a command's declarations at one registration site. +The implemented `CommandDescriptor` direction in [ADR 0008](0008-command-descriptor-registry.md) unifies a command's declarations at one registration site. It derives these surfaces from that site: - Public catalog. @@ -89,7 +93,7 @@ It derives these surfaces from that site: - Daemon registry. Its purpose is to remove the drift between these tables. -Convention keeps several of these surfaces aligned against that drift. +Types and registry tests enforce the declarations and projections. **This ADR's decision remains accepted.** Its boundary concerns ownership and the predicate interface, not the physical file that contains a trait. @@ -97,7 +101,8 @@ Its boundary concerns ownership and the predicate interface, not the physical fi That property remains true for a projected or derived backing table. A derived daemon registry is therefore permitted **only if** it preserves all these invariants: -1. **Daemon-owned declaration.** A daemon-owned facet under `packages/daemon/src/` declares route and request-policy traits. +1. **Daemon-owned declaration.** The separately typed `daemon` facet in `packages/command-registry/src/registry.ts` declares route and request-policy traits. + `DaemonCommandDescriptor` in `packages/command-registry/src/daemon-command-descriptor.ts` owns its type. The registration composes that facet. The public command contract in `src/commands/**` or `packages/command-registry/src/catalog.ts` must never contain these traits as inline fields. A registration call may be co-located with public declarations. @@ -116,5 +121,11 @@ A single flat public descriptor that exposes daemon fields in public views is ** This would combine daemon policy with a public command registry, which this ADR rejects. Compose facets owned by their domains. Derive the registry from those facets. -Until that derivation is implemented and the registry tests pin it, the hand-authored `packages/daemon/src/core/daemon-command-registry.ts` remains the source of truth. -Until then, the operating rule in `AGENTS.md` does not change. +PR #906 introduced the descriptor and proved parity. +PR #907 made the derivation live and deleted the hand-authored table. +The daemon registry tests pin route and request-policy decisions. + +**Known invariant-3 gap.** The descriptor type exposes its `daemon` facet to public projection code. +`src/cli/command-explain.ts` reads that facet and displays daemon routes and request-policy traits. +Thus, types do not enforce invariant 3 for this CLI projection. +The invariant remains required; this ADR records the gap without changing runtime behavior. From a96f6ee5f7674edccd6616ddf3b6b51b9b6a4f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:03 +0200 Subject: [PATCH 02/58] docs(adr): match guarantee declarations and fixture consumers Items 1 and 2. packages/contracts/src/interaction-guarantees.ts declares 14 guarantees, including keyboardOcclusion, parentOwnedTouchPoint, settleObservation, resolutionDisclosure, outcomeObservation, and targetStability. Match the complete list. rg finds only the TypeScript snapshot-visibility-tap-point-policy.test.ts and RunnerTests+TapPointPolicyTests.swift reading tap-point-policy.json; the matrix parityTable is metadata. No fake runner reads it. Keep migration step 3 open. --- .../0011-interaction-guarantee-contract.md | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/adr/0011-interaction-guarantee-contract.md b/docs/adr/0011-interaction-guarantee-contract.md index 810dd0ad0e..2b248b7854 100644 --- a/docs/adr/0011-interaction-guarantee-contract.md +++ b/docs/adr/0011-interaction-guarantee-contract.md @@ -73,12 +73,18 @@ An unproven cell is at least a visible claim that has an owner. export const INTERACTION_GUARANTEES = [ 'disambiguation', // collapse one equivalent wrapper chain; distinct subtrees fail with candidates 'occlusion', // covered targets are refused + 'keyboardOcclusion', // refuse tap points behind the visible software keyboard + 'parentOwnedTouchPoint', // preserve parent identity; avoid interactive descendants 'offscreen', // tap point (rect center) must lie in the root viewport 'nonHittable', // promotion + targetHittable/hint annotation 'responseConstruction', // one shared response construction site (Layer 2) 'responseIdentity', // refLabel/selectorChain availability on this path 'verifyEvidence', // --verify baseline + post-action digest + 'settleObservation', // best-effort --settle diff after the action 'errorTaxonomy', // no-match/ambiguous/offscreen codes, messages, hints + 'resolutionDisclosure', // disclose the source of target resolution + 'outcomeObservation', // state what the path observes after dispatch + 'targetStability', // refuse tap positions that change between observations ] as const; ``` @@ -155,13 +161,15 @@ Some rules must run **runner-side**, because the direct iOS path cannot see the These rules get pure-function Swift twins that operate on plain geometry, without `XCUIElement`. `TapPointPolicy.isAllowed(elementFrame:windowFrame:)` is one example. **Golden fixture tables** enforce parity. -These tables are JSON files under `contracts/fixtures/`, and three test suites consume them: +These tables are JSON files under `contracts/fixtures/`. +`tap-point-policy.json` has two test consumers: -1. Vitest asserts the TypeScript rule against the table. -2. Runner Swift unit tests assert the Swift twin against the same table. - CI already compiles them through "Swift Runner Unit Compile". -3. The provider harness's fake runner derives its behavior from the table. - Integration scenarios therefore exercise the real contract instead of a manual approximation. +1. `packages/contracts/src/snapshot-visibility-tap-point-policy.test.ts` asserts the TypeScript rule against the table. +2. `apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+TapPointPolicyTests.swift` asserts the Swift twin against the same table. + CI already compiles it through "Swift Runner Unit Compile". + +The provider harness's fake runner does not consume this table. +Migration step 3 remains open for that consumer. Then, a TypeScript or Swift drift turns CI red on the side that changed, without a simulator. @@ -313,7 +321,7 @@ A daemon reset there destroyed every healthy app session that the daemon owned. Their divergence is priced and documented, instead of discovered on-device. - The Swift/TypeScript split stops being a parity blind spot. Golden tables are the single source of truth for cross-language rules. - The fake runner stops being a second implementation that drifts. + After the remaining migration step 3 work, the fake runner would consume the same table instead of a separate approximation. - The initial cost is mostly classification honesty. The first registry contains `waived('gap: ...')` cells, and that is the purpose. For example, the maestro non-hittable fallback intentionally waives `offscreen`. From 6bc7426844d04735171e480c5bbbe7a087c166a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:04 +0200 Subject: [PATCH 03/58] docs(adr): correct the default lease inactivity period Item 3. packages/daemon/src/resources/lease-registry-scope.ts DEFAULT_LEASE_TTL_MS = 60_000; normalizeLeaseTtlMs uses it when no positive TTL is supplied. The admitted-work section already states one minute. --- docs/adr/0007-remote-device-leases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0007-remote-device-leases.md b/docs/adr/0007-remote-device-leases.md index b829062b1f..993838b890 100644 --- a/docs/adr/0007-remote-device-leases.md +++ b/docs/adr/0007-remote-device-leases.md @@ -41,7 +41,7 @@ It stays below daemon device leases and should not be weakened or replaced by th `open` is the natural point to acquire a device lease because target resolution and session creation meet there. Commands after `open` must refresh the lease. -After five minutes without activity, the device should become available again. +After the default 60 seconds without activity, the device should become available again. These operations must run under the same daemon request lock: From de931474c58ce3704723e357d7765c8903647d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:04 +0200 Subject: [PATCH 04/58] docs(adr): record the active settle timing detector Item 4. gh issue view 1299 reports CLOSED, closedAt 2026-07-21T08:54:50Z. packages/maestro/test/conformance/differential/scenarios.ts settle-after-tap has no knownDivergence and requires settleLatches >= 1 and settleTimeouts <= 0. invariants.test.ts exercises synthetic traces. This is repository evidence, not a new live-device result. --- docs/adr/0015-direct-maestro-engine.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0015-direct-maestro-engine.md b/docs/adr/0015-direct-maestro-engine.md index 68a142c2b5..6fde5620e3 100644 --- a/docs/adr/0015-direct-maestro-engine.md +++ b/docs/adr/0015-direct-maestro-engine.md @@ -193,8 +193,10 @@ Each of the four #1217 regressions has a dedicated fixture: A pure unit test, rather than a device scenario, pins the runtime half of percent truncation, because a one-pixel delta is not app-observable. The detector for bug class 4 is an engine-side timing invariant that executes on the device path. Unit tests exercise that detector against synthetic traces. -On the device path, the detector is currently blocked by a declared `scrollUntilVisible` divergence (#1299). -The differential itself found that divergence on its first working run. +The `settle-after-tap` device scenario has no declared divergence. +It requires at least one settle latch and no settle timeouts. +The differential found the former `scrollUntilVisible` divergence on its first working run. +Issue #1299 closed on 2026-07-21; that divergence no longer blocks the detector. Documented intentional deviations are encoded as expected-divergence entries rather than silent mismatches: From 53b6a2b6491d493bb040d5e51ef407cb223ec978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:04 +0200 Subject: [PATCH 05/58] docs(adr): include recording consumers of the gesture codec Item 5. a13a6832ee (#1567) added target-authored drag. packages/daemon/src/execution/interaction/internal/interaction-gesture.ts calls gesturePayloadToPositionals; packages/daemon/src/core/session-script-writer.ts calls dragGesturePayloadFromPositionals. PUBLIC_GESTURE_SYNTAX still defines arity for the two parsers. --- docs/adr/0013-unified-gesture-plans.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/adr/0013-unified-gesture-plans.md b/docs/adr/0013-unified-gesture-plans.md index 1566af8feb..4ea742afed 100644 --- a/docs/adr/0013-unified-gesture-plans.md +++ b/docs/adr/0013-unified-gesture-plans.md @@ -187,12 +187,14 @@ Providers should compose transport/device bindings with the shared platform adap That `.ad` codec is the script format's own syntax, not a compatibility shim. It is not scheduled for removal (amended 2026-07, issue #1216). -Its only remaining callers are the CLI argv parse and the `.ad` line parse. -Both are the current public syntax, so nothing is left to migrate off. +Its callers include the CLI argv parse, the `.ad` line parse, and the recording path. +`packages/daemon/src/execution/interaction/internal/interaction-gesture.ts` encodes gesture inputs for recordings. +`packages/daemon/src/core/session-script-writer.ts` decodes recorded drag endpoints. +These are current syntax and recording consumers, so nothing is left to migrate off. A structured `.ad` gesture payload was rejected. It would make recordings unreadable and impossible to search with grep, for no behavioral gain. -One table, `PUBLIC_GESTURE_SYNTAX`, defines argument arity for both callers. +One table, `PUBLIC_GESTURE_SYNTAX`, defines argument arity for both parsers. Thus, a form removed from the CLI is removed from `.ad` in the same edit. If a `.ad` script still carries a removed positional, the script fails when it is parsed. This failure occurs before the replay executes any device action. From 5f891cd97a7a7a91bc5a68bc95114182755a8e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:04 +0200 Subject: [PATCH 06/58] docs(adr): record retired aliases and current client result gaps Items A and 6. 47abc8c416 (#907) activated the derived daemon table after parity in 4648051b09 (#906). Mark the hand-table inventory as pre-migration context. 189b062519 (#960) moved client-types.ts into client/. 494eb52f66 (#1877) removed read. gh issue view 1153 reports CLOSED, closedAt 2026-07-08T16:35:29Z, but src/client/client-types.ts alert and reactNative still return CommandRequestResult. Do not infer total narrowing from closure. --- docs/adr/0008-command-descriptor-registry.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/adr/0008-command-descriptor-registry.md b/docs/adr/0008-command-descriptor-registry.md index 337fec37e5..c74bf6d1d1 100644 --- a/docs/adr/0008-command-descriptor-registry.md +++ b/docs/adr/0008-command-descriptor-registry.md @@ -28,7 +28,7 @@ Completed process history is in git history. ## Context -Roughly ten manually maintained tables repeat command identity and require alignment by convention: +Before the descriptor migration, roughly ten manually maintained tables repeated command identity and required alignment by convention: - `PUBLIC_COMMANDS` in `src/command-catalog.ts`. - Per-command metadata and family facets in `src/commands/**`. @@ -36,7 +36,7 @@ Roughly ten manually maintained tables repeat command identity and require align - Daemon command registry in `packages/daemon/src/core/daemon-command-registry.ts` (ADR 0003). - Structured-batch allowlist in `src/batch-policy.ts`. - MCP exposure sets. -- Node client interface and implementation in `src/client-types.ts` and `src/client.ts`. +- Node client interface and implementation in `src/client/client-types.ts` and `src/client.ts`. - Generic-dispatch `switch` in `src/core/dispatch.ts`. The switch's `default: throw` makes a missing or renamed command a runtime error instead of a compile error. @@ -100,7 +100,8 @@ This design composes with ADR 0003's four invariants and is bound by them: A plain new command changes approximately 1–2 files. Per-platform behavior remains N implementations behind the descriptor's `execute`. -The descriptor is the prerequisite for typed per-command results in ADR 0010, which deletes the `src/client-types.ts` mirror. +The descriptor supports typed per-command results in ADR 0010. +`src/client/client-types.ts` retains the public client interface and some `CommandRequestResult` return types. It also supplies the capability facet used by the platform-plugin work in ADR 0009. During the migration, the import-cycle inversion was the principal risk. @@ -119,16 +120,18 @@ As of 2026-07, the descriptor registry is live for these surfaces: - MCP exposure list. - Capability-checked CLI command list. - Post-action observation traits. -- Platform dispatch command set, including the remaining dispatch-only alias `read`. +- Platform dispatch command set, which then included the dispatch-only alias `read`. + +PR #1877 removed `read` when `get` moved to the request-bound device runtime. +The daemon registry now derives its table from descriptor facets instead of a hand-maintained table (PR #907). Command-family surface metadata remains under `src/commands/**`, beside CLI grammar and client-backed executors. Coherence checks compare it with the descriptor CLI catalog. Thus, new surface names cannot differ from the descriptor root. -The remaining deferred work is the public Node-client typed-result narrowing. -[#1153](https://github.com/callstack/agent-device/issues/1153) tracks this work outside this registry migration. -Some commands still expose dynamic daemon response payloads or retain closed result contracts in runtime modules. -Before their client methods can stop returning `CommandRequestResult`, those contracts must move. +[#1153](https://github.com/callstack/agent-device/issues/1153), the former tracker for public Node-client typed-result narrowing, closed on 2026-07-08. +Some client methods still return `CommandRequestResult`, including `alert` and `reactNative` in `src/client/client-types.ts`. +Issue closure does not establish complete result narrowing. This ADR owns the decision and its constraints. The descriptor projections enforce the delivered end state. From 82f9b50c0916f1d695b2c3048314186c7feeb228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:10:04 +0200 Subject: [PATCH 07/58] docs(adr): exclude bridge preparation from the generation circuit Item 7. packages/platform-apple/src/snapshot-route.ts opensGenerationCircuit excludes failure.kind === preparing and WINDOW_COORDINATE_SPACE_UNRESOLVED. snapshot-route.test.ts pins that bridge preparation sends only the current capture to XCTest. Keep glance and body consistent. --- docs/adr/0004-ios-snapshot-backend-strategy.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/adr/0004-ios-snapshot-backend-strategy.md b/docs/adr/0004-ios-snapshot-backend-strategy.md index 723a07e8b9..21eafa530f 100644 --- a/docs/adr/0004-ios-snapshot-backend-strategy.md +++ b/docs/adr/0004-ios-snapshot-backend-strategy.md @@ -17,7 +17,8 @@ Structured snapshot quality verdicts and fallback warnings make degraded or reco - Route eligible local iOS Simulator snapshots through the host AX bridge. Present them once through the shared TypeScript engine. - If bridge acquisition or presentation fails, use one typed XCTest fallback. -- After fallback, disable the bridge for that app generation, except for capture-scoped failures. A new app generation enables it again. +- After fallback, disable the bridge for that app generation, except for `preparing` failures and capture-scoped failures. + A new app generation enables it again. - A refusal that provides evidence about the screen, not the app, has capture scope. - Keep the regular visible strategy and the raw diagnostic strategy explicit as the two public snapshot strategies. - The regular visible strategy must not use an arbitrary node-count cutoff. @@ -52,7 +53,9 @@ Keep XCTest as the iOS automation runner. Route eligible local iOS Simulator snapshots through the host AX bridge. Present them once through the shared TypeScript engine. If bridge acquisition or presentation fails, use one typed XCTest fallback. -After fallback, disable the bridge for that app generation, subject to the capture-scoped exception below. +After fallback, disable the bridge for that app generation, except for `preparing` failures and the capture-scoped exception below. +A `preparing` failure describes the bridge preparation queue, not the app. +It sends only that capture to XCTest. A new app generation enables it again. Physical devices, providers, custom-action captures, and interactions remain on their existing owners. From 3d04ceb51f827586964c9706219be31d09e5936a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:11:05 +0200 Subject: [PATCH 08/58] docs(adr): replace deleted migration references in code Item 8. ADR 0011 Layer 2 and ADR 0014 Mutation admission and read behavior / Command effects are daemon-owned and completely classified replace removed phase, step, and escape-hatch references. Change comments and one test name only. R56 evidence: c77bc40d48 (#2021) deleted the legacy src/core/dispatch.ts app-switcher arm and dispatch facet. The current descriptor retains daemon.route generic, but platformExecution is device-runtime; generic-runtime-execution.ts routes system buttons through system-button-runtime.ts, whose app-switcher row uses appSwitcherRuntimeUse. Retained generic routing is intentional, not evidence of a surviving legacy leaf or runtime bug. Clarify that distinction. --- packages/command-registry/src/registry.ts | 14 ++++++-------- packages/contracts/src/interaction-guarantees.ts | 2 +- packages/contracts/src/interaction.ts | 4 ++-- .../command-descriptor-ref-frame-effect.test.ts | 4 ++-- packages/daemon/src/core/ref-frame.ts | 7 +++---- .../interaction/runtime/ref-target-resolution.ts | 4 ++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/command-registry/src/registry.ts b/packages/command-registry/src/registry.ts index 76de176176..74525dd074 100644 --- a/packages/command-registry/src/registry.ts +++ b/packages/command-registry/src/registry.ts @@ -1613,16 +1613,14 @@ export const RAW_COMMAND_DESCRIPTORS = [ ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public', key: 'appSwitcher' }, frameworkTier: 'extended', - // R56 retires this command's capability bucket, its `dispatch` leaf, and its HarmonyOS - // overlay membership together: admission is the owner's `appSwitcher` fact, and the only - // execution is that one bound operation (ADR 0019 §9). + // R56 removed the legacy `dispatch` leaf, capability bucket, and HarmonyOS overlay membership. + // The daemon route remains `generic`; admission and execution use the owner's bound + // `appSwitcher` operation (ADR 0019 §9). recordsSessionAction: true, recordingEffect: 'mutates-app', - // ADR 0014: app-switcher previously reached the generic daemon leaf via the - // registry's generic fallback with no daemon facet, so it could not be - // classified. Add the facet (route unchanged) so its device mutation is - // covered by the completeness gate; this is the escape hatch the ADR calls - // out, not a new specialized route. + // ADR 0014, "Command effects are daemon-owned and completely classified": + // The `daemon` facet classifies this command's device mutation for the completeness gate. + // The route remains `generic`. daemon: { route: 'generic', refFrameEffect: 'may-invalidate' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: true, diff --git a/packages/contracts/src/interaction-guarantees.ts b/packages/contracts/src/interaction-guarantees.ts index 2d0a6280ae..1408370c2a 100644 --- a/packages/contracts/src/interaction-guarantees.ts +++ b/packages/contracts/src/interaction-guarantees.ts @@ -3,7 +3,7 @@ * * Every dispatch path an interaction command can take must classify EVERY * guarantee: enforced by shared runtime code, enforced runner-side (with a - * parity table once ADR 0011 phase 3 lands), delegated to another path, + * parity table under ADR 0011, Layer 2), delegated to another path, * inapplicable by construction, or explicitly waived with a reason. * * This registry plus its gate test is an HONESTY/COMPLETENESS gate, not a diff --git a/packages/contracts/src/interaction.ts b/packages/contracts/src/interaction.ts index 1585db217b..09da96514f 100644 --- a/packages/contracts/src/interaction.ts +++ b/packages/contracts/src/interaction.ts @@ -160,7 +160,7 @@ export type SurfaceScopedNodes = { presentedSurface?: PresentedSurface; }; -/** Which tree a resolved `@ref`'s node was read from (ADR 0014 step 5). */ +/** Which tree a resolved `@ref`'s node was read from (ADR 0014, "Mutation admission and read behavior"). */ export type RefObservationSource = 'observation' | 'authorized-frame'; export type ResolvedInteractionTarget = @@ -182,7 +182,7 @@ export type ResolvedInteractionTarget = recordingTarget?: RecordingTargetOverride; preAction?: SurfaceScopedNodes; /** - * ADR 0014 step 5: which tree the dispatched node was read from, reported by the + * ADR 0014, "Mutation admission and read behavior": the source tree of the dispatched node, reported by the * reconciliation that chose it. `'observation'` means the session's newest capture, so the * node's rect is a position that capture measured; `'authorized-frame'` means the older tree * the ref was authorized against, whose rect says nothing about where the target sits now. diff --git a/packages/daemon/src/core/__tests__/command-descriptor-ref-frame-effect.test.ts b/packages/daemon/src/core/__tests__/command-descriptor-ref-frame-effect.test.ts index aace09dc82..b3c127f6d7 100644 --- a/packages/daemon/src/core/__tests__/command-descriptor-ref-frame-effect.test.ts +++ b/packages/daemon/src/core/__tests__/command-descriptor-ref-frame-effect.test.ts @@ -5,7 +5,7 @@ import { test } from 'vitest'; import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { DAEMON_COMMAND_DESCRIPTORS, resolveRefFrameEffect } from '../daemon-command-registry.ts'; -// ADR 0014 migration step 2: the ref-frame-effect classification is an +// ADR 0014, "Command effects are daemon-owned and completely classified": the classification is an // honesty/completeness guard. Every command that reaches a session-owning daemon // leaf must classify how it relates to the authorized ref frame, so a device // mutation cannot be added on a path that silently leaves stale refs admissible. @@ -48,7 +48,7 @@ test('every public command is classified or explicitly non-daemon', () => { } }); -test('app-switcher no longer bypasses classification (ADR 0014 escape hatch)', () => { +test('app-switcher classifies its device mutation (ADR 0014 command effects)', () => { // app-switcher reaches the generic daemon leaf and mutates the device. Before // ADR 0014 it had no daemon facet and relied on the registry's generic // fallback, so it could not be classified. It must now resolve to a concrete diff --git a/packages/daemon/src/core/ref-frame.ts b/packages/daemon/src/core/ref-frame.ts index c8d4b6ff3e..ad984e4a8f 100644 --- a/packages/daemon/src/core/ref-frame.ts +++ b/packages/daemon/src/core/ref-frame.ts @@ -26,10 +26,9 @@ const outdatedObservations = new WeakSet(); * * The frame is expired at the device side-effect seam, carries a non-`all` * issuance scope after a partial publication, and its admission matrix is - * enforced fail-closed on every platform before dispatch (ADR 0014 steps 3–7). - * Read-only ref staleness is now derived from frame state (an expired frame - * warns; an active one does not) rather than a coarse client-stale marker, which - * migration step 8 removed. + * enforced fail-closed on every platform before dispatch (ADR 0014, "Mutation admission and read behavior"). + * Read-only ref staleness comes from frame state instead of a coarse client-stale marker. + * An expired frame warns; an active frame does not. */ /** diff --git a/src/commands/interaction/runtime/ref-target-resolution.ts b/src/commands/interaction/runtime/ref-target-resolution.ts index a7ec7e3c60..d8a846b4b5 100644 --- a/src/commands/interaction/runtime/ref-target-resolution.ts +++ b/src/commands/interaction/runtime/ref-target-resolution.ts @@ -46,7 +46,7 @@ type RefResolution = { /** The keyboard band the capture of `tree.nodes` measured, when it measured one (#2660). */ keyboard?: SnapshotKeyboardBandFact; /** - * Which tree `resolved.node` was read from, as the reconciliation decided it (ADR 0014 step 5). + * Which tree `resolved.node` was read from, as the reconciliation decided it (ADR 0014, "Mutation admission and read behavior"). * Absent for a caller-carried node (#1654), which no reconciliation of this module chose. */ adoptedFrom?: RefObservationSource; @@ -180,7 +180,7 @@ async function resolveSnapshotForRef( } /** - * ADR 0014 step 5: decouple Android freshness from ref authorization. The frame + * ADR 0014, "Mutation admission and read behavior": separate Android freshness from ref authorization. The frame * tree names WHICH node `@eN` authorizes. When a freshness (or other read-only) * capture has advanced the operational observation past the frame, adopt the * observation's node — its fresh on-screen coordinates — ONLY when its local From 186612113486eaf7bc71b74453b4b1ad89fcf879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:02 +0200 Subject: [PATCH 09/58] docs(adr): identify the current generic-dispatch rejection boundary Item 9. c77bc40d48 (#2021) retired src/core/dispatch.ts and dispatchKnownCommand. packages/daemon/src/server/request-router.ts calls resolveGenericRuntimeExecution before dispatchGenericCommand. packages/daemon/src/execution/generic-runtime-execution.ts rejects its default with COMMAND_FAILED / generic-route-runtime-missing. Its __tests__/generic-route-runtime-completeness.test.ts pins unknown-command rejection. No dispatch-keyboard.test.ts exists. Keep the proposed fragment INVALID_ARGS contract conditional and identify the absent format-specific proof rather than claim this routing-gap test proves it. --- docs/adr/0020-composable-recorded-fragments.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/adr/0020-composable-recorded-fragments.md b/docs/adr/0020-composable-recorded-fragments.md index cb3060e880..8f88a7ba78 100644 --- a/docs/adr/0020-composable-recorded-fragments.md +++ b/docs/adr/0020-composable-recorded-fragments.md @@ -99,7 +99,8 @@ But these invariants are fixed: It is a format envelope, not a `SessionAction`, and it has no device side effect. A composed native parent uses the corresponding `composition-v1` guard as its first executable line, before root `open` or any `include`. An old reader parses either guard as an unknown action. - The existing `dispatchKnownCommand` seam rejects it with `INVALID_ARGS` before it invokes any platform handler. + The generic-dispatch owner would reject the unknown guard before platform execution. + The fragment codec must prove the required `INVALID_ARGS` format rejection through that path. The fragment has no `open`, `close`, or hidden launch action. The writer always emits the effective `context platform` and `target`. A parser rejects a fragment that is missing either constraint. @@ -530,8 +531,10 @@ Readers that do not know the executable guard reject the artifact at its format The new reader consumes the native guard during source preflight. An old reader can get either a v1 fragment or a composed parent. It parses the guard as an ordinary action. -Then the existing `dispatchKnownCommand` rejection in `src/core/dispatch.ts` throws `INVALID_ARGS` before the platform handler is invoked. -Therefore, the old reader cannot execute a root `open`, a known prefix, or an expanded child body. +The current generic-dispatch owner is `resolveGenericRuntimeExecution` in `packages/daemon/src/execution/generic-runtime-execution.ts`. +Its default returns `COMMAND_FAILED` with reason `generic-route-runtime-missing` before platform execution. +The proposed fragment codec must establish the required `INVALID_ARGS` format rejection before any platform handler runs. +Then the old reader would not execute a root `open`, a known prefix, or an expanded child body. An old reader that gets an unguarded legacy `.ad` retains its existing behavior. This includes the possibility that it executes a known prefix before a later unknown command. That legacy case is explicitly outside the v1 composed-artifact acceptance contract, and it is not safety evidence. @@ -539,8 +542,8 @@ The new reader rejects a fragment supplied as a top-level replay artifact with ` Unknown future reference comments remain ordinary comments to an older reader. An unknown future executable guard fails before device work. -The old-reader evidence points to the actual unknown-command seam in `src/core/dispatch.ts`. -The current `src/core/__tests__/dispatch-keyboard.test.ts` does not prove this contract. +The current generic-dispatch evidence is `packages/daemon/src/execution/__tests__/generic-route-runtime-completeness.test.ts`. +Its unknown-command test proves the routing-gap response, not the proposed fragment format contract. The fragment codec implementation must add planted-red tests with these properties: - They run a guarded composition with `open` through the old dispatch path. @@ -676,8 +679,8 @@ The planted-red checks below are unit or fixture assertions. The slice that introduces each check owns it. They are not a claim that this docs-only spike already has a structural gate. -The old-reader dispatch seam is `src/core/dispatch.ts`. -The existing keyboard-dispatch test is not evidence for unknown-command rejection. +The current generic-dispatch seam is `resolveGenericRuntimeExecution` in `packages/daemon/src/execution/generic-runtime-execution.ts`. +Its completeness test does not prove the proposed old-reader fragment contract. 1. **Shared pure model and canonical fixtures.** Add typed composition nodes, fragment identity, invocation paths, local addresses, digest canonicalization, cycle/depth/size validation, and preflight result types without dispatch. From ef1ad66efe76ef5a77188b996f6a2b19cd01d31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:02 +0200 Subject: [PATCH 10/58] docs(adr): remove the dangling historical hard-reject pointer Item 10. git log -S hard-reject identifies 66fe801377 (#1177), which held the rejection rationale in Consequences, not Alternatives. The later structural-equivalence amendment superseded geometric mutation selection; 36957e8a1d rewrote the retained consequence. Remove only the dangling pointer in historical acceptance evidence. Do not restore an obsolete current selection rule. --- docs/adr/0012-interactive-replay.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0012-interactive-replay.md b/docs/adr/0012-interactive-replay.md index 8362cac3be..33f2d9e5ff 100644 --- a/docs/adr/0012-interactive-replay.md +++ b/docs/adr/0012-interactive-replay.md @@ -1468,7 +1468,7 @@ The 2026-08-07 structural-equivalence-or-reject amendment supersedes that policy Selector resolution used the opposite default. ADR 0011 classified runtime-selector `disambiguation` as `{ kind: 'runtime', via: '...selectors-resolve.ts#resolveSelectorChain' }` (`packages/contracts/src/interaction-guarantees.ts:176-179`). This proved consistent heuristic execution, not disclosure. - The acceptance-time design did not reconsider that default; its hard-reject alternative explains why. + The acceptance-time design did not reconsider that default. - **(f) Issue #1037 / PR #1040 provided partial precedent.** Apple Maps uniquely matched `text="Anthropic - Headquarters"` to a 30x30 pin instead of a recents row. `describeNonHittableTarget` exposed `targetHittable:false` and a hint (`resolution.ts:259-268`). It disclosed but did not prevent the incorrect tap. From fde65a2d19891c0af32f74b5fd6423bb8a3cf10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:02 +0200 Subject: [PATCH 11/58] docs(adr): align the proposed durable-resource amendment Item 11. ADR 0019 section 5 proposed amendment already requires native-path disposition, retention by fence generation until both facts settle, and a failed-finish test for each kind before its cleanup changes. ADR 0024 sections 2.4 and 2.5 already retain archived generations; glance rule 6 already has the per-kind test gate. Match section 2.7 to these constraints. Both ADRs remain Proposed for this amendment. --- .../adr/0024-screen-recording-provable-signal.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/adr/0024-screen-recording-provable-signal.md b/docs/adr/0024-screen-recording-provable-signal.md index 4619010989..bb0d587650 100644 --- a/docs/adr/0024-screen-recording-provable-signal.md +++ b/docs/adr/0024-screen-recording-provable-signal.md @@ -364,13 +364,15 @@ Section 5 currently has these requirements: - Uncertain cleanup stays `cleanup-pending`. - When finish throws, forced cleanup runs (in `transitions.ts`). -The amendment: -*Screen recording is a **stop-and-collect** resource. -It keeps the manifest, the fence on every destructive step, and the completed replay. -It replaces `reattach` and `cleanup` with the `stop(target)` and `collect` of the backend. -For this kind, it drops `cleanup-pending` as a phase and drops the admission ledger. -It records the recorder observation beside the committed export. -For every durable kind, forced cleanup is no longer inferred from a failed finish.* +The proposed amendment: +*Screen recording would be a **stop-and-collect** resource. +It would keep the manifest, the fence on every destructive step, and the completed replay. +Backend `stop(target, budget)` and `collect` would replace the `reattach` and `cleanup` facet operations. +This kind would remove the `cleanup-pending` phase and admission ledger. +It would record the recorder observation and native-path disposition beside the committed export. +A manifest would remain archived by fence generation until both are settled. +For each durable kind, failed finish would no longer imply forced cleanup once its failed-finish test states its retry requirements. +App-log, audio-probe, and perf-capture would retain the ADR 0019 contract unchanged.* ### 2.8 Android duration (separate decision) From 821e9a78566c16f81f3dc06ebd2db69c38333166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 12/58] docs(adr): point exhaustive runtime facts to section 2 Item 12. ADR 0019 section 2 Runtime use joins facts and narrows the bound runtime contains the exhaustive per-device-shape fact rule. Section 3 owns provider resolution. Change the citation only. --- docs/adr/0028-capability-family-cell-vocabulary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0028-capability-family-cell-vocabulary.md b/docs/adr/0028-capability-family-cell-vocabulary.md index 21389373e4..b567b8930a 100644 --- a/docs/adr/0028-capability-family-cell-vocabulary.md +++ b/docs/adr/0028-capability-family-cell-vocabulary.md @@ -48,7 +48,7 @@ This ADR proposes these rules: - One runtime row per capability family owns its cell key. The table derives the cell-key type instead of a separately maintained type. -- Each runtime owner still reports exhaustive facts for its exact device shape under [ADR 0019](0019-request-bound-platform-runtime.md), section 3. +- Each runtime owner still reports exhaustive facts for its exact device shape under [ADR 0019](0019-request-bound-platform-runtime.md), section 2. Derivation may enumerate cells. It must not derive an owner's answer for a cell. - There is no cross-family default denial set. From 7e3ac80f9c1b4ed562f81dc4b888d8cc546e931a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 13/58] docs(adr): cite the withdrawn allocator design for its historical move Item 13. ADR 0021 section 3 now states consequences, including deletion of managed-allocation. Section 4 records the withdrawn allocator design and its ownership. 4565606edb (#3287) is the historical collocation move; retain the historical row and point its rationale to section 4. --- docs/adr/0033-collocation-decision-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0033-collocation-decision-table.md b/docs/adr/0033-collocation-decision-table.md index 1590bbb700..e89735d208 100644 --- a/docs/adr/0033-collocation-decision-table.md +++ b/docs/adr/0033-collocation-decision-table.md @@ -63,7 +63,7 @@ All its consumers occupy one zone that already owns the operations represented b | `src/device-boot.ts` | keep | Device-selection, platforms, and daemon-server consume this boot-phase vocabulary. | | `src/facades/device.ts` | keep | This is the published facade of the contracts package. Facades are the package boundary, not movable content. | | `src/interaction-guarantees.ts` | keep | AGENTS.md names this declaration site for interaction dispatch paths and guarantee cells under ADR 0011. Cross-language golden tables in `contracts/fixtures/` bind it to this package. | -| `src/managed-device-allocation.ts` | move (batch 1, landed in #3287; package removed 2026-10-10) → `packages/managed-allocation` | Only managed-allocation and its dependent daemon composition consume this port and vocabulary; see [ADR 0021](0021-host-simlock-managed-device-allocation.md) §3. No other contracts file or zone imports it. Types therefore belong beside their implementing operations. | +| `src/managed-device-allocation.ts` | move (batch 1, landed in #3287; package removed 2026-10-10) → `packages/managed-allocation` | Only managed-allocation and its dependent daemon composition consume this port and vocabulary; see [ADR 0021](0021-host-simlock-managed-device-allocation.md) §4, the withdrawn design. No other contracts file or zone imports it. Types therefore belong beside their implementing operations. | | `src/wait-runtime-plan.ts` | keep | Command-registry and daemon-server share it. Both are spine zones; contracts is the only seam below them. | ### device-selection From 5fede4d07b6d7978e4fb3be2e5815e9a13fa40f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 14/58] docs(adr): record the coupling ban and its landing date Item 14. 1612e3372d (#3431) and b9005d8334 (#3430) both landed on 2026-10-12. The latter removed the remaining selector coupling and replaced R76 classification machinery with a ban. Status now matches Decision and enforcement; do not claim a current classification inventory. --- docs/adr/0022-daemon-platform-runtime-coupling.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0022-daemon-platform-runtime-coupling.md b/docs/adr/0022-daemon-platform-runtime-coupling.md index 94ed5119b5..191b5ec63a 100644 --- a/docs/adr/0022-daemon-platform-runtime-coupling.md +++ b/docs/adr/0022-daemon-platform-runtime-coupling.md @@ -3,10 +3,11 @@ ## Status Accepted (2026-09-06). This ADR implements issue #2278's audit deliverables. -Gates enforce the classification inventory and structural no-regrowth checks. -The ADR records decisions and references gates instead of duplicating the inventory. +R76 prohibits daemon coupling to root platform-runtime modules. +Gates enforce structural no-regrowth checks. +The ADR records decisions and references gates instead of duplicating their checks. -Amended 2026-10-11 by #3431. +Amended 2026-10-12 by #3431 and #3430. Daemon-runtime composition edges moved from `packages/daemon/src/` to process-root `src/daemon-composition.ts`: - Gateway, diagnostics, resource-cleanup, and lifecycle edges. From 29c74f8e64458102c34052c8a528e187ec762e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 15/58] docs(adr): record the active shape rule and shared test owner Items 15 and 16. Root src/__tests__/install-source.test.ts exercises shared provision-kit behavior and family adapters; cli-install-from-source.test.ts exercises the CLI boundary. Add the missing body owner matching the existing glance. efd210815a (#2808) added the no-fact-cell rule; 746e9b3f63 (#2806) enforces shape refusal at the web owner, pinned by packages/platform-web/src/runtime.test.ts. Mark the rule in force and correct its section pointer to section 2. Item 11: the section 5 proposed amendment is already correct and remains unchanged. --- docs/adr/0019-request-bound-platform-runtime.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0019-request-bound-platform-runtime.md b/docs/adr/0019-request-bound-platform-runtime.md index a825c62316..44e35a55cf 100644 --- a/docs/adr/0019-request-bound-platform-runtime.md +++ b/docs/adr/0019-request-bound-platform-runtime.md @@ -13,8 +13,9 @@ It extends `host` beyond diagnostics to host-scoped platform work through a narr Host diagnostics, daemon-owner resource cleanup, and managed host tooling share this execution category because none binds a request-scoped device runtime. Their domain services remain separate and do not form a generic host grab bag. -An amendment proposed on 2026-09-23 accompanies the web press-shape refusal. -It states in section 3 that a modifier on an admitted operation is not a second operation and therefore has no fact cell. +The amendment of 2026-09-23 accompanies the web press-shape refusal (#2806 and #2808). +Its rule is in force. +It states in section 2 that a modifier on an admitted operation is not a second operation and therefore has no fact cell. A per-shape refusal on the owning leaf mechanic rests on that statement. Without that statement, the next contributor can reasonably reach for a per-shape fact key. The operation-facts mapped type cannot express such a key. @@ -147,6 +148,7 @@ They may not import these items: R13 covers static, type-only, dynamic, and re-export edges. Package-owned tests may import their own public façade. +Root `src/__tests__/` owns shared install-source tests, including `install-source.test.ts` and `cli-install-from-source.test.ts`. Contracts may depend on kernel vocabulary. Contracts never depend on concrete platform packages or daemon implementation types. From b86af9f73492efd614003d2506cd4052461f3bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 16/58] docs(adr): correct the direction of the superseded hop comparison Item 17. The ADR trace counts 41 distinct files for press/Android and 30 shared plus 17 AX / 19 runner for snapshot/iOS (47/49). Both exceed the superseded 38/29. The facade PRs predate e624ef9d3f, so they cannot explain the difference; the old count is not higher. --- docs/adr/0023-end-state-hop-trace.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0023-end-state-hop-trace.md b/docs/adr/0023-end-state-hop-trace.md index 3dd528e9c8..5b3269dd3a 100644 --- a/docs/adr/0023-end-state-hop-trace.md +++ b/docs/adr/0023-end-state-hop-trace.md @@ -343,7 +343,7 @@ The builders are in `packages/contracts/src/*-runtime.ts`. They are side-calls o The plan sheet suggested façade PRs #2178, #2222, and #2232 as a possible explanation. All merged before `e624ef9d3f`, the commit cited for the ADR's 38/29 measurement. -Thus, they cannot explain why that count is higher than this one. +Thus, they cannot explain the difference between 38/29 and the current 41 and 47/49 counts. They were already in effect when 38/29 was recorded. This trace measures 41 hops for `press`/Android and 47/49 per arm for `snapshot`/iOS. From 908584785f9dead17e1c61cfd4058d6367afb070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 17/58] docs(adr): date hinge-stream evidence by toolchain Item 18. 057dfb7c1f (2026-09-20) recorded the initial toolchain examination; e8da1e10d6 (2026-09-21, #2717) and 6cb160a632 (#2734) recorded a working stream. 590f171c33 (2026-10-14, #3587) records no motion subcommand in devicectl 651.13.5 / Xcode 27.1 build 27A9275, then called RC, and adds dispatched-unverified refusal. Read-only host check on 2026-10-16: xcodebuild -version reports Xcode 27.1, 27A9275; devicectl --version reports 651.13.5; device help lists no motion subcommand. Preserve dated successes without claiming every 27.1 build supports the stream. Sync the glance to existing no-sample behavior. No device state changed. --- docs/adr/0025-foldable-apple-panels.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/adr/0025-foldable-apple-panels.md b/docs/adr/0025-foldable-apple-panels.md index 045d7a5191..054495b1dc 100644 --- a/docs/adr/0025-foldable-apple-panels.md +++ b/docs/adr/0025-foldable-apple-panels.md @@ -24,8 +24,9 @@ Only inference answers the second question. | Zero or multiple lit panels | Name the `primary` panel. Emit `apple_display_capture_ambiguous`. Report pose as `unknown`. | | One integrated panel | Preserve pre-panel behavior exactly: no display flag, no pose, unchanged scale probe. | | Density normalization | Use the captured panel's `pointScale`. For runner fallback, use that capture's reported pixels-per-point. | -| Pose report | From panel power alone, report only `closed`, `fully-open`, or `unknown`, never a narrower pose. `fold` reports the exact pose because it reads the hinge angle. | +| Pose report | From panel power alone, report only `closed`, `fully-open`, or `unknown`, never a narrower pose. `fold` reports an exact pose only after hinge readings verify it. | | Pose request | `agent-device fold ` sends private HID inside the simulator. Then it reads the hinge angle back from CoreDevice until the angle agrees. For `half-open`, two consecutive readings must agree. If the readings never agree, refuse the pose. | +| The first hinge read returns no sample | Refuse with `fold-pose-dispatched-unverified`. Panel power does not verify the pose. | | The hinge reaches `half-open` but keeps moving | Refuse with `fold-pose-unsettled` and the observed and previous angles. An angle inside the open interval is an observed category, not a pose that the hinge holds. | | External display | It is not a panel. It creates neither multi-screen classification nor a pose. | | CoreDevice cannot answer | Return unresolved inventory. Preserve single-panel capture. An absent host feature is not a capture failure. | @@ -230,12 +231,16 @@ The split above still holds: the press is not evidence, and the read-back is evi The headless HID amendment above replaces the Device Hub press. Git history retains the superseded Device Hub process. -`xcrun devicectl device motion hinge-angle --device ` streams the Duo simulator hinge angle over `Range:0-180°`. +In the 2026-09-21 run, `xcrun devicectl device motion hinge-angle --device ` streamed the Duo simulator hinge angle over `Range:0-180°`. Measured presets are Closed 0°, Book 130°, and Open 180°. `--session-timeout` does not stop the stream. Each read therefore uses devicectl's own `--timeout`, whose minimum accepted value is 5 seconds. The sample printed before timeout is the reading. -This read path was checked on the shipping 27.1 toolchain. +This run did not establish support in every 27.1 toolchain. +On 2026-10-16, the host reports Xcode 27.1 build 27A9275 and devicectl 651.13.5. +Its `device` help lists no `motion` subcommand. +The 2026-10-14 fix (#3587) records the same limitation for that build, then identified as 27.1 RC. +Without a sample, the pose remains unconfirmed as described below. `closed` and `open` are hinge end stops. One read at the stop proves the pose. @@ -286,7 +291,7 @@ Attempt count, per-read timeout, cancellation, and open/closed end-stop rules re An unsettled fold still consumes four hinge streams. If the first hinge read returns no sample, the pose remains unconfirmed regardless of cause. -For example, devicectl 651.13.5 in Xcode 27.1 RC build 27A9275 lacks `device motion`. +The 2026-10-14 evidence identifies devicectl 651.13.5 in Xcode 27.1 build 27A9275 as an example without `device motion`. An unreachable device also prints no sample. HID has already dispatched, so refusal uses `fold-pose-dispatched-unverified`. This distinguishes unconfirmed dispatch from an observed contradictory pose. @@ -311,8 +316,8 @@ The shipping-toolchain investigation recorded these candidate results: | `simctl` subcommands | No `hinge`/`fold`/`pose` surface; No such token in the `simctl` binary or in `CoreSimulator.framework` | | `simctl io` | `screenshot`/`recordVideo` take `--display`; `screenConfig` sets `power` and `geometry` per screen — Neither changes hinge state. | | XCUITest | No hinge token in any `XCUIAutomation` header; `XCUIDeviceButton` is Home/VolumeUp/VolumeDown/Action/Camera only | -| CoreDevice | `com.apple.coredevice.action.streamhingeangle`, `HingeAngleManager`, `DeviceHingeAngleSnapshot` and `HingeAngleStreamConfig` exist in `/Library/Developer/PrivateFrameworks/CoreDeviceUtilities.framework` (and `dtdeviceinfod`), They **stream** an angle. The Duo reports no such feature. `devicectl` exposes no CLI for it. | -| `devicectl` | `device info displays` reports panel state only | +| CoreDevice | The initial 2026-09-20 examination found `com.apple.coredevice.action.streamhingeangle`, `HingeAngleManager`, `DeviceHingeAngleSnapshot`, and `HingeAngleStreamConfig` in `/Library/Developer/PrivateFrameworks/CoreDeviceUtilities.framework` and `dtdeviceinfod`. The Duo then reported no such feature. The 2026-09-21 run above later observed a CLI stream. | +| `devicectl` | `device info displays` reports panel state only. Hinge-stream availability depends on the toolchain; the 2026-10-16 host examination above found no `device motion`. | | `Xcode.app` frameworks | No hinge reference in `Xcode.app/Contents/{SharedFrameworks,Frameworks}`, Device Hub control is therefore not a hinge API shipped inside Xcode. | Two searches came back empty, not negative. This ADR records them as limits, not as refuted claims: From 831b62ab6f458f9d8e2a1d288d9cd880a39a704d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 18/58] docs(adr): describe the permitted capture-only second bind Item 19. ADR 0019 section 9 permits one second bind for --settle when the action use has no capture, after facts admission. The second bind is capture-only; captureSnapshot in the action use reuses the cached binding. Correct the fixture comment only. --- .../__tests__/support/handlers/snapshot-handler.fixtures.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts index 39c66efea8..a86a337aab 100644 --- a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts +++ b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts @@ -97,8 +97,8 @@ export function snapshotRequest( } /** - * The suite's snapshot runtime with a bind counter, so a test can assert whether the handler - * bound the device at all (ADR 0019 §9: at most one bind per request). + * The suite's snapshot runtime counts device binds. + * ADR 0019 §9 permits one execution bind and a second, capture-only bind for `--settle`. */ export function countingSnapshotRuntime(): Readonly<{ inspectFacts: InspectDeviceRuntimeFacts; From 7e572b5aecf3db0b4e259af33155765ba0f38d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:13:03 +0200 Subject: [PATCH 19/58] docs(adr): date the completed root-pass move correction Item 20. gh issue view 3294: CLOSED 2026-10-08T07:57:29Z. gh issue view 3281: CLOSED 2026-10-07T18:48:47Z. 8c5bce0806 (#3297) moved daemon diagnostics to src/daemon-contracts; 75b414a17b (#3299) moved the assembly pair into src/command-runtime. Current owners are packages/daemon-contracts/src/daemon-diagnostics-scope.ts and src/command-runtime/{runtime-command-surface,runtime-factory}.ts. Add a 2026-10-16 correction to the dated observation without declaring every root move complete. --- docs/dependency-graph-findings.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 159018d69d..46d2494e54 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -268,7 +268,11 @@ moving `DaemonRequest` is most of what §1's second cluster needs. > `bin.ts`, `cli.ts` and `daemon.ts`, and R80 asserts that the zone graph of static value imports is > acyclic. That reduces `(root)` in the logical graph only: the modules still live directly under > `src/`, and the declaration is a bridge whose rows go as groups move into zone folders or -> packages. The physical moves remain outstanding, starting with #3294. Ranking the composition roots did not weaken R2: it still rejects a direct `daemon/` -> +> packages. Correction (2026-10-16): #3294 and #3281 are closed. +> PR #3297 moved daemon diagnostics to `src/daemon-contracts/`. +> PR #3299 moved the runtime assembly pair to `src/command-runtime/`. +> Package extraction later moved daemon diagnostics to `packages/daemon-contracts/`. +> Ranking the composition roots did not weaken R2: it still rejects a direct `daemon/` -> > `commands/` import, and the daemon reaches the command surface through the ranked > `command-runtime` zone, which the zone graph now shows instead of hiding. The SDK entries rank > above `daemon-client`, because the typed client they publish reaches the daemon through it. From f4c77fd12a681f94dcd67301a8c290abafcc042a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 20/58] docs(adr): record daemon trait enforcement gaps M1. Registry tests pin the derived table; types do not fully enforce invariants 3 and 4. Keep both invariants required and synchronize Rules at a glance. Evidence: packages/command-registry/src/types.ts:213 makes daemon optional. DaemonCommandDescriptor requires command and route but makes the other traits optional. packages/daemon/src/core/daemon-command-registry.ts:34-35 defaults missing descriptors to generic. src/cli/command-explain.ts:205-210 reads daemon traits in the public CLI projection. Registry policy pins are in packages/daemon/src/core/__tests__/daemon-command-registry.test.ts. Maintainer follow-up: determine how types should enforce invariant 4 without weakening the accepted declaration boundary. --- docs/adr/0003-daemon-command-registry.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/adr/0003-daemon-command-registry.md b/docs/adr/0003-daemon-command-registry.md index 341968f139..cb60c57bc9 100644 --- a/docs/adr/0003-daemon-command-registry.md +++ b/docs/adr/0003-daemon-command-registry.md @@ -12,7 +12,7 @@ Accepted - Request modules consume named predicates from the registry. They do not recreate command string sets. - Handler modules own execution logic only. They do not export duplicate coverage tables to prove route membership. - The daemon registry must not define CLI grammar, Node.js client options, MCP schemas, user-facing help, or platform capability support. -- Preserve all four invariants in the 2026-06 update. The CLI explanation has the known invariant-3 gap described below. +- Preserve all four invariants in the 2026-06 update. Types do not fully enforce invariants 3 and 4, as described below. ## Context @@ -93,7 +93,8 @@ It derives these surfaces from that site: - Daemon registry. Its purpose is to remove the drift between these tables. -Types and registry tests enforce the declarations and projections. +Registry tests pin the derived tables. +Types do not fully enforce invariants 3 and 4. **This ADR's decision remains accepted.** Its boundary concerns ownership and the predicate interface, not the physical file that contains a trait. @@ -129,3 +130,9 @@ The daemon registry tests pin route and request-policy decisions. `src/cli/command-explain.ts` reads that facet and displays daemon routes and request-policy traits. Thus, types do not enforce invariant 3 for this CLI projection. The invariant remains required; this ADR records the gap without changing runtime behavior. + +**Known invariant-4 gap.** `CommandDescriptorBase.daemon` is optional in `packages/command-registry/src/types.ts`. +In `DaemonCommandDescriptor`, every trait except `command` and `route` is optional. +If a command has no daemon descriptor, `getDaemonCommandRoute` returns `generic`. +Thus, types do not require every daemon trait at the registration site. +Invariant 4 remains required; this ADR records the gap without changing runtime behavior. From 260a8361dc26ff5f0b13ab49c557f996e3579b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 21/58] docs(adr): describe pending bridge preparation precisely L6. Describe a bridge preparation that is still running. Do not invent a preparation queue. Evidence: packages/platform-apple/src/snapshot-source/preparation.ts:119-123 reports bridge-preparation-pending through attempts.value. packages/platform-apple/src/snapshot-route.ts:314-315 excludes preparing failures from the generation circuit. The existing glance exception already agrees. --- docs/adr/0004-ios-snapshot-backend-strategy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0004-ios-snapshot-backend-strategy.md b/docs/adr/0004-ios-snapshot-backend-strategy.md index 21eafa530f..65f87a7da4 100644 --- a/docs/adr/0004-ios-snapshot-backend-strategy.md +++ b/docs/adr/0004-ios-snapshot-backend-strategy.md @@ -54,7 +54,7 @@ Route eligible local iOS Simulator snapshots through the host AX bridge. Present them once through the shared TypeScript engine. If bridge acquisition or presentation fails, use one typed XCTest fallback. After fallback, disable the bridge for that app generation, except for `preparing` failures and the capture-scoped exception below. -A `preparing` failure describes the bridge preparation queue, not the app. +A `preparing` failure describes a bridge preparation that is still running, not the app. It sends only that capture to XCTest. A new app generation enables it again. Physical devices, providers, custom-action captures, and interactions remain on their existing owners. From 282426be7d3457e16362ae81683a444f7947b681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 22/58] docs(adr): restore client result migration prerequisites M2, L1, L2. Restore the contract-move prerequisite and its reason. State the full extent of broad client results. Retain the historical client path and restore prerequisite wording without the unrelated ADR 0010 pointer. Synchronize the client-contract rule in Rules at a glance. Evidence: src/client/client-types.ts has 21 Promise methods, including alert, reactNative, and interactions.swipe. packages/command-registry/src/command-result.ts:41-47 documents dynamic payload exclusions; its result map remains nonexhaustive. packages/daemon/src/execution/interaction/internal/interaction-gesture.ts:59-74 retains local result types and Record payloads. Git commit 189b062519 (#960) moved src/client-types.ts into src/client/. gh issue view 1153 confirms CLOSED at 2026-07-08T16:35:29Z. --- docs/adr/0008-command-descriptor-registry.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/adr/0008-command-descriptor-registry.md b/docs/adr/0008-command-descriptor-registry.md index c74bf6d1d1..c2f19c99b0 100644 --- a/docs/adr/0008-command-descriptor-registry.md +++ b/docs/adr/0008-command-descriptor-registry.md @@ -25,6 +25,7 @@ Completed process history is in git history. - Keep the cross-process client `invoke` seam and the in-daemon `execute` seam distinct. Never collapse the process boundary. - Preserve all four invariants from ADR 0003. - The descriptor module must own the `Input`/`Result` types. The client must be derived as a view type, and a lint boundary enforces this. +- Before client methods can stop returning `CommandRequestResult`, move the result contracts described in Consequences. ## Context @@ -36,7 +37,7 @@ Before the descriptor migration, roughly ten manually maintained tables repeated - Daemon command registry in `packages/daemon/src/core/daemon-command-registry.ts` (ADR 0003). - Structured-batch allowlist in `src/batch-policy.ts`. - MCP exposure sets. -- Node client interface and implementation in `src/client/client-types.ts` and `src/client.ts`. +- Node client interface and implementation in `src/client-types.ts` and `src/client.ts`. - Generic-dispatch `switch` in `src/core/dispatch.ts`. The switch's `default: throw` makes a missing or renamed command a runtime error instead of a compile error. @@ -100,7 +101,7 @@ This design composes with ADR 0003's four invariants and is bound by them: A plain new command changes approximately 1–2 files. Per-platform behavior remains N implementations behind the descriptor's `execute`. -The descriptor supports typed per-command results in ADR 0010. +The descriptor is the prerequisite for typed per-command results. `src/client/client-types.ts` retains the public client interface and some `CommandRequestResult` return types. It also supplies the capability facet used by the platform-plugin work in ADR 0009. @@ -130,7 +131,10 @@ Coherence checks compare it with the descriptor CLI catalog. Thus, new surface names cannot differ from the descriptor root. [#1153](https://github.com/callstack/agent-device/issues/1153), the former tracker for public Node-client typed-result narrowing, closed on 2026-07-08. -Some client methods still return `CommandRequestResult`, including `alert` and `reactNative` in `src/client/client-types.ts`. +In `src/client/client-types.ts`, 21 client methods still return `CommandRequestResult`. +They include `alert`, `reactNative`, and `interactions.swipe`. +Some commands still expose dynamic daemon response payloads or retain closed result contracts in runtime modules. +Before their client methods can stop returning `CommandRequestResult`, those contracts must move. Issue closure does not establish complete result narrowing. This ADR owns the decision and its constraints. From 0eee6ee94ca6ac6a8bc31fa4dbaf953311711ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 23/58] docs(adr): restore the retained rejection rationale pointer L7. Repoint the historical hard-reject rationale to Consequences instead of deleting the useful pointer or restoring an obsolete alternative. Evidence: git log -S hard-reject identifies 66fe801377 (#1177), 36957e8a1d, and 93620261cc. The current Consequences paragraph at line 1356 retains the reason blanket rejection is unsuitable: RN row/button/text wrapper chains. The structural-equivalence amendment remains authoritative. --- docs/adr/0012-interactive-replay.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/adr/0012-interactive-replay.md b/docs/adr/0012-interactive-replay.md index 33f2d9e5ff..b4d1771bce 100644 --- a/docs/adr/0012-interactive-replay.md +++ b/docs/adr/0012-interactive-replay.md @@ -1469,6 +1469,7 @@ The 2026-08-07 structural-equivalence-or-reject amendment supersedes that policy ADR 0011 classified runtime-selector `disambiguation` as `{ kind: 'runtime', via: '...selectors-resolve.ts#resolveSelectorChain' }` (`packages/contracts/src/interaction-guarantees.ts:176-179`). This proved consistent heuristic execution, not disclosure. The acceptance-time design did not reconsider that default. + The structural-equivalence rationale in Consequences explains why blanket rejection remains unsuitable. - **(f) Issue #1037 / PR #1040 provided partial precedent.** Apple Maps uniquely matched `text="Anthropic - Headquarters"` to a 30x30 pin instead of a recents row. `describeNonHittableTarget` exposed `targetHittable:false` and a hint (`resolution.ts:259-268`). It disclosed but did not prevent the incorrect tap. From b90d8acdcd3c5dc5f72265f4d8c77468b35dc3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 24/58] docs(adr): include replay and publication gesture codec callers L3. Add the two omitted consumers of the positional drag codec. Evidence: packages/replay-port/src/daemon-port/target-token.ts:13 calls dragGesturePayloadFromPositionals to select a source or destination target. packages/daemon/src/core/session-script-active-publication.ts:129 calls it through readRecordedDrag; assertPortableDragBindings at lines 102-123 validates portable endpoints and recorded target evidence before publication. No syntax or migration rule changes. --- docs/adr/0013-unified-gesture-plans.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/adr/0013-unified-gesture-plans.md b/docs/adr/0013-unified-gesture-plans.md index 4ea742afed..657ab31298 100644 --- a/docs/adr/0013-unified-gesture-plans.md +++ b/docs/adr/0013-unified-gesture-plans.md @@ -190,6 +190,8 @@ It is not scheduled for removal (amended 2026-07, issue #1216). Its callers include the CLI argv parse, the `.ad` line parse, and the recording path. `packages/daemon/src/execution/interaction/internal/interaction-gesture.ts` encodes gesture inputs for recordings. `packages/daemon/src/core/session-script-writer.ts` decodes recorded drag endpoints. +`packages/replay-port/src/daemon-port/target-token.ts` decodes drag endpoints to select the replay target token. +`packages/daemon/src/core/session-script-active-publication.ts` decodes drag endpoints to validate target evidence before script publication. These are current syntax and recording consumers, so nothing is left to migrate off. A structured `.ad` gesture payload was rejected. It would make recordings unreadable and impossible to search with grep, for no behavioral gain. From 047b8dd2787b41fbe1e8d51e36fde5d8c26cea73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:48 +0200 Subject: [PATCH 25/58] docs(adr): trace old-reader guard failures without a codec promise M3. Keep failure before platform execution as the proposed safety property. Record the session-dependent guard-request errors and the unresolved INVALID_ARGS requirement. A future codec cannot change an old reader. Keep Rules at a glance and both body descriptions aligned. Evidence path: packages/ad-script/src/internal/script.ts:270-283,483 accepts an unknown command as an action. packages/ad-replay/src/internal/verify-dispatch.ts:65,280-287 dispatches the unannotated action. packages/replay-port/src/daemon-port/session-replay-runtime-engine-adapter.ts:221-237 calls invokeReplayAction. session-replay-action-runtime.ts:171,199 preserves the command. packages/daemon/src/server/request-router.ts:407,658-662 returns unknown replay actions to the full request path because core/daemon-command-registry.ts:58-59 has no replayScopedAction trait for them. After admission, request-router.ts:346,393 reaches sessions/request-handler-chain.ts:127 and core/daemon-command-registry.ts:34-35. The unknown command routes to generic; the handler at request-handler-chain.ts:110-112 returns null. request-router.ts:596-598 returns noActiveSessionError with no session; packages/kernel/src/contracts.ts:263-264 sets SESSION_NOT_FOUND. With a session, request-router.ts:602-617 reaches execution/generic-runtime-execution.ts:116-123, which returns COMMAND_FAILED / generic-route-runtime-missing before dispatchGenericCommand at request-router.ts:619-620. The replay adapter wraps the failure through session-replay-runtime-engine-adapter.ts:295-303,384-398 and session-replay-runtime-failure.ts:106. session-replay-runtime-failure-response.ts:108 emits REPLAY_DIVERGENCE. packages/ad-replay/src/internal/step-loop.ts:178-185 stops the loop before later open or include actions. These are the current paths after admission, not universal error guarantees for every historical release. The existing generic-route completeness test proves only the resolver branch. Maintainer question: accept the existing old-reader errors, or keep INVALID_ARGS as an unresolved requirement? --- .../adr/0020-composable-recorded-fragments.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/adr/0020-composable-recorded-fragments.md b/docs/adr/0020-composable-recorded-fragments.md index 8f88a7ba78..1551bedef0 100644 --- a/docs/adr/0020-composable-recorded-fragments.md +++ b/docs/adr/0020-composable-recorded-fragments.md @@ -67,8 +67,9 @@ Consequently, the design must solve four boundaries together: The entry guard and element actions reuse ADR 0012's `target-v1`/`targets-v1` annotations and verification. Every v1 native fragment and composed parent starts with a required first-executable format guard (`fragment-v1` or `composition-v1`). A new reader consumes that guard during preflight. - An old reader treats the guard as an unknown action and rejects it before dispatch. - Thus the old reader cannot execute a root `open` or a known prefix before it reaches `include`. + An old reader treats the guard as an unknown action. + The required safety property is failure before platform execution, before a root `open` or any known prefix. + The current request path does not prove an `INVALID_ARGS` format rejection; see Annotation compatibility and versioning. Other fragment metadata and references remain reserved comments. Thus old readers ignore those annotations, and do not silently flatten the composition. 7. **Native fragment repair is explicit re-recording.** @@ -99,8 +100,8 @@ But these invariants are fixed: It is a format envelope, not a `SessionAction`, and it has no device side effect. A composed native parent uses the corresponding `composition-v1` guard as its first executable line, before root `open` or any `include`. An old reader parses either guard as an unknown action. - The generic-dispatch owner would reject the unknown guard before platform execution. - The fragment codec must prove the required `INVALID_ARGS` format rejection through that path. + The required safety property is failure before platform execution. + The current request path returns session-dependent errors; see Annotation compatibility and versioning for the evidence gap. The fragment has no `open`, `close`, or hidden launch action. The writer always emits the effective `context platform` and `target`. A parser rejects a fragment that is missing either constraint. @@ -526,23 +527,38 @@ No error projection is allowed to collapse a fragment failure to a parent global The first entry `wait` and every element-targeting action continue to carry the existing `target-v1`/`targets-v1` evidence. That evidence keeps its size limits, canonical field order, and fail-closed `verification` behavior. Readers that do not know the reference annotation ignore that comment. -Readers that do not know the executable guard reject the artifact at its format boundary. +Readers that do not know the executable guard must fail before platform execution. The new reader consumes the native guard during source preflight. An old reader can get either a v1 fragment or a composed parent. It parses the guard as an ordinary action. -The current generic-dispatch owner is `resolveGenericRuntimeExecution` in `packages/daemon/src/execution/generic-runtime-execution.ts`. -Its default returns `COMMAND_FAILED` with reason `generic-route-runtime-missing` before platform execution. -The proposed fragment codec must establish the required `INVALID_ARGS` format rejection before any platform handler runs. -Then the old reader would not execute a root `open`, a known prefix, or an expanded child body. +The current parser is `parseReplayScriptLine` in `packages/ad-script/src/internal/script.ts`. +It accepts an unknown command name without format validation. +The replay engine dispatches that action through `invokeReplayAction` in `packages/replay-port/src/daemon-port/session-replay-action-runtime.ts`. +An unknown command has no `replayScopedAction` trait, so `createReplayScopedActionInvoker` uses the full request path. +`getDaemonCommandRoute` selects `generic`, and `runRequestHandlerChain` returns no handler response. + +After request admission succeeds, `dispatchGenericForLockedScope` in `packages/daemon/src/server/request-router.ts` checks the session first. +If no session exists, it returns `SESSION_NOT_FOUND` through `noActiveSessionError`. +If a session exists, it calls `resolveGenericRuntimeExecution` in `packages/daemon/src/execution/generic-runtime-execution.ts`. +For an unknown guard, that resolver returns `COMMAND_FAILED` with reason `generic-route-runtime-missing` before platform execution. +These are responses for the guard request, not the outer replay response. +The replay adapter wraps this dispatch failure as `REPLAY_DIVERGENCE`. +The replay engine stops at the failed action before a later root `open`, known prefix, or expanded child body. + +**Open evidence gap:** this trace proves failure before platform execution for an unknown guard that reaches generic dispatch. +It does not prove the specified `INVALID_ARGS` format rejection for old readers. +A new fragment codec cannot change an old reader's behavior. +**Maintainer question:** should the old-reader contract accept these existing errors, or retain `INVALID_ARGS` as an unresolved requirement? An old reader that gets an unguarded legacy `.ad` retains its existing behavior. This includes the possibility that it executes a known prefix before a later unknown command. That legacy case is explicitly outside the v1 composed-artifact acceptance contract, and it is not safety evidence. The new reader rejects a fragment supplied as a top-level replay artifact with `fragment_composition_required`. Unknown future reference comments remain ordinary comments to an older reader. -An unknown future executable guard fails before device work. +An unknown future executable guard has the same required safety property. -The current generic-dispatch evidence is `packages/daemon/src/execution/__tests__/generic-route-runtime-completeness.test.ts`. +`packages/daemon/src/execution/__tests__/generic-route-runtime-completeness.test.ts` verifies the resolver's unknown-command branch. +It does not exercise the full replay path or the missing-session branch. Its unknown-command test proves the routing-gap response, not the proposed fragment format contract. The fragment codec implementation must add planted-red tests with these properties: From 449b595dcbd5df4b7c1b80c22ab1e200abdd07a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:49 +0200 Subject: [PATCH 26/58] docs(adr): scope hop counts to their recorded trace L9. Attribute 41 and 47/49 to this trace, without claiming that they are current repository counts. Evidence: the Counting definition names commit 27a97ee619 and the retained trace tables measure press/Android and the two snapshot/iOS arms. Only the attribution changes; the counts and scope remain unchanged. --- docs/adr/0023-end-state-hop-trace.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0023-end-state-hop-trace.md b/docs/adr/0023-end-state-hop-trace.md index 5b3269dd3a..9f5115b893 100644 --- a/docs/adr/0023-end-state-hop-trace.md +++ b/docs/adr/0023-end-state-hop-trace.md @@ -343,7 +343,7 @@ The builders are in `packages/contracts/src/*-runtime.ts`. They are side-calls o The plan sheet suggested façade PRs #2178, #2222, and #2232 as a possible explanation. All merged before `e624ef9d3f`, the commit cited for the ADR's 38/29 measurement. -Thus, they cannot explain the difference between 38/29 and the current 41 and 47/49 counts. +Thus, they cannot explain the difference between 38/29 and this trace's 41 and 47/49 counts. They were already in effect when 38/29 was recorded. This trace measures 41 hops for `press`/Android and 47/49 per arm for `snapshot`/iOS. From d9edbc866adad09fec122d7c48ac35e6f5e00308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:49 +0200 Subject: [PATCH 27/58] docs(adr): retain the outstanding physical module moves L4. Retain the dated corrections for completed moves and explicitly state that other physical moves remain outstanding as of 2026-10-16. Evidence: scripts/layering/root-module-zones.ts:9-14 states that logical classification is not physical collocation and that the table should shrink. Its core, command-runtime, and platform-runtime entries still name root src/*.ts modules. No layering rule or completed-move claim changes. --- docs/dependency-graph-findings.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 46d2494e54..ca2e55c751 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -272,6 +272,7 @@ moving `DaemonRequest` is most of what §1's second cluster needs. > PR #3297 moved daemon diagnostics to `src/daemon-contracts/`. > PR #3299 moved the runtime assembly pair to `src/command-runtime/`. > Package extraction later moved daemon diagnostics to `packages/daemon-contracts/`. +> Other physical moves remain outstanding, as shown by `scripts/layering/root-module-zones.ts` on 2026-10-16. > Ranking the composition roots did not weaken R2: it still rejects a direct `daemon/` -> > `commands/` import, and the daemon reaches the command surface through the ranked > `command-runtime` zone, which the zone graph now shows instead of hiding. The SDK entries rank From 17a25c653560e25eaa603ca651cc72d34705c775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:35:49 +0200 Subject: [PATCH 28/58] docs(adr): qualify the capture-only second bind L5. Add the missing condition to the fixture comment: the settle observation may acquire a second capture-only bind only when the action use has no capture. Evidence: docs/adr/0019-request-bound-platform-runtime.md:762-768 permits that second bind and requires reuse when the action already requires captureSnapshot. This commit changes only the comment. Validation: pnpm format passed. All five fixture consumer suites and generic-route-runtime-completeness.test.ts passed: 6 files, 58 tests. --- .../__tests__/support/handlers/snapshot-handler.fixtures.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts index a86a337aab..8f81b24975 100644 --- a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts +++ b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts @@ -98,7 +98,8 @@ export function snapshotRequest( /** * The suite's snapshot runtime counts device binds. - * ADR 0019 §9 permits one execution bind and a second, capture-only bind for `--settle`. + * ADR 0019 §9 permits one execution bind. + * If the action use has no capture, `--settle` may acquire a second, capture-only bind. */ export function countingSnapshotRuntime(): Readonly<{ inspectFacts: InspectDeviceRuntimeFacts; From b9096c1a7100ffa662b2b155df2be681607cfef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 18:44:42 +0200 Subject: [PATCH 29/58] docs(adr): state that the ADR 0020 old-reader trace starts after admission Admission can resolve a device for lock and provider selection (request-binding.ts, request-platform-provider-context.ts). It only reads the inventory, so the safety property holds, but the trace does not cover admission-time failures. Found in review round 2. --- docs/adr/0020-composable-recorded-fragments.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/adr/0020-composable-recorded-fragments.md b/docs/adr/0020-composable-recorded-fragments.md index 1551bedef0..123c632d3e 100644 --- a/docs/adr/0020-composable-recorded-fragments.md +++ b/docs/adr/0020-composable-recorded-fragments.md @@ -548,6 +548,8 @@ The replay engine stops at the failed action before a later root `open`, known p **Open evidence gap:** this trace proves failure before platform execution for an unknown guard that reaches generic dispatch. It does not prove the specified `INVALID_ARGS` format rejection for old readers. +Admission can resolve a device for lock and provider selection; that resolution reads the device inventory and runs no command. +This trace does not cover failures during admission. A new fragment codec cannot change an old reader's behavior. **Maintainer question:** should the old-reader contract accept these existing errors, or retain `INVALID_ARGS` as an unresolved requirement? An old reader that gets an unguarded legacy `.ad` retains its existing behavior. From b52f0c8c2243cc9f2012619bcf484b162fb6e609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 20:08:50 +0200 Subject: [PATCH 30/58] docs(adr): align ADR 0003 invariants 3 and 4 with the shipped derivation The 2026-06 update required types to stop every public projection from reading daemon traits and a compile error for a missing trait. The shipped derivation (#906, #907) does neither: the daemon facet is optional, a command without one gets the generic route, and command explain displays the facet. Invariant 3 now bans public contracts (CLI grammar, Node options, MCP schemas, help, capability support) that derive from daemon traits, and allows a diagnostic display. Only command-explain.ts and the daemon's own policy file (daemon-policy-file.ts) read the facet. Invariant 4 now requires one daemon facet per command and no second table. Decided with the maintainer. --- docs/adr/0003-daemon-command-registry.md | 30 ++++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/adr/0003-daemon-command-registry.md b/docs/adr/0003-daemon-command-registry.md index cb60c57bc9..cf8f363264 100644 --- a/docs/adr/0003-daemon-command-registry.md +++ b/docs/adr/0003-daemon-command-registry.md @@ -2,7 +2,7 @@ ## Status -Accepted +Accepted. Amended 2026-10-16: invariants 3 and 4 now describe the shipped derivation (#906, #907). ## Rules at a glance @@ -12,7 +12,9 @@ Accepted - Request modules consume named predicates from the registry. They do not recreate command string sets. - Handler modules own execution logic only. They do not export duplicate coverage tables to prove route membership. - The daemon registry must not define CLI grammar, Node.js client options, MCP schemas, user-facing help, or platform capability support. -- Preserve all four invariants in the 2026-06 update. Types do not fully enforce invariants 3 and 4, as described below. +- Preserve all four invariants in the 2026-06 update. +- Do not derive a public contract from daemon traits. A diagnostic display, such as `command explain`, may read them. +- A command without a `daemon` facet uses the `generic` route with default request policy. ## Context @@ -94,7 +96,6 @@ It derives these surfaces from that site: Its purpose is to remove the drift between these tables. Registry tests pin the derived tables. -Types do not fully enforce invariants 3 and 4. **This ADR's decision remains accepted.** Its boundary concerns ownership and the predicate interface, not the physical file that contains a trait. @@ -112,11 +113,11 @@ A derived daemon registry is therefore permitted **only if** it preserves all th 2. **Predicate interface unchanged.** Consumers continue to use named predicates such as `getDaemonCommandRoute`, `isLeaseAdmissionExempt`, and `shouldLockSessionExecution`. Only the daemon registry exposes these predicates. Derivation changes table construction, not table access. -3. **No leakage into public projections.** Types must prevent the catalog/CLI/MCP/help/capability projections from reading daemon-only traits. - The daemon registry must still not define CLI grammar, Node.js options, MCP schemas, user-facing help, or capability support. -4. **One declaration per concern, enforced by types.** At the single registration site, a missing or duplicate daemon trait must cause a compile error. - This replaces alignment by convention. - This structural improvement justifies derivation instead of a separate hand-authored table. +3. **No daemon traits in public contracts.** CLI grammar, Node.js client options, MCP schemas, user-facing help, and capability support must not derive from daemon-only traits. + A diagnostic display may read the `daemon` facet. `command explain` in `src/cli/command-explain.ts` displays routes and request-policy traits this way. +4. **One declaration per command.** Each command has at most one `daemon` facet, at its registration in the descriptor registry. + No second table declares the same traits. This replaces alignment by convention, and it justifies derivation instead of a separate hand-authored table. + A command without a `daemon` facet uses the `generic` route with default request policy. The daemon registry tests pin the derived table. A single flat public descriptor that exposes daemon fields in public views is **not** permitted. This would combine daemon policy with a public command registry, which this ADR rejects. @@ -126,13 +127,6 @@ PR #906 introduced the descriptor and proved parity. PR #907 made the derivation live and deleted the hand-authored table. The daemon registry tests pin route and request-policy decisions. -**Known invariant-3 gap.** The descriptor type exposes its `daemon` facet to public projection code. -`src/cli/command-explain.ts` reads that facet and displays daemon routes and request-policy traits. -Thus, types do not enforce invariant 3 for this CLI projection. -The invariant remains required; this ADR records the gap without changing runtime behavior. - -**Known invariant-4 gap.** `CommandDescriptorBase.daemon` is optional in `packages/command-registry/src/types.ts`. -In `DaemonCommandDescriptor`, every trait except `command` and `route` is optional. -If a command has no daemon descriptor, `getDaemonCommandRoute` returns `generic`. -Thus, types do not require every daemon trait at the registration site. -Invariant 4 remains required; this ADR records the gap without changing runtime behavior. +**Amendment 2026-10-16.** The 2026-06 text required types to stop every public projection from reading daemon traits, and required a compile error for a missing daemon trait. +The shipped derivation does neither: `CommandDescriptorBase.daemon` is optional (`packages/command-registry/src/types.ts`), and a command without a facet gets the `generic` route. +Invariants 3 and 4 now state the property that matters and that the code holds: no public contract derives from daemon traits, and no second table declares them. From 843b7ef7a1c518d65bcaac55ea9a8c073f021e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 16 Oct 2026 20:09:10 +0200 Subject: [PATCH 31/58] docs(adr): accept existing old-reader errors in ADR 0020 An old reader cannot learn a new error from a later codec. The traced path already fails before platform execution with SESSION_NOT_FOUND or COMMAND_FAILED (generic-route-runtime-missing), wrapped as REPLAY_DIVERGENCE. The proposal now accepts these errors for old readers and keeps INVALID_ARGS for readers that ship with the fragment codec. Decided with the maintainer. --- docs/adr/0020-composable-recorded-fragments.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/adr/0020-composable-recorded-fragments.md b/docs/adr/0020-composable-recorded-fragments.md index 123c632d3e..0653e651d4 100644 --- a/docs/adr/0020-composable-recorded-fragments.md +++ b/docs/adr/0020-composable-recorded-fragments.md @@ -69,7 +69,7 @@ Consequently, the design must solve four boundaries together: A new reader consumes that guard during preflight. An old reader treats the guard as an unknown action. The required safety property is failure before platform execution, before a root `open` or any known prefix. - The current request path does not prove an `INVALID_ARGS` format rejection; see Annotation compatibility and versioning. + An old reader fails with its existing errors, not `INVALID_ARGS`; see Annotation compatibility and versioning. Other fragment metadata and references remain reserved comments. Thus old readers ignore those annotations, and do not silently flatten the composition. 7. **Native fragment repair is explicit re-recording.** @@ -101,7 +101,7 @@ But these invariants are fixed: A composed native parent uses the corresponding `composition-v1` guard as its first executable line, before root `open` or any `include`. An old reader parses either guard as an unknown action. The required safety property is failure before platform execution. - The current request path returns session-dependent errors; see Annotation compatibility and versioning for the evidence gap. + An old reader returns its existing, session-dependent errors; see Annotation compatibility and versioning. The fragment has no `open`, `close`, or hidden launch action. The writer always emits the effective `context platform` and `target`. A parser rejects a fragment that is missing either constraint. @@ -546,12 +546,13 @@ These are responses for the guard request, not the outer replay response. The replay adapter wraps this dispatch failure as `REPLAY_DIVERGENCE`. The replay engine stops at the failed action before a later root `open`, known prefix, or expanded child body. -**Open evidence gap:** this trace proves failure before platform execution for an unknown guard that reaches generic dispatch. -It does not prove the specified `INVALID_ARGS` format rejection for old readers. +This trace proves failure before platform execution for an unknown guard that reaches generic dispatch. Admission can resolve a device for lock and provider selection; that resolution reads the device inventory and runs no command. This trace does not cover failures during admission. A new fragment codec cannot change an old reader's behavior. -**Maintainer question:** should the old-reader contract accept these existing errors, or retain `INVALID_ARGS` as an unresolved requirement? +Thus the old-reader contract accepts these existing errors: `SESSION_NOT_FOUND`, or `COMMAND_FAILED` with reason `generic-route-runtime-missing`, wrapped as `REPLAY_DIVERGENCE`. +The contract does not require `INVALID_ARGS` from an old reader. +A reader that ships with the fragment codec would refuse an unknown or malformed guard with `INVALID_ARGS` during source preflight. An old reader that gets an unguarded legacy `.ad` retains its existing behavior. This includes the possibility that it executes a known prefix before a later unknown command. That legacy case is explicitly outside the v1 composed-artifact acceptance contract, and it is not safety evidence. From 2887aad490ed0c11134cb8111fe2affa9f210693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:03 +0200 Subject: [PATCH 32/58] docs(adr): list the full provider-scenario exclusion buckets in ADR 0001 --- ...01-provider-first-integration-scenarios.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/adr/0001-provider-first-integration-scenarios.md b/docs/adr/0001-provider-first-integration-scenarios.md index ad71b6bc9b..8ab82f1e5c 100644 --- a/docs/adr/0001-provider-first-integration-scenarios.md +++ b/docs/adr/0001-provider-first-integration-scenarios.md @@ -192,13 +192,20 @@ The script is the source of truth for: These flags stay in their owning unit/CLI suites: -- Config. -- Remote transport. -- Metro preparation. -- Parser/client-only. -- Report-writing. -- Boot-fallback. - +- Config, output, diagnostics, and transport. +- Remote connection and session-lock policy. +- Cloud artifact provider lookup. +- Metro and React Native runtime preparation. +- Apple launch and perf artifact options. +- Parser/client-only command flags. +- Daemon lifecycle control. +- Platform boot fallback without a provider seam. +- Open foreground auto-resolution (RFC prototype). +- Local device contention waiting. +- Apple simulator screenshot, private-AX, and observe-only capture options. +- Daemon screenshot selector crop. + +`summarizeProviderScenarioFlagExclusions` in `scripts/integration-progress-model.ts` is the full list. The report lists these flags as explicit exclusions, so they are not silently missing from the denominator. Provider pressure separates semantic Apple `simctl`/`devicectl`, macOS helper, and macOS host usage from generic Apple host-tool usage. It also separates semantic Linux desktop/accessibility/clipboard/screenshot/input usage from generic Linux tool usage. From 8dfcba84b5408b29e2defdd4e0e7daf6ebf5e24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:03 +0200 Subject: [PATCH 33/58] docs(adr): scope the Android helper rule to instrumentation touch in ADR 0002 --- docs/adr/0002-persistent-platform-helper-sessions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0002-persistent-platform-helper-sessions.md b/docs/adr/0002-persistent-platform-helper-sessions.md index 276fa8f198..2db4cba683 100644 --- a/docs/adr/0002-persistent-platform-helper-sessions.md +++ b/docs/adr/0002-persistent-platform-helper-sessions.md @@ -14,7 +14,7 @@ Accepted - When startup costs are high and an automation context is reusable, use persistent platform helper sessions. - The daemon owns helper lifetime policy. Platform modules own helper-manager mechanics. - Unless a separate cache contract has explicit invalidation, a helper must execute each command against fresh platform state. -- On Android, snapshot capture, gesture viewport resolution, and planned-touch injection share one bundled automation helper. +- On Android, snapshot capture, gesture viewport resolution, and instrumentation-backed planned-touch injection share one bundled automation helper. - On Android, after a failed session command, retry one-shot only for idempotent reads, and only after the failed session has stopped. - On Android, return a session gesture failure directly. Do not retry it one-shot. - For Android snapshots, if the packaged helper is unavailable or invalid, agent-device must fail closed. It must not substitute the legacy `adb uiautomator dump` snapshot engine. @@ -87,7 +87,7 @@ If that helper is unavailable or invalid, agent-device must fail closed. It must not substitute the legacy `adb uiautomator dump` snapshot engine. Android permits only one reliable instrumentation-owned `UiAutomation` context per device. -Thus, snapshot capture, gesture viewport resolution, and planned-touch injection share one bundled automation helper (amended 2026-07, issue #1275, consistent with ADR 0013). +Thus, snapshot capture, gesture viewport resolution, and instrumentation-backed planned-touch injection share one bundled automation helper (amended 2026-07, issue #1275, consistent with ADR 0013). A live persistent helper session executes touch commands directly over its session socket. Without a live persistent session, the same helper runs one-shot. Before that amendment, touch synthesis shipped as a separate instrumentation helper. From 978f52e82b52de6bd2cc00bf4a02d474c63208a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:03 +0200 Subject: [PATCH 34/58] docs(adr): record the exempt commands and grown allowlist in ADR 0005 --- docs/adr/0005-ios-runner-interaction-lifecycle.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/adr/0005-ios-runner-interaction-lifecycle.md b/docs/adr/0005-ios-runner-interaction-lifecycle.md index a165c91b6a..c8a5ecf25e 100644 --- a/docs/adr/0005-ios-runner-interaction-lifecycle.md +++ b/docs/adr/0005-ios-runner-interaction-lifecycle.md @@ -7,7 +7,7 @@ Accepted ## Rules at a glance - For iOS/macOS selector taps on an element with a frame, use coordinate-first resolved element activation. tvOS remains focus/remote-driven. -- Before a command to a ready session, send an `uptime` preflight, except for the stated startup, readiness, and recent-mutation cases. +- Before a command to a ready session, send an `uptime` preflight, except for the stated startup, readiness, exempt-command, and recent-mutation cases. - Keep recent-mutation health only on the `RunnerSession` and only for the stated app, commands, response checks, and 5-second window. - Never count snapshots or other read-only responses as health signals. - Future optimization work should reduce the `uptime` preflights only after runner status survives command-induced XCTest teardown and can prove that the session still serves new requests. @@ -45,6 +45,7 @@ tvOS remains focus/remote-driven because tvOS does not support normal coordinate Before command send, ready runner sessions receive a short `uptime` preflight. Read-only startup commands still skip that preflight because, for a newly launched runner, the first successful command is the readiness proof. Readiness probe commands skip preflight to avoid recursion. +A ready session also skips the preflight for the preflight-exempt commands `activate`, `terminate`, and `targetReset` (`readinessPreflightExempt` in `RUNNER_COMMAND_TRAITS`). The daemon may also skip the ready-session `uptime` preflight for an explicit allowlist of mutating interactions: @@ -53,6 +54,8 @@ The daemon may also skip the ready-session `uptime` preflight for an explicit al - `drag`. - `swipe`. - `scroll`. +- `desktopScroll`. +- `gesture`. - `sequence`. The skip requires that the same session produced a healthy mutating response for the same `appBundleId` within 5 seconds. From 3ecf51be5349736ea916f4e2afce322de5faefcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:03 +0200 Subject: [PATCH 35/58] docs(adr): state per-fate wire-gate outcomes in ADR 0006 --- docs/adr/0006-daemon-rpc-protocol-version.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/adr/0006-daemon-rpc-protocol-version.md b/docs/adr/0006-daemon-rpc-protocol-version.md index 432ae7a8f3..20e42f7ca8 100644 --- a/docs/adr/0006-daemon-rpc-protocol-version.md +++ b/docs/adr/0006-daemon-rpc-protocol-version.md @@ -91,7 +91,9 @@ Checks enforce the increment rules above, so nobody has to remember them (#1432) `test/wire-compat/ledger.json` records the hash of each declaration and the protocol version of that hash. The unit lane fails if the shape of a declaration changes and the ledger does not change with it. `pnpm check:daemon-wire-compat` compares the ledger with the last released tag. -Drift since that release must carry either a protocol increment or a `compatibleChanges` acknowledgment that invokes the additive list above. +A declaration removed since that release needs a protocol increment; an acknowledgment cannot cover a removal. +A declaration changed since that release needs a protocol increment or a `compatibleChanges` acknowledgment that invokes the additive list above. +Declarations added since that release need neither. One bullet is only partly digestible: the `/health` and `/rpc` path literals inside `http-server.ts`. For that gap, the manifest records the gap and its reason instead of implying coverage. See `test/wire-compat/README.md`. From 4e62f8edeac283b6b1043cbe4c6c2ec728dd4c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:09 +0200 Subject: [PATCH 36/58] docs(adr): name the platformExecution facet and execute seam in ADR 0008 --- docs/adr/0008-command-descriptor-registry.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0008-command-descriptor-registry.md b/docs/adr/0008-command-descriptor-registry.md index c2f19c99b0..94a46fba0a 100644 --- a/docs/adr/0008-command-descriptor-registry.md +++ b/docs/adr/0008-command-descriptor-registry.md @@ -60,7 +60,7 @@ Introduce one `CommandDescriptor` per command. It **composes facets owned by their domains**. Every consumer table is **derived** through pure projection: -- The descriptor composes catalog identity, command-family surface projection hooks, a `capability` facet, a `daemon` facet, platform dispatch membership, response data transforms, and typed-result hooks. +- The descriptor composes catalog identity, command-family surface projection hooks, the `platformExecution` declaration (successor to the retired `capability` facet), a `daemon` facet, platform dispatch membership, response data transforms, and typed-result hooks. `src/commands/**` remains the owner of the CLI schema/reader and executable surface metadata. ADR 0003 shapes the route and request-policy traits of the `daemon` facet. - Narrow command traits that affect multiple projections belong to the descriptor. @@ -100,10 +100,11 @@ This design composes with ADR 0003's four invariants and is bound by them: ## Consequences A plain new command changes approximately 1–2 files. -Per-platform behavior remains N implementations behind the descriptor's `execute`. +Per-platform behavior remains N implementations behind the in-daemon `execute` seam. +The descriptor's `platformExecution` declaration names the runtime use that seam binds. The descriptor is the prerequisite for typed per-command results. `src/client/client-types.ts` retains the public client interface and some `CommandRequestResult` return types. -It also supplies the capability facet used by the platform-plugin work in ADR 0009. +It also supplied the capability facet used by the platform-plugin work in ADR 0009. That facet is retired; [ADR 0009](0009-apple-platform-consolidation.md) records the retirement. During the migration, the import-cycle inversion was the principal risk. At that time, `command-catalog.ts` had approximately 95 importers. From bb6f153f6ead4b210a07b4dfbcf9dedcbb1ab01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:09 +0200 Subject: [PATCH 37/58] docs(adr): point the visionOS discovery claim at its test file in ADR 0009 --- docs/adr/0009-apple-platform-consolidation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0009-apple-platform-consolidation.md b/docs/adr/0009-apple-platform-consolidation.md index 02c538c6d0..82cd0d9640 100644 --- a/docs/adr/0009-apple-platform-consolidation.md +++ b/docs/adr/0009-apple-platform-consolidation.md @@ -113,7 +113,7 @@ Implementation status as of 2026-08: Discovery never produces it. - visionOS profile/build/discovery, plus simulator-deployment evidence. - Decision-only support boundary: - - visionOS discovery and simulator deployment are supported and unit-tested in `packages/platform-apple/src/inventory-classification.ts` and `packages/platform-apple/src/deployment/runtime.test.ts`. + - visionOS discovery and simulator deployment are supported and unit-tested in `packages/platform-apple/src/inventory-classification.test.ts` and `packages/platform-apple/src/deployment/runtime.test.ts`. - App deployment is also admitted for CoreDevice-backed physical devices. - XCTest-backed physical deployment is unsupported. - Push remains simulator-only. From a9dd82d263f81d342cc2658fa5a92e5841efe92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:09 +0200 Subject: [PATCH 38/58] docs(adr): state the remote logPathUnavailable case in ADR 0010 --- docs/adr/0010-error-system.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/adr/0010-error-system.md b/docs/adr/0010-error-system.md index 04fe858d7d..0c680c8dca 100644 --- a/docs/adr/0010-error-system.md +++ b/docs/adr/0010-error-system.md @@ -79,7 +79,9 @@ Nothing documented the conventions, so each new site decided them again. Rehydration helpers (`throwDaemonError`, `toDaemonHttpRpcError`) copy all fields. `NormalizedError` hoists the typed signals, so `--json` consumers see them. 6. **Observability.** Failed requests always flush diagnostics. - A `diagnosticId` and an ndjson `logPath` accompany every error. + A `diagnosticId` and an ndjson `logPath` accompany every error produced on the local machine. + On a remote path, the client keeps only a `logPath` it can open. + When it cannot produce one, `logPathUnavailable` replaces `logPath` and names the reason (#1801). Daemon-side diagnostics are under the session state dir. Client-side diagnostics are under `~/.agent-device/logs`. Messages must not embed raw stderr dumps or secrets. From 111130127212258b99f798610abe661f8d3dcdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:09 +0200 Subject: [PATCH 39/58] docs(adr): correct the scroll keyboard-probe condition in ADR 0011 --- docs/adr/0011-interaction-guarantee-contract.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0011-interaction-guarantee-contract.md b/docs/adr/0011-interaction-guarantee-contract.md index 2b248b7854..a669d69414 100644 --- a/docs/adr/0011-interaction-guarantee-contract.md +++ b/docs/adr/0011-interaction-guarantee-contract.md @@ -394,7 +394,7 @@ That Swift policy is the source of truth, because the current table has only thr - A coordinate synthesized tap never probes keyboards and may use the coordinate fallback. This applies to a standalone tap, a `sequence` step, and the in-app `back` top-leading tap. -- Default iOS scroll probes keyboards only after AX is known healthy. +- Default iOS scroll probes keyboards whenever AX is not stamped unavailable, including while health is still unknown (#2500). It must not fall back to `XCUICoordinate`. - Synthesized one-contact `gesture` plans may still use the coordinate fallback before AX health is known. After a snapshot stamps AX unavailable, they stop using it. From 68bb7e59574383b5d6a95d7565216228d27b74a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:16 +0200 Subject: [PATCH 40/58] docs(adr): scope the linear-schedule sentence to endpoint-hold in ADR 0013 --- docs/adr/0013-unified-gesture-plans.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0013-unified-gesture-plans.md b/docs/adr/0013-unified-gesture-plans.md index 657ab31298..c0547d2d30 100644 --- a/docs/adr/0013-unified-gesture-plans.md +++ b/docs/adr/0013-unified-gesture-plans.md @@ -137,7 +137,7 @@ Platform adapters consume the canonical plan: - **Apple.** iOS lowers one-contact endpoint-hold plans to the established fast-swipe synthesis profile. That profile reaches the endpoint in 100 ms, then holds there for the planned duration before it lifts. This matches Maestro's XCTest driver. - One-contact plans are linear and therefore carry only their start and end samples, with the authored duration between them. + An endpoint-hold plan is linear and therefore carries only its start and end samples, with the authored duration between them. Two-contact plans convert every planned point to native orientation and send the exact arrays to the private XCTest event bridge. macOS lowers a one-contact plan to its drag executor. tvOS lowers a one-contact plan to remote direction. From 1c3b8ea28ad99a7505f2225291ddfff2f113ad11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:16 +0200 Subject: [PATCH 41/58] docs(adr): name the frame value module and empty-result rule in ADR 0014 --- docs/adr/0014-session-ref-frame-lifetime.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0014-session-ref-frame-lifetime.md b/docs/adr/0014-session-ref-frame-lifetime.md index 45d87bea33..1a4ffb907c 100644 --- a/docs/adr/0014-session-ref-frame-lifetime.md +++ b/docs/adr/0014-session-ref-frame-lifetime.md @@ -25,11 +25,12 @@ The ref-frame vocabulary is promoted into `CONTEXT.md`. The frame is one value on `SessionState`. A transition replaces the frame whole and never edits it in place. The frame type is nominal (`#`-private fields). - Thus, no module outside `packages/daemon/src/core/ref-frame.ts` can construct a frame, edit a frame, or derive a frame from an existing frame. + Thus, no module outside `packages/daemon/src/core/ref-frame-slot.ts` can construct a frame, edit a frame, or derive a frame from an existing frame. The type cannot judge a whole frame that moves unchanged, for example when code clears the field or assigns the frame of another session. The field-owner gate still covers these moves. - A complete snapshot activates an `all` frame. `find`, settled diffs, and replay divergence screens activate a bounded partial frame that supersedes the prior frame. + A partial result with no issued ref does not supersede the prior frame. Internal read-only captures never activate, reindex, or expire a frame. - Every mutating leaf expires the frame at the side-effect seam: after all pre-action guards, immediately before the device operation. There is no success-only rollback. From edb03cfe6275b71eb5310c13ba04939f28a865c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:16 +0200 Subject: [PATCH 42/58] docs(adr): add the we-are-lenient outcome to ADR 0015 --- docs/adr/0015-direct-maestro-engine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0015-direct-maestro-engine.md b/docs/adr/0015-direct-maestro-engine.md index 6fde5620e3..5d8554a554 100644 --- a/docs/adr/0015-direct-maestro-engine.md +++ b/docs/adr/0015-direct-maestro-engine.md @@ -159,7 +159,7 @@ That oracle replaced the original hand-typed parser fixture. The pinned parser is SHA-verified at regeneration. Layer 1 captures each parse. The deterministic verifier replays that capture against our engine. - The verifier classifies every flow as identical / both-reject / we-reject / mismatch. + The verifier classifies every flow as identical / both-reject / we-reject / mismatch / we-are-lenient. Each non-identical outcome must be a declared, on-the-record divergence. 2. Layer 2 reads upstream constants (retry cap, animation-wait threshold/timeout, erase cap) straight from the pinned bytecode. It also reads parser-observed defaults (swipe duration) straight from the pinned bytecode. From 4c6bbaee271118c10f9259622d2b648746f1a10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:16 +0200 Subject: [PATCH 43/58] docs(adr): state the session event hides fill text in ADR 0017 --- docs/adr/0017-parameterized-recorded-inputs.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0017-parameterized-recorded-inputs.md b/docs/adr/0017-parameterized-recorded-inputs.md index a5ee3e419d..5e845278fe 100644 --- a/docs/adr/0017-parameterized-recorded-inputs.md +++ b/docs/adr/0017-parameterized-recorded-inputs.md @@ -9,7 +9,8 @@ Accepted - The fill-only `recordAs` input parameterizes a recorded fill value. The CLI spells it `--record-as `. - The live fill dispatch receives the literal. - The public response, recording state, session event, and published script of the fill use `${VAR}`. + The public response, recording state, and published script of the fill use `${VAR}`. + The session event hides the fill value as ``. - Variable names use replay's uppercase `[A-Z_][A-Z0-9_]*` grammar. `AD_*` remains reserved for runtime built-ins. - `--record-as` is rejected unless script recording is armed. @@ -52,7 +53,8 @@ agent-device session save-script ``` The live fill dispatch receives the literal. -Its public response, recording state, session event, and published script use `${PASSWORD}`. +Its public response, recording state, and published script use `${PASSWORD}`. +The session event hides the fill value as ``. Later, replay resolves the placeholder from `AD_VAR_PASSWORD` or `--env PASSWORD=` before the fill is dispatched. Variable names use replay's uppercase `[A-Z_][A-Z0-9_]*` grammar. From 4db9f74b53ad517856a916ae60cac2174bd45a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:20 +0200 Subject: [PATCH 44/58] docs(adr): include translation hops in the collapse rule of ADR 0022 --- docs/adr/0022-daemon-platform-runtime-coupling.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0022-daemon-platform-runtime-coupling.md b/docs/adr/0022-daemon-platform-runtime-coupling.md index 191b5ec63a..d52cfd6036 100644 --- a/docs/adr/0022-daemon-platform-runtime-coupling.md +++ b/docs/adr/0022-daemon-platform-runtime-coupling.md @@ -119,8 +119,8 @@ The ADR 0019 trace recorded routes of approximately 24 files without an accepted - Runtime lifecycle participation deepens through the existing lifecycle phases, with no generic hook bag (#2333). - Open-target planning separates plan/result from platform mechanics and preserves one construction path. - Session state/store authority retains its access structure and ratchets the handler-owned portion through R75. - - Route depth records the re-traced routes and collapses only the proven pass-through hops. - This change collapses none. + - Route depth records the re-traced routes and collapses only the hops that the deletion test proves removable. + The deletion test covers pass-through and translation hops. This change collapses none. ## 3. Rationale From a27da083ea373b82f89b0c0ee5e1a0a9c789acb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:21 +0200 Subject: [PATCH 45/58] docs(adr): separate the runner capture path and complete refusal causes in ADR 0025 --- docs/adr/0025-foldable-apple-panels.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/adr/0025-foldable-apple-panels.md b/docs/adr/0025-foldable-apple-panels.md index 054495b1dc..cb29c268e7 100644 --- a/docs/adr/0025-foldable-apple-panels.md +++ b/docs/adr/0025-foldable-apple-panels.md @@ -19,7 +19,8 @@ Only inference answers the second question. | Situation | Behavior | | --- | --- | -| Any iOS simulator capture | First resolve the CoreDevice display table. Capture the panel that the system currently lights. | +| iOS simulator capture through `simctl` | First resolve the CoreDevice display table. Capture the panel that the system currently lights. | +| Runner fallback after a `simctl` failure | Follow the target app's window and capture the display that hosts it. Use the facts that the runner reports. | | More than one integrated panel | Pass `--display=` on every `simctl io screenshot`. Never use the implicit default. | | Zero or multiple lit panels | Name the `primary` panel. Emit `apple_display_capture_ambiguous`. Report pose as `unknown`. | | One integrated panel | Preserve pre-panel behavior exactly: no display flag, no pose, unchanged scale probe. | @@ -137,7 +138,8 @@ SpringBoard owns the home-screen window. Thus, a session without an app, or with a stopped app, can still resolve a window's display. The result always names a display occupied by a window. It never defaults to `XCUIScreen.main`, which is dark on a Duo when the app uses the inner panel. -A capture fails closed with a typed reason only when neither window resolves. +A capture fails closed only in three cases. Neither window resolves. A resolved window names no display. A resolved display yields an unrenderable image. +The typed reasons are `unresolvedWindow`, `unresolvedScreen`, and `unrenderableImage` (`RunnerAppScreenCapture.swift`). The query uses both `exists` and frame evidence to refuse an app without a window. Such an app returns `(0,0 0x0)` from `frame` without an exception. Geometry alone cannot prove that no window exists. @@ -407,7 +409,8 @@ Only `AVAssetExportPresetHighestQuality` preserves arbitrary geometry. Hardware encoding makes full-resolution export inexpensive: approximately 1–2s for 90s at `1206x2622`. Thus, `high` was not slower. -`recording-overlay.swift` now always exports through the geometry-preserving preset at both quality tiers. +`recording-overlay.swift` exports every composited overlay through the geometry-preserving preset at both quality tiers. +An empty gesture list skips the overlay export and copies the raw capture unchanged. `--quality` no longer reduces capture resolution. Before adoption, it verifies that the composited track's resolved size matches the capture. It also rejects uniformly black output when the raw capture had visible content. From 2414d66a6770fff2facb28ac435debc576f67a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:21 +0200 Subject: [PATCH 46/58] docs(adr): scope the novelty fact to verbatim transplants in ADR 0027 --- docs/adr/0027-descriptor-root-vs-eager-closure-budget.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0027-descriptor-root-vs-eager-closure-budget.md b/docs/adr/0027-descriptor-root-vs-eager-closure-budget.md index 75286abbd1..401a13b5e5 100644 --- a/docs/adr/0027-descriptor-root-vs-eager-closure-budget.md +++ b/docs/adr/0027-descriptor-root-vs-eager-closure-budget.md @@ -99,7 +99,8 @@ Three facts determine eligibility from the closure graphs and merge-base tree, w It removes or replaces none. 2. **Added-module novelty.** Each module newly evaluated by the head is absent from the merge-base tree at its rename-canonicalized path. - It must also carry no code from a source deleted since the merge-base. + It must also carry no verbatim transplant of code from a source deleted since the merge-base. + The probe detects identical normalized statements. A rewritten transplant can pass the probe. The probe limits below record that fact. The check matches an added module when it shares two or more identical normalized top-level statements with one deleted source; that is a move signal. One shared statement counts as coincidence. Short constants, such as schema versions or default timeouts, repeat across the repository. From a3c541809ac15caabd064c44b674dd13b042205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:13:21 +0200 Subject: [PATCH 47/58] docs(adr): correct the freshness row and cycle wording in ADR 0033 --- docs/adr/0033-collocation-decision-table.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0033-collocation-decision-table.md b/docs/adr/0033-collocation-decision-table.md index e89735d208..a29f413657 100644 --- a/docs/adr/0033-collocation-decision-table.md +++ b/docs/adr/0033-collocation-decision-table.md @@ -43,7 +43,7 @@ Re-evaluate those files against their remaining edges. | `src/capture-admission/screen-recording-admission-ledger.ts` | keep (proposed move rejected) | The same direction is rejected. Capture-kit's screen-recording session resource and stop-recovery consume it. | | `src/durable-json.ts` | keep (proposed move rejected) | Managed-allocation already imports this capture-kit module. Reversal forms a package cycle unless that import disappears first. A design that first removes that import stays open. Contracts beside `json.ts` was also rejected: under R18, contracts holds wire vocabulary, not durable-descriptor validation mechanics. Three managed-allocation `record-*` consumers explain the community coupling and can remain. | | `src/snapshot/snapshot-evidence.ts` | keep | This is snapshot-capture mechanics. The daemon-dominated community represents consumers. A daemon-server home would force every platform and provider to depend on the daemon zone for capture evidence. | -| `src/snapshot/snapshot-freshness/index.ts` | keep | The reason for the row above applies. This is platform-neutral freshness policy. All its consumers, capture-kit itself, platforms, and daemon, import it downward today. | +| `packages/capture-kit/src/snapshot/snapshot-freshness/index.ts` (moved from `src/snapshot/snapshot-freshness/index.ts` in #2385) | keep | The reason for the row above applies. This is platform-neutral freshness policy. Its consumers today are the root command layer and the daemon; capture-kit owns the module. | | `src/snapshot/touch-reference-frame.ts` | keep | Interaction and capture share these snapshot-geometry mechanics. Only capture-kit is below both. | ### contracts @@ -154,7 +154,7 @@ They are not independent proof that the placement is optimal. | File | Decision | Reason | | --- | --- | --- | | `src/daemon-diagnostics-scope.ts` | moved → `packages/daemon-contracts/src/daemon-diagnostics-scope.ts` ([#3297](https://github.com/callstack/agent-device/pull/3297) into `src/daemon-contracts/`, then the package extraction) | The complete zone resides in `packages/daemon-contracts`. Its directory derives zone ownership without per-file `ROOT_MODULE_ZONES` rows. An internal rename therefore needs no ownership-table edit. | -| `packages/daemon-contracts/src/device-claim-recovery.ts` | keep (shared-below-both) | The owner-scoped claim reconciler recovers durable resources from a dead owner's state directory. Daemon `open`/startup and daemonless `device release --stale` both run it. It therefore belongs below both. Runtime recovery may reside in `daemon-contracts` only when both sides run it and root composition supplies only the gateway, per ADR 0019 section 1. The package depends on `capture-kit`. Capture-kit cannot depend back on it, because that dependency forms a project-reference cycle through `selectors`. | +| `packages/daemon-contracts/src/device-claim-recovery.ts` | keep (shared-below-both) | The owner-scoped claim reconciler recovers durable resources from a dead owner's state directory. Daemon `open`/startup and daemonless `device release --stale` both run it. It therefore belongs below both. Runtime recovery may reside in `daemon-contracts` only when both sides run it and root composition supplies only the gateway, per ADR 0019 section 1. The package depends on `capture-kit`. Capture-kit cannot depend back on it: that dependency forms a direct project-reference cycle between `daemon-contracts` and `capture-kit`. | | `src/daemon-policy-file.ts` | moved → `packages/daemon-contracts/src/daemon-policy-file.ts` | This shared daemon/client contract follows #2559 and #3288's `daemon-contracts` declaration. Rank-5 daemon-client dynamically imports it, so the spine requires a home below both. Package extraction supplies that physical home with manifest-owned exports. | | `src/daemon.ts` | keep | This is the `internal/daemon` bundle entry. Umbrella §4 keeps composition roots at the root. | | `src/platform-runtime-apple-runner-owner.ts` | retain for this batch | This is a `platform-runtime-*` composition seam. R13's exact-importer rule and ADR 0022's R76 ban on the `src/platform-runtime-*.ts` family key on its path. Relocation requires coordinated ownership changes to both declarations. The rules constrain the move without independently proving placement. | From 6f62e8c62955c5315983c8eec51e7fa08c3125db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:43:26 +0200 Subject: [PATCH 48/58] docs(adr): state the platformExecution execution shapes in ADR 0008 platformExecution is a discriminated union of none, host, inventory, and device-runtime (packages/contracts/src/command-platform-execution.ts). The active design claimed it always names a runtime use; only the device-runtime variants do. Describe the shape and qualify the bound runtime use to those variants. Fixes #3796 review thread discussion_r4306650944. --- docs/adr/0008-command-descriptor-registry.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0008-command-descriptor-registry.md b/docs/adr/0008-command-descriptor-registry.md index 94a46fba0a..719ebb6fab 100644 --- a/docs/adr/0008-command-descriptor-registry.md +++ b/docs/adr/0008-command-descriptor-registry.md @@ -101,7 +101,8 @@ This design composes with ADR 0003's four invariants and is bound by them: A plain new command changes approximately 1–2 files. Per-platform behavior remains N implementations behind the in-daemon `execute` seam. -The descriptor's `platformExecution` declaration names the runtime use that seam binds. +The descriptor's `platformExecution` declaration names the execution shape: `none`, `host`, `inventory`, or `device-runtime`. +For the `device-runtime` variants it also names the runtime use that seam binds. The descriptor is the prerequisite for typed per-command results. `src/client/client-types.ts` retains the public client interface and some `CommandRequestResult` return types. It also supplied the capability facet used by the platform-plugin work in ADR 0009. That facet is retired; [ADR 0009](0009-apple-platform-consolidation.md) records the retirement. From f4de62911f5a814e73f459b9a1e078c29ba95b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:43:26 +0200 Subject: [PATCH 49/58] docs(adr): match the active design to platformExecution in ADR 0009 The active design still said it composes with ADR 0008's descriptor capability facet. That facet is retired; the per-OS predicates live in request-bound runtime facts (packages/platform-apple/src/gesture-facts.ts and siblings). Match ADR 0008's platformExecution declaration. Dated history and the retirement notes keep their wording. Fixes #3796 review thread discussion_r4306650938. --- docs/adr/0009-apple-platform-consolidation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/adr/0009-apple-platform-consolidation.md b/docs/adr/0009-apple-platform-consolidation.md index 82cd0d9640..71888dc896 100644 --- a/docs/adr/0009-apple-platform-consolidation.md +++ b/docs/adr/0009-apple-platform-consolidation.md @@ -93,7 +93,8 @@ Snapshot fidelity is uneven. The deep-RN AX-server fallback is iOS-simulator-only. Public leaf output remains a separate compatibility projection. -This design composes with ADR 0008's descriptor capability facet and ADR 0003. +This design composes with ADR 0003 and the descriptor `platformExecution` declaration in ADR 0008. +Per-OS behavior is data in request-bound runtime facts. Implementation status as of 2026-08: From d73610d8ed42b4ea417e815ae2b2f0f0e1b39ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:43:27 +0200 Subject: [PATCH 50/58] docs(adr): scope the diagnostic fields to the request scope in ADR 0010 Malformed RPC method params throw in packages/daemon/src/server/http/http-rpc-methods.ts before the router opens the request diagnostics scope (request-router.ts handleRequestOnLedger), so http-server.ts answers with normalizeError output that carries neither diagnosticId nor logPath; the validation tests pin that 400 / -32602 envelope. Scope the flush and field guarantee to errors inside a request diagnostics scope and state that pre-scope failures omit the fields. The remote logPathUnavailable rule is unchanged. Fixes #3796 review thread discussion_r4306650954. --- docs/adr/0010-error-system.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/adr/0010-error-system.md b/docs/adr/0010-error-system.md index 0c680c8dca..4d023ddee2 100644 --- a/docs/adr/0010-error-system.md +++ b/docs/adr/0010-error-system.md @@ -16,7 +16,7 @@ - If the per-code default hint would mislead, supply a hint. If the default suffices, omit the hint. - When you wrap an error, preserve an existing hint and the cause chain. - Every consumer surface must carry at least code, message, and hint. Rehydration helpers copy all fields. -- Failed requests always flush diagnostics. Messages must not embed raw stderr dumps or secrets. +- A failed request inside a request diagnostics scope always flushes diagnostics. A failure before that scope opens may omit `diagnosticId` and `logPath`. Messages must not embed raw stderr dumps or secrets. ## Context @@ -78,8 +78,11 @@ Nothing documented the conventions, so each new site decided them again. - Events timeline. Rehydration helpers (`throwDaemonError`, `toDaemonHttpRpcError`) copy all fields. `NormalizedError` hoists the typed signals, so `--json` consumers see them. -6. **Observability.** Failed requests always flush diagnostics. - A `diagnosticId` and an ndjson `logPath` accompany every error produced on the local machine. +6. **Observability.** A failed request inside a request diagnostics scope always flushes diagnostics. + Inside that scope on the local machine, the error carries a `diagnosticId` and an ndjson `logPath`. + The router opens the scope at `handleRequestOnLedger` (`packages/daemon/src/server/request-router.ts`). + A failure before it opens, such as malformed RPC method params rejected in + `packages/daemon/src/server/http/http-rpc-methods.ts`, returns an envelope that omits both fields. On a remote path, the client keeps only a `logPath` it can open. When it cannot produce one, `logPathUnavailable` replaces `logPath` and names the reason (#1801). Daemon-side diagnostics are under the session state dir. From 92a59a0ab07a07d91a57fd3f6fde5ee86875dbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:46:39 +0200 Subject: [PATCH 51/58] docs(adr): confine frame construction claims to the slot class in ADR 0014 ref-frame.ts calls issuedRefFrame (ref-frame.ts activateRefFrame) and derives expiry through expiredRefFrame, both exported factories of ref-frame-slot.ts. The original sentence claimed no module outside the slot file can construct or derive a frame value. Confine the claim to the private class, its private fields, and direct construction; state the exported factory APIs and keep transitions and admission owned by ref-frame.ts. The slot-file comment carried the same overstatement and is corrected at the same scope. Fixes #3796 review thread discussion_r4306650966. --- docs/adr/0014-session-ref-frame-lifetime.md | 4 +++- packages/daemon/src/core/ref-frame-slot.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/adr/0014-session-ref-frame-lifetime.md b/docs/adr/0014-session-ref-frame-lifetime.md index 1a4ffb907c..596c9349b3 100644 --- a/docs/adr/0014-session-ref-frame-lifetime.md +++ b/docs/adr/0014-session-ref-frame-lifetime.md @@ -25,7 +25,9 @@ The ref-frame vocabulary is promoted into `CONTEXT.md`. The frame is one value on `SessionState`. A transition replaces the frame whole and never edits it in place. The frame type is nominal (`#`-private fields). - Thus, no module outside `packages/daemon/src/core/ref-frame-slot.ts` can construct a frame, edit a frame, or derive a frame from an existing frame. + Thus, no module outside `packages/daemon/src/core/ref-frame-slot.ts` can construct the frame class directly, access its private fields, or edit a frame in place. + Callers construct and derive frames only through the exported factory APIs `issuedRefFrame` and `expiredRefFrame`. + `packages/daemon/src/core/ref-frame.ts` owns the transitions and admission that call these factories. The type cannot judge a whole frame that moves unchanged, for example when code clears the field or assigns the frame of another session. The field-owner gate still covers these moves. - A complete snapshot activates an `all` frame. diff --git a/packages/daemon/src/core/ref-frame-slot.ts b/packages/daemon/src/core/ref-frame-slot.ts index 3006dc8727..a7581ae2b0 100644 --- a/packages/daemon/src/core/ref-frame-slot.ts +++ b/packages/daemon/src/core/ref-frame-slot.ts @@ -30,7 +30,8 @@ type RefFrameFields = Readonly<{ /** * One value, replaced whole by the transitions below. The class is not exported, so nothing - * outside this file can construct, edit, or derive a frame; its ECMAScript private field also + * outside this file can construct it directly or edit its private fields; frame construction + * and derivation go through the exported factories below. Its ECMAScript private field also * makes the exported `RefFrame` nominal, so no object literal can stand in for one. */ class SessionRefFrame { From 2f6718b7c733db7caef8c5b5d2045c5de999e70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:46:39 +0200 Subject: [PATCH 52/58] docs(core): state the pinned-generation warning exception in ref-frame.ts resolveRefStalenessWarning (execution/session-snapshot.ts) warns on an active frame when a pinned ref's generation differs from the frame epoch; the existing test covers all three branches (expired any-ref, active plain/matching pin clean, active mismatched pin warns). The header comment claimed an active frame never warns. State the exact behavior. Fixes #3796 review thread discussion_r4306650958. --- packages/daemon/src/core/ref-frame.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/daemon/src/core/ref-frame.ts b/packages/daemon/src/core/ref-frame.ts index ad984e4a8f..5a7dc43a91 100644 --- a/packages/daemon/src/core/ref-frame.ts +++ b/packages/daemon/src/core/ref-frame.ts @@ -28,7 +28,9 @@ const outdatedObservations = new WeakSet(); * issuance scope after a partial publication, and its admission matrix is * enforced fail-closed on every platform before dispatch (ADR 0014, "Mutation admission and read behavior"). * Read-only ref staleness comes from frame state instead of a coarse client-stale marker. - * An expired frame warns; an active frame does not. + * An expired frame warns for any ref; an active frame warns only when a pinned ref's + * generation differs from the frame epoch (execution/session-snapshot.ts, + * `resolveRefStalenessWarning`). */ /** From e9f426ce391b5d75efcb20ead8b37bad7f887036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:46:49 +0200 Subject: [PATCH 53/58] docs(adr): qualify the runner fallback policy and record the panel refusal in ADR 0025 shouldFallbackToRunner gates the runner path in packages/platform-apple/src/core/screenshot.ts: a failure the policy rejects rethrows before any runner capture. The glance row implied the runner follows every simctl failure. State the policy condition. The app-window display facts are unchanged. #3761 merged into main as feca8cb2fc (2026-10-18). When neither the sole lit panel nor CoreDevice's primary panel identifies a panel, resolveAppleCaptureTarget now refuses with COMMAND_FAILED and details.reason capture-panel-unidentified (display-inventory.ts); array order is never used. Update the Capture target body and add the glance row. The former deferred claim is removed. Fixes #3796 review thread discussion_r4306650971. --- docs/adr/0025-foldable-apple-panels.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/adr/0025-foldable-apple-panels.md b/docs/adr/0025-foldable-apple-panels.md index cb29c268e7..3041cff79f 100644 --- a/docs/adr/0025-foldable-apple-panels.md +++ b/docs/adr/0025-foldable-apple-panels.md @@ -6,6 +6,7 @@ Accepted (2026-09-20). The pose-settle amendment is dated 2026-09-21 under #2730. The touch-overlay export diagnosis was corrected on 2026-09-22 under #2707. The Android emulator pose amendment is dated 2026-10-10. +The no-panel refusal correction (#3761) is dated 2026-10-18. This ADR covers iPhone Duo (iOS 27.1, `iPhone19,4`) and Apple devices with more than one integrated CoreDevice display. The amendment below also covers foldable Android emulators. @@ -20,9 +21,10 @@ Only inference answers the second question. | Situation | Behavior | | --- | --- | | iOS simulator capture through `simctl` | First resolve the CoreDevice display table. Capture the panel that the system currently lights. | -| Runner fallback after a `simctl` failure | Follow the target app's window and capture the display that hosts it. Use the facts that the runner reports. | +| Runner fallback after an eligible `simctl` failure | Follow the target app's window and capture the display that hosts it. Use the facts that the runner reports. Only a failure the fallback policy (`shouldFallbackToRunner`) accepts reaches the runner; other failures rethrow. | | More than one integrated panel | Pass `--display=` on every `simctl io screenshot`. Never use the implicit default. | | Zero or multiple lit panels | Name the `primary` panel. Emit `apple_display_capture_ambiguous`. Report pose as `unknown`. | +| No sole lit panel and no `primary` panel | Refuse with `COMMAND_FAILED` and `details.reason: capture-panel-unidentified`. Never choose a panel by array order. | | One integrated panel | Preserve pre-panel behavior exactly: no display flag, no pose, unchanged scale probe. | | Density normalization | Use the captured panel's `pointScale`. For runner fallback, use that capture's reported pixels-per-point. | | Pose report | From panel power alone, report only `closed`, `fully-open`, or `unknown`, never a narrower pose. `fold` reports an exact pose only after hinge readings verify it. | @@ -69,10 +71,12 @@ It is the only reader of CoreDevice display information. If the primary panel is also largest, `primary` still decides. In that case, emit `apple_display_primary_geometry_conflict`. Do not silently replace identity with a geometry rule. - **Capture target.** - `resolveAppleCaptureDisplay` returns a panel for every multi-panel device. + `resolveAppleCaptureDisplay` resolves a panel or a refusal for every multi-panel device. It returns `undefined` only for a single-panel device or unresolved inventory. If exactly one panel is lit, name that panel. - Otherwise, name `primary`, set `ambiguous`, and emit `apple_display_capture_ambiguous`. + Otherwise, if CoreDevice marks a `primary` panel, name it, set `ambiguous`, and emit `apple_display_capture_ambiguous`. + If neither identifies a panel, refuse with `COMMAND_FAILED` and `details.reason: capture-panel-unidentified` (#3761, merged 2026-10-18). + Array order is not identity; the resolver never falls back to it. A multi-panel device never receives a capture without a display name. The implicit default caused the black-image, exit-0 failure. An explicitly named, possibly dark panel makes failure visible and diagnosable. From c1c251cdd6686628f4e3326f0037cf9600910423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:46:58 +0200 Subject: [PATCH 54/58] docs(registry): separate app-switcher admission from execution admitRuntimeOperations (packages/daemon/src/core/runtime-admission.ts) performs the side-effect-free facts inspection of the owner's appSwitcher fact and only then hands back the binding gateway; execution (packages/daemon/src/execution/system-button-runtime.ts) uses the bound operation. The comment implied admission itself uses the bound operation. State the two stages separately. Fixes #3796 review thread discussion_r4306650973. --- packages/command-registry/src/registry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/command-registry/src/registry.ts b/packages/command-registry/src/registry.ts index 74525dd074..462eb6d106 100644 --- a/packages/command-registry/src/registry.ts +++ b/packages/command-registry/src/registry.ts @@ -1614,8 +1614,8 @@ export const RAW_COMMAND_DESCRIPTORS = [ catalog: { group: 'public', key: 'appSwitcher' }, frameworkTier: 'extended', // R56 removed the legacy `dispatch` leaf, capability bucket, and HarmonyOS overlay membership. - // The daemon route remains `generic`; admission and execution use the owner's bound - // `appSwitcher` operation (ADR 0019 §9). + // The daemon route remains `generic`; admission checks the owner's `appSwitcher` fact, + // and execution uses that one bound operation (ADR 0019 §9). recordsSessionAction: true, recordingEffect: 'mutates-app', // ADR 0014, "Command effects are daemon-owned and completely classified": From 278baaad8ab01a18ab30c41dfcda85c3ca9e4cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:46:58 +0200 Subject: [PATCH 55/58] test(help): pin the foldable first-screen rule to the merged #3761 contract The gate asserted the pre-#3761 help literal, so the merged pair (#3788 test, #3761 help owner) fails only in combination on main. Replace the stale rule with the current full contract line, including the capture-panel-unidentified refusal. The help text itself is unchanged; the assertion now matches the rendered first screen. CI evidence: Coverage job 117649571201 at 73d562bb4b fails this assertion on unchanged-base integration; it is not caused by this PR's edits and not a flake. --- src/commands/schema/cli-help-first-screen.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/schema/cli-help-first-screen.test.ts b/src/commands/schema/cli-help-first-screen.test.ts index f11eafce1a..21aeb61feb 100644 --- a/src/commands/schema/cli-help-first-screen.test.ts +++ b/src/commands/schema/cli-help-first-screen.test.ts @@ -101,7 +101,7 @@ const FIRST_SCREEN_RULES: Record = { '9. Reopen the app bundle under test', ], foldable: [ - "Each iOS simulator capture resolves the CoreDevice display table, captures the lit panel explicitly, and normalizes density with that panel's own point scale. Do not add a screen flag to the normal loop", + "Each iOS simulator capture resolves the CoreDevice display table, selects the sole lit panel when power identifies it, and normalizes density with that panel's own point scale. There is no screen flag. When panel power names no single lit panel, the capture names CoreDevice's primary panel, which may be dark and yield an all-black PNG. Check the pose with UIHinge.status before trusting that capture. With no primary panel either, capture fails with COMMAND_FAILED and reason capture-panel-unidentified rather than pick a panel by list order.", 'The two panels are different sizes (iPhone Duo: 466x678 points closed on the outer panel, 669x951 open on the inner). A pose change therefore invalidates every ref and coordinate. Re-snapshot after any pose change and never carry coordinates or refs across', 'Expect a fold to take 10-16 seconds: each hinge read is a five-second devicectl stream, and half-open waits for the hinge to stop moving. Re-snapshot after every fold', 'fold sends a private HID hinge event inside the selected simulator', From 0d830625e878b6e93773f547c3f3992dc1fce67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:56:43 +0200 Subject: [PATCH 56/58] docs(adr): state the attempted flush and field conditions in ADR 0010 withDiagnosticsScope (packages/host-kit/src/internal/diagnostics.ts) runs no finalizer. Only finalizeThrownRequestError (request-router.ts) flushes, and it is reached by thrown errors alone: the token refusal in runRequestWithinScope is a returned normalizeError output without context, and flushDiagnosticsToSessionFile returns no record for zero events or a write failure, so an in-scope thrown error can still omit logPath. State that a thrown request error attempts one forced flush, diagnosticId requires an active scope, logPath appears only when the flush produces a file, and returned or pre-scope failures may omit both fields. The remote logPathUnavailable rule and the no-secrets rule are unchanged. Follow-up qualification on discussion_r4306650954. --- docs/adr/0010-error-system.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/adr/0010-error-system.md b/docs/adr/0010-error-system.md index 4d023ddee2..385c4ec401 100644 --- a/docs/adr/0010-error-system.md +++ b/docs/adr/0010-error-system.md @@ -16,7 +16,7 @@ - If the per-code default hint would mislead, supply a hint. If the default suffices, omit the hint. - When you wrap an error, preserve an existing hint and the cause chain. - Every consumer surface must carry at least code, message, and hint. Rehydration helpers copy all fields. -- A failed request inside a request diagnostics scope always flushes diagnostics. A failure before that scope opens may omit `diagnosticId` and `logPath`. Messages must not embed raw stderr dumps or secrets. +- Thrown request errors attempt one final diagnostics flush. `diagnosticId` requires an active request diagnostics scope, and `logPath` appears only when that flush produces a file. Returned or pre-scope failures may omit both fields. Messages must not embed raw stderr dumps or secrets. ## Context @@ -78,11 +78,14 @@ Nothing documented the conventions, so each new site decided them again. - Events timeline. Rehydration helpers (`throwDaemonError`, `toDaemonHttpRpcError`) copy all fields. `NormalizedError` hoists the typed signals, so `--json` consumers see them. -6. **Observability.** A failed request inside a request diagnostics scope always flushes diagnostics. - Inside that scope on the local machine, the error carries a `diagnosticId` and an ndjson `logPath`. - The router opens the scope at `handleRequestOnLedger` (`packages/daemon/src/server/request-router.ts`). - A failure before it opens, such as malformed RPC method params rejected in - `packages/daemon/src/server/http/http-rpc-methods.ts`, returns an envelope that omits both fields. +6. **Observability.** Thrown request errors reach `finalizeThrownRequestError` + (`packages/daemon/src/server/request-router.ts`), which emits a `request_failed` diagnostic and attempts one forced + flush. It attaches a `diagnosticId` only when a request diagnostics scope is active, and an ndjson + `logPath` only when that flush produces a file; the flush returns no record for zero events or a write failure. + `withDiagnosticsScope` itself runs no finalizer. + A returned (not thrown) failure, such as the token refusal in `runRequestWithinScope`, and a pre-scope + failure, such as malformed RPC method params rejected in + `packages/daemon/src/server/http/http-rpc-methods.ts`, omit both fields. On a remote path, the client keeps only a `logPath` it can open. When it cannot produce one, `logPathUnavailable` replaces `logPath` and names the reason (#1801). Daemon-side diagnostics are under the session state dir. From c190504e92ebbc0c510a5f7a8f6b45f82cf5688a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 12:56:43 +0200 Subject: [PATCH 57/58] docs(adr): scope the primary-panel fallback row to a primary panel in ADR 0025 buildInventory names the primary panel and emits apple_display_capture_ambiguous only when a primary panel exists (display-inventory.ts: the capture display is the sole lit panel or the primary panel); with neither, resolveAppleCaptureTarget refuses with capture-panel-unidentified. The two glance rows overlapped on zero or multiple lit panels. Qualify the fallback row condition. Fixes #3796 review thread discussion_r4306742872. --- docs/adr/0025-foldable-apple-panels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0025-foldable-apple-panels.md b/docs/adr/0025-foldable-apple-panels.md index 3041cff79f..2854d28b77 100644 --- a/docs/adr/0025-foldable-apple-panels.md +++ b/docs/adr/0025-foldable-apple-panels.md @@ -23,7 +23,7 @@ Only inference answers the second question. | iOS simulator capture through `simctl` | First resolve the CoreDevice display table. Capture the panel that the system currently lights. | | Runner fallback after an eligible `simctl` failure | Follow the target app's window and capture the display that hosts it. Use the facts that the runner reports. Only a failure the fallback policy (`shouldFallbackToRunner`) accepts reaches the runner; other failures rethrow. | | More than one integrated panel | Pass `--display=` on every `simctl io screenshot`. Never use the implicit default. | -| Zero or multiple lit panels | Name the `primary` panel. Emit `apple_display_capture_ambiguous`. Report pose as `unknown`. | +| Zero or multiple lit panels with a `primary` panel | Name the `primary` panel. Emit `apple_display_capture_ambiguous`. Report pose as `unknown`. | | No sole lit panel and no `primary` panel | Refuse with `COMMAND_FAILED` and `details.reason: capture-panel-unidentified`. Never choose a panel by array order. | | One integrated panel | Preserve pre-panel behavior exactly: no display flag, no pose, unchanged scale probe. | | Density normalization | Use the captured panel's `pointScale`. For runner fallback, use that capture's reported pixels-per-point. | From 9edf321f25ccdb9cdf5f99f03609a89c6ef6c8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 18 Oct 2026 13:01:35 +0200 Subject: [PATCH 58/58] docs(adr): scope the flush owner to the router finalizer in ADR 0010 The previous sentence generalized over every thrown request error, but a malformed-RPC throw never reaches the router. Name the owner: the router's thrown-error finalizer attempts the flush. Replace the generic returned/pre-scope sentence with the two concrete cases that omit both fields, so the text no longer claims every returned or pre-scope failure must omit them; normalization preserves any existing metadata. Completes the qualification on discussion_r4306650954. --- docs/adr/0010-error-system.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/adr/0010-error-system.md b/docs/adr/0010-error-system.md index 385c4ec401..873ecb7c8e 100644 --- a/docs/adr/0010-error-system.md +++ b/docs/adr/0010-error-system.md @@ -16,7 +16,7 @@ - If the per-code default hint would mislead, supply a hint. If the default suffices, omit the hint. - When you wrap an error, preserve an existing hint and the cause chain. - Every consumer surface must carry at least code, message, and hint. Rehydration helpers copy all fields. -- Thrown request errors attempt one final diagnostics flush. `diagnosticId` requires an active request diagnostics scope, and `logPath` appears only when that flush produces a file. Returned or pre-scope failures may omit both fields. Messages must not embed raw stderr dumps or secrets. +- The router's thrown-error finalizer attempts a diagnostics flush. `diagnosticId` requires an active request diagnostics scope, and `logPath` appears only when that flush produces a file. Returned or pre-scope failures may omit both fields. Messages must not embed raw stderr dumps or secrets. ## Context @@ -78,14 +78,13 @@ Nothing documented the conventions, so each new site decided them again. - Events timeline. Rehydration helpers (`throwDaemonError`, `toDaemonHttpRpcError`) copy all fields. `NormalizedError` hoists the typed signals, so `--json` consumers see them. -6. **Observability.** Thrown request errors reach `finalizeThrownRequestError` - (`packages/daemon/src/server/request-router.ts`), which emits a `request_failed` diagnostic and attempts one forced +6. **Observability.** The router's thrown-error finalizer, `finalizeThrownRequestError` + (`packages/daemon/src/server/request-router.ts`), emits a `request_failed` diagnostic and attempts one forced flush. It attaches a `diagnosticId` only when a request diagnostics scope is active, and an ndjson `logPath` only when that flush produces a file; the flush returns no record for zero events or a write failure. `withDiagnosticsScope` itself runs no finalizer. - A returned (not thrown) failure, such as the token refusal in `runRequestWithinScope`, and a pre-scope - failure, such as malformed RPC method params rejected in - `packages/daemon/src/server/http/http-rpc-methods.ts`, omit both fields. + The token refusal returned by `runRequestWithinScope` and malformed RPC method params rejected in + `packages/daemon/src/server/http/http-rpc-methods.ts` omit both fields. On a remote path, the client keeps only a `logPath` it can open. When it cannot produce one, `logPathUnavailable` replaces `logPath` and names the reason (#1801). Daemon-side diagnostics are under the session state dir.