* fix: recover completed Android recording from a pending-only manifest
When the daemon crashes in the brief window between writing the pending recovery
manifest (before screenrecord starts) and upgrading it to a `current` manifest, the
screenrecord process can still finish and leave a complete MP4 on the device. record
stop previously discarded it as stale because the pending-only recovery path never
checked for an on-device file, unlike the `current` path which already recovers a
finished recording. Extend the pending-only path to recover the completed file with the
same finished-recording warning, and skip the stop signal when the recovered recording
has no tracked pid (a pending chunk never records one, and probing an empty pid is
unsafe).
* fix: treat JSON arrays as invalid Android recovery manifests
isRecord accepted arrays (typeof [] === 'object'), so a stray `[]` recovery manifest
was classified as blocked rather than deleted, wedging every subsequent record stop.
Reject arrays and null so a non-object manifest is cleaned up like other malformed
metadata.
* feat: --settle returns the settled diff in the interaction response (#1101)
press/click/fill/longpress --settle executes the action, waits for the UI
to go quiet (wait stable's loop, shared via stable-capture.ts), and returns
the settled diff vs the pre-action tree in the same response — one round
trip instead of the interact -> observe pair.
- payload: changed lines only (bounded), summary counts, added-line refs,
refsGeneration; best-effort (settled:false + hint on never-quiet content,
never an action failure); --verify shares the settle captures
- ref issuance: the settled tree becomes the session snapshot; a
diff-carrying settle response clears snapshotRefsStale and the MCP layer
merge-only re-pins added-line refs at the settle generation
- grammar: --settle + --settle-quiet <ms> + --timeout <ms> (flag-sourced
descriptor budget with new envelope:'widen' semantics mirroring wait)
- ADR 0011: new settleObservation guarantee classified on every path with
contract scenarios per enforced/delegated cell
* test: give the two contention-flaky doctor scenarios explicit budgets
The doctor provider scenarios sit at ~5s of real daemon-harness work on a
loaded host and flake at vitest's 5s default during full-suite runs (the
known contention flake AGENTS.md documents). Same in-file precedent as the
Metro-probe scenario's 10s budget.
* fix: move SettleParams to contracts to satisfy the layering DAG
daemon/handlers/interaction-flags.ts imported the type across the
daemon -> commands boundary (R2 commands-floor). The tuning params are
part of the interaction contract like SettleObservation, so they live
in contracts/interaction.ts and both layers import from there.
* feat: keep settle diffs content-first — drop Key nodes, added lines win the cap
Bluesky dogfood: a fill that summons the iOS keyboard spent 49 of the 80
capped diff lines spelling out QWERTY keys, and a screen transition with
269 removals could starve out the added lines entirely. Key-type nodes
are now filtered from both diff sides (the [keyboard] container line
still signals presence), and under truncation added lines — the ones
carrying fresh refs — win slots over removals.
* docs: state the core loop in the top-level help starting point
Benchmarked with headless haiku/sonnet agents given only --help: both
models skipped the help-workflow pointer and started with plain
snapshot (38KB payloads they then had to re-read from files). One
core-loop line at the starting point is what teaches snapshot -i and
--settle to models that never read a second help page.
* fix: preserve settle digest refs for mcp
* fix: reduce settle fallow complexity
* fix: surface settle output in CLI text
* fix: complete settle handling for longpress
* refactor: localize daemon timeout envelopes
* refactor: deepen post-action observation
* refactor: centralize post-action observation planning
* refactor: derive settle capability from descriptors
* refactor: trim settle descriptor helpers
* test: split the Android platform test aggregation and share the scripted adb stub
AGENTS.md names the platform index.test.ts aggregations as offenders to
shrink opportunistically; this splits the 2,735-line Android one along
its (already well-factored) source modules, every test moved verbatim
(92 tests before and after):
- ui-hierarchy.test.ts (22): parseUiHierarchy/androidUiNodes
- app-lifecycle-install.test.ts (13): install/resolve/infer/launch
component parsing
- app-lifecycle-open.test.ts (19): open/close, deep links, launch args,
TV category, fallback resolve-activity
- input-actions.test.ts (11): type/fill/swipe/scroll/rotate
- settings.test.ts (14): appearance/clear-app-state/fingerprint/
permissions
- notifications.test.ts (2), app-parsers.test.ts (1)
- keyboard state/dismiss tests (10) appended to the existing
device-input-state.test.ts
Consistency fix folded in: the file carried a local withMockedAdb fork
because it needs scripted per-subcommand adb responses, which the shared
arg-recorder helper cannot express. The fork now lives in
src/__tests__/test-utils/mocked-binaries.ts as withScriptedAdb next to
withMockedAdb, and hands each call a fresh copy of the shared
ANDROID_EMULATOR fixture.
The copy matters: the Android TV test mutated the callback's device
(device.target = 'tv'), which the old per-call object literal absorbed
silently. With a shared fixture that mutation leaked into the next test
and flipped its launch to LEANBACK. The helper now clones per call and
the TV test builds { ...device, target: 'tv' } instead of mutating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
* test: serialize the scripted-adb group and repoint its slow-test pins
Review follow-up for the android index.test.ts split: the monolith
implicitly serialized the env-mutating adb-stub tests (PATH,
AGENT_DEVICE_TEST_ARGS_FILE) in one worker, and the split let vitest
run them across parallel files. Make the contract explicit:
- new android-adb vitest project runs the six scripted-adb test files
in a single fork (singleFork), keeping the pre-split execution
semantics; ui-hierarchy and app-parsers stay in the parallel unit
project (pure parsing, no env mutation)
- test/test:unit scripts run both projects
- the five slow-test ratchet pins that referenced index.test.ts keys
now point at the split file names, so the pinned real-time offenders
keep their exemption instead of failing at 2x budget under load; the
reporter's own pinned-key fixture updated to match
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
* test: use vitest 4 android adb serialization
* docs: update unit project readiness guidance
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: bound iOS capture stalls and make runner recovery session-preserving (#1105)
Runner (Swift):
- Coalesce duplicate transport sends of one commandId onto the in-flight
execution instead of enqueueing them again behind it (capture pileup).
- Fail fast with RUNNER_BUSY while watchdog-abandoned main-thread work is
draining; escalate to RUNNER_WEDGED past 120s so the daemon recycles.
- Carry the capture-plan deadline into the query-sweep and private-AX
ladder tiers so chained recovery cannot stack past the watchdog.
- Penalize the tree backend after a slow (>5s) or abandoned capture and
lead subsequent regular plans with private-AX for that bundle (sticky,
120s), stamped recovered/budget so the deferral stays observable.
Daemon (TS):
- Per-request runner recycle budget: at most one invalidate+reboot per
request, then fail fast with an actionable, session-preserving hint.
- RUNNER_WEDGED joins the runner-fatal invalidation reasons.
- Interaction commands (click/fill/longpress/press/type/get/is) preserve
the daemon on request timeout like snapshot/wait/find: resetting it
destroyed every healthy app session the daemon owned.
* fix: suppress AX-broken-screen snapshot issues so the runner survives capture
XCTest records 'Failed to get matching snapshot: kAXErrorIllegalArgument'
issues for every XCUIApplication query on AX-broken screens; after a few
of them the test case tears down the moment the in-flight command
completes, killing the long-lived runner after every capture of the
screen (the restart loop behind #1105). The capture plan already
classifies and recovers from AX failures, so this issue class is noise:
swallow exactly it in record(_:); everything else still records and
still drives XCTEST_RECORDED_FAILURE.
* feat: time-slice the XCTest tree capture on a worker thread
The tree snapshot XPC is a single blocking call whose duration moves
with live content (4s to minutes on Bluesky profile screens); no
in-process budget could bound it on the main thread. Run it on a worker
bounded to an 8s slice: on timeout the plan penalizes the tree backend,
skips the XCTest-backed tiers while the abandoned XPC drains (they
would block behind it inside testmanagerd), and recovers through the
private AX backend, which does not use testmanagerd.
* tune: lower the tree-backend penalty threshold to 3s
The Bluesky profile tree grind measures ~4.5s before kAXErrorIllegalArgument,
just under the old 5s threshold, so every capture re-paid the doomed grind
(9s each). At 3s the second capture onward defers to private AX (2.4s
snapshot, 4.9s press on the live repro).
* fix: harden the AX-issue suppression per review
- Require the kAXError token: 'Failed to get matching snapshot: Timed out
while evaluating UI query.' is a genuinely-hung-query signal and must
keep recording (and keep driving XCTEST_RECORDED_FAILURE). Sibling AX
server codes (kAXErrorCannotComplete, ...) are deliberately included:
any AX-server rejection inside a matching-snapshot fetch is the same
capture-plan noise.
- State honestly that the override is suite-global and why (tap-triggered
queries record the same noise; command outcomes stay honest via their
own error paths).
- Lock-guarded suppressed-issue counter following the file's existing
abandoned-work counter pattern, logged with each suppression.
- Unit-test the pure classifier (record(_:) itself is not invoked: the
must-record variants would record real failures in the test run).
* test: split the args.test.ts aggregation along source topology
AGENTS.md file-size tripwires now apply to tests with no exemption, and
test files are expected to mirror source topology 1:1. args.test.ts was
a 2,503-line aggregation in src/utils/__tests__ while the code it
exercises lives in src/cli/parser. Split it into six focused files with
every test moved verbatim (142 tests before and after):
- src/cli/parser/__tests__/args-parse-interaction.test.ts (29 tests):
parseArgs shapes for press/click/swipe/gesture/type/record/screenshot
and friends
- src/cli/parser/__tests__/args-parse-session.test.ts (41 tests):
parseArgs shapes for session/daemon/device flags, passthrough,
install/metro/connect/proxy/auth and friends
- src/cli/parser/__tests__/args-validation.test.ts (17 tests): strict/
compat modes, rejections, deterministic errors
- src/cli/parser/__tests__/cli-help-topics.test.ts (15 tests): global
usage and help topics
- src/cli/parser/__tests__/cli-help-command-usage.test.ts (35 tests):
per-command usage copy
- src/utils/__tests__/command-schema-guards.test.ts (5 tests): schema/
catalog/capability guards and the cli.ts dispatch-literal walk (the
oxc-parser helpers live here)
AGENTS.md testing-matrix and help-source pointers updated to the new
paths, including the stale src/utils/cli-help.ts and cli-flags.ts
locations (both live under src/cli/parser/).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
* docs: fix cli parser paths in agent guide
---------
Co-authored-by: Claude <noreply@anthropic.com>
Follow-ups from the bundler/CI speed work, re-validated against latest
main. The typescript package is gone from the toolchain:
- pnpm typecheck stays on tsgo; the typecheck:tsc escape hatch is
removed along with the typescript devDependency.
- args.test.ts extracted cli.ts dispatch literals through the
TypeScript compiler API - the only remaining consumer. It now walks
the same AST via oxc-parser (matching the OXC lint/format/build
stack); both implementations extract an identical 14-literal set
from cli.ts, verified side by side before the swap. The
substitution-free template case ts.isStringLiteralLike covered is
preserved.
- dts bundling is unaffected: the tsdown build uses the tsgo backend
and builds green with no typescript package installed.
Test fixes for containerized agent environments:
- The missing-binary doctor-guidance web provider test pins Node 24
via the file's existing withNodeRuntimeVersion helper, so it asserts
the setup hint instead of inheriting the host Node and failing on
Node 22 (the supported engines floor).
- The clean-xcuitest cleanup-failure smoke test skips as root: chmod
0o500 cannot force a removal failure when the process bypasses
directory permissions.
AGENTS.md toolchain notes updated to match.
Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
Co-authored-by: Claude <noreply@anthropic.com>
* feat: versioned snapshot refs with MCP auto-pinning
Refs are positional indexes into the latest stored session tree; #1093's
coarse snapshotRefsStale marker warns honestly but cannot say WHICH tree
a ref came from. Give the session a monotonically increasing
snapshotGeneration, advanced wherever the stored tree is replaced: the
setSessionSnapshot choke point and the snapshot/diff command path that
bypasses it.
Token economy (non-negotiable): the snapshot tree output is unchanged —
plain e12 refs on every node. Ref-issuing responses (snapshot command,
find ref outputs) carry the generation ONCE as the additive
refsGeneration field. Ref-consuming commands (press/click/fill/longpress/
get/wait) accept both forms: plain @e12 keeps today's behavior including
the coarse #1093 warning; pinned @e12~s3 is clean when the generation
matches the stored tree, gets a precise warning naming both generations
when it does not, and a malformed suffix is INVALID_ARGS with a grammar
hint. Warn-only this release — tightening comes later per the compat
ladder.
The MCP layer auto-pins at zero token cost: it sees snapshot/find
responses before the model does, remembers the last refsGeneration per
session name, and rewrites plain @ref tool arguments to the pinned form
before forwarding. The model never sees or types suffixes; with no
remembered generation, refs pass through unpinned (never guess).
Replay parsing and script writing strip and IGNORE pins — generations
are meaningless outside the session that minted them.
Refs #1076
* docs: CONTEXT.md vocabulary for ref generation pins
Moved from #1097 per the review sequencing note: the term lands with
the behavior it describes.
* docs: teach the ref pin syntax in CLI help
MCP agents get pins transparently (auto-pinning), but CLI-driving
agents only ever met the coarse warning — refsGeneration arrived in
snapshot responses with nothing explaining it, making pins an
undiscoverable feature on the primary agent surface. One help line in
the agent loop guidance closes that; warnings stay short (they fire
repeatedly, teaching belongs in once-read surfaces).
* fix: per-ref MCP pin provenance and seeded generations
Review findings on the first cut:
1. The MCP layer kept ONE refsGeneration per session, so after
snapshot(s12) -> find(s13) a plain @e37 from the pre-find snapshot got
pinned ~s13 and read as current — recreating the find-blessing hole at
the pinning layer. Replace it with per-ref provenance:
Map<pinScope, Map<refBody, generation>>, scoped by state dir + session
name (stateDir is a per-call MCP config field, so one server process
can face multiple daemons). Merge-only updates: refs present in a
ref-issuing response (snapshot nodes, digest refs, the find ref) move
to its generation; absent refs KEEP their older pins — an old pin on a
replaced tree is what makes the daemon warn. Never-issued refs pass
through unpinned; an issuing response without refsGeneration clears
the scope; memory bounded to the ~1000 most recently issued pins.
2. Generations were per-lifetime counters from 1, so a reopened
session's ~s1 collided silently with the previous lifetime's. Seed
the first bump at a random 6-digit base (crypto randomInt):
cross-lifetime collisions become ~1e-6 — probabilistic (seeded), not
identity-based, documented on the field. Pin format unchanged;
within-lifetime comparisons stay exact.
Tests: the MCP blessing scenario (pre-find ref stays pinned to ITS
generation), the daemon half in the provider scenario (find must not
bless a pre-find pin), reopen/reseed at unit + handler level, state-dir
scope isolation, digest-ref merging; generation fixtures made
seed-agnostic (relative bumps, echo the observed seed).
Refs #1076
* test: slow-test ratchet, budget-derived emulator poll, speed guidance from experiments
Measured (2026-07-04, full unit suite: 340 files / 3,210 tests / 48s wall):
wall clock was bounded by the slowest FILE (44.6s android monolith at ~7x
file-level parallelism), and the slowest tests were sleeping through real
production budgets (10.8s proving 'times out' by waiting the constant out,
8s emulator polls at 1Hz, real retry backoff). Two config experiments
rejected with data: --no-isolate exploded the suite to 205s (module state
thrashes across files sharing workers) and --pool=threads changed nothing.
- scripts/vitest-slow-test-reporter.ts: the slow-test ratchet. Unit budget
2.5s / integration 15s; failure at 2x budget (the band between reports
without failing so host-load variance cannot make the gate cry wolf);
36 pinned offenders, exact keys, ratchet-only pin (tracking #1098).
- waitForAndroidEmulatorByAvdName: poll cadence derives from the caller's
budget (min 1s, floor 50ms, ~timeout/20) — devices.test.ts 25.6s -> 2.8s
(9x) in isolation, and short-budget production calls stop sampling at
1Hz against small budgets.
- vitest.config: slowTestThreshold 500 for local visibility; reporter
wired; isolation/pool decisions documented with the measurements.
- docs/agents/testing.md 'Speed rules' + AGENTS.md testing bullet: the
three conversion patterns in preference order (budget-derived cadence,
budget-wiring assertion, fake clocks), the no-seam constraint, and the
file-granularity Amdahl argument that makes the monolith test split a
wall-clock fix, not just navigation.
* fix: fallow findings on the slow-test gate — import edge, factory reporter, unit tests
The string-path reporter wiring read as a dead file (fallow cannot see
vitest's reporter loading); the config now imports the factory, making
the edge real and type-checked. The class shape tripped the
unused-class-members rule (framework callbacks are invisible to
reference analysis) — converted to a factory returning the Reporter
object, with the classification and rendering logic extracted as pure
exported functions. Those functions now carry their own unit tests
(budget bands, integration budgets, pin matching, warn-vs-fail
rendering), which also grounds the CRAP estimate in real references.
Canary re-verified: unpinned 5.2s sleeper fails the run with exit 1;
clean runs exit 0.
* docs: refocus AGENTS.md on principles and gates; index ADRs; extend CONTEXT.md vocabulary
AGENTS.md: replace the routing/command-family prose maps (already
drifting from the code) with pointers to the self-describing,
parity-tested registries; add the two sections agents actually cannot
rediscover cheaply — Principles (one line per incident-backed lesson)
and Enforcement gates (the classify-don't-suppress index); extend the
module-size guidance from raw LOC caps to answer-one-question files,
1:1 test topology mirroring (removing the integration-aggregation
exemption that produced 3,400-line test files), sibling fixture
modules, claim collocation, and boundary-only barrels; record the
dev-loop staleness triple (dist/daemon/adopted-runner), the tsgo
typecheck, the Gatekeeper first-node-exec stall, the DEVICE_IN_USE
signature, and the contention-flake protocol; append the two gate
steps to the new-flag checklist.
CONTEXT.md: vocabulary for the ADR 0011 domain (dispatch path,
guarantee cell, owned waiver, parity table, coverage manifest,
delegation-on-error, ref generation pin) and an architecture paragraph
positioning ADR 0011 as ADR 0008's interaction-semantics counterpart.
docs/adr: flip 0011 to Accepted (implemented through Layer 3) and add
a read-this-when index that names the registries as the living source
of truth over ADR prose.
* docs: defer versioned-ref references to the implementing PR
Review sequencing note on #1097: these lines described #1096 behavior
not yet on main. They move to #1096's branch so docs land with the
implementation and the two PRs merge in any order.
tapTarget/fillTarget are wired solely to the web provider's clickRef
(interaction-runtime.ts); apple/android backends never define them.
Found while designing the #1088 retirement experiment, which this
dissolves: the hypothesized iOS runner round trip does not exist.
Refs are positional indexes into the latest stored session tree; any
selector-based command's resolution capture silently reshuffles them
(#1076). Track an honest marker on the daemon session
(snapshotRefsStale): set wherever the stored snapshot is replaced by a
response that does not hand the new refs to the client, cleared only
where the client demonstrably receives them (snapshot responses, find
ref outputs). Commands consuming @ref arguments while the marker is set
(press/click/longpress/fill/get/wait) attach a warning and still
execute — the geometric guards keep catching detectable drift.
Closes#1076
* feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011)
Runner ELEMENT_NOT_FOUND/AMBIGUOUS_MATCH on the direct iOS selector
dispatch now delegate to the tree-based runtime path, which supplies
runtime disambiguation, occlusion refusal, non-hittable
promotion/annotation, and rich selector diagnostics/hints.
Maestro replay dispatches (allowNonHittableCoordinateFallback) keep the
runner-native error shapes: the new delegateSemanticFailures option is
threaded from the dispatch flag, and the query path
(allowElementNotFound at selector-runtime) keeps its exact semantics.
Registry: direct-ios-selector errorTaxonomy/nonHittable/occlusion flip
from gap waivers to delegated cells; the pinned gap list shrinks 6 -> 3.
Success-path parity cells (disambiguation, responseIdentity)
intentionally remain gaps per ADR 0011.
Refs #1081
* test: contract coverage for the three newly-delegated direct-iOS cells
The coverage gate demanded scenarios the moment the rebase flipped
errorTaxonomy/nonHittable/occlusion to delegated — as designed. Three
transcript scenarios prove the delegation end to end: runner
ELEMENT_NOT_FOUND falls back to runtime no-match diagnostics, to the
covered refusal, and to an annotated coordinate tap (the transcript
asserts the fallback tap is coordinate-keyed).
Three measured dev-loop/CI cuts, no signal loss:
- typecheck now runs tsgo (already trusted for declaration emit by the
tsdown build): 21.7s -> 5.3s locally, and check:tooling drops to ~18s
total. tsc stays available as typecheck:tsc; verified tsgo fails on
type errors and respects noUnusedLocals.
- remove the Unit Tests CI job: Coverage runs the same unit +
provider-integration suites under coverage thresholds, so the job
reran ~64s of tests every PR for no extra signal.
- Size workflow: skip docs-only paths (same paths-ignore as CI) and
cache the base commit's dist keyed on base SHA, since dist is fully
determined by that commit. Startup medians are still measured fresh
on the same runner so the base/PR startup comparison stays
same-machine; the cache is saved immediately after the base
measurement so the PR build never poisons it.
Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS
Co-authored-by: Claude <noreply@anthropic.com>