178 Commits

Author SHA1 Message Date
Michał Pierzchała f53d572f87 fix: align Maestro swipe semantics across platforms (#1179)
* fix: preserve explicit Android Maestro swipe lanes

* fix: align Maestro swipe semantics across platforms

* fix: avoid replaying iOS Maestro gestures

* refactor: make swipe coordinate policies explicit
2026-07-10 16:41:54 +02:00
devin-ai-integration[bot] db492cbaed test(output-economy): add routine-workflow output-behavior oracle (#1190)
* test(output-economy): add routine-workflow output-behavior oracle

Add a deterministic routine-workflow measurement (#1180) that pairs
response bytes with follow-up behavior: fallback-observation count,
retry count, and whether an actionable failure preserves the session.
Refs chain across one recorded checkout session and counts derive from
the real formatters, so dropping settled-diff refs, the unchanged-
interactive tail, or a recovery handle fails the suite. Adds a matching
non-gating help-conformance next-command case. Response defaults
unchanged.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(output-economy): make routine-workflow ref-surfacing depend on rendered output

Address review on #1190:
- Drop the raw mutation-confirm/failure MCP data samples that leaked e4/e5
  and mislabeled the surface; e4/e5 now surface only from the rendered CLI
  settled-diff, so dropping added refs genuinely raises the fallback count.
- Track the failure once as its projection-invariant normalized payload
  (workflow.failure.shared.json) instead of duplicate cli/mcp raw copies.
- Reuse the shared REF_TOKEN_PATTERN from economy-metrics.ts.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(output-economy): reuse shared fixtures in routine-workflow oracle

Address review finding #2 on #1190: chain the workflow onto the shared
per-surface fixtures instead of copy-pasting them.
- orient/recheck reuse SNAPSHOT_RESULT + SNAPSHOT_DAEMON_RESULT; the
  first mutation reuses SETTLE_ADDED_REF_RESULT, so session identity and
  ref generations come from ./fixtures.ts and the two suites cannot drift.
- Only the genuinely workflow-specific pieces remain local: the unchanged
  recheck, a tail retargeted onto a settled-diff ref (SETTLE_TAIL_RESULT
  taps an unsurfaced @e6 and can't chain), the in-session timeout failure,
  and its recovered retry. routine-workflow.ts drops ~110 LOC.
- Rendered-output ref guard preserved: @e5 (settled diff) and @e7 (tail)
  surface only from formatter output; recovery semantics unchanged.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 15:27:24 +02:00
devin-ai-integration[bot] ae74c51abd chore: add agent-efficiency regression guards (#1174)
* chore: ratchet architecture dependency graph

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: ratchet agent-facing output economy

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat: derive command navigation explanations

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: keep efficiency checks fallow-clean

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(layering): enforce back-edge ceiling monotonicity and cover root src files

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* ci(layering): pin back-edge-ceiling ratchet to PR merge-base

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(output-economy): baseline-independent actionability floors, policy-derived error, like-for-like screenshot surfaces

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(explain): resolve true CLI aliases, canonical usage, and derived owners

Surface true CLI aliases from parser normalization (long-press, metrics,
tap, launch, relaunch) distinct from catalog keys, preserving implied-flag
semantics (relaunch => open --relaunch). Extract the canonical single-line
usage builder to src/utils/cli-usage.ts so schemas without usageOverride
include positionals and flags. Replace guessed handler paths with a
completeness-checked daemon-route owner map keyed by the closed
DaemonCommandRoute union, fixing silently-dropped non-kebab routes
(reactNative, recordTrace) and generic dispatch. Add table-driven coverage
for aliases, synthesized usage, split-family/route-variant/dispatch owners,
structured output, and explain:command CLI exit/stdout/stderr.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: enforce exact ratchets and compact command explain

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: colocate command ownership metadata

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: bind daemon owners to production routes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: preserve generic dispatch bundling

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: enforce monotonic output budgets

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 11:54:09 +02:00
Michał Pierzchała 983625fc5d feat: fix codex runner, add --override-doc grading, port skillgym quiz cases to help-conformance bench (#1176)
* feat: fix codex runner, add override-doc grading, port skillgym quiz cases

The 2026-07-09 evaluation of scripts/help-conformance-bench.mjs found it
structurally right but broken for the codex runner (two bugs), thin on
coverage (4 cases), sequential, and unable to grade a draft help rewrite
without a rebuild.

- Fix runCodex: (1) codex exec reads stdin until EOF when not attached to
  a TTY, and execFile never closes the child's stdin, so every codex call
  hung until RUN_TIMEOUT_MS with empty output — close stdin right after
  spawn. (2) `-o outFile` writes the same final JSON that codex also
  prints to stdout, so concatenating both produced two back-to-back JSON
  objects that broke every JSON.parse candidate and silently zeroed
  extractCommands() — prefer the clean -o payload, fall back to stdout
  only when it's empty.
- Add `--override-doc <topicId>=<path>` (repeatable): loads a topic's
  text from a file instead of shelling out to
  `node bin/agent-device.mjs help <topic>`, so a draft help rewrite can
  be A/B graded with zero rebuild.
- Port three cases from
  test/skillgym/suites/agent-device-smoke-suite.ts
  (settle-diff-is-observation, sample-output-settled-diff-next-target,
  sample-output-not-settled-needs-observe) as self-contained
  "next-command quiz" cases, generalizing the scorer to support regex
  matchers/forbidden patterns alongside the existing named expectations.
  Fixture output text matches the CURRENT settle rendering in
  src/commands/interaction/output.ts, including the "unchanged
  interactive (N):" tail added by #1167/#1172.
- Parallelize the runner x case matrix with a concurrency cap
  (HELP_BENCH_CONCURRENCY, default 4); results still print in the
  original matrix order.
- Extend test/skillgym/README.md's existing pointer to this bench with
  the new flags.

Validated with real LLM calls (both runners, all 7 cases, 14 calls,
~$0.25 total): 13/14 pass; the one fail (claude-haiku-4-5 on
dogfood-mode) is a genuine model miss (returned an empty command plan
asking for the app name instead of committing to a generic plan), not a
bench bug. `--override-doc` demonstrated live: stripping the dogfood
doc's evidence-command examples regresses codex:gpt-5.4-mini from 3/3 to
2/3 on the same case, showing the flag both loads and changes grading.

* fix: apply live-doc post-processing to --override-doc, fail fast on bad overrides

Review findings on the initial version (all reproduced):

- HIGH: an override for the --help:first30 doc id skipped the live path's
  firstLines(text, 30) cap, so a 49-line draft leaked lines 31-49 into the
  prompt — grading content a live run never shows, on the doc id every case
  uses. loadDoc now splits source (live shell-out vs override file) from
  post-processing, and the post-processing applies to both, so an override
  differs ONLY in where the text comes from.
- MEDIUM: an --override-doc topic id no selected case uses was silently
  ignored (exit 0, real doc graded). Now fails fast listing the valid doc
  ids for the selection.
- LOW: a missing override file threw a raw ENOENT stack trace; expected
  failures now print one clean Error line. Added --help usage text that
  documents last-wins semantics for repeated same-topic overrides and the
  post-processing parity.

Guard tests (scripts/__tests__/help-conformance-bench.test.ts, wired into
the unit-core vitest project by explicit path): a 49-line fixture whose
prompt must keep line 30 and drop line 31, unknown-topic fail-fast with
valid ids listed, clean no-stack error for a missing file, and last-wins
for repeated overrides. All spawn the script in --dry-run with every
required doc overridden, so they need no LLM calls and no built CLI.

Live re-validation: a 33-line override of --help:first30 whose lines
31-33 instruct the model to emit a sentinel command; neither
claude-haiku-4-5 nor codex:gpt-5.4-mini emitted it (both scored 4/4,
matching the live-doc baseline), proving the cap applies end-to-end.
2026-07-10 09:06:58 +02:00
Michał Pierzchała b8ac75893a fix: make settle tail list real actionable targets (#1172)
* fix: make settle tail list real actionable targets

Post-merge benchmark of #1167 (React Navigation prevent-remove flow, iOS
sim, claude-haiku/sonnet) found the unchanged-interactive tail regressing
to chrome-only noise in exactly the cases it was built for:

- buildSettleTailEntries required `hittable === true`, stricter than what
  `snapshot -i` itself shows for the same interactive-only capture. Right
  after a dismiss animation, real buttons commonly report `hittable:
  false`/undefined while application/window containers pass, so the tail
  surfaced two useless chrome lines and dropped the actionable button.
  Fixed by dropping the hittable requirement and excluding structural
  application/window roles instead.
- withoutKeyboardKeys only stripped `Key` nodes; real keyboard chrome
  (shift/Emoji/return/Dictate/Next keyboard) are XCUIElementTypeButton
  nodes and leaked through as fresh added-line refs, which suppressed the
  tail trigger for exactly the post-fill case it exists for. Fixed by
  detecting the whole keyboard subtree structurally (via parentIndex, not
  a locale-fragile label list): descendants collapse out of the diff, and
  the keyboard container's own ref no longer counts as a "meaningful"
  added ref for the trigger decision.

Nobody presses shift via settle diff refs, so collapsing keyboard chrome
does not block a user from explicitly targeting the keyboard itself.

* fix: classify the whole iOS keyboard window as settle chrome

Live-device review of the first Bug B fix found "Next keyboard" and
"Dictate" still leaking into the settled diff as added refs: on a real
iPhone 17 Pro simulator they live in a SIBLING subtree of the [Keyboard]
container (the candidate bar), not under it, so the container-descendant
walk missed them. A raw hierarchy capture shows the software keyboard in
its own dedicated window hosting both the container and the candidate
bar, so the structural rule is now: every node inside a window that has
a [Keyboard] descendant is keyboard chrome. Conservative guard: a window
also hosting an editable text node outside the container (iOS puts
inputAccessoryView composers in the keyboard window) is never
window-classified — those fall back to the container-descendant walk.

The live run also showed the filled field re-labeling itself with its
new value (ancestor wrappers inherit it), which produced added refs that
suppressed the tail even with chrome fixed. The trigger now also ignores
self-echo refs — added lines whose settled node rect contains the action
point — since they re-describe the acted-on element, not a new target.

The fill-keyboard provider fixture is now a trimmed REAL capture from
the benchmark flow (sibling candidate bar, main app window absent from
the interactive settled capture, self-echo relabels) instead of a
hand-built tree that hid the sibling-branch shape.
2026-07-09 21:07:15 +02:00
Michał Pierzchała a3885351c2 fix: stabilize android maestro gestures (#1171)
* fix: stabilize android maestro gestures

* fix: address maestro android gesture review
2026-07-09 19:31:11 +02:00
Michał Pierzchała 8878399272 feat: include unchanged interactive refs in settle output (#1167)
* feat: include unchanged interactive refs in settle output

Benchmarks (gpt-5.4-mini + claude-haiku, July 2026) showed 27% of --settle
actions were followed by a fallback snapshot -i because a change-only diff
omits refs for elements that did not change: after a modal dismiss the diff
shows only removals, so the next button to press is invisible.

Add an unchanged-interactive tail to SettleObservation, attached only when
the diff's added lines carry zero refs (the modal-dismiss/toast-only
signature). It lists the settled tree's remaining hittable, uncovered
elements so the response stays actionable without an extra round trip.
Rides the CLI text, MCP digest view, ref pinning, and output schema the same
way the diff's added-line refs already do.

* refactor: address fallow audit findings on the settle tail

- drop the unused export on buildSettleTail (tests exercise the trigger
  through the public interaction path and the filter via
  buildSettleTailEntries)
- extract the digest tail capping from interactionSettleView into a
  module-private helper to stay under the complexity gate
2026-07-09 19:18:47 +02:00
Michał Pierzchała 888984169b fix: make record app-scoped by default (#1163)
* fix: reject recording for failed iOS simulator session

* fix: make record app-scoped by default
2026-07-08 21:48:35 +02:00
Szymon Dziedzic cfef0a4bca feat: add session event timeline (#1032)
* feat: add session event timeline

* fix: support cursor-only event reads

* refactor: simplify event log formatting

* refactor: trim event log helpers

* docs: document session event timeline

* refactor: tighten session event log internals

* fix: redact event log action positionals by default

* fix: align event log after rebase

* test: cover events in provider output guard

* fix: harden session event privacy

* fix: harden event message redaction

* fix: harden session event logging

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-07-08 21:35:45 +02:00
Michał Pierzchała 106c238697 fix: narrow client result contracts (#1155) 2026-07-08 18:35:27 +02:00
Michał Pierzchała b91eaad885 refactor: make iOS synthesized gesture policy explicit (#1152)
* refactor: make iOS synthesized gesture policy explicit

* test: harden settle observation under coverage

* fix: preserve first-command synthesized drag behavior

* refactor: simplify synthesized frame policy

* refactor: inline synthesized command policies

* refactor: simplify sequence synthesized context

* refactor: clarify synthesized drag fallback policy

* refactor: keep synthesized gesture policy runner-local
2026-07-08 17:15:42 +02:00
Michał Pierzchała f18d0b2e92 fix: improve settle observation guidance (#1154) 2026-07-08 17:12:19 +02:00
Michał Pierzchała 7f61df30ae feat: add TV remote command (#1147)
* feat: add TV remote command

* feat: improve TV remote ergonomics

* test: cover tv-remote provider scenario

* fix: preserve focused Android TV nodes

* docs: tighten PR description guidance

* fix: remove d-pad command alias

* docs: clarify tv-remote hold syntax

* feat: add tv-remote longpress CLI sugar
2026-07-08 10:59:48 +02:00
Michał Pierzchała 9dfebbe3be fix: omit interaction uptime from wire responses (#1142)
* fix: omit interaction uptime from wire responses

* fix: simplify interaction wire sanitizer
2026-07-07 16:53:44 +02:00
Michał Pierzchała 8f28c31c86 fix: recover completed Android recording from pending-only manifest (#1141)
* 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.
2026-07-07 12:04:58 +02:00
Michał Pierzchała 0dcc1aa553 fix: normalize interaction response wire shapes (#1114)
* fix: normalize interaction response wire shapes

* fix: reduce interaction response complexity
2026-07-07 11:29:01 +02:00
Michał Pierzchała 7e583c4136 fix: simplify Android recording recovery (#1135)
* fix: harden android recording recovery

* fix: reduce android recording recovery fallow complexity

* test: fix android recording recovery rebase

* fix: block uncertain android recording fallback

* fix: address android recording recovery review

* fix: address android recording recovery review followup

* fix: simplify Android recording recovery

* fix: address android recovery ownership review

* refactor: reuse android recovery manifest helpers

* refactor: split android pending recovery resolution

* fix: clarify scoped android recovery hint
2026-07-07 10:50:57 +02:00
Michał Pierzchała 5c5fa012f7 feat: --settle returns the settled diff in the interaction response (#1101) (#1106)
* 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
2026-07-06 20:18:44 +02:00
Michał Pierzchała db69124c00 feat: add capabilities command (#1133) 2026-07-06 19:35:46 +02:00
Michał Pierzchała 54f6d45b32 refactor: extract host process primitives (#1134) 2026-07-06 19:01:32 +02:00
Michał Pierzchała be4bd092b6 fix: recover Android recordings after daemon restart (#1129)
* fix: recover android recordings after daemon restart

* refactor: reduce recording recovery complexity

* fix: address android recording recovery feedback
2026-07-06 15:28:27 +02:00
Michał Pierzchała 5a362ec752 build: drop tsc entirely and fix sandbox-hostile tests (#1100)
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>
2026-07-04 21:02:36 +02:00
Michał Pierzchała 86736a5f87 feat: versioned snapshot refs with MCP auto-pinning (#1096)
* 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
2026-07-04 21:02:10 +02:00
Michał Pierzchała 6ee483c33f feat: warn when @refs outlive the session snapshot they came from (#1093)
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
2026-07-04 17:11:22 +02:00
Michał Pierzchała ad754ac0a4 feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011) (#1091)
* 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).
2026-07-04 16:41:03 +02:00
Michał Pierzchała f3e07ff236 test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3) (#1092)
* fix: preserve the runner's non-hittable fallback marker through direct selector press

The direct iOS selector press handler spread successText('Tapped <selector>')
after the runner payload, clobbering the 'tapped via non-hittable coordinate
fallback' message that directIosSelectorFallbackDetails keys on — so
maestroNonHittableCoordinateFallbackUsed could never be true end-to-end (the
existing unit test passes because it mocks dispatchCommand above this layer).
Found by the ADR 0011 Layer-3 maestro-fallback contract scenario.

Share the marker string as MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE and keep it
as the success message when the runner reports fallback usage.

* test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3)

test/integration/interaction-contract/ holds one scenario file per dispatch
path, each with a sibling .coverage.ts manifest declaring which guarantee
matrix cells it proves (scenario strings double as the vitest test titles).
index.ts aggregates the manifests statically, and a new Layer-3 gate in
src/contracts/__tests__/interaction-contract-coverage.test.ts fails when any
enforced (runtime/runner/delegated) cell lacks a scenario or when a scenario
claims a waived/inapplicable cell — coverage of the matrix is by
construction in both directions.

Path forcing is natural (no test-only env switch needed): selector/ref
targets take the runtime path, a tapTarget backend takes the native-ref fast
path, x/y takes the coordinate path, and simple-selector clicks on an iOS
provider transcript take the direct runner path (the transcript itself
proves which path ran via assertComplete).

Fixtures are the permanent Bluesky shapes: closed drawer, drawer + visible
twin, edge-grazing container, covered button, non-hittable cell.

Refs #1081
2026-07-04 16:02:41 +02:00
Stanisław Chmiela 9407839f6e feat: tag daemon artifacts with semantic types (#1066)
* feat: tag daemon artifacts with semantic types

* Normalize artifact type threading through daemon tracking

* Add timeout error to http server artifact test helper

* test: restore artifact wait helper behavior

* test: fail artifact wait helper on timeout

* refactor: model artifactType as optional on wire shapes

Applies the design-review tweak: producer-owned APIs (reserveOutput,
trackDownloadableArtifact, finalization callbacks) keep the required
'DaemonArtifactType | undefined' form so artifact owners must explicitly
decide, while public/wire/result shapes (DaemonArtifact, both artifact
inventory entry types) become 'artifactType?:' — missing metadata is
valid, JSON drops undefined, and remote/older daemons may omit the
field. Construction sites now omit the key for untyped artifacts, and
the finalization test asserts key absence (toEqual cannot distinguish
absent from explicitly-undefined).

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-07-04 12:19:47 +02:00
Michał Pierzchała 664b713130 feat(interaction): opt-in --verify evidence for press/click/fill (#1064)
* feat(interaction): opt-in --verify evidence for press/click/fill (#1047)

Adds an opt-in --verify flag that returns cheap post-action evidence
(foregroundApp, nodeCount, interactiveNodeCount, digest,
changedFromBefore) instead of requiring a full follow-up snapshot to
confirm a mutating command had an effect. The digest hashes the
(type, label, identifier) multiset of an interactive-only capture,
order-independent so it doesn't flip on harmless re-ordering; the
node tree is never serialized back to the client, only the digest and
counts. Default behavior (no --verify) is byte-identical to today.

Implements the approved design from the #1047 issue comment:
- src/utils/ax-digest.ts: new standalone digest module.
- Pre-action digest reuses the snapshot the resolution path already
  captures for ref/selector targets (zero extra cost); point targets
  opt into one extra baseline capture only when --verify is set.
- Post-action: one interactive-only capture through the same capture
  helper, digested and discarded.
- --verify threaded through the CLI flag schema, MCP input schema,
  interactionResultExtra allowlist, and MCP output schemas, following
  the same plumbing as --double-tap and the #1040 targetHittable
  precedent.
- The native-ref/direct-iOS-selector fast paths are skipped when
  --verify is set, since they bypass the resolution/capture path
  evidence depends on.

* test: cover press --verify with a provider scenario; classify the flag

CI's architecture-progress gate failed on 1 unclassified public flag.
--verify drives real device captures, so it belongs in the
device-observable list backed by an actual provider scenario rather than
the intentionally-outside bucket: the new scenario asserts evidence
(changedFromBefore, digest, nodeCount) on press @ref --verify, that the
verify capture's tree is never serialized into the response, and that
the transcript completes (snapshot -> tap -> verify snapshot; the @ref
path reuses the session snapshot as its baseline, so no extra
resolution capture entry exists).

* fix: type the verify scenario transcript entries

* fix: include interaction extras in the fill @ref response branch

The ref branch of dispatchFillViaRuntime rebuilt responseData from
backendResult/coordinates, dropping interactionResultExtra(result) — so
fill @ref --verify returned no evidence even though the post-action
capture ran (live E2E gap found in PR #1064 review). Spreading the
extras also gives fill @ref the same ref/refLabel/selectorChain (and
conditional targetHittable/hint) fields press @ref already returns.

Adds daemon tests for fill @ref --verify evidence and for the no-verify
path staying evidence-free with no post-action capture.
2026-07-04 10:48:54 +02:00
Michał Pierzchała 4711bc434b fix: report expired daemon resources as expiry, not INVALID_ARGS (#1069)
* fix: report expired daemon resources as expiry, not INVALID_ARGS

TTL/GC'd registry entries (downloadable/uploaded artifacts, resumable
uploads, materialized paths) surfaced as INVALID_ARGS, so agents got
"Check command arguments and run --help" when the resource had simply
expired. Add a shared requireTenantOwnedEntry helper that reports these
as COMMAND_FAILED with reason RESOURCE_EXPIRED and a per-resource
recovery hint, and dedupes the paired wrong-tenant UNAUTHORIZED check.

Also: give request cancellation an explicit "canceled intentionally"
hint instead of the misleading retry default; replace the ad-hoc
INTERNAL_ERROR code (not in the KnownAppErrorCode union) with UNKNOWN
plus a report-a-bug hint; and fix materialized-path TTL expiry of
tenant-owned entries, which threw UNAUTHORIZED in a void-ed promise
(unhandled rejection) and leaked the entry and its temp dir.

* fix: report expired resumable upload tickets as expiry, not INVALID_ARGS

Route requireResumableUpload() through the shared requireTenantOwnedEntry
helper so a missing/expired upload ticket on PUT /upload/direct/:id and
POST /upload/finalize returns the COMMAND_FAILED + RESOURCE_EXPIRED shape
(with a recovery hint) instead of the misleading INVALID_ARGS/400.

Genuinely invalid input (malformed preflight args, bad content-range,
incomplete upload, hash mismatch) stays INVALID_ARGS.
2026-07-04 10:46:49 +02:00
Michał Pierzchała 50a6a3533a fix: surface stderr excerpts from hand-rolled exec failure wraps (#1072)
* fix: surface stderr excerpts from hand-rolled exec failure wraps

normalizeError only replaces a generic COMMAND_FAILED message with the
first meaningful stderr line when details.processExitError is true, but
only createExitError in utils/exec.ts set that flag. Hand-rolled wrap
sites (throw new AppError('COMMAND_FAILED', msg, {stdout, stderr,
exitCode}) after an allowFailure run) missed it, so users saw messages
like "xcrun exited with code 22" with a generic hint instead of the
actual stderr excerpt.

Add execFailureDetails(result, extra?) to utils/exec.ts returning the
stdout/stderr/exitCode spread plus processExitError: true (extras spread
after the base keys so call sites can keep truncation or String()
normalization), and convert ~42 exit-guarded wrap sites across the apple
platform (apps launch/terminate paths, devicectl, simulator boot, perf,
perf-xctrace, runner modules, dsym/symbolication, macOS host provider)
and android/web (app-helpers, logcat, devices, perf, settings,
snapshot helpers, app-lifecycle, device-input-state, multitouch-helper,
agent-browser provider).

Sites reachable at exit 0 (parse/stdout checks), failure aggregations,
timeout/forced-kill errors, and sites that already build richer messages
(macos helper JSON, runner-contract early-exit, maestro run-script) are
deliberately left unflagged so stderr enrichment never replaces a more
specific message.

* fix: append stderr excerpts to curated wrap messages instead of replacing them

Spreading processExitError to curated call sites made normalizeError
replace specific messages like 'uiautomator dump did not return XML'
with the bare stderr line, losing the operation context (caught by the
provider-failures integration test). Keep wholesale replacement only for
the information-free generic '<tool> exited with code N' wraps; curated
messages now keep their specific reason and gain the stderr excerpt as a
': <excerpt>' suffix (skipped when already present).

Separate commit rather than amend so the semantics change to
maybeEnrichCommandFailedMessage stays individually reviewable on the PR.
2026-07-04 10:25:03 +02:00
devin-ai-integration[bot] d18066ff2b fix(cloud-webdriver): stop artifacts command from allocating a new provider session (#1030)
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 17:44:21 +02:00
devin-ai-integration[bot] 7777fa549e fix(aws-device-farm): send requested platform in WebDriver capabilities (#1028)
The AWS Device Farm runtime is registered with a hardcoded platform of 'android', so baseSessionForLease builds the createSession capabilities with platformName 'Android' before prepareSession resolves the real platform. On an iOS device Device Farm rejects this with "reserved ... invalid values: ['platformName']". Rebuild webdriverCapabilities in the AWS prepareSession using the resolved platform and device name (mirroring the BrowserStack provider).

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 16:33:50 +02:00
Michał Pierzchała fcb7e32f1c fix: clarify clean-xcuitest output (#1018)
* fix: clarify clean-xcuitest output

* fix: simplify clean-xcuitest formatter

* fix: clarify clean-xcuitest failure output
2026-07-02 13:53:42 +02:00
Michał Pierzchała 996d93e979 perf: cut iOS open --relaunch from 11s to 2.85s (#1010)
* perf: keep iOS simulator runner hot across open --relaunch

open --relaunch tore down the XCUITest runner session before closing the
app, then paid a full xcodebuild test-without-building restart (~6s) after
reopening it. Simulator close/open go through simctl and never touch the
runner, so a healthy runner survives the relaunch; stale runners are
recovered by the readiness preflight + invalidate/restart path that landed
after #705 reverted #700's version of this.

Steady-state open --relaunch on iPhone 17 Pro sim: 11.0s -> 4.9s
(startup.durationMs 5929 -> 1574). Real devices keep the conservative
teardown.

* perf: drop redundant simctl inventory listings from the iOS open path

A single open --relaunch spawned `simctl list devices -j` (~0.7s each)
three times: session-device re-resolve, closeIosApp's booted check, and
launchIosSimulatorApp's booted check.

- ensureBootedSimulator now keeps a 5s recently-observed-Booted memo
  (mirroring DEVICE_READY_CACHE_TTL_MS at the daemon layer) so repeated
  boot checks inside one request cost nothing; shutdownSimulator
  invalidates it, boot transitions seed it.
- refreshSessionDeviceIfNeeded skips the re-resolve while the device's
  XCUITest runner session is alive - a live runner attached to the UDID
  already proves the simulator exists and is booted.

With the runner kept hot across relaunch, steady-state open --relaunch on
iPhone 17 Pro sim drops 4.9s -> 2.85s (startup.durationMs ~1574 -> ~760).
Combined with the previous commit: 11.0s -> 2.85s.

* test: expect single simctl listing in tvOS provider flow

The recently-observed-Booted memo removes the repeat state listings the
launch and terminate boot checks used to make; the transcript now records
one listing per flow.
2026-07-02 11:33:43 +02:00
Michał Pierzchała 36917157ab test: surface-wide guard that no command response emits internal apple platform (#1005)
Adds a provider-integration guard (apple-platform-output-guard.test.ts) that
stands up a fake-provider daemon for BOTH a macOS Apple session and an
iOS-simulator Apple session, drives EVERY public command off PUBLIC_COMMANDS,
and deep-scans each serialized response for the internal 'apple' platform token
(any string VALUE or object KEY that exactly equals 'apple').

The guard is catalog-driven: a partition test fails if a new public command is
neither in DRIVEN_COMMANDS nor SKIPPED_COMMANDS, so a new command can't silently
escape the check. All 50 public commands are driven; the skip-set is empty.

Caught + fixed a leak PR #1004 misses: doctor's data.platform (session-doctor.ts)
echoed the raw internal device.platform ('apple') when doctor ran against a bound
session with no --platform flag. Now projected via publicPlatformString(device).

doctor's byPlatform.apple KEY leak stays tracked to PR #1004 via a narrow,
documented allowlist entry (not duplicated here).
2026-07-02 07:18:56 +02:00
Michał Pierzchała cd1551bf42 refactor: collapse public Platform ios/macos into apple (#979) (#1002)
* refactor: collapse public Platform ios/macos into apple (#979)

Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.

Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
  PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
  post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
  projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).

Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.

Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.

Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).

Refs #979 (part of #972).

* fix: project platform to the public leaf at open/perf response sites (#979)

The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)

Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.

Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.

* fix: project platform to the public leaf at nested output sites (#979)

The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:

- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
  response.support.platform / artifact.support.platform, plus the
  sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
  cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
  snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
  target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
  Apple devices (reachable via deviceFieldsFromPublicPlatform).

Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
2026-07-01 19:29:46 +02:00
Michał Pierzchała f0b926c21f fix: trim root type exports (#999) 2026-07-01 14:47:03 +02:00
Michał Pierzchała d21b8ce32e feat: add doctor command (#883)
* feat: add doctor command

* fix: reduce doctor command complexity

* fix: classify doctor integration flags

* fix: simplify doctor setup

* refactor: split doctor checks

* fix: simplify doctor check set

* fix: include stopped android avds in devices

* fix: report doctor device inventory

* refactor: reuse device inventory selectors

* fix: summarize doctor inventory by platform

* fix: show metro cwd in doctor

* refactor: simplify metro doctor lookup

* fix: update doctor imports after apple consolidation

* feat: make doctor Metro probe controllable and surface hidden toolchain failures

Two gaps found while verifying the doctor command on a real environment:

- Metro host/port were uncontrollable from the CLI: --metro-host/--metro-port
  were rejected by allowedFlags, and readDoctorOptions only read them from
  req.runtime (populated by remote/connection profiles, never a plain CLI
  flag). The Metro check's own hint told users to 'pass the correct
  --metro-host/--metro-port', which did not exist. Declare the flags and read
  them from req.flags (runtime kept as fallback) so the probe can target any
  endpoint, e.g. from outside an RN/Expo project directory.

- A broken per-platform toolchain was silently hidden: readDoctorDeviceInventory
  dropped inventory failures whenever any other platform returned devices, so a
  broken Xcode or Android SDK still reported a green 'pass'. Keep the failures
  and surface each as a warn (device-<platform>) when other platforms have
  devices; scoped --platform runs stay quiet.

* fix: align doctor CI expectations

* feat: extend doctor preflight checks

* fix: keep doctor checks within ci gates

* fix: simplify doctor metro surface

* refactor: trim doctor bundle impact

* fix: restore useful doctor diagnostics

* refactor: reuse doctor output helpers

* refactor: share device inventory grouping

* refactor: keep doctor focused on preflight checks

* refactor: simplify doctor toolchain probes

* fix: keep scoped simulator hint generic

* fix: clarify doctor Xcode selection context

* fix: recognize provider scope in remote doctor

* fix: address doctor review gaps

* fix: keep doctor metro checks inferred
2026-07-01 14:24:32 +02:00
Michał Pierzchała 56b41a53ab feat: add cross-platform audio probe (#880)
* feat: add web audio probe

* fix: stabilize web audio probe

* test: cover audio probe review gaps

* fix: address audio probe review feedback

* feat: support macOS audio probe

* docs: document audio probe help

* feat: support simulator audio probe

* test: account for host audio platform support

* refactor: deepen audio probe lifecycle

* perf: trim audio probe package size

* refactor: address audio probe review comments

* refactor: remove audio probe leftovers

* fix: encode audio probe eval options as data

* fix: document audio probe eval sanitization

* fix: sanitize audio probe eval options

* fix: use codeql-recognized eval option sanitizer

* fix: allowlist audio probe eval options

* fix: avoid json-stringified audio eval action

* refactor: trim audio probe input surface

* fix: align audio probe with apple helper paths

* test: update audio capability parity oracle

* refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! test: update audio capability parity oracle
2026-07-01 13:27:45 +02:00
Michał Pierzchała a707348b9b feat: add hosted WebDriver provider support (#948)
* feat: add cloud webdriver artifacts

* fix: clean up local session after provider release failure

* fix: tag cloud webdriver provider requests

* feat: connect hosted webdriver providers

* docs: document hosted provider credentials

* refactor: tighten cloud webdriver provider internals

* refactor: consolidate cloud webdriver provider definitions

* refactor: collapse hosted webdriver runtime wrapper

* refactor: reduce cloud webdriver smell surface

* docs: clarify hosted provider interfaces

* fix: avoid regex slash trimming in webdriver urls

* docs: rename hosted providers to device clouds

* fix: align provider profile imports with remote modules

* fix: skip local android recovery for provider devices

* test: classify cloud provider integration flags

* fix: close active cloud connection session

* test: cover provider disconnect cli flow

* fix: make cloud webdriver sessions launchable

* fix: align cloud webdriver input gestures

* fix: avoid keyboard input during cloud scroll

* fix: constrain cloud webdriver scroll gestures

* refactor: isolate cloud webdriver scroll frame

* refactor: deduplicate cloud webdriver helpers

* refactor: tighten cloud webdriver action types

* fix: harden cloud webdriver release

* fix: polish provider disconnect diagnostics

* refactor: group connection profile helpers

* fix: repair rebased internal paths

* fix: satisfy cloud webdriver CI guards
2026-07-01 13:01:47 +02:00
Michał Pierzchała e229957602 refactor: split daemon server runtime into daemon/server/ — Phase 5 (#985)
Phase-5 §5.5 folder move (server side; the daemon/client/ split shipped in
#962). Extracts the process-bootstrap / server-runtime cluster into
src/daemon/server/ as a pure, behaviorless path codemod — no logic changes.

Moved (server bootstrap/runtime — the layer that spins up the daemon and
owns the platform graph; each imported only by the bootstrap layer + each
other):
  src/daemon-runtime.ts          -> src/daemon/server/daemon-runtime.ts
  src/daemon/http-server.ts      -> src/daemon/server/http-server.ts
  src/daemon/transport.ts        -> src/daemon/server/transport.ts
  src/daemon/server-lifecycle.ts -> src/daemon/server/server-lifecycle.ts
  src/daemon/server-shutdown.ts  -> src/daemon/server/server-shutdown.ts

Left in src/daemon/ root (request core / shared wire helpers, out of scope):
  request-router.ts, handlers/, session-store.ts, lease-registry.ts, context.ts
  (the daemon's request layer) and http-contract.ts / http-health.ts /
  http-errors.ts / config.ts (HTTP wire contract + daemon config shared across
  client, remote, and cli — not server-only).

Left: src/daemon.ts (the thin process entry) stays at src/ with the other
package entrypoints; it is coupled to its physical path by four non-import
string references (rslib entry, config dev-mode sentinel, process-identity
detection regex, daemon-client launch srcPath), so moving it is beyond a pure
import codemod.

Rewrote every from/import/import()/type-only specifier per importer
(resolve-based path.relative recompute) across src and test, and renamed the
fallow health-baseline key for http-server.ts. daemon-runtime's static
platforms/ import is now inside the daemon-server seam the layering lint
(#984 R3) allows.

Verification: tsc --noEmit 0; layering check (branch script) unchanged (3
pre-existing R3 violations, 0 new); oxfmt clean; oxlint --deny-warnings 0;
fallow audit --base origin/main clean (14 files); rslib build 0
(internal/daemon entry still emits); vitest 17 passed (daemon-entrypoint,
http-server-rpc-validation, server-shutdown + 3 provider-integration).
2026-07-01 10:02:59 +02:00
Michał Pierzchała 26ac865c63 refactor: consolidate Apple platform internals (#968) 2026-06-30 21:30:46 +02:00
Michał Pierzchała 1cbe446df5 chore: prune public package exports (#961)
* chore: prune public package exports

* chore: drop extra facade exports

* chore: remove dead export leftovers

* docs: align public API docs
2026-06-30 15:51:37 +02:00
Michał Pierzchała 189b062519 refactor: extract client/ folder — Phase 5 (#960)
Move the SDK client + companion-tunnel cluster out of the src/ root into a
dedicated src/client/ folder, per plans/perfect-shape.md §5.5 (~8k LOC
client/remote unfoldered at src root; remote/ already extracted in #951).

Files moved (9): client, client-types, client-shared, client-normalizers,
client-companion-tunnel{,-contract,-worker}, client-react-devtools-companion,
companion-tunnel.

- git renames; 68 importers repointed via the resolve-based codemod
  (intra-client stays ./, staying deps recomputed)
- companion-tunnel.ts keeps rslib key 'internal/companion-tunnel' so dist
  output stays dist/src/internal/companion-tunnel.js (public subpath
  byte-identical; verified by build)
- update non-src importers (3 integration tests, vitest coverage include),
  .fallowrc.json entrypoint, fallow-baselines/health.json keys

backend.ts and daemon-client*.ts are intentionally left for follow-up
(daemon/client split). Behaviorless path codemod; all gates green.
2026-06-30 14:36:59 +02:00
Michał Pierzchała 548bf0c47c refactor: extract remote/ folder — Phase 5 (#951)
Move the remote/proxy/upload subsystem out of the src/ root cluster into a
dedicated src/remote/ intent folder, per plans/perfect-shape.md §5.5:

  daemon-proxy · daemon-artifacts · upload-client(-artifact) · remote-config
  · remote-config-core · remote-config-schema · remote-connection-state

- 8 files moved (git renames); imports repointed via a resolve-based codemod
  (path.relative recomputation — correctly distinguishes the root remote-config
  from the unrelated src/utils/remote-config.ts)
- rslib entry keeps key 'remote-config' so dist output stays
  dist/src/remote-config.js; public 'agent-device/remote-config' byte-identical
- update .fallowrc.json entrypoint + fallow-baselines/health.json keys +
  vitest.config.ts coverage include + the integration test import paths

Behaviorless path codemod. typecheck/lint/build/fallow/tests all green.
Stacked on #950 (contracts→kernel).
2026-06-30 11:47:39 +02:00
Michał Pierzchała f67b72d7db refactor: delete the WaitCommandResult/AlertCommandResult mirror types — Phase 2c (#946)
BREAKING (intentional, approved): removes the last two hand-written result-type
mirrors from client-types.ts. wait and alert are genuinely dynamic — wait's
daemon data is toDaemonWaitData's Record, and alert's iOS path is a generic
runner Record — so per the typed-result doctrine they should be the untyped
CommandRequestResult, not an invented closed shape.

- delete WaitCommandResult and AlertCommandResult from client-types.ts; the
  client.command.wait/alert methods now return CommandRequestResult.
- drop their public exports from index.ts and the now-unused AlertInfo import.
- the android-lifecycle integration test that read the typed alert.alert.source
  now casts the untyped bag.

This completes the result-type half of the client-types.ts mirror deletion (the
13 closed commands already live in src/contracts/* via CommandResultMap). The
Options-type half (deriving from inputSchema) is a separate follow-up.

Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, Layering
Guard empty, 476 client/contracts/mcp tests pass.
2026-06-30 08:21:47 +02:00
Michał Pierzchała 7a1640e53f refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3 (#940)
* refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3

Relocates the foundational primitive trio from src/utils/ into the kernel/ layer
(joining snapshot.ts from slice 2), per the target folder DAG in
plans/perfect-shape.md §5.5. A pure path codemod, no behavior change.

They form a closed cluster — device -> errors -> redaction, with redaction a
leaf — so kernel/ takes no upward dependency, and every importer becomes a clean
downward import toward kernel. errors.ts is the most-imported module in the
tree; device.ts the §5.5-named headliner. Moving all three atomically avoids a
half-state where one would import another across the utils/kernel boundary.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the moved files, so the unrelated commands/management/device.ts and
other same-named files are untouched): 483 sites across 402 files. The two
platform-descriptor doc comments and the fallow health baseline key for
device.ts are updated to the new path; the contracts-schema-public guard that
asserts the error helpers pull no diagnostics/node: deps now reads kernel/.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, rslib build, full vitest
suite (2877 pass), fallow audit clean (411 changed files), Layering Guard empty;
kernel/ files import only within kernel.

* docs: update guidance references to kernel/{device,errors} after the move

AGENTS.md (Apple-family sync rule + normalizeError), ADR-0009, and
plans/apple-platform-consolidation.md still named the old src/utils/ paths.
Point them at src/kernel/. plans/perfect-shape.md's utils/device.ts mention is
left as-is — it describes the pre-move diagnosis.
2026-06-30 07:25:02 +02:00
Michał Pierzchała dae80c70c0 refactor: add provider device runtime foundation (#933) 2026-06-29 19:12:30 +02:00
Michał Pierzchała 41358fe281 feat: typed command results, batch 3 (clipboard, appstate) — Phase 2 (#937)
Wire two more commands into the CommandResultMap spine as closed shapes,
grounded in the handlers' literal returns:

- clipboard (src/core/dispatch.ts handleClipboardCommand) -> a discriminated
  union on `action`: read => { text }, write => { textLength, message }.
- appstate (src/daemon/handlers/session-state.ts handleAppStateCommand) -> a
  discriminated union on `platform`: Apple (ios/macos) session state — now
  including the iOS-only device_udid / ios_simulator_device_set locators the
  previous hand-written mirror OMITTED — or Android package/activity.

Both result types move from the open client-types.ts mirror (DaemonResponseData
& {…}) into new src/contracts/{clipboard,app-state}.ts and are wired through
CommandResult<'clipboard'> / CommandResult<'appstate'>. Public export names are
preserved (re-exported via client-types.ts -> index.ts), so no API break.

Tightening clipboard to a closed union surfaced an unguarded .text/.textLength
access in an Android integration test (previously masked by the Record index
signature); fixed with discriminant guards. The parity test now pins all 12
migrated commands; the public-root export test gains clipboard/appstate samples.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, fallow audit clean,
Layering Guard empty, 791 tests across core/contracts/client/commands pass.
2026-06-29 19:08:39 +02:00
Michał Pierzchała 0be806cc50 refactor: seed src/kernel with snapshot.ts type module — Phase 5 slice 2 (#932)
Establishes the kernel/ layer from the perfect-shape DAG (plans/perfect-shape.md
§5.5) by moving the foundational snapshot type module (Rect, SnapshotNode,
SnapshotQualityVerdict, centerOfRect — re-exported by contracts.ts and imported
by ~130 files) from src/utils/snapshot.ts to src/kernel/snapshot.ts.

Pure path codemod, no behavior change. snapshot.ts is a leaf (zero imports, after
the preceding cycle-break PR), so kernel/ takes no upward dependency — and every
one of its ~130 importers (utils, core, daemon, platforms, commands, tests)
becomes a clean downward import toward kernel. This also unblocks the future
snapshot/ AX-domain extraction: those domain files now import DOWN into kernel
rather than sideways within utils.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the old module, so the other snapshot.ts files under commands/platforms/
daemon are untouched). 138 sites across 128 src files + 2 integration worlds.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, full vitest suite
(308 files / 2863 tests), fallow audit clean (133 files), rslib build, Layering
Guard empty; kernel/snapshot.ts confirmed import-free.
2026-06-29 17:30:46 +02:00