24 Commits

Author SHA1 Message Date
Michał Pierzchała 33084c7748 perf(ios): decide Simulator AX bridge viability (GO, Node-direct guest reader) (#2237)
* test(ios): add guest simulator AX bridge evidence

* test(ios): make alert cleanup selector unique

* test(ios): admit recovered alert cleanup surface

* test(ios): narrow AX spike to guest evidence path

* docs(ios): record guest AX bridge decision

* chore(ios): remove unused spike import

* docs(ios): correct simulator bridge verdict

* test(ios): drive the guest Simulator AX bridge directly from Node

Replace the idb companion + Python reader in the #2192 spike with a Node client
for idb v1.5.2's in-Simulator SimulatorFrameworkBridge: one private guest per
session spawned through simctl, 4-byte length-prefixed JSON over a UNIX socket,
single-fetch traversal with automation mode asserted per request, nested trees
flattened to parent-linked raw nodes with XCTest type names, and typed
crash/timeout/cancel/stale-generation failures.

The targeted harness now observes app readiness with a throwaway probe instead
of admitting on pid presence, relaunches the app per bootstrap sample, records
host load per sample, and runs recovery probes through the adapter. Hard tiers
follow the corrected #2192 contract (warm 300/500 ms, relaunch 500 ms); the
former 75/150 ms and 250 ms values are reported as stretch findings. Preboot
preference edits are optional and unused by the guest path.

The prototype's targeted artifact is preserved under a -python-prototype name;
its bootstrap and recovery samples measured the packaging, not the mechanism.

* test(ios): narrow Simulator bridge decision evidence

* docs: publish Simulator bridge evidence out of tree

* fix: tighten iOS bridge evidence gates

* docs: publish corrected bridge evidence

* docs: point to post-rebase bridge evidence
2026-09-04 07:38:36 +02:00
Michał Pierzchała 057b2da233 ci: run coverage in one job again (#2079)
The Coverage lane was split into two matrix shards plus a Coverage Report
job that downloaded both blob reports and merged them. That claimed three
runner slots per PR and put a barrier in front of the merge: the report
job could not start until the slower shard finished, and the blobs it
waited on are tens of MB to upload and download.

One job asks for one slot and reports its own thresholds where it runs, so
the lane finishes when the suite finishes. Everything the split needed goes
with it: the shard/merge switches in vitest.config.ts, the blob reporter
swap, the zeroed per-shard thresholds, and the env blanking that
`test:fuzz-worker` carried only to keep the second leg from inheriting them.
2026-08-27 13:28:54 +02:00
Michał Pierzchała 02d548dfc9 ci: consolidate CI workflow from 15 jobs to 8 (#1996)
* ci: consolidate CI workflow from 15 jobs to 8

Merge single-gate ubuntu jobs into grouped jobs sharing one checkout and
install: Lint & Format (plus the static text assertions), Repo Guards
(layering/selector/wiring/maestro/mcp-metadata), Compatibility &
Provenance (shared fetch-depth: 0 checkout), Typecheck & Package, and
Integration Tests (absorbs the web smoke with step-scoped env). Every
gate remains an independently named run-gate step; the gate manifest
derives lane ownership structurally.

Drop the Bun setup from FreeRange: @chenglou/freerange's bin is a plain
Node script. It stays GitHub-owned; only the runtime requirement is
retired.

* ci: fold FreeRange into Repo Guards and skip no-op fixture release jobs

FreeRange runs on plain Node now, so its gate joins Repo Guards as the
last step instead of occupying its own worker for the slowest guard.
The fixture release matrix filters to entries that will actually build,
so a cached-fingerprint PR starts zero release runners.

* ci: fold host XCTests into the macOS smoke lane and shard Coverage

The macOS lane now builds one unit-test-flagged runner bundle that both
the host XCTest run and the replay smoke consume, so the host lane no
longer occupies its own macos-26 runner behind a separate queue. The
host lane's file moves with it, and check:xctest-selection follows.

Coverage shards across two runners via blob reports and merges them on
a report job that evaluates thresholds once over the full suite and
produces every coverage artifact. The tmpdir leak check runs per shard,
since a leak lands on whichever runner executed the file.

* ci: drop local shard-smoke artifacts from tracking

* ci: enforce coverage thresholds only on the merged run

A shard evaluates its own half-suite coverage, so the global gate fired
per shard. Shards now report without gating; Coverage Report keeps the
real thresholds over the full merged suite.

* ci: include hidden files when uploading coverage blobs
2026-08-24 16:42:43 +02:00
Michał Pierzchała 93f8ae0096 chore: ignore host-local workspace artifacts (#1942) 2026-08-21 17:27:26 +02:00
Michał Pierzchała 76453add71 refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) (#1494)
* refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0)

Extend the workspace with packages/* and move the kernel behind an
enforced public API: packages/kernel with nine consumer-earned subpath
exports (errors, device, snapshot, contracts, collections, rect,
redaction, daemon-error, bounds — the last absorbed from utils as Rect
vocabulary). Every kernel import repo-wide becomes the
@agent-device/kernel/<sub> specifier; kernel tests move to
src/__tests__/kernel/ and exercise the package surface. The root
declares the package in devDependencies (workspace:*), tsdown bundles
it (noExternal) so the published artifact and its runtime dependency
manifest are unchanged.

Gate rewiring in the same change, per the W0 brief:
- R1 kernel-sink retires (physically subsumed); new R11
  package-boundaries guards no-root-back-imports, relative tunnelling
  past exports maps, undeclared workspace deps, and non-exported
  subpaths, with runtime resolution pins via import.meta.resolve.
- resolveImportEdges and mutation ownership follow workspace
  specifiers through exports maps, keeping R4 cycle checks, depgraph,
  and derived test ownership connected across the seam (kernel-errors
  still owns 495 tests). listSourceFiles includes packages/*/src.
- kernel becomes an unranked zone; mutation registry, stryker mutate
  globs, and the mutation-affected workflow path filter move to
  packages/kernel/src/errors.ts.
- check:affected gains packages/ ownership (manifests fail open);
  vitest and coverage include packages/*/src; fallow ignores
  packages/** (its resolver cannot follow workspace specifiers).
- The affected-selector CI job installs dependencies: its closure now
  crosses workspace specifiers, and the R8 relative exception is
  unsafe for production src files (Node ESM does not realpath, so dual
  specifier/relative loads would instantiate modules twice). The R8
  zero-dep set is pinned empty with that rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: address W0 review — mutation sandbox, exports-map resolution, tsc -b

Review findings on #1494, all five:

1. contracts-schema-public.test.ts reads the kernel source at its
   packages/ path (fs access invisible to the codemod and typecheck).
2. Mutation lane: Stryker sandboxes the tree but pnpm's node_modules
   symlink resolves @agent-device/* back to the real repo, so mutants
   in the sandbox never load and vitest.related finds no tests.
   vitest.mutation.config.ts now aliases each EXPORTED specifier to
   its source (derived from exports maps, never a wildcard), keeping
   resolution inside the mutated tree. Validated: kernel-errors module
   runs end to end (dry run 3,984 tests, mutants killed, exit 0).
3. Layering/depgraph resolve workspace specifiers through the
   exports-derived map (workspaceSpecifierTargets) instead of
   reconstructing paths, so '.'-facade packages resolve; the
   positional fallback remains only for map-less fixtures (P0 pin).
4. Per-package project references implemented: packages/kernel is
   composite (emitDeclarationOnly -> dist-types, gitignored), the root
   references it, and typecheck becomes tsc -b — probed to catch type
   errors on both sides under TypeScript 7 native.
5. R11's relative-route exception now requires membership in an actual
   R8 zero-dep job closure (zeroDepClosureFiles walks entries), not
   mere scripts/ placement — closing the dual-instantiation bypass.

Also from review discussion: daemon-error moves out of the kernel
package to src/client/ — its consumers (cli, client facade) rehydrate
wire DaemonErrors client-side; the daemon only produces them. Kernel
drops to 8 exported subpaths before any of them ship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* refactor: one exports-map reader for mutation alias and ownership

Fallow flagged workspaceExportAliases (cognitive 15, CRAP 90). The
manifest-reading logic already exists as workspaceSpecifierTargets in
scripts/layering/package-boundaries.ts, so both the Stryker sandbox
alias table and the mutation ownership walker now consume it instead
of carrying near-clones. Behavior unchanged; mutation suite 45/45 and
changed-code fallow green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: composite kernel without a root references edge

FreeRange runs plain `tsc -p tsconfig.json`, and a root `references`
entry makes non-build-mode TypeScript demand the referenced project's
built declarations (TS6305) — a standing "build first" tax on every
plain -p consumer (fr, editors). Keep the per-package composite
project and build it in typecheck (`tsc -b packages/kernel` before the
root and examples/sdk passes), but drop the root references edge: root
consumption resolves through exports to source, identical to runtime
and to the bundler. Probed: plain -p green with no prebuilt output;
kernel-side type errors still caught by its own build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: R11 uses the layering parser; mutation config is a fallow entry

Review blockers on #1494:

- R11's private single-quote regex could miss a double-quoted or
  re-export route into packages/*/src. specifierSites now delegates to
  the layering model's parseImports (both quote styles, side-effect
  imports, re-exports, dynamic imports), with direct regressions for
  each formerly-invisible form.
- vitest.mutation.config.ts becomes a declared fallow entry instead of
  a tolerated unused-file finding: the full-repo audit now reports it
  reachable (unused files 2 -> 1; the remainder predates this PR).

FreeRange clean-checkout evidence: with packages/kernel/dist-types and
every *.tsbuildinfo deleted, `pnpm check:freerange` reports 0 findings
on this head — the TS6305 topology died with the root references edge
in the previous commit; check:freerange has no build precondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 12:12:46 +02:00
Michał Pierzchała 53e4be5f86 Remove SkillGym suite and repo-health snapshot infrastructure (#1480)
* chore: drop SkillGym and the repo-health aggregator (#1412 descope)

Remove the SkillGym harness (test/skillgym/), its check-affected lane,
package scripts, and devDependency — the help-conformance bench is now
the single non-gating small-model oracle. skills/ markdown classifies
as docs in the affected-check selector instead of failing open.

Remove scripts/repo-health: its only gating assertion duplicated the
Layering Guard job, its case-count metric imported the deleted SkillGym
suite, and its sole planned consumer (#1424 / PR #1477) was closed with
the Track C descope on #1412.

Verified: check-affected node --test suites, oxfmt, oxlint, tsc,
check:layering, fallow audit vs origin/main, and the full unit suite
(unit-core + subprocess-stub) all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix(scripts): fold slow-test budgets into the reporter for production-exports

The Fallow production-exports gate flagged all three budget exports:
their in-file consumer (SLOW_TEST_RATCHET) and the repo-health entry
point that kept the module reachable were both removed in the descope,
leaving the config-loaded reporter as the only consumer — invisible to
--production analysis. The data-only module's second consumer is gone,
so per the boundaries-are-earned norm the constants move into the
reporter instead of gaining a suppression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* docs: align skills/ format policy and purge last SkillGym mention

Address both P2 review findings on #1480: the testing-matrix row and
the selector's formatGate both still claimed oxfmt covers skills/,
while selectChecks classifies skills/*.md docs-only (oxfmt ignores
**/*.md, so the claim was a no-op even before). The matrix now states
the docs-only policy and formatGate drops the dead underSkills fact.
The merged examples/README.md index (from #1469) loses its skillgym
mention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 11:45:22 +02:00
Michał Pierzchała a0aa02579b build(android): unify the 4 helper build/package scripts behind one parameterized pair (#1466)
* build(android): unify snapshot/ime helper build+package scripts

Replace the four ~75%-duplicated shell scripts with one parameterized
build script and one parameterized package script, mirroring
scripts/build-xcuitest-apple.sh's env-var-driven pattern. The helper
is selected via AGENT_DEVICE_ANDROID_HELPER or a first positional arg;
per-helper differences (HELPER_DIR/PACKAGE_NAME, snapshot's
test-compile+run step, ime's aapt2 resource-compile step, and the
manifest JSON fields) live in small case blocks.

All package.json entry points keep their names and output paths.
Verified byte-level equivalence between main and this branch: identical
unzip -l listings, identical classes.dex SHA-256 for both helpers, and
identical manifest fields (only the per-signing-run sha256 differs).

Fixes #1461

* chore: drop stale android/multitouch-helper .gitignore entries

The multitouch helper was consolidated away in #1281; these two lines
were never cleaned up.
2026-07-28 17:39:35 +02:00
Michał Pierzchała 9b610fbd1e feat(replay): recorded landmark identity for wait, is coverage — read-only step identity (#1349) (#1381)
* refactor(replay): extract shared target-evidence tree helpers into src/replay

Move buildIndexMap/buildAncestryChain/filterIdentitySet out of the daemon's
session-target-evidence into the shared replay zone so the commands runtime
(wait's polling loop, #1349) can consume them without importing the daemon;
press-retarget drops its private buildIndexMap duplicate.

* feat(replay): recorded landmark identity verification for wait, get-pattern coverage for is (#1349)

- New CommandDescriptor trait targetIdentityVerification pins the
  evidence-carrying command set and routes wait to a post-resolution
  phase so an annotated wait never enters the generic pre-dispatch
  verification (an absent landmark is its expected starting condition).
- wait <selector> records landmark-mode target-v1 evidence (existence
  self-check; identity-empty matches record no annotation) and, on
  replay, keeps polling until a selector match carries the recorded
  identity; a deadline with only impostor matches fails closed as an
  identity-mismatch REPLAY_DIVERGENCE, a recorded-unverifiable
  annotation refuses before polling, and a plain timeout stays an
  action-failure divergence.
- is (except exists) joins the get pattern: evidence at record time,
  generic pre-dispatch verification, and the post-resolution guard
  threaded through dispatch; direct-iOS fast paths for wait/is are
  gated during recording and guarded replays.
- Read-only find stays intentionally unannotated (fuzzy-locator
  resolution has no selector-chain identity token), proven by test.

* feat(publication): destination guard requires verified recorded landmark identity (#1349)

A qualifying ADR 0016 guard is now a selector wait whose target-v1
annotation is verified; identity-less or unverifiable guards are refused
with a recovery hint. Adds the reshuffled-screen false-pass regression:
record -> publish -> replay against a same-label/different-ancestry tree
diverges as identity-mismatch (matchCount >= 1 proving the selector
alone would have false-passed).

* refactor(replay): dedupe post-dispatch identity-mismatch shaping, trim evidence-writer complexity

Shared buildPostDispatchIdentityMismatchResponse behind the guard and
wait-landmark conversions; extracted payload-ceiling helpers from
computeTargetEvidence; identity-refusal conversion split out of
resolveReplayStepResponse. Docs: ADR 0012 decision 3 amendment (#1349),
ADR 0016 guard strengthening, help workflow/save-script text.

* refactor(replay): make landmark evidence's record-time verification explicit, trim ADR-restating docs

The landmark-mode self-check was provably a tautology (the winner is a
member of its own identity set whenever the parent walk is intact), so a
membership scan defended only by a comment is replaced with the explicit
decision: broken walk fails closed, landmark is verified by construction,
action mode keeps decision 3's step-5 self-check. Doc comments that
re-argued the ADR amendment now state behavior and point to it.

* chore: untrack multitouch-helper build artifacts, ignore its build/dist dirs

Generated Android helper output swept into the earlier refactor commit by
accident; analogous snapshot-helper/ime-helper build dirs were already
ignored.

* fix(interaction): wait polls ride out content-unreadable captures (live-validated on Android)

Live ADR 0016 validation on a Pixel emulator showed a destination-guard
wait replayed immediately after a navigation press deterministically
dies: the first poll's capture lands mid-transition and the Android
helper's 'insufficient foreground app content' verdict threw out of the
polling loop. iOS already yields the same state as a sparse verdict
with no matches, so the loop kept polling there — this makes wait
semantics platform-consistent. A content-verdict capture failure
(isUnreadableCaptureContentError) now counts as a no-match poll for
selector and text waits; a wait whose screen never became readable
rethrows the last capture verdict at the deadline, so persistent
breakage keeps its diagnosis. Other capture failures still throw
immediately.

* fix(snapshot): narrow unreadable-capture classification to enumerated content verdicts

Android stamps androidSnapshotHelperFailureReason on mechanism failures
too (helper timeouts, adb failures, missing helper artifact — free-form
reason strings), so matching any string made waits poll those to their
deadline instead of failing immediately. The predicate now matches only
the enumerated content-recovery reasons, and
AndroidHelperContentRecoveryDecision derives its reason union from the
same list so a new content verdict cannot miss the predicate. Adds the
realistic wrapped mechanism-error regression the synthetic test missed.

* test(interaction): make the wait mechanism-failure regressions revert-sensitive

Assert exactly one capture attempt: the broad any-string classifier
would poll the repeated fixture error to the fake-clock deadline and
rethrow the same message, passing the message-only assertion. Verified
the mechanism test fails against the broadened classifier and passes
against the narrowed one.
2026-07-24 20:05:09 +02:00
Michał Pierzchała b2b7ddd0d0 chore: gitignore .env files (#1322)
The repository is public and the root `.env` was untracked but matched no
ignore rule, so it showed up in `git status` and any `git add .` or `git add -A`
would have staged it — publishing whatever it holds to the repo and every fork.

It has never been committed to any ref, so this is prevention rather than a
leak; nothing needs rotating.

`.env.*` subsumes the old `examples/test-app/.env.local` entry, which was the
only dotenv rule here and covered a single path. The rules match basenames at
any depth, so every variant is now covered wherever it appears. `!.env.example`
keeps templates trackable; none exist today, but the negation is what makes the
broad `.env.*` safe to add.
2026-07-17 12:39:10 +02:00
Michał Pierzchała 1a1ef7c419 feat(android): one persistent automation helper owning snapshot + viewport + canonical injection (#1281)
* feat(android): consolidate touch injection and gesture viewport into the persistent snapshot helper (#1275)

One Android automation helper now owns snapshot capture, gesture viewport
resolution, and canonical one-/two-pointer plan injection. A live persistent
helper session executes gesture/viewport commands over its socket protocol;
without a session the same APK runs one-shot via am instrument. The separate
one-shot multitouch helper APK is deleted (atomic replacement, no fallback).
Touch scheduling/injection is extracted into focused Java classes
(TouchPlan, TouchPlanInjector, PointerEventSchedule, GestureViewportReader)
instead of growing SnapshotInstrumentation. ADR 0013 amended.

* fix(android): stop a structurally-failed helper session before the one-shot viewport retry

A structured ok=false viewport response leaves the session process alive, and
Android permits only one instrumentation owner of UiAutomation - running the
one-shot fallback against a still-live helper contends with it and masks the
original structured failure. Stop the session first; regression pins that the
one-shot retry only executes once the session is gone.

* refactor(android): extract helper touch dispatch into focused classes; split session tests; document helper API v2 (PR #1281 review)

Addresses findings 2 and 3 from PR #1281 review (finding 1, viewport
session-stop ordering, was already fixed in 5961b9247).

- Extract SnapshotInstrumentation.java's one-shot/session touch dispatch
  into TouchCommandHandler.java (viewport/gesture population, UiAutomation-
  parameterized) and SessionResponseWriter.java (session response encoding),
  with shared PROTOCOL/HELPER_API_VERSION/OUTPUT_FORMAT constants moved to
  a tiny HelperProtocol.java. SnapshotInstrumentation.java shrinks from 908
  to 803 lines; wire format (header keys/values, error shapes) is unchanged.
- Split touch-helper.test.ts (~720 lines) into touch-helper.test.ts
  (normalize/parse/one-shot gesture+viewport+result envelope) and
  touch-helper-session.test.ts (persistent-session transport + fake-session
  harness), moving shared device/plan/install-probe fixtures used by both
  files into touch-helper.fixtures.ts.
- Update android/snapshot-helper/README.md to document helper API v2: the
  one-shot viewport/gesture modes, the android-touch-plan-v1 payload shape,
  and the persistent session's socket command/response contract.

* fix(android): invalidate helper session after APK replacement; recycle viewport windows; align ADR 0002 (PR #1281 re-review)

- prepareAndroidTouchHelper now mirrors the snapshot path: when
  ensureAndroidSnapshotHelper replaces the APK (install.installed), the
  persistent session started against the previous binary is stopped before
  any touch command, so gestures run one-shot against the fresh install
  instead of a dead/stale session socket. Regression: 'an APK replacement
  stops the stale session and the gesture runs one-shot' drives a live fake
  session through an outdated-install probe (new outdatedVersionAdb fixture)
  and asserts the session socket receives no gesture, the one-shot
  instrumentation path executes, and the session is gone.
- GestureViewportReader.read no longer leaks AccessibilityWindowInfo: a
  single pass copies the active/focused and first-application bounds into
  locals, every window is recycled in a finally, and the existing precedence
  (active/focused app bounds, root-in-active-window, fallback app bounds,
  IllegalStateException) is applied afterwards, unchanged.
- ADR 0002's touch-synthesis paragraph is amended (2026-07, issue #1275) to
  the shared-helper model, consistent with ADR 0013: a live persistent
  helper session executes touch commands directly, one-shot otherwise; the
  old stop-before-gesture requirement is kept as historical context.

* fix(android): resolve touch helper artifact from the ADB provider like snapshots do (PR #1281 re-review)

- prepareAndroidTouchHelper now uses the same artifact precedence as
  snapshot capture: the scoped adbProvider's snapshotHelperArtifact when
  present, otherwise the bundled resolver (whose strict unavailable error
  is preserved). The provider artifact drives both the install decision
  and the instrumentationRunner used for one-shot commands, so an
  ADB-backed provider that supplies a helper artifact but no native touch
  override runs snapshots and gestures against the same single helper
  (issue #1275). Regression: 'a provider-supplied snapshotHelperArtifact
  overrides the bundled artifact for touch' pins the provider packageName
  on the install probe, the provider apkPath on the install call, the
  provider instrumentationRunner on the am instrument args, and that the
  bundled resolver is never invoked.
- ADR 0002 now states explicitly that one-shot retry applies only to
  idempotent reads (viewport) after the failed session is stopped;
  non-idempotent gesture failures surface directly.
- Helper README session transport corrected: a persistent process serving
  one short-lived socket connection per request (the server closes each
  accepted connection), not a single long-lived connection.

* fix(android): guard touch session reuse on helper identity, stop mismatched sessions (PR #1281 re-review)

Persistent helper sessions are keyed by device, so touch reuse must also
prove the live session runs the helper binary the command selected. The
session record now stores its helper identity (packageName, runner,
helperVersion, helperVersionCode — the same values that feed the snapshot
session identity), and runAndroidSnapshotHelperSessionTouchCommand takes
the requesting helper identity: on mismatch (packageName/runner always;
version/versionCode when both sides define them) it stops the session and
returns undefined, so the touch command runs one-shot against the selected
artifact — gestures never start sessions; the next snapshot restarts one
with the right artifact. Matching identity reuses the session as before.
Snapshot capture identity and behavior are unchanged.

Regression: 'a provider artifact that mismatches the live session helper
stops it and runs one-shot' — a live fake session from the bundled fixture
artifact, then a gesture through an ADB provider supplying an
already-current artifact with a distinct packageName/runner (no install):
the old session socket receives zero gesture commands, the session is
stopped, the one-shot am instrument args end with the provider runner, and
helperTransport is 'instrumentation'.

* fix(android): include artifact sha in helper session identity; evict stale install memo entries (PR #1281 re-review)

Same-version binary replacement changes only the APK sha, so identity
guards keyed on package/runner/version/versionCode could not detect a
crossover between two artifacts that differ only in bytes:

- The artifact sha256 now joins the helper identity end-to-end:
  AndroidSnapshotHelperCaptureOptions gains helperSha256 (snapshot.ts
  passes artifact.manifest.sha256 alongside version/versionCode), the
  session record stores it, createSessionIdentity includes it (making
  snapshot session reuse sha-aware, consistent with the install path's
  existing sha check), and the touch identity guard compares it via the
  same both-defined rule.
- ensureAndroidSnapshotHelper's install memo now evicts every other
  cached decision for the same device+package when it records an
  install/current decision, so installing B invalidates A's stale
  'current' memo and a later command selecting A re-inspects the device
  instead of skipping the sha check.

Regressions: 'a same-version artifact with a different sha stops the live
session and runs one-shot' (touch-helper-session.test.ts — B owns the live
session, a gesture selecting same-version different-sha A sends zero
commands to B's socket, stops it, and completes one-shot) and 'installing
a same-version different-sha helper evicts the stale install memo'
(snapshot-helper.test.ts — A:current cached, B installed, selecting A
re-inspects and reinstalls instead of serving the stale memo). Both
verified to fail without their fix.
2026-07-16 17:04:42 +02:00
Michał Pierzchała e58cbcdb5f refactor: colocate native platform sources under android/, apple/, linux/ (#1273)
Move the scattered root-level native projects into per-platform folders and drop
the now-redundant platform prefix:

- android-ime-helper/        -> android/ime-helper/
- android-multitouch-helper/ -> android/multitouch-helper/
- android-snapshot-helper/   -> android/snapshot-helper/
- apple-runner/              -> apple/runner/
- macos-helper/              -> apple/macos-helper/
- src/platforms/linux/atspi-dump.py -> linux/atspi-dump.py

Only repo source paths move. Identity surfaces stay frozen so no user's runner
cache is invalidated on upgrade: the derived-cache key hashes source paths
relative to AgentDeviceRunner and excludes packageVersion, and the
~/.agent-device/{apple-runner,macos-helper} namespaces, the
agent-device-android-*-helper artifact/manifest/protocol names, the
AgentDeviceRunner Xcode project, and the `prepare ios-runner` CLI command are
unchanged. Updates build/package scripts, CI, package.json files+scripts,
ignore/attr/fallow configs, runtime path resolvers, and test fixtures.

Also: re-base repo-root-relative refs inside the moved apple/runner for the
added nesting level (gated XCUITest fixture walk + two doc links), and clean the
legacy dist/apple-runner packaged output so the relocated runner can't
double-ship into the wholesale-included dist (with a regression test).
2026-07-15 21:47:38 +02:00
Michał Pierzchała d4146c7f1b feat: add Android test IME helper for deterministic text entry (#1198) (#1201)
* feat: add Android test IME helper for deterministic text entry (#1198)

Ships a headless InputMethodService (android-ime-helper) as a third Android
helper APK, replacing the visible system keyboard during automated sessions.
Renders zero accessibility nodes and accepts Unicode/CJK/emoji text over a
base64-encoded broadcast channel, fixing both the settle-diff IME-chrome
flood and the ASCII-only adb-shell text entry limit in one structural fix.

- android-ime-helper/: InputMethodService + build/package scripts on the
  existing helper-APK toolchain (javac+d8+aapt2+zipalign+apksigner).
- src/platforms/android/ime-helper.ts, ime-lifecycle.ts: install/version
  lifecycle (shared with the other two helpers via the new
  helper-package-install.ts), activation on session open, and on-device
  restore-hygiene (previous IME persisted to a device settings key so any
  daemon/state-dir can recover it; restored on close, daemon teardown, and
  daemon startup for orphans left by a crashed run).
- input-actions.ts: fill/type route through the helper's broadcast channel
  when active, unicode-safe; unchanged ASCII-shell fallback otherwise.
- doctor: new android-test-ime check flags a stuck helper IME with a
  copy-pasteable `adb shell ime set` remediation command.
- Gating: default-on for emulators, opt-in via `open --test-ime` on real
  devices.
- Dead-weight: rewrote the manual ADBKeyBoard workaround doc, dropped the
  now-provably-live skillgym non-ASCII eval case, updated the ASCII
  fallback's error message to point at the helper instead of dead-ending.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(#1201 review): permission-gate the IME receiver, fix CI, add opt-out

Addresses the independent review's blockers and should-fixes.

SECURITY (blocker 1): the text-injection receiver was RECEIVER_EXPORTED with
no gate — any co-installed app could inject text into the focused field while
the test IME was active. Fixed by requiring the WRITE_SECURE_SETTINGS sender
permission on the (in-process, dynamically-registered) receiver: adb shell
holds it, third-party apps cannot. The reviewer's suggested exported=false +
explicit-component approach was tried first but empirically breaks delivery on
API 36 (adb shell cannot reach a non-exported receiver there) — documented in
the helper README. Live-verified: a purpose-built rogue APK's broadcasts
(implicit and package-scoped, no permission) are silently dropped, field
unchanged; adb shell's bare broadcast still injects. Added
ime-helper-security.test.ts asserting the permission gate and that no
permissionless exported registration returns.

CI (blocker 2): (a) added `testIme` to integration-progress-model flag buckets
(Integration Tests was red on the unclassified flag). (b) mocked
resolveAndroidImeHelperArtifact in session-doctor-android / ime-lifecycle /
input-actions-test-ime tests so they no longer depend on android-ime-helper/dist
existing on disk (Coverage was red on a fresh checkout); verified by running
them with dist removed.

Should-fixes: added `--no-test-ime` to opt out on emulators (tri-state gating,
parser-tested); PR body's "byte-identical" claim corrected to size/CRC-match.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(#1201): pin the API-36 exported-receiver constraint in a comment

The RECEIVER_EXPORTED flag cannot express why it must stay exported. Add a
one-line note so a future hardening pass doesn't switch to RECEIVER_NOT_EXPORTED
and silently break the CLI (adb shell can't deliver explicit broadcasts to
non-exported components on API 36+; WRITE_SECURE_SETTINGS is the actual gate).

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(#1201 review): harden IME restore lifecycle (blockers 1 & 2)

Blocker 1 — a failed restore no longer deletes the recovery value. restore now
reads back default_input_method after `ime set` and only clears the persisted
previous-IME record on a confirmed-successful restore; a failed set keeps the
value so a later retry / startup recovery / doctor remediation can still
un-strand the user off the helper IME.

Blocker 2 — startup orphan-recovery no longer overwrites/races user state.
It only restores when the device's CURRENT default IME is still our helper
(so a user who legitimately switched away is left alone), and skips any device
a live session in this process owns (the fire-and-forget startup vs. concurrent
`open` race — activate now marks the device active BEFORE the `ime set`, so any
recovery pass that could observe the helper active also observes the flag and
skips). Never persists the helper itself as the previous IME. activate also
verifies its own switch via read-back.

Exported ANDROID_IME_HELPER_SERVICE_COMPONENT so restore compares the active IME
without reading the packaged artifact from disk. Tests: failed-restore keeps the
value (+ later recovery succeeds), startup no-op when current != helper, startup
skips a live-owned device.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(#1201): delete unused ACTION_ENTER path, baseline test-only export seams

Rebased onto main (#1202 production-unused-exports gate). Two follow-ups:

- Deleted the unused ACTION_ENTER broadcast end-to-end (TS sendAndroidImeHelperEnter
  + its test, Java handler, README): nothing routes through it — `keyboard enter`
  uses the keyevent ENTER path — so the new production-exports gate flagged it as
  dead production code. Removed rather than grandfathered.
- Added the three legitimate test-only seams (resetAndroidImeHelperInstallCache,
  resetAndroidTestImeActivationCacheForTests, setAndroidTestImeActiveForTests) to
  fallow-baselines/production-unused-exports.json, matching how the sibling helper
  reset functions (resetAndroidMultiTouchHelperInstallCache, ...) are already
  grandfathered there.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(#1201): stop daemon-startup adb spawn on non-Android hosts (macOS Smoke)

Root cause of the red macOS Smoke shard (proven, not hand-waved): the
fire-and-forget restoreOrphanedAndroidTestImeOnDaemonStartup ran `adb devices`
at EVERY daemon startup, on every platform. GitHub macOS runners ship the
Android SDK, so this cold-started the adb server mid-replay and destabilized the
macOS System Settings replay timing — the failed job's cleanup shows
"Terminate orphan process: pid (N) (adb)"; main's green runs spawn no adb.

Fix: gate the startup orphan scan behind a host-side marker written in the
daemon state dir when a session activates the test IME (mirrors the
managed-web-browser orphan-cleanup `installed` gate). A host that never uses the
Android test IME — the macOS CI runner included — never writes the marker and so
never spawns adb at startup. The marker is cleared once nothing is left stuck.

Adds SessionStore.resolveStateDir(); tests: startup recovery does not scan adb
when no marker exists (+ marker cleared after a clean scan).

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(#1201): suppress fallow class-member false-positive on state-dir accessor

CI's Fallow audit flags SessionStore.resolveDaemonStateDir as an unused class
member, but it is called via sessionStore.resolveDaemonStateDir() in
session-open.ts — fallow's class-member tracer just doesn't resolve a method
call sited inside a call argument. Renamed for clarity (avoids the collision
with config.ts's free resolveStateDir) and added the localized
fallow-ignore-next-line unused-class-member suppression.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(#1201 review): durable persist before switch + device-scoped recovery markers

Addresses devin-ai-integration's two P1 restore-safety blockers on 19cbce79d.

P1.1 — durably persist the restore target BEFORE the global IME switch.
writePersistedPreviousIme now checks the `settings put` exit code AND reads the
value back, returning a boolean. activate persists first and, if it cannot be
persisted, fails open to the existing input path WITHOUT switching — a rejected
`settings put` can no longer strand the user on the helper with no restore
target. Regression test added.

P1.2 — close the marker crash/offline blind spot. Recovery intent is now
recorded per device, BEFORE the switch (ordering: durable record -> marker ->
ime set), eliminating the post-switch/pre-marker crash window. Markers are
device-scoped and each is retained until that device is actually observed clean:
an offline/disconnected-but-stuck device keeps its marker and is recovered on
reconnect instead of being cleared because the current `adb devices` scan saw no
set-failed. Close-time restore clears only that device's marker (stateDir plumbed
through teardown/close). Tests cover the persist-failure, post-switch/pre-marker
crash, offline-then-reconnect, live-session-owned, and user-switched-away cases.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 19:38:37 +02:00
Michał Pierzchała 4cd40aa621 feat: polish replay test progress reporter (#998)
* feat: polish replay test progress reporter

* test: stabilize replay reporter cursor test in CI

* refactor: dedupe replay reporter live progress checks

* fix: make Expo build cache path configurable
2026-07-01 16:21:56 +02:00
Michał Pierzchała c32249503b test: cover least-tested parsing modules + verify public exports reach npm; drop dead daemon barrels (#749)
* test: cover least-tested CLI grammar and config parsing modules

Add focused unit tests for the modules the coverage report flagged as
least covered, exercising real behavior rather than padding metrics:

- utils/source-value: env/config value parsing (booleans, enums, enum
  flags with setValue, int bounds, multiple) — 42% -> 100% lines
- commands/cli-grammar/gesture: CLI<->daemon gesture argument
  translation for every gesture kind incl. error paths — 41% -> 100%
- commands/cli-grammar/system: back/rotate/keyboard/clipboard/
  react-native readers and writers incl. validation — 57% -> 100%
- core/device-rotation: orientation parsing with aliases/errors — 100%
- core/dispatch-payload: push payload loading from inline JSON and
  files, with temp-file I/O and JSON/shape error handling

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

* chore: gitignore generated coverage report directory

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

* test: verify public exports reach the npm build; drop dead daemon barrels

Strengthen the package-exports test so it verifies the real
publish-time invariant: every package.json "exports" subpath maps to a
configured rslib build entry that points at an existing source module
which actually exposes named exports. This catches a subpath being added
to package.json without a matching build entry (which would ship a
broken import), and keeps import/types targets in lockstep.

Remove four internal re-export barrels under src/daemon that were pure
1:1 forwarders to their src/utils source, and repoint all importers at
the real module so it is obvious where the code lives:

- daemon/is-predicates.ts      -> utils/selector-is-predicates.ts
- daemon/selectors-build.ts    -> utils/selector-build.ts
- daemon/snapshot-diff.ts      -> utils/snapshot-diff.ts
- daemon/snapshot-processing.ts -> utils/snapshot-processing.ts

The curated daemon/selectors.ts facade is kept; only blind forwarders
were removed. No behavior change.

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 16:28:01 +02:00
Michał Pierzchała 45cfad5cc5 feat: e2e command perf benchmark harness + nightly CI (#630)
* feat: add e2e command perf benchmark harness + nightly CI

Adds scripts/perf, a cheap end-to-end perf benchmark that drives the built
CLI through an ordered Settings tour of ~24 commands for N rounds, on a fully
isolated daemon/state-dir and self-cleaning device, and emits JSON + Markdown
reports. Per-command timing comes from wrapping each batchable command in its
own single-step batch (daemon durationMs) plus wall-clock around the process.

Wires a scheduled + workflow_dispatch CI job (perf-nightly.yml) that reuses the
cached iOS XCUITest runner (setup-apple-replay) and the Android replay host, and
runs the CLI from source via --experimental-strip-types (no dist build).

* refactor(perf): drive the harness CLI via runCmdSync, not spawnSync

Review (P2): repo rule is to spawn processes through src/utils/exec.ts, not
node:child_process directly. Switch the perf harness's invokeCli to runCmdSync
(allowFailure so non-zero exits are recorded as samples) and add a maxBuffer
option to ExecOptions/runCmdSync (snapshot payloads exceed Node's ~1MB default).

* perf(harness): warm the runner after open so the first measured command is clean

The first interaction after open/relaunch pays the one-time iOS XCUITest runner
startup (~10s+ cold) and a per-relaunch first-AX-query settle cost (~4s). That was
landing on the first measured command each round (snapshot -i), inflating it ~10x
vs the next snapshot. Run an untimed warmup snapshot -i after establishSession, after
each round's reset-open, and after every freshRoot relaunch, so no measured command
absorbs runner startup. Noted in the report header.

* refactor(perf): address review + fix Fallow CI

- exec.ts: extract spawnRejectionError + commandCloseFailure helpers, deduping the
  error/close handler clones (Fallow duplication ✗ that surfaced once the maxBuffer
  change pulled exec.ts into the audit scope).
- .fallowrc: exclude scripts/perf/** (non-shipped benchmark tooling, like examples/
  test-app) so its naturally-moderate functions don't trip the complexity gate.
- config.ts: drop unused exports CLI_BIN/DEFAULT_OUT_DIR; add readIntValue so
  --n/--rounds/--warmup report the actual flag + reject non-integers clearly.
- harness.ts: extract toSample(); type sampleError param as CliResult.
- scenario.ts: ScenarioStep is now a discriminated union on execMode (removes step.step!/
  step.args ?? []).
- comment/legend rewords (platform defaults are local-convenience/CI-overridden;
  elements = node count). check:fallow now green; typecheck/lint/unit pass.

* perf(harness): downgrade sample ok when a batch step reports ok:false

Defensive belt-and-suspenders for the Codex review note: stop-only batch already
surfaces a failed step as a top-level failure (caught by invokeCli), but if an
on-error=continue mode ever keeps the batch ok while a step fails, don't silently
count that step as a successful sample — derive ok from the step's own result.ok.
2026-05-31 14:37:59 +02:00
Michał Pierzchała 47b981c8ad feat: add gesture command coverage (#576)
* feat: add gesture command coverage

* fix: align iOS fling provider fixture

* feat: group gesture commands

* fix: clarify android gesture support

* feat: add android multitouch gestures

* fix: address gesture review feedback

* refactor: simplify gesture plumbing

* fix: keep gesture subcommands internal

* fix: update iOS provider pan transcript
2026-05-22 18:01:58 +02:00
Michał Pierzchała d77a9211ab feat: add Android snapshot helper (#454)
* feat: add android snapshot helper

* fix: harden android snapshot helper packaging

* fix: harden android xml attribute parsing

* fix: capture android helper window roots

* fix: validate android helper install args

* fix: harden android helper manifest and traversal

* refactor: drop unused helper timeout parameter

* feat: bundle android snapshot helper in npm package

* feat: enable bundled android snapshot helper by default

* refactor: simplify android snapshot helper resolution

* fix: harden android snapshot helper artifacts

* refactor: split android snapshot helper modules

* fix: use type-only snapshot helper imports
2026-04-27 16:01:37 -04:00
Michał Pierzchała 7c5b7670c8 feat: add skillgym tests (#453) 2026-04-26 20:49:59 -04:00
Michał Pierzchała f07e82e308 chore: add fallow quality gate (#449)
* chore: add fallow quality gate

* fix: refresh pnpm lockfile
2026-04-26 13:53:35 -04:00
Michał Pierzchała caf0e834b8 feat: add Linux desktop automation support via AT-SPI2 (#356)
* feat: add Linux desktop automation support via AT-SPI2 (Phase 1+2)

Add Linux as a first-class platform using AT-SPI2 accessibility framework
via node-gtk for accessibility tree snapshots. This mirrors the macOS
desktop automation approach using accessibility snapshots.

New files:
- src/platforms/linux/atspi-bridge.ts: Core AT-SPI2 bridge using node-gtk
  with lazy loading, recursive tree traversal (max 1500 nodes, depth 12)
- src/platforms/linux/role-map.ts: AT-SPI2 role normalization (~100 roles
  mapped to existing snapshot type conventions)
- src/platforms/linux/snapshot.ts: Snapshot entry point with surface,
  scope, depth, and interactive-only filtering support
- src/platforms/linux/devices.ts: Local device discovery for Linux
- src/platforms/linux/node-gtk.d.ts: Type declarations for node-gtk

Integration:
- Extended Platform type with 'linux', backend union with 'linux-atspi'
- Wired snapshot into dispatch.ts and snapshot-capture.ts
- Added Linux device discovery to dispatch-resolve.ts
- Added stub interactor (input actions deferred to Phase 3)
- Added 'linux' to CLI --platform flag
- node-gtk added as optional dependency (only installs on Linux)

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* refactor: code review cleanup for Linux platform support

- Extract SnapshotBackend type alias to replace repeated string union
  across 5 files (snapshot.ts, snapshot-capture.ts, session-replay-heal.ts,
  interaction.test.ts)
- Remove duplicate scope/interactive/depth filtering from
  linux/snapshot.ts — let the existing buildSnapshotState pipeline handle
  it, same as Android
- Extract isDesktopBackend() helper in snapshot-capture.ts to consolidate
  the "skip mobile semantics" pattern for macos-helper and linux-atspi
- Collapse 17 repetitive throw statements in Linux interactor stubs
  into a linuxStub() factory function

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* feat: Linux input synthesis, screenshots, and app lifecycle (Phase 3+4)

Add xdotool/ydotool input actions (tap, swipe, scroll, type, fill,
right/middle click, long press, double click), screenshot capture via
grim/scrot, and app lifecycle management (open, close, back, home).
Wire Linux interactors with real implementations and fix device
discovery order so Linux doesn't displace Android in auto-selection.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* refactor: consolidate Linux env detection, simplify input actions

- Extract linux-env.ts with cached display server + input tool detection
  so every action avoids repeated `which` lookups
- Add moveTo/clickButton/sendKey helpers to eliminate repeated
  mousemove boilerplate across 5 mouse actions
- Make scrollLinux respect amount/pixels options instead of hardcoded
  scroll count
- Have backLinux/homeLinux reuse sendKey instead of duplicating tool
  detection

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* feat: add Linux CI smoke test with Xvfb and AT-SPI2

Add GitHub Actions workflow that boots a virtual X11 display (Xvfb),
installs AT-SPI2 accessibility tooling and xdotool, opens
gnome-calculator, takes screenshots, and captures an accessibility
snapshot. Screenshots are uploaded as artifacts for visual verification.

Also adds 'linux' to replay script metadata platforms and a
test:replay:linux script to package.json.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: remove pre-session screenshot from Linux replay test

The replay runner requires an active session before any commands can
run. Move the screenshot after the open command that creates the
session.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: Linux CI — add missing node-gtk build deps and AT-SPI2 env

- Add gobject-introspection, libcairo2-dev, build-essential for
  node-gtk native compilation
- Split AT-SPI2 registry start into its own step so it picks up
  DBUS_SESSION_BUS_ADDRESS from GITHUB_ENV
- Set GTK_A11Y=atspi, GTK_MODULES=gail:atk-bridge, NO_AT_BRIDGE=0
  to ensure GTK apps expose their accessibility tree on headless CI
- Set GSETTINGS_BACKEND=memory to avoid dconf failures
- Add node-gtk verification step to catch build failures early

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: explicitly rebuild node-gtk native module in Linux CI

pnpm install silently skips failed optional dependency builds and
the pnpm cache may not include the native binary. Force a rebuild
after install to ensure the node-gtk .node binding is compiled
against the system GI/cairo headers.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: use node-pre-gyp directly to build node-gtk from source

pnpm rebuild doesn't trigger node-pre-gyp properly for optional deps.
Run node-pre-gyp install --fallback-to-build --update-binary directly
inside the node-gtk package directory to force compilation when no
prebuilt binary exists for the current Node ABI (v127 / Node 22).

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* refactor: replace node-gtk with Python subprocess for AT-SPI2

node-gtk is a native C++ addon that requires compilation against
specific Node ABI versions and GObject Introspection headers. This
proved unreliable on CI (no prebuilt binaries for Node 22 ABI v127,
silent optional dep build failures, pnpm cache staleness).

Replace it with a Python helper script (atspi-dump.py) that uses
PyGObject — the reference GObject Introspection consumer. python3-gi
is trivially installable on any Linux distro with no compilation step.
The Node bridge spawns `python3 atspi-dump.py` and parses JSON output.

- Remove node-gtk from optionalDependencies
- Remove node-gtk.d.ts type stub
- Add atspi-dump.py (~200 lines) doing the same tree traversal
- Rewrite atspi-bridge.ts to use subprocess instead of in-process GI
- Simplify CI workflow: no more native build deps or rebuild steps

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* chore: drop pre-installed packages from Linux CI apt-get

python3-gi, gir1.2-atspi-2.0, at-spi2-core, and dbus-x11 are already
present on Ubuntu GitHub Actions runners.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* feat: surface support for Linux, unit tests, stronger CI assertions

- Allow --surface desktop and --surface frontmost-app on Linux
  (previously only macOS could use --surface)
- Add unit tests for atspi-bridge (9 tests: JSON parsing, role
  normalization, null coercion, error handling, arg forwarding)
- Add unit tests for role-map (3 tests: common roles, case
  normalization, PascalCase fallback)
- Improve .py script path resolution (walk upward instead of
  hardcoded relative paths)
- CI replay test now asserts snapshot contains calculator UI
  nodes via is-exists

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* docs: add cross-platform snapshot traversal contract

Document the shared schema, traversal rules, surface semantics, and
normalized role types that all snapshot backends (Swift, Python,
Android) must conform to. This serves as the single source of truth
when adding or modifying platform backends.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: update lockfile after removing node-gtk optional dependency

pnpm-lock.yaml still referenced node-gtk after it was removed from
package.json, causing pnpm install --frozen-lockfile to fail in CI.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: address review findings in Linux platform code

- atspi-dump.py: use ctx dict for traversal limits instead of globals,
  fix rect filter (width/height <= 0 should use `or`), add surface validation
- input-actions.ts: make sendKey scancodes required to prevent silent
  no-op on ydotool, fix ydotool longPress/swipe to use click --down/--up
- app-lifecycle.ts: use pkill -x (exact match) instead of pkill -f
- linux-env.ts: emit diagnostic warning when falling back to xdotool on Wayland

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix(ci): explicitly install all Linux a11y dependencies

Ubuntu runners may not have at-spi2-core, python3-gi, gir1.2-atspi-2.0,
or dbus-x11 pre-installed. Install them explicitly instead of assuming
they exist. Also make the verify step's tree dump non-fatal since no
apps are running at that point.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: quote multi-word role value in Linux smoke test selector

The selector parser tokenizes on whitespace, so `role=push button`
was split into two tokens causing a parse failure. Use single quotes
inside the selector: `role='push button'`.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: use valid selector keys in Linux smoke test

appName is not a valid selector key. The supported keys are: id, role,
text, label, value, visible, hidden, editable, selected, enabled,
hittable. Simplified to use label and role only.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* chore: cleanup pass — menubar warning, fix contract doc example

- snapshot.ts: emit diagnostic warning when menubar surface is
  requested on Linux (falls back to desktop silently otherwise)
- SNAPSHOT_CONTRACT.md: fix unmapped role example to use a role
  that isn't actually mapped (was "color chooser" which maps to Dialog)

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* chore: add Python bytecache to gitignore

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* feat: harden Linux platform — capability matrix, CI, error handling

P0: Add explicit Linux capability matrix with 3-way platform routing
(Apple/Linux/Android) in isCommandSupportedOnDevice. Linux now correctly
blocks unsupported commands (clipboard, rotate, scrollIntoView, etc.)
at capability level rather than throwing at runtime. Includes tests.

P0: Expand Linux CI to run typecheck + unit tests before smoke tests.
Add AT-SPI2 registry health probe with fail-fast on missing registry.

P1: Harden atspi-dump.py — arg parsing now produces JSON errors on bad
int values, and a top-level catch wraps unexpected exceptions in JSON.

P1: Add 10s per-action timeout to xdotool/ydotool input commands to
prevent indefinite hangs.

P1: Tighten smoke test selectors to calculator-specific signals
(digit labels) instead of generic role='push button'.

P2: Document Linux surface mapping, supported commands, and known
limitations in SNAPSHOT_CONTRACT.md.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: apply depth/interactive filtering to Linux snapshots

Linux snapshots were bypassing snapshotInteractiveOnly and snapshotDepth
filtering that macOS-helper gets via shapeDesktopSurfaceSnapshot. Route
Linux through the same function so snapshot -i and --depth flags work.

Renamed shapeMacOsSurfaceSnapshot → shapeDesktopSurfaceSnapshot since
it's now shared between macOS and Linux desktop backends.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* fix: address review findings — error reporting, Wayland, timeout

- app-lifecycle.ts: emit diagnostic on fire-and-forget app launch
  failure instead of silently swallowing errors
- linux-env.ts: make xdotool on Wayland a hard error instead of
  a broken fallback (xdotool doesn't work on Wayland)
- atspi-bridge.ts: increase Python subprocess timeout from 15s to
  30s for safety on slow/loaded systems with large a11y trees

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* feat: appName/windowTitle selectors, clipboard, input-action tests

Selectors:
- Add appname and windowtitle as selector keys for desktop platforms.
  Both macOS and Linux snapshots already populate these fields — now
  they're usable in selector expressions (e.g., "label=OK appname=Calc").
  Keys are case-insensitive.

Clipboard:
- Implement readLinuxClipboard/writeLinuxClipboard using xclip/xsel
  (X11) or wl-copy/wl-paste (Wayland) with descriptive TOOL_MISSING
  errors. Enable clipboard in Linux capability matrix. 7 unit tests.

Input action tests:
- Add 18 unit tests covering xdotool and ydotool code paths: press,
  right/middle click, double click, sendKey, type, scroll, swipe,
  focus, fill. Tests mock runCmd and verify correct tool + args.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

* chore: cache tool detection for screenshot/clipboard, extract get_app_info helper

Avoid repeated `which` calls on every screenshot/clipboard operation by
caching the resolved tool on first use, matching the input-action pattern.
Extract duplicated app_name/pid retrieval in atspi-dump.py into get_app_info.

https://claude.ai/code/session_01H9hrmueNF5pcBM8JeX81mT

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 13:14:04 +02:00
Michał Pierzchała d1d2f3852a refactor: migrate leaf modules from test-only DI to vitest mocking (#289)
* refactor: migrate leaf modules from test-only DI to vitest mocking

Drop optional DI parameters from 6 production modules that existed
solely as test seams. Replace with vi.mock() in new vitest test files.

Production files simplified:
- recording-telemetry.ts: remove writeTelemetry?
- find.ts: remove dispatch?, use dispatchCommand directly
- install-source.ts: remove entire deps? object (7 params)
- session-runtime-command.ts: remove clearRuntimeHints?
- android-system-dialog.ts: remove 4 DI params, simplify internals
- record-trace-recording.ts: remove writeRecordingTelemetry from deps

Closes #281

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore DI seams broken by premature removal, fix test script

- request-router.ts: remove 4 dead DI params (snapshotAndroidUi,
  reopenAndroidApp, readAndroidAppState, execCommand) from
  RequestRouterDeps since recoverAndroidBlockingSystemDialog no
  longer accepts them
- session-runtime-command.ts: restore clearRuntimeHints? param that
  handleSessionCommands threads through — removing it broke callers
  that override the seam
- session.ts: restore clearRuntimeHints pass-through to runtime branch
- session.test.ts: restore the end-to-end runtime clear DI test
- package.json: include vitest in the default `test` script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rename *.vitest.test.ts → *.vitest.ts to avoid node:test collision

Node's default test discovery matches *.test.ts, which picked up vitest
files and crashed. Rename to *.vitest.ts so the two runners have disjoint
file sets without needing find-based exclusions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use vi.mocked().mock.calls instead of module-level mutable state

Replace manual clearCalls array with direct assertion on the mock,
consistent with the pattern used in request-router-android-modal.vitest.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:01:46 +01:00
Michał Pierzchała 3ddd3a1ef5 Complete issue #39 boot diagnostics, telemetry, and boot command (#46)
* Complete issue #39 phase diagnostics telemetry and boot command

* Address review findings for boot diagnostics and command gating

* Use agent-device boot preflight in iOS CI workflow

* Run iOS boot preflight via source CLI in CI
2026-02-12 15:00:27 +01:00
Michał Pierzchała fb53d72b63 feat: AX snapshot goes brrrr 2026-01-31 12:52:04 +01:00
Michał Pierzchała 4da4745a6c initial commit 2026-01-30 19:41:00 +01:00