mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
codex/2178-interaction-facade
120 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7672454304 | refactor: move interaction handlers behind daemon facade | ||
|
|
7646a73b1b |
perf: avoid redundant physical iOS runner health check (#2215)
* perf: avoid redundant physical iOS runner health check * test: isolate iOS runner prewarm coverage |
||
|
|
fd4ab84166 |
refactor(ios): define snapshot acquisition and presentation contracts (#2203)
* refactor(ios): define snapshot acquisition and presentation contracts * refactor(ios): isolate snapshot planning exports |
||
|
|
f513b1d4ae |
refactor: extract daemon replay behind one application facade (#2166)
* refactor: extract daemon replay behind application facade * fix: address replay facade review findings * test: close replay ownership import scan gap * fix: tighten replay capability boundaries |
||
|
|
34e8cbb7a2 |
docs+ux: make device ownership discoverable end to end (#2165)
* docs+ux: make device ownership discoverable end to end Complete the #1320 agent experience so 'busy? -> inspect -> choose or release' is discoverable from every surface an agent actually reads: - devices now projects the blocking claim owner per row (claimedBy with session and workspace, observe-policy projection; provably dead owners are excluded because the next open replaces them automatically), so an agent told a device is busy can pick a free one from the same listing. - help debugging gains a 'Device busy and ownership' section separating the two DEVICE_IN_USE flavors and their exact recoveries. - AGENTS.md documents both flavors; docs/agents/device-verification.md retires the last ps/kill recovery guidance in favor of device status, daemon stop --state-dir, and device release --stale (Stage 5 of #1320). - ADR-0010 no longer calls DEVICE_IN_USE 'the only retriable code' without naming the claim path's non-retriable override. - The rendered cross-worktree claim error gains a help-conformance quiz case binding (sample-output-device-claim-inspects-owner). - README points at device status / device release --stale. Part of #1320. * fix: key ownership projection by canonical device identity end to end Review findings on #2165: - blockingClaimOwnersByDevice keyed claims and inventory rows by bare device.id, so a live Android claim could project claimedBy onto an unrelated same-id Apple/Harmony/Vega row, with scan order picking the displayed owner. Both sides now use the canonical local device key (claim.deviceKey against canonicalLocalDeviceKey of the row's claim identity). The cross-family same-id regression was observed red against the bare-id keying. - The projection is now asserted across every hop the PR promises: client normalization preserves well-formed claimedBy and drops malformed ones, and the devices CLI formatter carries it through JSON data and renders the text line (MCP shares the same serialization). |
||
|
|
70fb783729 |
fix: reuse canonical Maestro visibility context (#2156)
* fix: reuse canonical Maestro visibility context * perf: skip empty Maestro visibility filtering * refactor: centralize snapshot visibility context |
||
|
|
48eb1b06e3 |
feat: open Limrun uploaded apps (#2110)
* feat: open Limrun uploaded apps * refactor: generalize deferred provider app selection * refactor: fold limrun preinstall into allocation * refactor: keep app selection provider independent * refactor: enforce provider-independent commands * refactor: restrict provider imports from commands * refactor: keep provider guard lightweight * fix: enforce provider-independent commands * fix: enforce provider catalog lease admission * refactor: deepen provider app catalog admission * refactor: simplify provider capability projection * fix: restrict Limrun uploaded apps on public daemon * refactor: separate provider app catalog lookup * fix: simplify uploaded app provider boundaries |
||
|
|
d9677301f4 |
feat: add human takeover controls (#2078)
* feat: add human takeover controls * fix: harden human takeover controls * fix: align host XCTest selection count * fix: address takeover readiness feedback * fix: handle macos runner permission prompt in smoke tests * fix: detect background macos permission dialog * fix: dismiss inaccessible macos privacy sheet visually * fix: map macos privacy prompt without accessibility * refactor: own human-control holds in lease registry * fix: cancel pending human takeover on disconnect |
||
|
|
9abcd7fe03 |
refactor: move Apple platform family into package (#2118)
* refactor: move Apple platform family into package * fix: preserve Apple facade sync contracts * fix: complete Apple W4 rebase review fixes |
||
|
|
c7f42ccedc |
refactor: move Android family behind package exports (#2117)
* refactor: move Android family behind package exports * fix: address Android W5 review feedback * fix: update relocated routing fixture assertion |
||
|
|
af6f12e391 |
chore: adopt shared oxlint config (#2115)
* chore: adopt shared oxlint config * fix: preserve project lint boundaries * fix: remove redundant oxlint config |
||
|
|
ddb415a2c7 |
refactor: sink package-closed src modules into existing packages (#2106)
* refactor: sink package-closed src modules into existing packages Move closed modules into contracts, kernel, capture-kit, and ad-script, and declare DaemonCommandDescriptor in core so R6/R9 can pin the remaining provider-webdriver type cycle. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: keep contracts and capture-kit off generic sinks Move interaction-outcome, snapshot warning rendering, and inventory ALS behind focused owners, and plant R18/R70 domain-shape gates so they cannot return as package export-map growth. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop moved implementation comments from owner modules Names, types, and tests already carry those invariants; the relocated files should not keep review-history or control-flow narration. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: drop the empty snapshot-quality layering zone W1 moved the verdict into capture-kit and this PR moved warning rendering into snapshot-presentation, so the ranked zone no longer has production files. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
2e87347bee | fix(wait): surface runner restart timeout evidence (#2113) | ||
|
|
7a6284bb36 |
refactor(platforms): break the four upward edges out of src/platforms (#2090)
* refactor(platforms): break the four upward edges out of src/platforms (#2082 W2) src/platforms carried four imports that point up into root src, each of which would become an R11 violation the moment its family moves into a platform package: - android/app-helpers.ts reached the composition root for the foreground parser. The parser is pure dumpsys vocabulary, so it moves to @agent-device/contracts/android-observation; the platform-android app-state module and app-helpers both consume it from there, and the composition wrapper plus the package facade's lazy re-export retire (R13 allows only the composition root to import platform packages, so vocabulary relocation is the inversion that stays legal). - web/provider.ts and web/agent-browser-network.ts type-imported the backend diagnostics/network-dump vocabulary from src/backend.ts. Those six types move to @agent-device/contracts/backend-diagnostics; backend.ts re-exports them for its SDK consumers. - snapshot/snapshot-desktop-surface.ts split three ways: the pure projection (scope/interactive/depth) moves to @agent-device/contracts/snapshot-desktop-projection, the per-family captures move to platforms/linux/surface-snapshot.ts and platforms/apple/os/macos/surface-snapshot.ts beside the code they dispatch to, and the root file keeps only the device-dispatching runtime host behind R3-tolerated dynamic imports. apple/interactor now reaches macOS surface capture family-internally instead of through root. src/platforms -> root src is now zero edges. Contracts grows two entries (pinned, budgeted); the android foreground-parser tests move beside the parser. Left for a later pass: app-parsers.ts shares the marker-walk loop shape with the contracts parser but parameterizes it for blocking-dialog parsing - generalizing that is a design change, not a move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * refactor(platforms): keep platform parsing with its family; give snapshot shaping its capture owner ADR-0019's amendment forbids satisfying R13 by moving implementation into contracts, so this wave's two relocations invert instead of sink: - The Android dumpsys foreground parser returns to @agent-device/platform-android with its owning test, and contracts/android-observation goes back to observation vocabulary only. src/platforms/android/app-helpers exposes createAndroidAppStateReader(parseForegroundApp) and never imports upward; the composition seam in src/sdk/android-adb.ts injects the root-composed parser, keeping the published getAndroidAppStateWithAdb(adb) signature intact. - The desktop snapshot projection moves to @agent-device/capture-kit/snapshot-desktop-projection beside the rest of the capture-side snapshot behavior; contracts exports the snapshot-scope vocabulary it consumes. The #1832 history narration in the projection test is gone; the test name and golden fixture carry the invariant. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * refactor(platforms): the adb app-state read lives whole behind the platform-android seam The injected-parser loop retires: platform-android's app-state module owns the complete adb-executor read/parse (readAndroidAppStateWithExecutor, beside its host-based twin), the façade exposes it lazily, the composition root wraps it, and src/sdk/android-adb.ts reaches it through that root in one hop. app-helpers keeps only the app-list helpers, and the SDK-route tests live in SDK topology as src/sdk/android-adb.test.ts. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: drop the seam wrapper explainer comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: drop the projection docblocks the test names already carry Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ea1d6b8c55 | refactor: remove retired capability matrix (#2089) | ||
|
|
7b48531d3b | refactor: retire ADR-0019 cutover scaffolding (#2081) | ||
|
|
c794c11d7e |
refactor: close the daemon platform boundary (#2072)
* refactor: move runtime resource mechanics out of daemon * refactor: move Apple resource access out of daemon * chore: enforce the terminal daemon platform boundary |
||
|
|
3378d9c05a | refactor: move Android system observation out of daemon (#2071) | ||
|
|
03f0f408c2 | refactor: move platform provider composition out of daemon (#2070) | ||
|
|
b44f882c83 |
docs(batch): name the step shape and the accepted commands in help batch and in its refusals (#2067)
* docs(batch): name the step shape and the accepted commands in help and refusals
`batch` accepts one step shape and `help batch` documented none of it: the
usage line, one sentence, and the flags. Every refusal named only what was
wrong. A caller reaching for `press` through `batch` therefore saw
"Invalid batch step 1." for `["press @e12"]` and "unknown field(s): args" for
`{"command":"press","args":[...]}`, and reasonably concluded the mutating verbs
were excluded (#2062).
They are not, and never were: `press`, `click`, `fill`, `longpress`, `scroll`
and `back` all carry `batchable: true` in the command-descriptor registry,
including at 0.20.10. The exclusions are `batch`/`replay` (which never nest) and
the session/daemon/connection/host-tooling commands. Nothing about the
allowlist changes here; what changes is that it is stated.
- `help batch` documents the step shape, serial semantics, and RENDERS the
accepted commands from the registry's `batchable` trait, so the listing cannot
drift from the runtime allowlist.
- The step-shape refusals (non-object step, unknown field, non-object input)
share one hint naming `{"command":"<name>","input":{...}}`, owned by
`batch-contract.ts` next to the checks that raise them.
- The non-batchable-command refusal points at that listing and says which
families are excluded and why.
- `assertAllowedKeys` takes an optional hint so the batch call sites can attach
theirs without a second unknown-key check.
Closes #2062
* docs(batch): ground the step-shape guidance in the structured schema and keep contracts surface-neutral
`help batch` now prints runnable snapshot/press/fill steps carrying the real
structured field names (`target: {kind, ref}`, `text`, `interactiveOnly`), which
no `help <command>` text states, and says so instead of pointing at command help
for them. `cli-help-examples.test.ts` reads those steps back out of the rendered
help and runs each `input` through its own command's `readInput`, so a renamed
field fails there rather than shipping guidance nobody can run.
Fixes the stale batch guidance the audit missed: `help workflow` named
`batch ./steps.json`, which positional input rejects, and `help scripting` still
weighed the removed positionals/flags shape against the accepted one.
`BATCH_STEP_SHAPE_HINT` in `@agent-device/contracts` describes the shape only;
`readBatchStepRecord`/`readBatchStepInputObject` take the hint as a parameter so
the CLI attaches its own `agent-device help batch` recovery step while the Node
client and MCP tools keep the surface-neutral one.
* test(batch): pin every advertised step key, and hint the removed-shape refusal
The example-validation test accepted a step whose optional key the
reader silently dropped — readInput ignores unknown keys, so a renamed
settle or interactiveOnly kept the test green while help advertised a
step that does less than it claims. Every printed key must now survive
into the parsed input.
The removed positionals/flags refusal carries the CLI shape hint like
its three sibling refusals.
* fix(batch): keep the availability refusal surface-neutral; CLI attaches its recovery
readStructuredBatchCommandName emitted 'Run agent-device help batch'
unconditionally, and the same reader backs the MCP/Node batch metadata
— an MCP caller got a terminal-only, unrunnable recovery step. The
shared default now states the exclusion boundary itself with no
terminal vocabulary (MCP/Node read the accepted commands off the step
schema's command enum), and the CLI admission appends the help pointer
via the same optional-hint parameter the shape hint uses.
Found while fixing it: hint strings are redaction-capped at 400
characters, so enumerating the derived roster inline truncates the
hint — recovery pointer and all. The regression pins neutrality AND
that the hint survives the cap whole.
Addresses the P2 review on #2067.
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
|
||
|
|
ed8d5efb00 |
fix(daemon): address cwd-scoped sessions by their store key, not their public name (#2068)
* fix(daemon): address cwd-scoped sessions by their store key, not their public name An implicitly cwd-scoped session is NAMED `default` and STORED under `cwd:<hash>:default`. Three surfaces built caller-facing text from the name, so each pointed at something that does not exist: - `DEVICE_IN_USE` reported `session "default"` and its recovery hint said `agent-device close --session default`. `--session` marks the session explicit, which disables cwd scoping, so following the hint addressed a different, absent session: `SESSION_NOT_FOUND`, and the device stayed held — the residual half of #2031 that #2057's superseded-daemon reconciliation does not cover. - `session list` reported `sessionStateDir` and `runnerLogPath` under `<state>/sessions/default`, a directory that is never created; the session's real artifacts sit in `<state>/sessions/cwd_<hash>_default`, which is what `open` already answers with. `SessionStore.entries()` exposes the key alongside the record, and `buildSessionRecoveryHint` takes the session's address explicitly. Call sites that only hold the session the current request named pass its name — the address there — so their text is unchanged, as are explicitly named sessions everywhere. Live before/after on an iOS simulator: `open` (cwd-scoped default), then `open --session qa` on the same device. Before, the hint's own `close --session default` answered `SESSION_NOT_FOUND` and the retry failed again; after, the hint names `cwd:8bea844ab16aa9b3:default`, that close releases the device, and the retry opens. Refs #2031, #1394 * fix(daemon): thread the resolved session address through every recovery producer The store key a request resolves — `cwd:<hash>:default` for an implicit session — was known upstream but dropped before the selector-conflict and lock-conflict producers, which then named `SessionState.name`. Both emitted `close --session default`, the unreachable recovery this PR fixes for DEVICE_IN_USE: `--session` marks the session explicit, so that command addresses a different session. `SessionRef` ({ address, session }) now carries the pair, so a recovery producer cannot be handed a record whose address was never resolved. `buildSessionRecoveryHint`, `assertSessionSelectorMatches` and `applyRequestLockPolicy` take it; `prepareLockedRequestBinding` and the Maestro replay route build it from the store key they already hold. Replaces the broad `SessionStore.entries()` with the store's own narrow lookups — `lookup`, `findByDevice`, `listRefs` — so callers receive the session with its address instead of enumerating raw key/record tuples. `session list` reports `address` alongside `name`, through the client contract and serializer, so its discovery output names a value `--session` accepts. Regressions run the production routes, not the helpers: router-level device-in-use, selector-conflict, lock-policy-conflict and `session list` cases open an implicit session and read the address back off the store, plus a typed-Maestro selector-conflict case. Each was observed red against the pre-fix behavior it pins. Refs #2031, #1394. * fix(daemon): doctor names same-device sessions by address; green the fallow gate - sessionChecks enumerated same-device sessions by SessionState.name: the printed close --session default was the exact unreachable recovery this PR removes elsewhere, the name-based dedupe hid two cwd-scoped default sessions from each other, and the evidence sessionStateDir pointed at <state>/sessions/default, which never exists. Candidates now come from listRefs() and report addresses. - findByDevice joins values/delete in .fallowrc.json usedClassMembers (same resolution false positive on this class; the call site is session-open-execution.ts). - the scoped-paths test narrows its response once instead of nine optional-chaining hops, which tripped the CRAP gate. * style: format .fallowrc.json --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
4b8bcaca60 |
feat(interaction): accept fill <target> "" as the clear-field primitive (#2066)
* feat(interaction): accept fill <target> "" as the clear-field primitive
Emptying an input was not expressible: `fill` refused the empty string
("Expected text to be a non-empty string"), `type` only appends, and `keyboard`
has no delete verb. Clearing a field before typing is a routine QA step, so the
only route was the app's own clear button or N locale-dependent keyboard delete
presses read out of a snapshot.
`fill <target> ""` now means "replace with nothing". Both platforms already own
the clear half of replace, so this is the validation and reporting that stood in
front of it, not a new interaction:
- `stringField` takes an opt-in `allowEmpty`, used only by `fill`'s `text`.
`requiredField` still refuses a MISSING text, so `fill @e57` stays an error
rather than silently erasing the field — `readFillTargetFromPositionals` now
reports `undefined` for "no text argument" instead of collapsing it to `''`.
`type` keeps refusing an empty text: appending nothing is not a clear.
- The Apple runner's empty-text early return skipped the clear while reporting
"typed". For a replacement it now runs `clearTextInput` and verifies the field
came back empty (secure fields stay unverifiable, as elsewhere).
- Android already clears before typing and skips an empty shell/IME write, but
its verifier read a cleared field's absent `text` attribute as a mismatch
against `''`. An empty expectation now accepts null or "".
Whitespace-only text keeps its established per-shape rules; only `''` is new.
Closes #2063
* fix(interaction): fail the empty-fill clear closed on every backend
Addresses the P1 review on #2066, then closes the same fail-open class
on the backends the PR did not reach:
- Android: an empty expectation no longer matches when the verification
scan observed NO input node at all — actual is null both for a cleared
field and for a wrong point/lost focus, and three empty samples of
nothing were a stable success for a clear that never touched a field.
- Apple runner: when the empty-replacement path cannot resolve a clear
target (including the synthesized first-responder route, whose target
carries no element), it returns the typed TEXT_INPUT_NOT_FOCUSED
failure instead of falling through to the vacuous-typing
verified-success return. Regression runs in the ios.yml XCTest lane.
- webdriver: fill is tap + sendKeys and owns no clear mechanism, so an
empty fill refuses as UNSUPPORTED_OPERATION before touching the
device, instead of reporting a clear it cannot perform.
- linux + web coordinate fill: typing zero characters over the
select-all selection left the old value intact; the empty fill now
deletes the selection.
- recording: an empty --record-as literal matches inside every string;
it now parameterizes only the fill's own text field instead of
rewriting every empty field and empty evidence label in the entry.
(The session-wide echo registry already excluded empty literals.)
- help: the text-entry topic taught agents that fill "" is not a
clear-field command; it now states the new contract.
Each new test was observed red against the pre-fix code.
* fix(android): read hint-showing from the helper so a cleared field verifies
Live Pixel 9 emulator, adb-shell channel: clearing the Settings search
field succeeded on the device but reported 'Android fill verification
failed', because a cleared EditText dumps its HINT as text — getText()
returns the hint for an empty field on modern Android, so 'Search
settings' read back as a residual value. This is the same
placeholder-as-value trap the Apple runner already handles with
treatingPlaceholderAsEmpty.
The helper now emits hint-showing (isShowingHintText, API 26+), the
hierarchy parser carries it, and fill verification matches against the
field's VALUE — hint-only text is an empty value, for empty and
non-empty expectations alike. A field whose real value equals its hint
string keeps failing the clear check: only the authoritative flag, never
the text, says it is a hint. Raw uiautomator dumps carry no such fact
and keep the fail-closed behavior.
Live evidence, both admission channels, after this fix: test-ime and
adb-shell clears both report Filled 0 chars with the field back on its
placeholder; the pre-fix adb-shell run failed closed (never a false
success).
* fix(interaction): close the adversarial-review findings on the empty-fill clear
- android adb-shell: the delete burst is sized from the value being
REMOVED (pre-mutation read; the attempt's cap when unreadable), not
from the empty incoming text, which sent the 12/24-delete minimums and
could never empty a field longer than 36 characters.
- android: the unconfirmed soft-success no longer applies to an empty
expectation — nothing app-formats the empty value, so residue after a
clear is a failed clear, and the soft-success also skipped the second,
bigger delete burst.
- android masked fields: an empty expectation accepts an observed masked
node with no dump text (a masked field WITH content dumps its bullet
run), so clearing a password field no longer fails after the clear
worked — matching iOS, where a secure-field clear succeeds unverified.
- find: 'find <q> fill ""' now reaches the fill leaf as the clear
request on both the CLI reader and the daemon positional parse; a
MISSING value keeps its refusal at each producer, so the typed
value: string contract is unchanged.
- maestro export: a recorded clear exports as tapOn + eraseText instead
of a vacuous inputText: "" (with the 50-character-default warning).
- the missing-text refusals teach the clear form: (use "" to clear
the field).
Full unit suite green (1061 files); each behavioral fix carries a test
observed red against the prior code.
* refactor(interaction,android): extract the fill parse and shell-attempt branches
The review commits pushed parseFillTarget and fillAndroid over the
complexity gate (13 cyclomatic each). Each fill target shape parses in
its own function sharing one missing-text response, and the adb-shell
attempt (clear sizing + clear + type + verify) moves out of the fill
loop. Behavior-preserving; the existing tests cover every branch.
* refactor(interaction,android): one owner per empty-fill fact
Design pass after review: the missing-vs-empty rule and the observed-
value rule each had several owners; now each has one.
- parseFillTarget decodes ONCE through readFillTargetFromPositionals —
which already owns shape detection and documents the undefined-vs-''
contract on DecodedFillTarget — and keeps only what the wire owns:
versioned-ref admission, the selector whitespace rule, and the daemon
responses. This deletes the point branch's duplicated slicing, the
hasFillText guard, and the three per-shape parse functions.
- observedAndroidValue() is the single statement of Android's value
rule (absent attribute and hint-only text are the empty value); the
text branch, the match rule, and the masked branch all consume it.
The masked branch thereby gains the hint-showing collapse it was
missing, and isAcceptableAndroidFillMatch narrows to plain strings.
- The empty-text-is-clear contract is stated once, on Interactor.fill
in contracts, instead of implied per backend.
Behavior-preserving except the masked+hint gain; the existing tests
cover every branch (494 Android, 15 fill-target).
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
|
||
|
|
71214e11da |
refactor(runtime): close residue execution units (#2054)
* refactor(runtime): close residue execution units * refactor(runtime): address residue ownership review * fix(runtime): preserve viewport diagnostic log path |
||
|
|
9f16fc885c | refactor: migrate perf to device runtime (#2061) | ||
|
|
8af2660332 |
refactor(android): extract the adb executor and IME cluster into packages/platform-android (#2041) (#2044)
* refactor(android): extract adb executor and IME cluster into packages/platform-android (#2041) Implementation moves behind an injected adb host port (R13-clean); shared vocabulary moves to contracts (android-touch-plan, android-helper-artifacts) and kernel (keyed-lock); root keeps thin re-export shims plus the composition wiring that binds the port. session.ts/session-observability.ts no longer import platform transport types (opaque unknown, R62 pattern). * refactor(android): trim shims to consumed surface, break helper-install cycle, add gate coverage Fallow-clean: shim re-exports carry only names root still consumes; helper-package-install imports the package subpath directly (no cycle) and reuses the contracts decision types; transitional R13 table gets planted-red tests; ADR-0019 records the transitional exception; eager-closure rows added for the new entry surfaces. * review: restore maxBuffer on spawn options, scope the transitional test allowance Adversarial review round 1: AndroidAdbSpawnOptions keeps maxBuffer for spawn- signature width parity with the pre-move ExecBackgroundOptions; the R13 test allowance narrows from any test file to the cluster's own src/platforms/android __tests__ directory, with planted-red coverage for a foreign test file; ADR text records the scoped allowance. * review: keep an unbound adb host port loud in listAndroidAdbSerialsQuick * style: format policy test * refactor(android): split the extracted adb and IME modules into focused owners Review: the moved adb-executor.ts (652 lines) and ime-lifecycle.ts (487 lines) carried three concerns each across the package boundary. The entry subpaths are now thin surfaces over focused modules — adb: transport vocabulary, failure classification+enrichment, provider normalization, provider scope/routing, port-reverse ownership, pull/install transfers; IME: ownership state, on-device settings record, activation transaction, restore+orphan recovery — every module under 250 lines, each with colocated tests over an in-memory host-port stub and a shared fake IME device. R31's fence invariant repoints to ime-activation.ts; eager-closure pins move to the split shape. * rebase: reconcile the transitional android table with the #2050 mechanics-facet model The R13 declaration check now composes both enumerated subpath sets on the exact-list model #2050 introduced; the policy test fixture declares android's transitional subpaths the same way it declares apple's runner facet. |
||
|
|
74a70f1764 | refactor: remove next-major compatibility surfaces (#2046) | ||
|
|
aeb2ff8402 |
refactor(contracts): retire the platform/interaction compatibility façades (#2048)
#1959 granularized the contracts entry surface but left the wide platform/interaction façades in place as a compatibility surface for ~490 type-only importers. This mechanically moves every importer (~350 files) onto the granular subpath each symbol actually lives on, deletes the two façade files, and removes the fallow ignoreExports entries and eager-closure-budget rows that existed only to cover them. Eight previously-unexported source files needed new package.json subpaths (clipboard, keyboard, network-traffic, platform-plugin, platform-providers, runner-lease-context, screen-recording-runtime-host), and ./interaction now points directly at src/interaction.ts instead of the deleted barrel. .oxlintrc.json's no-restricted-imports rules for the two façades are removed since there's no wide facade left to warn against value-importing. |
||
|
|
494f1c5ad0 |
Migrate audio probe to platform runtime with durable resource lifecycle (#2038)
* refactor(daemon): migrate audio onto the request-bound device runtime (R60) Wave 6 closure unit 1 of 2 for #1739. The audio probe leaves legacy execution for exact-owner runtime facts with the logs/record durable treatment: - contracts: audio-probe-runtime operations (audioProbeStart/Reattach/Cleanup for the durable host capture, audioProbeQuery for the stateless web page probe), audio-runtime-plan action-selected uses + shared positional grammar, audio-probe-runtime-host seams; two new required unavailable cells. - capture-kit: one shared host-capture implementation (descriptor codec v1 with cleanup-only recovery, start pipeline waiting on the sampler's first status publication, live handle, exact-identity recovery operations) used by both darwin-hosted owners. - owners: apple and android state their exact capture cells (macOS host only; the legacy bucket's physical-iOS over-claim becomes a stated refusal), web owns the page probe, all other families and both providers state refusals. - daemon: audio-probe.resource.json envelope via the DurableCaptureResource coordinator, fence-minting admission ledger, session slot becomes the neutral handle+envelope pair (store-owned under R7), teardown/close finish through the coordinator, startup recovery registered in the device-claim reconciler; the handler admits by facts inspection and binds once per plan. - deleted: the capability bucket, the WEB_QUERY_COMMANDS graft and its matrix pass, both supportsByDefault closures, src/daemon/audio-probe.ts, src/platforms/audio-probe-backend.ts, and the macOS backend shim. - gates: cutover row R60 (durable tier, lifecycle proof on the session slot), R7/R11 baselines moved for the slot reclassification and new subpaths. Known parity delta, itemized: status/stop with no active probe now answers without backend-specific notes (the daemon no longer knows a backend before a capture starts); the wire shape is otherwise unchanged. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(daemon): doctor becomes host-scoped execution behind the host-diagnostics surface (R62) Wave 6 closure unit 2 of 2 for #1739, carrying the ADR-level discriminator decision the pre-unit record names: CommandPlatformExecution gains { kind: 'host' } — host-scoped diagnostics contributed by platform families through a neutral surface, binding no device runtime of its own. 'none' remains barred as a migration target; doctor's device legs already ride the migrated inventory gateway, facts inspection, and the apps unit's use. - contracts: host-diagnostics facet (toolchain/device/ambient/warmup methods over the existing DoctorCheck vocabulary, a per-call context carrying the neutral inputs, and an opaque provider transport override the one owning family narrows back); the discriminator assert, entry gate (host is held to the same no-bucket rule as none), and error text extend to the new kind. - probes move to their owning families: apple (xcodebuild/xcode-select + runner-cache warmup), android (adb/SDK/license toolchain, Metro reverse, orphaned test-IME), harmonyos (hdc), vega (tool provider + VVD inventory read via the context), web (managed browser census); one shared first-line probe helper. Wire shapes and check ids are byte-identical. - composition: createHostDiagnostics with per-family lazy loading, injected from daemon-runtime through router/chain/session params — the daemon never imports the composition root, so no transitive platform edge returns. - daemon: session-doctor.ts keeps orchestration only; its six platform imports and the three probe-owning modules (session-doctor-{toolchain,android,web}.ts) are deleted. - gates: HostCutover row variant with a gateway-identity proof (R17's shape); R62 row; the descriptor-row completeness test now mandates rows for host descriptors; ADR 0019 rules-at-a-glance amended. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(audio): fold the per-family probe factories into one capture-kit operation set Post-migration fallow pass over R60/R62: apple and android carried byte-identical audio-probe operation factories, so the shared createHostAudioProbeCaptureOperations now lives in capture-kit and both bind arms call it directly; the family files keep only their stated capture facts. Also un-exports the plan/recovery symbols nothing consumes anymore, splits the durable-descriptor field validation out of the decoder, names the Linux audio refusal by its file's convention, and drops a stale session-doctor-web mock from the relocated doctor test. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(coverage): repoint platform audio/doctor coverage claims at the migrated evidence The R60/R62 rewrite renamed the daemon audio contract tests and moved the web doctor suite, which broke the macos-coverage smoke gate on CI and five sibling coverage claims that only fail on non-darwin hosts. Repoints the macOS, web, and iOS-simulator manifests at the surviving tests (adding an iOS-simulator start contract the manifest already named), converts the Linux audio row from capability-denial to a fact-owned contract backed by new stated-refusal assertions in the Linux runtime denominator test, and retires the darwin-dependent capability special-cases: audio admission is owned by the exact-owner runtime fact now, not the catalog. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(daemon): forward hostDiagnostics through the session command route Adversarial review of the R62 cutover found the wiring gap that broke every doctor request served through the daemon: handleSessionCommands re-composed its handler params without the hostDiagnostics gateway, so requireHostDiagnostics always threw. Forwards it, composes createHostDiagnostics() in the provider-scenario harness the same way the daemon runtime does (all nine doctor integration tests pass again), merges a duplicate test import that failed lint, applies oxfmt to the files the branch left unformatted, and trims blank-line residue from a deleted capabilities test. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(audio): refuse to publish a capture without an exact process identity Review P1 on the R60 unit: start permitted resolveManagedProcessIdentity to yield no marker, recovery then mapped the markerless descriptor to missing, read a possibly in-progress status file as completed, and terminalized the durable resource without proving or terminating the child. The marker is now required end to end: start terminates the helper and fails when the process exposes no identity, the descriptor codec rejects markerless bodies so a foreign or corrupted record routes to manual recovery instead of a guessed outcome, and a planted-red regression pins that a markerless record with a live status file is never read as completed or missing. Also splits the capture-kit audio-probe module along its concerns (descriptor codec / status reads / cleanup-only recovery / live-process owner) per the same review, moving the eager-closure budget rows the three new files cost. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * fix(audio): let timed probes complete and never adopt a stale status file Live exact-head evidence on macOS surfaced two lifecycle defects: - The helper's runAudioProbe parked the CLI's main thread in a semaphore while an unstructured Task ran the capture loop; with no run loop ever spinning in the one-shot process, the loop stalled at its first Task.sleep suspension, so every timed probe froze after its first bucket and never completed on its own. The probe is now fully synchronous: ScreenCaptureKit's completion-handler APIs bridged through semaphores (the pattern the helper's screenshot path already uses in production) and a plain Thread.sleep cadence loop. - startHostAudioProbe accepted a pre-existing audio-probe.json, so restarting in a session that had already run a probe returned the previous run's snapshot as the new probe's first status. The status path is cleared before the spawn — the previous handle's finish() has already terminated and awaited its process, so any file observed after the spawn was written by the new helper. A planted-red regression pins that a stale file is neither adopted nor left behind. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN * test(audio): complete runtime host fixtures * fix(audio): surface helper death after a running checkpoint instead of completing it Review P1 on the live-evidence pass: the live handle discarded the helper's terminal result, so a sampler that died after publishing a running checkpoint read as running forever and stop fabricated a normal stopped completion; marker-missing recovery compounded it by finalizing any persisted status — a running checkpoint included — as completed. The handle now observes process.wait: a non-terminal status file plus an observed exit fails status and stop with the helper's exit detail, so the durable coordinator records the failed terminal transition and the record resolves through descriptor cleanup rather than a fabricated result. Recovery treats only a terminal stopped publication as a completion; a running checkpoint with the exact PID gone reports missing, which terminalizes the envelope as already-missing with no completion metadata. Planted-red regressions cover both: the live handle with a running checkpoint plus child exit, and a daemon-restart recovery over an orphaned running checkpoint. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cd7fCKcxuDU3KrXF4MF9rN --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c77bc40d48 |
refactor(daemon): Wave 6 — migrate clipboard, app-switcher, trigger-app-event, settings, alert, react-native and capabilities onto request-bound runtimes (R55–R63) (#2021)
* refactor(daemon): migrate clipboard onto request-bound runtimes (R55) Wave 6 unit 1 of the ADR 0019 platform-free daemon migration (#1739). `clipboard` leaves the legacy dispatch projection: admission is now the action-selected `readClipboard`/`writeClipboard` fact the parsed subcommand names, and the only execution is that one bound operation. - new `@agent-device/contracts/clipboard-runtime` facet, riding the existing `Interactor` seam through `interactor-operation-binding.ts`; read and write are separate cells because a provider can genuinely expose one half only. - every owner states its own cells: Apple gains a `system/` facts module (simulator or the macOS host, matching the retired `supportsHostOrSimulatorSurface` closure), Android admits every real kind, Linux the desktop device, and HarmonyOS/Vega/web refuse -- none ever carried a bucket. Limrun reuses the local Android interactor and refuses on iOS; WebDriver rides interactor reachability like `back`/`home`. - retires the `core/dispatch.ts` clipboard arm and handler, the descriptor's capability bucket and `dispatch` leaf, and the Apple plugin's clipboard admission closure. `handlers/session.ts` loses its inline handler (and its last `dispatchCommand`/`requireCommandSupported` imports) to the new `handlers/session-clipboard.ts`. - `bindLocalInteractorOperationSet` collapses the byte-identical local interaction bind list Android and Linux each held a copy of. Cutover row R55 with its retirement, admission-member and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate app-switcher onto request-bound runtimes (R56) Wave 6 unit 2 of the ADR 0019 platform-free daemon migration (#1739). `app-switcher` leaves the legacy dispatch projection: admission is the owner's `appSwitcher` fact and the only execution is that one bound operation, resolved by the generic route alongside back/home/orientation/tv-remote. - new `@agent-device/contracts/app-switcher-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. - Apple states one springboard reading for `home` and `app-switcher` (parity: the retired `supportsAppAndDeviceLifecycle` closure gated both off the same per-AppleOS row, so macOS and watchOS refuse); Android admits every real kind; HarmonyOS admits both kinds, restating the retired overlay membership; Linux/Vega/web refuse. Limrun reuses the local Android interactor and refuses on iOS; WebDriver rides interactor reachability. - retires the `core/dispatch.ts` arm, the capability bucket, the `dispatch` leaf, `HARMONYOS_SUPPORTED_COMMANDS` membership, the Apple plugin closure, and the now-readerless `appAndDeviceLifecycle` row in the per-AppleOS table. - router tests that used `app-switcher` as their legacy-dispatch stand-in move onto bound operations; the typed-error `supportedOn` test moves to `perf`, the one command that keeps a capability-matrix row after this wave. Cutover row R56 with its retirement, admission-member and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate trigger-app-event onto request-bound runtimes (R57) Wave 6 unit 3 of the ADR 0019 platform-free daemon migration (#1739). `trigger-app-event` leaves the legacy dispatch projection: admission is the owner's `triggerAppEvent` fact and the only execution is that one bound operation. The split follows ADR 0019 §2 — a facet input names no command, request, or CLI flag. The event name pattern, the payload size limit, and the per-platform `AGENT_DEVICE_*_APP_EVENT_URL_TEMPLATE` are daemon policy and stay in `core/app-events.ts`; what reaches the owner is a resolved URL to open. They also stay downstream of admission, where the retired `dispatchCommand` ran them, so an unsupported device still reports its unsupported cell rather than an argument error. - new `@agent-device/contracts/app-event-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. - Apple admits every leaf with a constructible interactor (no closure ever gated this command beyond its bucket), Android every real kind, and Linux/HarmonyOS/Vega/web refuse. It is the one system leaf both Limrun legs serve, since each implements `open`; WebDriver rides interactor reachability. - retires the `core/dispatch.ts` arm and handler, the capability bucket, the `dispatch` leaf, and the session route's last capability-gate-then-`dispatchCommand` thunk: every leaf on that route now supplies a bind-and-execute thunk. - the end-to-end delivery tests keep their shell-level assertions and move onto the migrated composition. Cutover row R57 with its retirement and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate settings onto request-bound runtimes and retire the legacy dispatcher (R58) Wave 6 unit 4 of the ADR 0019 platform-free daemon migration (#1739). `settings` was the last `DISPATCH_HANDLERS` arm, so this change closes the command and retires the legacy command dispatcher whole. - new `@agent-device/contracts/settings-runtime` facet on the shared `Interactor` seam, bound through the interactor catalog. What reaches the owner is its own settings vocabulary (setting, state, resolved app id, typed coordinates); the CLI parse, the macOS setting-name gate, the clear-app-state app-id check and the coordinate typing are daemon policy and stay daemon-side, downstream of admission where the retired leaf ran them. - Apple shares clipboard's exact host-or-simulator reading (the retired admission intersected the `settings` bucket with the same `supportsHostOrSimulatorSurface` closure); Android admits every real kind; HarmonyOS matches its retired overlay membership; Linux/Vega/web refuse. Limrun splits Android-reuse / iOS-refusal like `app-switcher`; WebDriver refuses unconditionally, since its interactor declares settings unsupported. - retires `dispatchCommand`, `dispatchWithInteractor`, `dispatchKnownCommand`, `DISPATCH_HANDLERS`, `listRegisteredDispatchCommandNames`, and the request router's `executeGenericPlatformCommand` fallback. `core/dispatch.ts` keeps only `dispatchGestureViewport`, whose last consumers are replay/test. Retiring the dispatcher surfaced two callers broken since Wave 5 moved `press` onto a bound runtime: react-native overlay dismissal and the opt-in interaction no-change retry both called `dispatchCommand(device, 'press', …)`, which has thrown `INVALID_ARGS: Unknown command: press` on main since R48. Both now run the same bound `tapPoint` every other touch leaf uses. The retry declares its own callback seam rather than importing runtime admission, so the policy stays readable without the binding stack — and that inversion, plus the dispatcher's retirement, drops the largest type-level import cycle from 25 files to 21. Cutover row R58 with its retirement and single-bind claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(daemon): migrate alert, react-native and capabilities onto facts (R59/R61/R63) Wave 6 units 5, 7 and 9 of the ADR 0019 platform-free daemon migration (#1739), plus the residue reclassification the tracker asks for as an analysis task. R59 `alert` — new `@agent-device/contracts/alert-runtime` facet with four action-selected legs (`readAlert`, `awaitAlert`, `acceptAlert`, `dismissAlert`) on the shared `Interactor` seam. The daemon route admits and binds exactly the leg the parsed subcommand names, and the poll and retry windows move to the owners with it: how long a transient sheet takes to appear, and how many times to re-ask a runner that says it is not there yet, are family mechanics, not request policy. `src/platforms/apple/alert.ts` now holds the Apple windows verbatim (with the macOS-helper / XCTest-runner split), and Android's legs read the same presented tree `snapshot` publishes, which is why their occlusion reading still holds. Apple's cell is the retired `supportsAlertSurface` closure restated as facts — the host-or-simulator reading widened by physical iOS — and that closure was the per-AppleOS capability table's last reader, so `src/platforms/apple/capabilities.ts` goes with it. R61 `react-native` — the command's device work moved onto a bound `tapPoint` with R48; this retires the capability gate that still stood in front of it and moves admission ahead of the observing capture, so an owner that cannot dismiss an overlay refuses without first spending a snapshot on it. That exposed a real defect: the request handler chain never forwarded the request's runtime bindings to this route, so the dismissal leg had been reaching a missing gateway ever since R48 — only the no-overlay-detected path returned early enough to hide it. Fixed, with a chain-level regression test. R63 `capabilities` — the projection now reads each command's own declared `platformExecution` uses instead of a hand-written map plus a "no capability bucket means supported everywhere" fallback. That fallback is what let a stopped Android AVD advertise `snapshot press fill` it cannot run, and a Vega VVD advertise every migrated command; both collapse to the fact-derived set here. The command itself executes nothing on a device, so it declares `none`. Residue: `batch`, `debug` and `events` reclassify to `none` — each reaches no device and delegates nothing that does. `replay`/`test` keep their gesture viewport and boot-diagnostics edges, `daemon`/`web` hold platform imports in their own CLI modules, and `react-devtools` still injects device-runtime `runtime`, so all five stay `legacy`. Cutover rows R59 and R61 with their retirement and single-bind claims. Descriptors: 32 legacy at the wave checkpoint, 9 now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(daemon): restore two settings/alert sequences the migration had shifted Self-review of the Wave 6 diff against `origin/main` found two places where the migrated routes were faithful in what they did but not in when: - `settings` typed its location coordinates before expiring the ref frame, so a request that failed on a bad coordinate no longer expired it. The retired route expired the frame first, then emitted its diagnostic, then typed the coordinates inside the leaf. Same order again. - `alert` narrowed a frontmost-app session to "no bundle" in the daemon, which also stripped the bundle from the XCTest runner leg. That narrowing was only ever the macOS helper's, and it already lives in `platforms/apple/alert.ts`; the runner leg gets `session.appBundleId` unconditionally again, pinned by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(daemon): address adversarial review of the Wave 6 cutovers Three independent reviews (behavior parity, correctness, ADR 0019 conformance) ran against the branch. What they found, and what changed: Correctness - The R48 retry seam was unreachable. `captureSnapshot` builds it from the request's runtime bindings, but no caller forwarded them, so every retry resolved to a skip. The `snapshot` route now threads `inspectFacts`/ `bindDevice` through `createSnapshotRuntime` and the daemon snapshot backend down to the capture. - A retry tap that rejected escaped the capture it was decorating and turned a plain `snapshot` into an error. It is caught and reported as a skip, matching what the seam's own contract already claimed. - The attempt is spent before the device work again, as the retired route did, so an owner that fails mid-flight cannot be re-attempted from a full budget. - `react-native dismiss-overlay` reached its required `tapPoint` through `?.` and answered `dismissed: true` when the operation was absent. It refuses. - `factOwnedCapabilityAvailable` indexed the facts map unguarded, and treated an empty `required` as proof (`[].every` is vacuously true). Both fail closed. ADR 0019 conformance - §6 forbids a `none` descriptor from binding a device, and `capabilities` bound three times to answer `logs`/`network`/`record`. Every owner composes a binding's facts with the same function `inspectFacts` calls, so those probes read back values the single inspection already carries — at the cost of a device claim on a read-only query. They are gone, and with them the last three empty-`required` admission uses. - §9 is one admission per handler; the retry tap re-admitted on every retry round. It memoizes per device. - `installFamilyCapabilityAvailable` was scaffolding this wave was scheduled to retire: the general projection returns the same verdict for all four install-family commands. Deleted. Leftovers the cutovers created - `requireCommandSupported` lost its last production caller when R56 migrated `app-switcher`: every generic-route command is admitted from owner facts before the dispatcher runs. The dead arm, the function, and `commandUsesDeviceRuntimeExecution` are removed. - `CommandDispatchFacet`, `descriptor.dispatch`, and `explain`'s `dispatch=` field described a dispatcher R58 deleted. - `request-router-android-modal.test.ts` asserted on a `dispatchCommand` mock whose module export no longer exists, so three assertions were vacuous. - `generic-route-runtime-completeness.test.ts` now exists — a comment claimed it did. It pins the routing table as total over the generic route. - Comments and test names describing the retired dispatcher, the deleted AppleOS capability table, and a react-native regression that never shipped. Also records two deliberate provider cell changes the migration made (physical Apple `clipboard` admitted, provider `alert` refused) and the react-native widening to Linux, web and HarmonyOS, and drops a scratch probe file that was committed by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(apple): type the alert-absence retry instead of matching error prose Review blocker 1 on #2021. The Apple alert legs decided retry and hint eligibility by substring-matching error messages for "alert not found" / "no alert", and `alert wait` swallowed *every* read failure. A dead runner, an unreachable macOS helper or a canceled request was therefore spent as poll budget and finally reported as `alert wait timed out`, hiding the real cause. Both backends now state absence as typed evidence: - The XCTest runner answers `ErrorPayload(code: "ALERT_NOT_FOUND", ...)`. It is diagnostic-only, so it stays `COMMAND_FAILED` on the wire and surfaces as `details.runnerErrorCode` — the same shape `RUNNER_BUSY` already used. - The macOS helper adds `reason: "alert-not-found"` to its JSON error details, which the helper client already forwards verbatim. `isAlertNotFoundError` reads only those two fields. `awaitAppleAlert` re-throws anything that is not a typed absence instead of polling through it, and the scoped-snapshot fallback hint attaches to typed absence alone. The three tests the review asked for, plus coverage the daemon-altitude copies could not express: a non-absence failure propagates immediately from `wait`; an action does not retry a failure whose message merely reads like an absence; the macOS helper's typed reason is retried like the runner's. The daemon-level non-absence test moved to the family suite that owns this policy since R59, lowering that file's size pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(runtime): admit clipboard and provider operations from what execution checks Review blocker on #2021, reproduced on a Pixel 9 Pro XL / Android 36 emulator: `capabilities` advertised `clipboard`, then `clipboard read` failed with `UNSUPPORTED_OPERATION: Android shell clipboard read is not supported on this device.` Admission and execution were consulting different authorities, which ADR 0019 §2 forbids — a bound operation must already be admitted. Android. `cmd clipboard` has no shell implementation on every build, and the retired bucket admitted both halves on every real Android kind, leaving the leaf to discover the refusal after the fact. Support is now a fact: the owner probes once per device (cached for its lifetime — a build's shell command set cannot change while the device is up) and states `owner-capability-missing` when adb names the condition. The probe is definitive in one direction only: adb saying so means unsupported, a probe that cannot run means unknown, and reporting unknown as unsupported would hide a working clipboard behind a transport hiccup. The predicate moves to `@agent-device/contracts/android-clipboard-support` so admission and the leaf's own defense-in-depth check cannot drift apart. Cost, stated plainly: the first facts inspection per device now spends one adb round trip, including for requests that never touch the clipboard. WebDriver. `webdriver-interactor.ts` refuses through `capabilitySupported`, while fact generation admitted from interactor reachability alone — so a provider configured with `capabilityOverrides: { 'clipboard.read': 'unsupported' }` was admitted and then thrown out of. The declared capability map is now an input to fact generation, and the refusal carries the map author's own note. Applied to every operation with an unambiguous capability key, not just the two named in review: the mechanism is identical and a half-applied fix would leave the same defect for `back`/`home`/`orientation`/`tap`/`fill`/`type`/`scroll`. Behavior is unchanged by default — every one of those is `supported` or `partial` in the base map — so only an explicit override bites. `focus`, the gesture tiers and `trigger-app-event` keep reachability: no capability key maps to them 1:1. Also collapses the eight identical `*RetiredDispatchProjectionProof` wrappers in the cutover table into one parameterized factory (second review point). Parity tests: an Android build reporting either unsupported-shell phrasing, the probe cache, an adb failure staying admitted, and a WebDriver override refused at admission for each keyed operation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(layering): split the Wave 6 cutover rows into a sibling module Second review P2 on #2021. `runtime-command-cutover-table.ts` had reached 1,325 lines, past the point where one read covers it. Wave 6's eight rows move to `runtime-command-cutover-table-wave6.ts` and are spread back in, leaving the table at 1,095 lines. The split is by wave because that is how these rows are retired: a wave's rows are deleted together once the ADR declares its commands' migrations closed, and deleting a whole file is a cleaner end than excising a run of literals from the middle of a larger one. `retiredDispatchProjectionProof` moves to the shared extensions module, since both tables now use it — the main table for `snapshot`/`diff`, the sibling for its own eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): never fabricate clipboard availability from a failed probe Review blocker on #2021. The probe I added had a `catch { return true }`, then cached that result by device id for the runtime owner's lifetime. A transient adb offline or timeout therefore made `capabilities` advertise the clipboard on a build with no clipboard shell — recreating the exact lie the fix was for, and pinning it for the rest of the session. A test locked the behavior in. Support is now a typed verdict with three states, because "we could not ask" is not "it works": `supported | unsupported | probe-failed`. Only a definitive answer is cached; `probe-failed` refuses conservatively with a hint saying support could not be determined, and is deliberately not remembered, so the next inspection asks again. The same change repairs the ownership boundary. Turning raw adb stdout/stderr into a verdict is Android tool knowledge, so it belongs to the Android owner, not to shared vocabulary — `@agent-device/contracts/android-clipboard-support` now carries the typed union alone. The parser returns to `src/platforms/android/adb.ts` and runs in exactly one place, behind a new `AndroidToolHost.probeClipboardShellSupport` that hands owners the verdict. That also settles which Android home owns it: R13 lets only `src/platform-runtime.ts` import `@agent-device/platform-android`, so a parser shared between the package and the root leaf cannot live in the package either. Tests now cover the failure path the previous ones locked the wrong way: a failed probe refuses instead of admitting, its refusal says it could not determine support rather than claiming the build lacks it, and it is not cached — a second inspection re-probes and admits once the device answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * refactor(contracts): declare each interactor operation once Second review P1 on #2021. `interactor-operation-catalog.ts` declared the same operation set three times — a name tuple, a complete local binder map, and a complete provider binder map — and each facet carried a mirrored `bindLocal…Interactor`/`bindProvider…Interactor` pair whose only difference was which interactor source to use and which label a refusal names. There is now one row per operation, carrying its facts key, its provider refusal label, and the facet's own executor. The local/provider split lives in the two adapters, which differ by exactly the thing that differs: the interactor source. Adding an operation is adding one row. Deleted: the parallel tuple, both binder maps, 32 mirrored wrappers across ten facet modules, and the per-facet `Local…`/`Provider…InteractorResolver` aliases that existed only to be re-exported. Kept: every facet's typed executor, now exported as its binding surface. Net −563 production lines in `packages/contracts`. Two consumers moved onto the catalog's public entry point rather than keeping a private path to a single operation: the app-event delivery test and the provider scenario fixture, whose two hand-bound keyboard legs are now whichever legs its facts admit. Each facet's tests spell out the composition the retired wrappers performed, so every assertion still exercises one executor reached through one source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): let only a clean adb exit prove clipboard support Third review P1 on #2021. The typed verdict landed one layer too high. The adapter probe runs `adb shell cmd clipboard get text` with `allowFailure`, so a non-zero exit comes back as an ordinary result rather than a throw — and the only thing standing between that result and `supported` was the missing-shell prose check. A device that had gone offline, was unauthorized, timed out, or failed for any other reason produced none of that prose, so it fell through to `supported` and was then cached by device id for the runtime owner's lifetime. The `catch` I added guarded the one path adb almost never takes. Each adb outcome now proves only what it can: - `exitCode === 0` is the sole evidence of support, because it is the only result that shows the command ran. - The recognized missing-shell prose is the sole evidence of absence, and is read before the exit code — adb reports that condition non-zero, so checking the code first would turn every honest `unsupported` into a refusal. - Everything else — non-zero without that prose, and the transport throw — is `probe-failed`, which admission refuses and the cache does not remember. The package tests mocked the typed verdict, so they sat downstream of the bug and could not see it. The regression is therefore at the adapter, over the raw adb result: four planted reds (offline, unauthorized, device-not-found, generic failure) that all returned `supported` before this change, plus the two definitive verdicts and the ordering case that keeps `unsupported` reachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(android): never read adb's refusal prose out of the clipboard's contents Fourth review P1 on #2021, and a second instance of the same bug it names. The previous fix read `isClipboardShellUnsupported(stdout, stderr)` before the exit code. On a *successful* `cmd clipboard get text`, stdout is the clipboard's contents — arbitrary user text. Anyone who had copied "unknown command" or "no shell command implementation" (from a terminal, a bug report, this repo) had their own working clipboard classified `unsupported`, and the runtime owner cached that for its lifetime. Ordering prose ahead of the exit code to keep `unsupported` reachable traded one wrong admission for another. The exit code is decisive on its own when it is zero, so it goes first. Only a call that failed can carry prose about the call itself, which makes the missing- shell phrases meaningful on non-zero exits alone: if (result.exitCode === 0) return 'supported'; return isClipboardShellUnsupported(...) ? 'unsupported' : 'probe-failed'; `isClipboardShellUnsupported` now states that precondition, because reading it on a successful call is exactly the mistake to prevent. The same defect was already shipped in the helper's other caller. `runAndroidClipboardShellCommand` in `src/platforms/android/device-input-state.ts` has checked the prose before the exit code since #1950, so `clipboard read` on a clipboard holding either phrase threw `UNSUPPORTED_OPERATION` — telling the user their device does not support a clipboard it had just read correctly. It is not this wave's code and not reachable from the migration, but it is the same helper misused the same way, and documenting a precondition while leaving a caller that violates it invites the next regression. Repaired here, with the failure ordering otherwise unchanged: a non-zero exit still reports missing-shell as `UNSUPPORTED_OPERATION` and anything else as the adb result error. Both repairs are pinned by regressions that fail against the code they replace: four exit-0 cases at the adapter (verified red against the ordering this commit removes), and three at `readAndroidClipboardWithAdb` (verified red against `origin/main`) covering contents that look like a refusal, a genuine missing command, and an unrelated non-zero failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * fix(cli): bring the workflow help card back under its size budget `Coverage (2)` has been red on `main` and on every PR branched from it since #2020, which replaced three short Bootstrap lines with one longer line carrying the new selection semantics. It updated the content matcher for that line but not the size assertion beside it, so the card went to 9003 bytes against the `< 9000` both `cli-help.test.ts` and `cli-help-topics.test.ts` enforce. Nothing #2020 added is removed here — all of it is pinned by the matcher it shipped, and it is the sentence agents most need. The bytes come back from a clumsy repetition elsewhere in the card, where "settle" named itself twice in one clause: ... only when you did not settle, settle reported not settled, or ... ... only when you did not settle, it reported not settled, or ... which reads better short and puts the card at 8999. That is one byte inside the budget, which is the real finding: the card has no slack left, and the next sentence anyone adds re-opens this. The durable fix is a base-owner call between raising the budget and moving a block down into its sub-topic — the mechanism the card already uses, and which its own test documents. Flagged on #2021 rather than decided here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX * test(cli): raise the workflow help-card budget to 9100 The card is a curated agent-facing reference, and #2020 grew it for a good reason: the selection semantics it added are what an agent needs to predict which device a bare `open` picks. Holding that content to a limit set before it existed just moves the cost onto whoever writes the next sentence. 9100 is headroom, not a target. The previous commit left the card at 8999 of 9000 -- one byte -- which is not a state anyone should have to work in, and I had already established there is no slack left to reclaim: no trailing whitespace, and the only repeated runs are the deliberate column alignment in the Escalate footer. Trimming further would have meant deleting content the tests pin as load-bearing. This is explicitly interim. The card is ~9KB of dense prose in one string, and the real answer is to move a block down into its owning sub-topic -- the mechanism the card already uses and its own test documents ("Deep content moved out of the compact card, not deleted"). Raising the ceiling buys room to do that deliberately instead of under a red CI. Both enforcement sites move together, since they measure the same card through different surfaces: `cli-help.test.ts` reads it through the CLI, and `cli-help-topics.test.ts` through `usageForCommand`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
214d3b2799 |
perf(daemon): revalidate the source code-signature cache by stat instead of rereading the graph (#2004)
* perf(daemon): revalidate the source daemon code signature by stat
Every CLI invocation from a source checkout fingerprinted the daemon's
import graph to decide whether the running daemon still matches this code
(isReusableDaemonInfo). Rediscovering the graph's edges meant reading all
~800 modules: 3,915 statSync + 803 readFileSync (4.6MB) per invocation,
~30ms of a ~245ms command.
The walk is now cached under os.tmpdir() and revalidated by statSync alone.
That is sound against the walk it replaces rather than merely close to it:
the signature already treats a file's size:mtime as the stand-in for its
contents, so if every previously visited file still carries its recorded
pair, no file's contents changed, therefore no import specifier changed,
therefore the graph and its signature are unchanged. Any mismatched,
vanished, or non-file entry, and any unreadable or malformed document, falls
back to the full walk and republishes. code-signature.ts is itself inside the
graph it walks, so changing the walk invalidates every stored document.
The dist entry keeps the direct walk: it is a ~120-chunk bundle at ~5ms, and
a bundled install has no source graph to amortize.
Warm source invocation: 803 statSync, 1 read, ~1.5ms for the signature
(was ~30ms); the command drops from ~245ms to ~205ms idle, and from ~342ms
to ~251ms under a loaded host.
resolveDaemonLaunchSpec (~8 existence probes) and readVersion are memoized
per process alongside it; both are immutable for the life of a process. The
signature deliberately is not, so a long-lived client still notices a daemon
rebuilt underneath it.
* refactor(daemon): read the divergence resume through its contract type
The client's keep-alive check and the daemon's repair-liveness stamp each
reconstructed `details.divergence.resume` inline: one through ad-hoc
Record<string, unknown> casts, the other through a private structural guard
next to the stamp. ReplayDivergenceResume already owns that shape, so the
narrowing moves next to the type in @agent-device/contracts and both callers
read through it.
The client is now as strict as the daemon, which is what it always meant:
repairSessionHeld is only ever written to a record this reader accepted
(markSessionHeldIfArmed), so no payload that carries the R7 signal can fail
it.
* fix(daemon): refuse a code-signature cache document that is not this graph's
Addresses the findings in REVIEW.md against this branch.
F1 (high): a `{"version":1,"files":[]}` document validated forever — every
stamp in an empty list matches vacuously — so the client answered
`graph:0:da39a3ee...`, disagreed with the daemon's own walk on every
invocation, and killed a healthy daemon each time, permanently. A stored
stamp list is now refused unless it carries the entry's own label, and
publishing is gated on the same predicate, so a document the reader would
refuse is never written. The label comes from `buildDaemonCodeFileLabel`, the
function the walk stamps with, so the two derivations cannot drift.
F2 (medium): `os.tmpdir()` is the shared `/tmp` on Linux. The cache directory
is now per-user (`agent-device-code-signature-<uid>`, 0700, documents 0600),
so no uid silently turns the cache off for every other one; and a document is
read through one descriptor and refused unless `fstat` says this user wrote
it, so a planted document cannot choose the signature the client compares a
running daemon against.
F4 (low): `readReplayDivergenceResume` now checks the two optional fields it
types — `repairSessionHeld` (present means exactly `true`) and
`alternateFrom` (an integer) — so an accepted payload cannot type as the R7
liveness signal while carrying something else.
F5 (low): a launch-spec test stubs `--experimental-strip-types` to route the
source arm of `resolveLocalDaemonCodeSignature`, the branch the cache exists
for and the one Vitest never reached in a built checkout.
Also: the cache key hashes realpaths, as `buildSourceCheckoutStateDirName`
already does, so symlinked paths to one checkout share one document; the
module comment now states the real bound (as strong as the walk for edits to
files already in the graph, weaker for a module that joins it while every
recorded stamp still matches) instead of claiming exact equivalence; and the
cache fixture redirects `os.tmpdir()` into its own root so it no longer
clobbers documents in the run's shared TMPDIR.
Every new test was observed red against the code it guards.
* fix(daemon): keep the code-signature cache out of the startup closure
PR #2004 CI: the coverage lane's eager-closure gate failed on two rows.
`packages/contracts/src/facades/divergence.ts` (3 -> 4): the new
`readReplayDivergenceResume` reached for `isRecord`, and `./json.ts` was not
otherwise in that facade's closure. It now narrows structurally and locally,
the way this module's other wire readers (`divergenceStepLine`,
`divergenceScreenLine`, `divergenceOverflowLine`) already do; the accepted
payloads are unchanged, and the rejection table proves it.
`src/cli.ts` (362 -> 365): three modules, only one of which had a lazy seam.
`src/daemon/code-signature-cache.ts` is reached only by a source checkout, so
an installed client statically evaluated it -- and `src/utils/atomic-file.ts`
behind it -- on every invocation without ever being able to use it. It now
loads through a function-scoped `await import` in the source arm of
`resolveLocalDaemonCodeSignature`, which is therefore async.
Propagating that await collapsed the duplicated reuse ladder: the same three
checks were written twice, once as `isReusableDaemonInfo` and once as
`resolveDaemonTakeoverReason`, whose `'not reusable'` fallback was already
dead. One function now answers both questions -- the reason, or `undefined`
when the daemon is reusable -- so the verdict and the notice explaining it
cannot disagree, and the signature is still resolved only after the version
check passes.
The remaining two are deliberate and stay eager, so `src/cli.ts` is pinned at
364 with the reason recorded next to the row: `src/utils/ttl-memo.ts` (the
per-process version/project-root memo this PR added to cut re-reads of
package.json) and `src/daemon/client/daemon-launch-spec.ts` (the launch-entry
probe split out of daemon-client-lifecycle.ts). Both sit on the path every
local command already takes; deferring either would move the same load, not
avoid it.
* fix(daemon): make the code-signature cache sound across resolution changes
Extensionless specifiers resolve .ts before .js, so adding dep.ts to a graph
whose './dep' resolved to dep.js changed a fresh walk while leaving every
cached stamp untouched — the stale signature was silently reused. The walk now
records every candidate probed-and-missed ahead of a winner (and every
candidate of an unresolved specifier) as absent paths; the cache revalidates
that each is still missing and re-walks when any appears. Red-first:
cached-vs-fresh dep.js -> newly added dep.ts regression, plus lower-precedence
appearance staying warm and a nothing-resolving specifier gaining a target.
|
||
|
|
17cfd8ca8a |
feat: add deterministic device selection resolver (#2020)
* feat: add deterministic device selection resolver * test: adapt open selection harnesses * chore: keep context glossary within budget * fix: separate device identity from selection filters * refactor: make the selection resolver the sole owner of selection provenance Simplifies the deterministic device-selection resolver (net -114 lines vs the previous head) while fixing the outstanding app-aware provenance finding: - Move the booted-simulator app-affinity narrowing into the resolver behind an appleSimulatorAppTarget param, with its own typed reason 'single-app-installed-local' (candidateCount 1). This removes the selectedDevice escape hatch that reported 'preferred-local' with candidateCount 2 for the app-narrowed pick, and gives the app-match errors the same platform-aware retry selectors as every other selection failure. - Delete dead code: the allowBootableLocal param (no caller ever passed it, so the eligibleDevices filter was unreachable), the hasExplicitProviderIdentity alias, the duplicated deviceCandidateDetails in dispatch-resolve, and the double candidate computation. - Shrink the public selection contract to what #1777 specifies: drop `booted` (it contradicted its own doc comment once markSelectionBootedAfterPreparation flipped it; bootOccurred plus the reason codes carry the same information) and drop `retrySelectors` from success metadata (the daemon only ever emits retry selectors inside error details). DeviceSelectionRetrySelector leaves the contracts facade. - Replace the typeof-import lazy seam and resolver threading through four context objects with one lazy forwarding wrapper; the dispatch eager closure stays at 83. - Collapse the Apple path to resolve -> optional simulator fallback; the provider branch goes through the generic resolver call directly. - Consolidate the five copy-pasted resolveTargetDeviceSelection test mocks into one shared stub (selectionFromResolveTargetDevice). - Add the requested regression: two booted simulators with the app on one now assert typed selection metadata through resolveTargetDeviceSelection, plus a resolver-level app-affinity provenance test. Validation: typecheck, oxlint, oxfmt, layering (184-check guard OK), DI seams, fallow changed-files, eager-closure 235/235, daemon suite 323 files / 2287 tests, core/commands/mcp/client suites 255 files / 2133 tests. |
||
|
|
777c7af8cc |
fix: persist daemon-owned child process records (#2019)
* fix: record daemon-owned child processes (#1882) * fix: harden owned child cleanup identities |
||
|
|
5b6feafe92 |
Extract snapshot policy from daemon to host-side facet (#1983) (#2014)
* refactor(snapshot): give the Wave 4 policies neutral host seams (#1983)
#2005 established the presentation ownership boundary and moved the iOS
presentation policies out of `src/daemon/`. It left the three remaining Wave 4
policies behind their existing daemon adapters. This closes that gap, so
`src/snapshot/` owns host-side snapshot policy generally rather than
presentation alone.
Freshness recovery: the window vocabulary, the Android staleness classification
and its thresholds, and the retry loop move to `src/snapshot/snapshot-freshness/`.
The loop is parameterized by a classifier and a retry schedule, so how long a
backend may lag behind a real transition is a policy input rather than a
constant the loop owns. `src/daemon/session-snapshot-freshness.ts` keeps only
what needs a session — reading and retiring the window on store-owned
`SessionState`, and choosing the comparison baseline from snapshot lineage — and
remains the declared R7 owner of `androidSnapshotFreshness`. The two call sites
#1739 named as the Wave 5 blockers, `selector-capture-runtime.ts` and
`deferred-interaction-outcome.ts`, now reach freshness through the seam.
Timeout evidence: whether a failure is the accessibility-timeout shape becomes a
policy in `src/snapshot/snapshot-timeout-policy.ts`. The published
`details.androidSnapshotTimeoutScreenshot` payload becomes vocabulary in
`@agent-device/contracts/snapshot-timeout-evidence`, built through constructors
so an assembly site cannot publish a fifth, undeclared arm. It gets its own
subpath rather than riding the shared capture facade, which keeps it out of the
CLI cold-start closure. Typed details, diagnostics and screenshot evidence are
unchanged.
Screenshot-overlay policy: which Android nodes earn an overlay ref, and what
rectangle an overlay covers, move to `src/snapshot/screenshot-overlay/`. The
daemon keeps approved artifact and ref assembly only — ranking, projection to
screenshot pixels, drawing and PNG IO.
The boundary test generalizes from the presentation subtree to the whole facet:
nothing under `src/snapshot/` may import `src/daemon/`. It gains a positive
control, because a filter that stopped matching would look identical to a
boundary being obeyed.
The residual call sites #1983 also named are audited and deliberately left in
place. `direct-ios-selector.ts` carries no presentation policy; its two pure
exports are selector derivation and ADR 0011 delegation-on-error, whose owner
would be the selector pipeline governed by R19, not this facet. ADR 0004 records
the finding so it does not have to be re-derived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLYhmt5ZNHQATG8T8ZFo7R
* refactor(snapshot): address adversarial review of the Wave 4 seams
Three findings from an adversarial pass over
|
||
|
|
50f460cce4 |
refactor(snapshot): establish presentation ownership boundary (#2005)
* refactor(snapshot): establish presentation ownership boundary * docs: keep context glossary within budget * fix(snapshot): address presentation boundary review * test(snapshot): ratchet eager closure budgets |
||
|
|
942a310fb3 |
perf(selectors): collapse the double tree scan for disambiguate/fail-closed rows (#2009)
resolveSelectorChainWithPolicy ran listSelectorChainMatches (one full scan per alternative) and then resolveSelectorChain (another full scan per alternative) for the disambiguate/fail-closed rows readText/readUnique use — the same defect class #1690 removed from replay's resolveRecordedTarget. resolveSelectorChainDomain now tracks the first alternative that matched anything (`firstMatch`) unconditionally, in the same pass that already decides the winning resolution, so resolveSelectorChainWithPolicy needs only one call for these rows. matchedNodes keeps naming the first alternative that matched (not the winner) when they differ, preserving the existing contract wait's landmark check and the ambiguous outcome rely on. Also fixes analyzeSelectorMatches's lazy isVisible: it now builds the viewport-root rect list once per alternative (via the newly extracted collectViewportRects) alongside the existing lazily-built byIndex map, instead of isNodeVisibleOnScreen re-deriving it on every ambiguous candidate. Closes #1970 Claude-Session: https://claude.ai/code/session_01YJoiggu7utUNDBmdzSBK2h Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
893ce4b866 |
fix(ci): repair nightly XCTest and conformance lanes (#1989)
* fix(ci): repair nightly XCTest and conformance lanes * fix(ci): harden nightly failure classification * fix(ci): stabilize macOS replay cleanup * fix(ci): close nightly review gaps * fix(ci): classify device claims as infrastructure |
||
|
|
054dcd4ea4 |
refactor(snapshot): type the acquisition producer beside the platform channel (#2000)
* refactor(snapshot): type the acquisition producer beside the platform channel Three producers with different guarantees share the backend: 'xctest' stamp (Apple runner, Appium page-source, limrun element trees), and the 'android' channel conflates the local uiautomator path with Appium trees the same way. Add SnapshotProducer as a required field on SnapshotResult so the compiler enumerates every producer, and carry it into SnapshotState. Types-before-semantics step for #1983; no consumer behavior changes. * refactor(snapshot): make provenance one kernel-owned pair table Review follow-up on #2000: backend and producer were independent unions, so cross-channel pairs type-checked. SnapshotProvenance now owns the legal channel<->producer pairs; SnapshotBackend is its projection, SnapshotResult embeds the strict pair, SnapshotState embeds the optional-producer variant, and buildSnapshotState carries the pair through a narrowing helper so the fields never decorrelate. Negative type-level regression pins that invalid pairs cannot compile. |
||
|
|
957a6727f8 |
fix(android): publish covered state from exact order evidence (#1981)
* fix(android): unify snapshot occlusion across API levels * fix(android): preserve exact occlusion evidence * fix(android): restore collective occlusion coverage * fix(android): preserve snapshot evidence across consumers |
||
|
|
bcca714a07 | refactor: move gesture family to platform runtime (#1952) | ||
|
|
759f175332 |
refactor: move Wave 5 touch commands to platform runtime (#1987)
* refactor: move touch commands to platform runtime * fix: require direct selector touch binding * fix: address touch runtime review * fix: classify maestro direct click guarantee * fix: distinguish Maestro direct selector dispatch |
||
|
|
296447707e |
refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime (#1955)
* refactor: migrate back/home/orientation/tv-remote/keyboard to the request-bound device runtime Continues the ADR 0019 platform-runtime migration (Wave 5 generic leaves): five generic-route commands move off dispatchKnownCommand/Interactor legacy dispatch onto fact-owned admission, one bind per handler. keyboard uses the R35 action-selected single-bind pattern (status/dismiss/enter each admit and bind independently). All 8 owner runtime packages gained fact-cell tests for the new operations; six smoke-coverage integration oracles and nine daemon/capability unit test files were updated for the retired capability- catalog admission these commands no longer carry. * refactor: extract shared interactor-resolution prelude in keyboard-runtime bindKeyboardStatus/Dismiss/Enter repeated the same signal-check + resolveInteractor call; factor it into resolveKeyboardInteractor so each binder is a two-line call instead of a six-line copy. No behavior change — the three contract-module mutants planted earlier in review still kill on this shape. * fix: refuse watchOS admission for back/home/orientation/keyboard; pin tv-remote non-TV parity P1: watchOS has no constructible Apple interactor (XCUITest cannot drive its UI, ADR-0009), matching the existing captureScreenshot/captureSnapshot/readTextAtPoint/findSelector pattern in this same file. appleBackFact/appleHomeFact/appleMobileInputEligible admitted every Apple OS but tvOS/macOS, wrongly including watchOS. Facts now refuse watchOS explicitly for back, home, orientation, and keyboard dismiss/enter, with a fact-cell test asserting no binding for every one of them. P2: verified the daemon's generic-route capability gate already reproduced the retired per-platform tv-remote hint text (message stays the generic "<command> is not supported on this device", hint carries the owner-specific text) for every device that could reach dispatch in the old system -- the retired handleTvRemoteCommand's own "supported only on TV targets" check was unreachable there and only exercised by a test calling dispatchCommand directly. Added a daemon-level test pinning the exact iOS and Android-mobile hint strings to make that parity explicit instead of implicit. Also fixes a fallow complexity finding the P1 test edit introduced by splitting the fact-cell assertions into five small named helpers instead of one large function. * fix: stop orientation-runtime.test.ts's router-join test from hitting real adb Root-caused the CI-only Coverage failure (unreproducible locally in isolation, reproducible 2/2 in the full CI run): every generic-route leaf this migration touches carries `androidBlockingDialogGuard: true`, and `dispatchGenericCommand` calls `ensureNoAndroidBlockingDialogReady` unconditionally for any `platform: 'android'` session reaching the real request router -- regardless of whether admission is fact-based or capability-based. That check calls `getAndroidBlockingDialogFocus`, which shells out to the real `adb` binary. orientation-runtime.test.ts's "request router joins..." test used a synthetic `platform: 'android'` device through `createRequestHandler` (the real router), without stubbing the platform ADB layer -- only the runtime gateway was mocked. On a host with a real `adb` binary (my machine) the subprocess fails fast and `allowFailure` tolerates it, costing ~800ms-1.1s but still succeeding. On a host with no `adb` binary at all (CI's Coverage job, a plain unit-test lane with no Android SDK) the spawn itself throws, which isn't something `allowFailure` catches, producing exactly the observed `ok: false` unsupported-operation response. back/home/tv-remote's equivalent router-join tests already use Apple/Vega devices, so they never reached this path. Switched orientation's fixture to match -- Apple, since the fixture's facts/execution are fully synthetic and platform-agnostic regardless. Also: renamed the widely-shared 'emulator-5554'/'ios-simulator' device-id literals in back/orientation/tv-remote/keyboard-runtime.test.ts to file-scoped ids. Device claims for a `local-family` owner binding hit the real on-disk `require-owner` claim file (keyed only by canonical device id), and 27+ pre-existing test files already share 'emulator-5554'; this migration added three more consumers of it under a `require-owner` policy that reaches real admission, which was worth eliminating as a source of doubt even though it wasn't the actual root cause here. * refactor: extract navigation/keyboard concepts into sibling modules; test the real Android dialog-guard path packages/platform-apple/src/runtime.ts and packages/provider-limrun/src/app-log-runtime.ts grew past the repo's 500-line extraction threshold. Move the new back/home/orientation/ tv-remote/keyboard facts and bindings into packages/platform-apple/src/navigation/runtime.ts (new sibling module, matching deployment/runtime.ts's existing pattern), and the new keyboard facts/bindings for limrun into the existing packages/provider-limrun/src/interaction-operations.ts (which already held the sibling navigation logic). Also fix orientation-runtime.test.ts's router-join test: it previously swapped its device fixture from Android to Apple to dodge the real adb-backed blocking-dialog guard, which masked the Android route that was actually failing in CI. Keep the Android fixture and stub getAndroidBlockingDialogFocus instead, the same seam request-router-android-modal.test.ts already uses. * refactor: adopt granular contracts subpaths for back/home/orientation/tv-remote/keyboard Following main's #1969 (facade granularization), give each of this branch's five new contract modules their own package.json entry subpath and move every value-importer (owner runtime packages, the daemon binders, and their tests) off the wide @agent-device/contracts/platform facade onto the specific module that owns the symbol — the same convention #1969 established for the rest of the vocabulary. Keeps this migration's files out of the contracts-entry-closure gate and out of the eager-evaluation cost #1969 measured for the daemon's permanent hubs (registry.ts, dispatch.ts). * refactor: shared navigation/keyboard binder table; dedupe keyboard admission; drop restated types Addresses the review's finding 1 (seven per-owner copies of the same "fact-keyed table of interactor binders" pattern) by extracting bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations into packages/contracts/src/interactor-operation-catalog.ts. Each owner now requests the subset of back/home/setOrientation/tvRemote/keyboard{Status, Dismiss,Enter} it admits, instead of hand-writing `facts.operations.<key>.available ? bind…(resolver) : {}` per operation. Applied across all seven call sites (apple, android, harmonyos, vega, linux, webdriver, limrun) and collapsed limrun's two separate bind functions (navigation, keyboard) into one shared call. Finding 3 (resolveBoundKeyboardRuntime copy-pastes admit-then-wrap three times): extracted a local admitKeyboardAction<...> helper mirroring resolveBoundGenericRuntime's admit-then-defer shape, so the three action branches (status/dismiss/enter) share one admission path. Finding 6 (execute* helpers hand-restate a contract that can drift): back/ home/orientation/tv-remote/keyboard's execute functions are now typed off `BoundDeviceRuntime<typeof xRuntimeUse>` (derived from the actual bind-use value) instead of a hand-written `Readonly<{ operations: Readonly<{...}> }>` shape. Also fixed provider-limrun's `RuntimeOperationUnavailability | { available: true }` restating RuntimeOperationFact by hand — folded away entirely once the bind functions it typed were removed. Finding 7 (naming/placement): platform-apple/runtime.ts's misleadingly-named `captureOperations` bucket (held deployment/network/recording/find, not just capture) collapsed into one flat `operations` object now that the navigation bucket is a single function call instead of six ternaries. Exported RuntimeAdmissionRequest from runtime-admission.ts (needed by the new keyboard admission helper). Added packages/contracts/src/ interactor-operation-catalog.test.ts for the new shared binder table. pnpm typecheck, check:fallow, check:layering, and the full unit-core suite (1010 files / 7513 tests, one known contention-flake excluded) are green. * refactor: split generic-mutating command traits from the legacy dispatch pair Addresses the review's finding 5: GENERIC_MUTATING_LINUX_DEVICE_COMMAND_TRAITS bundled two orthogonal things (daemon/recording traits, and the legacy capability+dispatch pair migration strips), forcing every migrated descriptor to hand-expand the constant minus two fields plus an explanatory comment. Split into GENERIC_MUTATING_COMMAND_TRAITS (the shared daemon/recording traits) and LEGACY_LINUX_DEVICE_EXECUTION (the dispatch/capability pair). back/home/orientation/tv-remote (this migration) and focus (an earlier one, same pattern, previously a stale reference to the retired constant name) now spread the trait constant directly instead of hand-expanding it; the still-legacy `scroll` descriptor spreads both pieces, equivalent to the retired constant. pnpm typecheck, check:fallow, check:layering, and the registry/daemon test suites are green. * refactor: table-ify packages/contracts/src/keyboard-runtime.ts's three-way duplication Finding 3's second half: the three bindKeyboardX functions and six bindLocal/ProviderKeyboardXInteractor entry points differed only by method name and label string. Replaced with one generic bindKeyboardAction<Key> dispatching off the operation key (interactor[key], resolved from a small label table) plus two shared local/provider dispatch helpers the six named exports each call with their own key — collapsing three copies of the bind logic into one and six near-duplicate entry-point bodies into one line each, while keeping every exported name and type signature unchanged. pnpm typecheck, check:fallow, check:layering, and pnpm check:affected --run are green. * refactor: parameterize runSessionOrSelectorDispatch with an execute strategy Addresses the review's finding 2: handleKeyboardCommand re-implemented runSessionOrSelectorDispatch's orchestration step for step (session/selector guard, device resolve, ref-frame expiry, record) instead of reusing it, because the shared function had no seam for keyboard's bind-and-execute admission — only the legacy requireCommandSupported + dispatchCommand path. That left the shared orchestrator with one caller instead of two, and set a precedent that would fork a new copy for each of the 28 remaining session-route migrations. Gave runSessionOrSelectorDispatch an `execute` parameter: the orchestration (guard, resolve device, admit-then-execute, expire ref frame if mutating, derive and record next session) stays in one place, and callers supply their own admission/execution strategy. Extracted `legacySessionDispatchExecute` for the still-legacy capability-gate-then-dispatchCommand shape `handleTriggerAppEventCommand` (the remaining legacy caller) now passes explicitly, and `keyboardSessionExecute` for keyboard's bind-and-execute shape. Deleted the now-fully-redundant `executeBoundKeyboardCommand` — its result recording duplicated what the shared orchestrator's tail already does. pnpm typecheck, check:fallow, check:layering, the full daemon test suite (321 files / 2271 tests), and pnpm check:affected --run are green. * refactor: extract limrun facts-runtime.ts; discriminate KeyboardDismissResult by owner app-log-runtime.ts was still 589 lines after the shared-abstraction fixes; moves fact assembly (limrunAppLogFacts/limrunAppLogRecoveryFacts/limrunLifecycleFacts/deploymentOptions) to a new facts-runtime.ts and the shared device-identity predicate to device.ts, the leaf both files already depend on. app-log-runtime.ts is now 336 lines. KeyboardDismissResult was an 11-field optional bag with executeKeyboardDismiss separately re-deriving platform from the device and projecting subsets by hand. Each owner (android, apple, harmonyos) now tags its own result with a `kind` discriminant, so an owner can only ever produce its own shape, and the daemon derives the wire `platform` label from `kind` instead of guessing from the device a second time. Wire output is unchanged. * fix: expire ref frame before the mutating call, not after; extract session/selector dispatch; derive catalog operations from facts runSessionOrSelectorDispatch awaited execute(device, session) — which bundled admission and the mutating invocation together — before expiring the ref frame, so a rejecting or timed-out invocation left a stale frame active (ADR 0014 requires expiry immediately before the mutating call, with no success-only rollback). Split the execute thunk into `prepare` (admission only) + a deferred `execute` invocation, so the orchestrator can expire between them regardless of how the invocation resolves. Added a regression test proving the frame still expires when the invocation rejects. Extracted runSessionOrSelectorDispatch and its keyboard/trigger-app-event callers into a new session-selector-dispatch.ts, matching this file's own convention of one file per command-group (session.ts shrinks from 571 to well under its 500-line budget). bindAdmittedLocalInteractorOperations/bindAdmittedProviderInteractorOperations accepted both a facts object and a separately hand-maintained `operations` array naming the same keys — a second source of truth that could drift from what the facts actually admit. Removed the array; the binder now walks the fixed set of navigation operations and lets each owner's own facts decide what binds, exactly as before but with one source of truth. * style: reformat legacySessionDispatchExecute call in session-selector-dispatch.ts * refactor: derive catalog operation list from one canonical tuple; move keyboard orchestration tests NAVIGATION_INTERACTOR_OPERATIONS was declared as a plain readonly array independently of the NavigationInteractorOperation union it walked, so a future union member could compile without ever being added to the walk list, silently preventing an admitted fact from binding. Made the tuple the single canonical value: the union type is now derived from it via `(typeof TUPLE)[number]`, so LOCAL_BINDERS/PROVIDER_BINDERS' Record<NavigationInteractorOperation, ...> completeness is checked against the same tuple, not a separately hand-kept list. Added a regression test binding all seven operations at once to pin the runtime walk, independent of the type-level guarantee. Moved the four keyboard-orchestration tests (the two ADR 0014 ref-frame seam tests plus the two session/selector-guard tests) out of the mixed appstate/perf test file into a new session-selector-dispatch.test.ts, colocated with the file they exercise. Strengthened the rejection regression test to assert the frame is already expired from inside the rejecting keyboardDismiss callback itself, pinning the exact pre-invocation seam rather than only checking the end state after the dispatch settles. * fix: restore back/home/orientation/tv-remote/keyboard-runtime exports lost in rebase Rebasing onto origin/main dropped these five package.json export entries during conflict resolution (the granular-subpath commit's package.json changes silently lost during merge). Restored, confirmed by pnpm typecheck across all 17 workspace packages and the full unit-core suite (1023 files / 7581 tests). * test: pin the exact point the live iOS email field value goes missing Two prior CI runs on this PR saw the seeded email field ("ada@example") end up containing only a typed suffix (".test") by the time the flow reads it back at the end — after fill, keyboard dismiss, coordinate refocus, and type. Since this PR touches executeKeyboardDismiss's response shaping, the reviewer asked to disprove keyboard dismiss as the cause rather than assume the pre-existing dropped-keystroke flake pattern applies. Added two read-back checkpoints: right after seeding (before dismiss runs at all) and right after dismiss (before the coordinate refocus + type steps that follow). If both hold "ada@example", the loss happens during refocus/type, not dismiss — matching the documented flake, not a regression in this PR's diff. * refactor: make keyboard status/enter owner-discriminated too; trim review-round prose KeyboardStatusResult and KeyboardEnterResult were bare objects; executeKeyboardStatus and executeKeyboardEnter derived the wire platform label from device.platform via keyboardPlatformLabel, the same re-derivation already fixed for dismiss. Each owner now tags its own result with a kind (android's status/enter as 'ime-probe' and 'android-acknowledged', harmonyos's enter as 'harmonyos-acknowledged', apple's enter as 'visibility-echo'), and the daemon derives platform from a kind-keyed lookup table for all three actions. keyboardPlatformLabel and its isIosFamily import are gone — nothing derives platform from the device anymore. Android and HarmonyOS's enter acknowledgments are structurally identical (empty besides kind), so the discriminant alone — not result shape — is what tells the daemon which owner actually ran. Added a harmonyos enter test alongside the existing ios/android ones so all three owners are covered for both dismiss and enter's kind-to-platform mapping. Also trimmed several comments that narrated which PR review round motivated them down to just the durable invariant or rationale — the type shape, test names, and assertions already carry the proof. |
||
|
|
6eb74d08ce |
feat(android): bound snapshot presentation quality (#1972)
* feat(android): bound snapshot presentation quality * fix(android): bound presentation footprint work * fix(android): account for presentation scan work * fix(android): budget scoped snapshot presentation * fix(android): admit bounded snapshot presentation * fix(android): validate presentation per window |
||
|
|
1d1367311a |
perf(find): reuse snapshot index once per ranking pass (#1971)
* refactor(find): isolate match ranking policy Move find's candidate ordering — on-screen preference, actionability scoring, and the area/input-order tie-break — out of find-match-resolution.ts into a focused sibling. Pure move: no score input, ranked order, ambiguity refusal, or --first/--last behavior changes. The extraction establishes the seam the follow-up indexing change needs, so the ranking pass has one production entry point to build a topology in. Refs #1690 * perf(find): index snapshot topology once per match ranking pass Ranking a mutating find's candidates asked the whole tree the same three questions once per candidate: same-rect descendants filtered every node, the nearest hittable ancestor rebuilt a full index map, and the overly-broad-ancestor check re-filtered every node for viewport roots. With m matches over an n-node capture that is O(m x n) full-tree work before find can act or refuse. buildActionableTouchTopology reads those three collections in one pass (nodes by index, children by parent index, normalized viewport-root rects). preferOnscreenMatches builds exactly one per multi-match pass and threads it through every score; resolveActionableTouchResolution takes it as an optional argument so one-off interaction callers keep the cheap two-argument shape. findNearestAncestor gained the same optional prebuilt map its snapshot-presentation sibling already accepted, and classifyActionableTouchCandidates now reuses one topology instead of building a bare index map and re-resolving per candidate. Score inputs, ranked order, area and input-order tie-breaks, ambiguity refusal, and --first/--last are unchanged; only the derivation is shared. Observed red first: with the wiring hunk removed, the new ranking regression reports builder calls 0 (expected 1) and 64 filter + 32 map whole-tree scans (expected 0) over 32 candidates. The topology docstring records two seams the reviewer asked for. #1690 names src/snapshot/snapshot-processing.ts as findNearestAncestor's home; that path is gone and packages/contracts/src/snapshot-tree.ts is the seam that replaced it, so the issue's file list is drifted rather than a second site to change. And viewportRootRects is not interchangeable with snapshot-visibility's precomputedViewportRects: normalizeRect drops negative width/height where hasValidRect keeps them, which changes which rect wins pickLargestRect. Refs #1690 * refactor: hide actionable touch indexing |
||
|
|
03c3984066 |
perf(contracts): granularize entry surfaces so hub importers stop evaluating the facade clump (#1969)
* perf(contracts): granularize entry surfaces so hub importers stop evaluating the facade clump
`@agent-device/contracts/platform` unions 32 vocabulary modules and
`/interaction` another 18. A file that value-imports either evaluates the whole
union to reach one function, and because permanent hubs sat behind them —
`command-descriptor/registry.ts`, `core/capabilities.ts`,
`interactors/register-builtins.ts`, `command-descriptor/platform-execution-entry.ts` —
that union rode into roughly half the unit suite's test graphs.
Give every vocabulary module its own entry subpath and move all value-importers
onto the module that owns the symbol. Type-only importers are left alone: `import
type` is erased, so it already evaluated nothing.
Measured with the #1950 eager-import-closure walker over all 974 unit-core test
files, against base
|
||
|
|
1f80c92a27 | refactor: derive every --settle surface from the descriptor trait (#1652) (#1945) | ||
|
|
d0547dcb97 |
refactor: complete the find cutover onto the request-bound runtime (#1944)
* refactor: complete the find cutover onto the request-bound runtime The deferred Wave 4 unit for #1739 (R35), unblocked by focus (R40) and type (R41). Find's read-only legs, focus leg, and type leg already ran bound; the one remaining direct platform execution was the mutating-target capture, which built createSelectorCaptureRuntime without a bound capture and fell through the legacy dispatch branch reserved for "the last one to migrate". - The mutating path now enters resolveBoundSelectorCapture — the selector family's shared admit-then-bind entry, which already named find in its intent table — and threads the bound capture into the target capture. - Find was that last one: `capture` on SelectorCaptureRuntimeParams is now required and the legacy fallback branch is deleted. The backend's `bound` becomes required-to-state, with the observation-free duration wait (`wait 400`) as the one declared absence — its runtime now carries no capture backend at all, so an accidental capture fails loudly instead of falling anywhere. - The descriptor flips to device-runtime with findRuntimePlanUses (the selector-text plans shared with get, plus focusRuntimeUse and typeTextRuntimeUse); the capability bucket and both overlay memberships (HARMONYOS_SUPPORTED_COMMANDS, WEB_QUERY_COMMANDS) are deleted. - R35 lands with the selector family's shared operation owners; the capture and backend tests move off the dispatch mock onto the bound seam, which is where the poll-deadline and private-ax-pin assertions actually live now. Wave 4 is complete: layering recognizes 26 migrated commands. * fix(find): one action-selected bind per mutating handler (ADR 0019 §9) Review P1 on #1944: a mutating find performed up to three separate facts/admit/bind projections — capture, then focus, then type re-admitted per leg. The request handler now resolves ONE action-selected plan and binds once: - New selector intents find-focus / find-type carry combined uses (capture + focusPoint, capture + focusPoint + typeText) through the same admit-then-bind path and plan machinery every selector capture uses; the new bind arms reuse the existing capture selectors, so the shared operation owners stay single. Delegated click/fill resolve targets on the plain capture pair. - The handler threads the one bind's operations to the shared executors: executeFocusPoint is extracted as the single lexical owner of the focusPoint call (R40's owner claim follows it), and executeBoundTypeText's runtime param narrows to the operations it actually uses so find can pass its own broader bind through it. - findRuntimePlanUses becomes the full action-selected set (eight uses), and the descriptor test pins each use's exact requirement list. - Regression: find focus and find type each assert exactly one facts inspection and one bindDevice call — the pre-fix handler fails both (two and three binds respectively). Live re-verified on iPhone 17 Pro at this head: find focus and find type both execute through the single bind, route synthesized-first-responder, typed text visible in the captured tree. |
||
|
|
81409f1a7c |
refactor: migrate type to the request-bound device runtime (#1935)
* refactor: migrate type to the request-bound device runtime
Wave 5 unit 2 for #1739 (ADR 0019), find's last blocker. `type "text"` and
`find <q> type "text"` now reach the device through one admitted, request-bound
`typeText` operation instead of the `handleTypeCommand` interactor leaf and its
dispatch-table arm.
- New `TypeTextRuntimeOperations` contract riding the same `Interactor` seam as
focus/screenshot/element-text; the operation returns the interactor's own
closed `TypeTextBackendResult`, so Apple route evidence passes through and
every other owner types blind, exactly as before. The iOS synthesized-type
commit wait (#1676) is Apple-interactor-internal and moves nowhere.
- The interaction backend's `typeText` member exists only when the `type`
handler admitted and bound a runtime — no caller can fall back to legacy
dispatch, so the command keeps exactly one execution path (R41).
- `executeBoundTypeText` reproduces the retired leaf byte-for-byte: leading-ref
rejection with the same hint, space-joined positionals, the 0-10000 delay
bound, and only textEntryRoute surviving from the owner's result. Its parse
pins moved from the dispatch-level tests into the daemon runtime test.
- Exact-owner facts replace the capability bucket (apple sim+device, android
all-but-simulator-row, harmonyos emulator+device, linux device, web device,
vega unavailable, providers wherever their interactor is reachable) and
`type` leaves HARMONYOS_SUPPORTED_COMMANDS / WEB_INTERACTION_COMMANDS.
- The Linux desktop replay types a digit on real hardware; the coverage
manifest promotes `type` contract -> live with the two-sided count pins.
- Android/webdriver facts helpers extracted (androidTouchFact, interactorCell)
to keep inspectFacts under the complexity gate.
`find` stays legacy: both of its direct execution legs now share bound
runtimes, so the atomic R35 cutover is next.
* refactor(type): single-pass daemon routing, shared binder source, owner cell tests
Review follow-ups on #1935.
- The type handler now calls the bound executor directly: the interaction-
runtime hop validated and formatted what executeBoundTypeText validates and
formats again, so it is gone — no boundTypeText backend member, no second
result rebuild. The ADR 0014 frame expiry moves to the handler.
- New contracts/interactor-operation-binding.ts: one local resolver and one
fail-closed provider resolver shared by the screenshot, focus, and type
binders — three private copies retired, provider error text preserved.
- provider-limrun/interaction-operations.ts: the interactor-backed interaction
cells move out of the app-log owner (586 -> 563 lines, below its pre-unit
size); text interaction is composed by that owner, not defined in it.
- Every owner runtime test now pins the focusPoint/typeText fact cells and
bound-operation presence for its exact kinds: apple, android (incl. the
synthetic-simulator refusal), harmonyos, linux, web, vega (refusal + hint),
webdriver (reachability-gated, incl. inactive session), limrun (live +
recovery). The webdriver unsupported-capability row documents that
interaction gates on interactor reachability, not capture declarations.
- The Linux replay assertion is now change-sensitive: type "555" then wait for
a 555 node — no calculator button carries that label, so the wait passes only
if the keystrokes landed in the display; deleting the type step turns it red.
* fix(replay): give the calculator focus before the Linux type assertion
The change-sensitive wait exposed what the review predicted: the typed digits
never landed, because `focus 100 100` clicks the DESKTOP and takes keyboard
focus away from the calculator. The old broad assertion masked exactly this.
The retries then wedged on a latent quirk: attempt-1 leaves the pointer at
(100,100), and the next attempt's `xdotool mousemove --sync` to the same point
waits for a motion event that never comes, so every retry dies at the focus
step with a 10s timeout — which is why the lane reported step 7, not the
failing wait.
New tail: `focus 100 100` (R40 evidence + survival assert), then
`click "label=1"` — a resolved press inside the window that restores keyboard
focus, proves pointer input lands in the app, and moves the pointer off
(100,100) so retries cannot trip the mousemove no-op hang — then `type "55"`
and `wait "label=155 || text=155 || value=155"`. No button is labelled 155, so
the wait passes only if the typed keystrokes reached the display.
* refactor(type): delete the fallow SDK typeText surface, drop dead surface fields
Thermo-nuclear review follow-ups (reviewed at 82fb8c2dc; the three-hop relay
it names was already deleted in
|
||
|
|
17da776350 |
feat(ios): add snapshot backend conformance (#1930)
* feat(ios): add snapshot backend conformance * fix(ios): load built SDK at live runtime * test(client): isolate snapshot forwarding regression * refactor(snapshot): keep backend capability metadata internal * fix(test): merge backend conformance imports * fix(snapshot): keep backend forcing internal * refactor(snapshot): isolate backend capability fixtures * refactor(snapshot): keep capability governance internal * fix(ios): align snapshot actionability contract |
||
|
|
46eff36f85 |
refactor: migrate focus to the request-bound device runtime (#1925)
* refactor: migrate focus to the request-bound device runtime Wave 5's first unit (#1739, ADR 0019). `focus x y` and `find <q> focus` now reach the device through one admitted, request-bound `focusPoint` operation instead of the `handleFocusCommand` interactor leaf and its dispatch-table arm. - New `FocusRuntimeOperations` contract with local and provider interactor binders, mirroring the screenshot/element-text seam rather than inventing a second way for one operation class to reach its mechanics. - Exact-owner facts replace the capability bucket: apple simulator/device, android emulator/device/unknown, harmonyos emulator/device, linux device, web device, vega none, providers wherever their interactor is reachable. That is the retired bucket's cell table, restated as facts. - `focus` leaves BASE_COMMAND_CAPABILITY_MATRIX and both hand-maintained overlays (HARMONYOS_SUPPORTED_COMMANDS, WEB_INTERACTION_COMMANDS). - R40 is the new parametrized cutover row; `focusPoint` has exactly one owner. - The `x y` positional parse moves to utils and is shared with the still-legacy touch siblings, so a migrated command cannot drift from them. `find` stays legacy: this unit owns its focus leg only, its `type` leg still dispatches, and R35 waits on the Wave 5 `type` unit. * test(focus): cover the owning interactor binders, lower the find ratchet Review follow-ups on #1925. P1: focus-runtime.test.ts bound a fake focusPoint, so deleting the interactor call inside bindLocalFocusInteractor left focus a successful no-op with every test green. Adds packages/contracts/src/focus-runtime.test.ts, which executes both binders and asserts resolver context, positional (x, y) forwarding, the structured missing-provider failure, and that an already-cancelled request never resolves an interactor at all. Two planted mutants confirm it bites: removing `await interactor.focus(input.point.x, input.point.y)` and transposing its two arguments each fail exactly the two forwarding tests, while the daemon-level focus and find suites stay green — which is the gap the reviewer named. Coverage: find.test.ts shrank to 1204 lines when its focus assertion moved off the dispatch mock; the ratchet pin follows it down. * test(focus): add live Linux focus coverage to the desktop replay The Linux `focus` claim rested on the provider scenario at command-contract level. The desktop replay runs on real Linux hardware in the Smoke lane, so it now runs a coordinate focus and re-asserts the session survived it. Coordinate, not selector: the step exists to prove the migrated `focusPoint` path executes on real hardware, so it must not be able to fail on match ambiguity or CI layout drift. Reclassifies focus contract -> live in the Linux coverage manifest and updates the two pinned counts. The manifest gate is two-sided — a live claim must name a command the replay actually invokes — so the claim cannot drift from the file. |
||
|
|
1f8fdd0b5d |
fix: preserve Maestro clickable-first ordering (#1917)
* fix: preserve Maestro clickable-first ordering * test: cover Android Maestro clickable-first path * fix: keep Maestro fixture Android-only * fix: reveal Android Maestro targets in smoke scenario * fix: quote Maestro smoke assertion text * fix: retain Android Maestro clickability evidence |