diff --git a/AGENTS.md b/AGENTS.md index 7bdd8dfe8a..c0532f8417 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,315 +1,233 @@ # AGENTS.md -AGENTS.md is this repo's README for coding agents: project context, commands, conventions, non-obvious traps, and PR expectations in one predictable place. Keep it high-signal and living; prefer durable source-of-truth pointers over file inventories that drift. +`agent-device` is a CLI + daemon that automates Apple-platform (iOS/tvOS/macOS), Android, and web +targets for coding agents. A long-lived daemon owns device sessions; commands route through a +registry-derived command surface to per-platform backends. + +This file carries the traps and invariants you cannot infer by reading the code. Everything +situational lives one hop away — load it when the task calls for it. + +| When the task involves | Read | +| --- | --- | +| Domain vocabulary, architecture language, capture-reliability contract | `CONTEXT.md` | +| Accepted architecture decisions | `docs/adr/README.md` (a "read when you touch…" index) | +| Which gates to run, test speed rules, shared fixtures | `docs/agents/testing.md` | +| Adding or changing a CLI flag | `docs/agents/cli-flags.md` | +| Opening a PR, or reviewing one | `docs/agents/pull-requests.md` | +| Running commands against a real device | `docs/agents/device-verification.md` | +| Issues, PRDs, triage labels | `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md` | +| Web automation backend setup/diagnostics | `docs/agents/web-backend.md` | +| Planning device automation commands | `agent-device help workflow`, then topic help (`debugging`, `react-native`, `react-devtools`, `physical-device`, `macos`, `dogfood`) | + +Versioned CLI help is the agent-facing source of truth for command behavior — prefer it over any +prose in this repo, including this file. -## Agent skills - -### Issue tracker - -Issues and PRDs live in GitHub Issues for `callstack/agent-device`; external PRs are not a triage request surface. See `docs/agents/issue-tracker.md`. +## Principles (expensive lessons — each cost an incident) -### Triage labels +- Guarantees erode at path boundaries. Any new dispatch path or fast path classifies its cells in + `src/contracts/interaction-guarantees.ts` first; the typechecker forces completeness, you supply + honesty. ADR 0011. +- A registry claim is not a semantic check: never mark a cell `runner` without reading whether the + Swift code implements the guarantee's *definition*, not just a similar-sounding behavior. +- Delegation-on-error is not success-path parity. A fast path that falls back on failure can still + succeed on a candidate the shared rules would refuse. +- Do not measure before confirming the code path can fire. An A/B whose B-arm cannot execute returns + two green runs masquerading as evidence. +- Typed signals over message sniffing: key on structured details (`details.timeoutMs`, reason codes), + never on error text. Remaining sniffs are owned debt with in-code rationale — do not copy them. +- Snapshot output is the token budget. Never add per-node bytes to the tree; response-level metadata + rides once per response. +- Warnings compose, never clobber. Append through the shared response builder; two clobber bugs + shipped before this rule. +- Unreleased API surface dies free. Before treating a field as wire-compat, check + `git tag --contains `; if it never shipped, delete it now. +- Push only behind `&&`-chained gates: `format:check && typecheck && lint && vitest && git push`. A + push that can run after a failed gate eventually will. + +## Derived registries — read the declaration site, not prose + +Command identity, routing, capability, and request-policy traits are *derived* artifacts. Inspect the +declaration site rather than any map someone wrote down: + +- one `CommandDescriptor` per command: `src/core/command-descriptor/registry.ts` (catalog, + capabilities, MCP/CLI projection, batch policy, timeout policy — ADR 0008) +- daemon route ownership + request-policy traits: `src/daemon/daemon-command-registry.ts` + (parity-tested) +- interaction dispatch paths × guarantees: `src/contracts/interaction-guarantees.ts` (ADR 0011) +- command names: `src/command-catalog.ts` — never re-create command string sets in handlers +- capabilities: `src/core/capabilities.ts` is the only home for command/device support checks -Follow the issue label workflow in `docs/agents/triage-labels.md`, including `ready-for-agent`. +`src/daemon.ts` stays a thin router and `src/daemon/request-router.ts` orchestration-only; command +logic belongs in handlers. New daemon handler-family commands update the daemon command registry. -### Domain docs +Shared selector parsing/matching/resolution lives in `src/selectors`; request cancellation/progress +primitives in `src/request`; cross-layer platform and command data contracts in `src/contracts`. CLI +grammar owns flag declarations under `src/commands/cli-grammar`; cross-surface CLI schema composition +lives in `src/cli-schema`. -Single-context repo. Read `CONTEXT.md` for domain language and testing/architecture vocabulary, and `docs/adr/` for accepted architecture decisions. See `docs/agents/domain.md`. +## Enforcement gates (a failing gate located your incomplete change) -## First 60 Seconds -- Classify task type: - - Info-only (triage/review/questions/docs guidance): no code edits and no test runs unless explicitly requested. - - Code change: make minimal scoped edits and run only required checks from **Testing Matrix**. -- State assumptions explicitly. If uncertain, ask. -- Read required context, not the whole repo: - - tooling/build/linting: `package.json` and `tsconfig*.json` - - architecture, routing, command contracts, platform boundaries, diagnostics, or review: relevant `docs/adr/` - - durable naming/testing vocabulary: `CONTEXT.md` -- Start with at most 3 files: the owning module, one shared helper, and one downstream caller/adapter if needed. Use `rg` before opening large files. -- Define verifiable success criteria before editing. -- Decide docs/skills impact up front. +Invariants here are self-declaring gates. The correct response to a failure is to classify or cover +the new thing — never to suppress or allowlist it. -## Principles (expensive lessons — each cost an incident) -- Guarantees erode at path boundaries. Any new dispatch path or fast path classifies its cells in `src/contracts/interaction-guarantees.ts` first; the typechecker forces completeness, you supply honesty. ADR 0011. -- A registry claim is not a semantic check: never mark a cell `runner` without reading whether the Swift code implements the guarantee's *definition*, not just a similar-sounding behavior. -- Delegation-on-error is not success-path parity. A fast path that falls back on failure can still succeed on a candidate the shared rules would refuse. -- Do not measure before confirming the code path can fire. An A/B whose B-arm cannot execute returns two green runs masquerading as evidence. -- Typed signals over message sniffing: key on structured details (`details.timeoutMs`, reason codes), never on error text. Remaining sniffs are owned debt with in-code rationale — do not copy the pattern. -- Snapshot output is the token budget. Never add per-node bytes to the tree; response-level metadata rides once per response. -- Warnings compose, never clobber. Append through the shared response builder; two clobber bugs shipped before this rule. -- Unreleased API surface dies free. Before treating a field as wire-compat, check `git tag --contains `; if it never shipped, delete it now. -- Push only behind `&&`-chained gates. `format:check && typecheck && lint && vitest && git push` — a push that can run after a failed gate eventually will. - -## Scope & Changes -- Keep changes scoped to one command family or module group unless the task explicitly crosses boundaries. If scope expands, stop and confirm. -- Preserve daemon session semantics and platform behavior. -- Do not inspect both iOS and Android paths unless the task is explicitly cross-platform. -- Ship the minimum code that solves the problem: no speculative features, no single-use abstractions, and no unrelated cleanup. -- Match existing style. Remove imports/variables your change made unused. -- Test through public interfaces when possible. Do not add unrelated exports just to make tests easier. -- Unit tests never wait real time: inject the budget, derive the cadence from it, or assert the budget is wired — the slow-test ratchet (`scripts/vitest-slow-test-reporter.ts`) fails tests past 2x budget; speed rules and conversion patterns in `docs/agents/testing.md`. -- Prefer type-level checks when TypeScript can enforce a contract or invalid shape. -- Use `unknown` only at trust boundaries: parsed JSON, daemon/runtime payloads, catch values, generic I/O, or parser callbacks. Once a value is validated or its producer has a known contract, narrow to a domain type or focused parser/helper instead of carrying `unknown` through internal helper and formatter signatures. -- Keep modules small for agent context safety. The unit is not lines, it is questions: a file should answer one question, so `rg` -> read-whole-file stays one cheap bounded read. - - numeric tripwires: target <= 300 LOC per implementation file; past 500, extract before adding behavior; past 1,000 is architecture debt unless it is generated data or a fixture snapshot. There is no exemption for tests (see below). - - name files by the domain concept they answer (`runner-cache.ts`, `interaction-touch-response.ts`), not by layer leftovers (`utils2.ts`, `common.ts` accretion). - - colocate machine-readable claims with the code they describe: coverage manifests beside contract tests, registry cells beside enforcement pointers, decision comments at the decision site — agents navigate by claims, not by directory listings. - - test files mirror source topology 1:1: when a source module splits, split its test file the same way in the same PR. A 3,000-line family test aggregation makes every fixture lookup a whole-file read; the worst offenders (`interaction.test.ts`, platform `index.test.ts`) predate this rule and shrink opportunistically — do not add to them. - - shared fixtures live as named exports in a sibling fixtures module (see `test/integration/interaction-contract/fixtures.ts`), never as inline literals repeated per test. - - long guidance/data tables live behind focused modules instead of sharing a file with parser/runtime logic. - - barrels only at package boundaries; internal barrels add a navigation hop per read. Legacy internal barrels are gated for removal (CONTEXT.md). - - prefer deep modules over mechanical splits: extract when it improves locality for a concept callers already need, not just to reduce line count. -- Before finalizing a code change, do one tightening pass over touched and directly adjacent areas: drop obsolete code, redundant tests, stale helpers/fixtures, and needless duplication made unnecessary by the change. -- Prefer existing helpers. Add a helper only when it reduces real repetition or clarifies domain behavior. -- Prefer composition at platform boundaries: public aliases normalize into shared primitives, and providers contribute transport/device bindings instead of cloning interaction runtimes. -- When adding new guidance, examples, schemas, or command metadata, decide whether it belongs in the command surface, CLI grammar, CLI help, MCP projection, or daemon runtime before editing. -- Prefer updating existing domain vocabulary in `CONTEXT.md` when naming a new durable module concept. Do not coin parallel names in docs, tests, and code. - -## Routing & command identity (read the registries, not this file) -Command identity, routing, capability, and request-policy traits are *derived* artifacts — inspect their declaration sites instead of prose maps: -- one `CommandDescriptor` per command: `src/core/command-descriptor/registry.ts` (catalog, capabilities, MCP/CLI projection, batch policy, timeout policy — ADR 0008) -- daemon route ownership + request-policy traits: `src/daemon/daemon-command-registry.ts` (parity-tested) -- interaction dispatch paths × guarantees: `src/contracts/interaction-guarantees.ts` (ADR 0011) -- command names: `src/command-catalog.ts`; never re-create command string sets in handlers -Keep `src/daemon.ts` a thin router and `src/daemon/request-router.ts` orchestration-only. New daemon handler-family commands update the daemon command registry; its tests guard the traits. - -Shared selector parsing, matching, resolution, and evaluation live in `src/selectors`; request -cancellation/progress primitives live in `src/request`; cross-layer platform and command data -contracts live in `src/contracts`. CLI grammar owns flag declarations under -`src/commands/cli-grammar`, while cross-surface CLI schema composition lives in `src/cli-schema`. - -## Toolchain Snapshot -- Package manager: `pnpm` only. Do not add or restore `package-lock.json`. -- Daemon state: packaged installs use `~/.agent-device`; source checkouts use worktree-scoped dirs under `~/.agent-device/dev/-`. Use `pnpm daemon:state-dir` to inspect it, `--state-dir`/`AGENT_DEVICE_STATE_DIR` to override it, and `pnpm clean:daemon --prune-dev` to prune stale dev dirs. Daemons are isolated by worktree, but devices are not; target different devices/simulators for concurrent worktrees. -- Runtime baseline is Node >= 22. Prefer built-in Node APIs such as global `fetch`, Web Streams, and `AbortSignal.timeout` over compatibility wrappers unless the surrounding code needs a lower-level transport. -- Lint/format stack is OXC: - - config: `.oxlintrc.json`, `.oxfmtrc.json` -- TypeScript is strict enough to surface dead code early: `strict`, `isolatedModules`, `noUnusedLocals`, and `noUnusedParameters` are enabled. -- The repo emits with `tsdown` (Rolldown) and typechecks with TypeScript 7 via `tsc`; `typescript` is a direct dev dependency. Declaration generation is configured to use the TypeScript 7 native executable; if declaration generation fails, inspect `tsconfig.lib.json` and `tsdown.config.ts` first. -- Dev-loop staleness has three layers; after editing runtime or runner code: `pnpm build` (dist), restart the daemon (it does not self-reload), and remember `shutdown` deliberately HANDS OFF a healthy simulator runner — the adopted runner keeps serving the old Swift binary until you kill its process or the source fingerprint changes. Verifying "my change did nothing" against an adopted runner is a classic false negative. -- `tsconfig.lib.json` needs an explicit `rootDir: "./src"` for declaration layout. -- Use the aggregate scripts in `package.json` when possible; they encode the expected validation bundles better than ad hoc command lists. - -## Exploration & Token Use -- Prefer these first-pass commands over broad reads: - - `rg -n "" src test` - - `rg --files src/daemon/handlers src/platforms/apple src/platforms/ios src/platforms/android` - - `git diff -- ` for active-branch context - - read `.oxlintrc.json` before treating lint output as source-level bugs -- For files over 500 LOC, search for the relevant type/function/section first, then read a bounded range. -- Do not run integration tests by default. -- Keep long help prose in `src/cli/parser/cli-help.ts`, flag definitions in - `src/commands/cli-grammar/flag-definitions-*.ts`, and command-specific usage/flag metadata with - the command family metadata that owns the command. -- If build/type errors mention declaration generation, inspect `tsconfig.lib.json` before reading platform code. -- If lint failures appear after toolchain edits, check whether the rule is from `eslint/*`, `typescript/*`, `import/*`, or `node/*` in `.oxlintrc.json` before assuming source bugs. - -## Apple Runner Seams -- The OS-agnostic Apple XCTest runner lives under `src/platforms/apple/core/runner/`; use `rg --files src/platforms/apple/core/runner` and read the seam you are changing before editing. -- Keep dependency direction clean: transport stays below client/session behavior, shared command/error contracts stay in the runner contract module, and xctestrun preparation/build/cache logic stays isolated from request execution. -- If changing runner connect errors, retry policy, or command typing, start in `src/platforms/apple/core/runner/runner-contract.ts` before touching client/transport files. - -## Adding a New CLI Flag - -A new snapshot/command flag touches only the layers that need to understand it. Follow this checklist in order: - -1. `src/contracts/cli-flags.ts`: add to `CliFlags`; add the definition to the matching - `src/commands/cli-grammar/flag-definitions-*.ts` owner and the relevant group in - `flag-groups.ts` (for example, `SNAPSHOT_FLAGS`). Then update the command family metadata/schema - that exposes the flag; find the owner with - `rg -n "|supportedFlags|allowedFlags" src/commands src/cli-schema src/cli/parser`. For - schema-only CLI commands (`cdp`, `auth`, `connect`, `proxy`, `react-devtools`, `web`), the owner - is `src/cli-schema/command-overrides.ts` (`SCHEMA_ONLY_CLI_COMMAND_SCHEMAS`). -2. `src/commands/cli-grammar/*`: read the CLI flag into command input when the CLI accepts it. -3. `src/commands/command-projection.ts` and command-family projection helpers: write the input into the daemon request only if the flag affects daemon execution. -4. `src/commands/*-command-contracts.ts`: add or update the command input schema only if the option should be available through Node.js or MCP as structured input. -5. `src/client/client-types.ts`: update the public typed client option only when the Node.js interface exposes the option. -6. `src/client/client-normalizers.ts`: update daemon flag normalization only when the request still needs a public-to-internal option translation. -7. `src/daemon/context.ts` and `src/core/dispatch-context.ts`: add the field only when it flows into platform dispatch. -8. Handler/platform modules: thread the option only after the command surface, grammar, and projection prove it belongs there. - -9. `scripts/integration-progress-model.ts`: classify the flag (device-observable vs intentionally-outside) — the architecture-progress gate fails CI on unclassified public flags. -10. If the flag changes interaction semantics, revisit the affected cells in `src/contracts/interaction-guarantees.ts` (command scoping via `appliesTo` when the flag exists only on some commands). - -Command-only flags (like `find --first`) that do not flow to the platform layer usually stop at steps 1-3 (plus step 9). - -## Enforcement gates (when one fails, it located your incomplete change) -This repo encodes invariants as self-declaring gates. The correct response to a gate failure is to classify/cover the new thing, never to suppress or allowlist: - public CLI flags must be classified: `scripts/integration-progress-model.ts` -- interaction guarantee matrix completeness + honesty: `src/contracts/__tests__/interaction-guarantees.test.ts` (gap waivers need `trackingIssue`; the pin list changes only in reviewed diffs) -- every enforced/delegated matrix cell needs a contract scenario: `src/contracts/__tests__/interaction-contract-coverage.test.ts` + `test/integration/interaction-contract/` -- interaction responses build only through `buildInteractionResponseData`: the construction-guard test -- every command declares a timeout policy on its descriptor: the timeout-policy completeness test -- TS/Swift rule parity: golden tables under `contracts/fixtures/` consumed by vitest and the gated XCTest -- cross-command apple-leak guard, folder DAG/import lint (including zero value-import cycles and - zero target-spine back-edges), fallow (dead code, duplication, complexity) +- guarantee matrix completeness + honesty: `src/contracts/__tests__/interaction-guarantees.test.ts` + (gap waivers need a `trackingIssue`; the pin list changes only in reviewed diffs) +- every enforced/delegated matrix cell needs a contract scenario: + `src/contracts/__tests__/interaction-contract-coverage.test.ts` + `test/integration/interaction-contract/` +- interaction responses build only through `buildInteractionResponseData` (construction-guard test) +- every command declares a timeout policy on its descriptor (timeout-policy completeness test) +- TS/Swift rule parity: golden tables under `contracts/fixtures/`, consumed by vitest and the gated + XCTest — change the rule only via the table +- cross-command apple-leak guard; folder DAG/import lint (zero value-import cycles, zero target-spine + back-edges); fallow (dead code, duplication, complexity) ## Hard Rules -- Use process helpers from `src/utils/exec.ts` for TypeScript process execution: `runCmd`, `runCmdStreaming`, `runCmdSync`, `runCmdBackground`, and `runCmdDetached`. Do not import raw `spawn`/`spawnSync` outside `src/utils/exec.ts`; add or extend an exec helper instead. Plain `.mjs` packaging fixtures that cannot import TypeScript helpers should keep child-process usage local and prefer `execFile`/`execFileSync` over spawn. -- Use daemon session flow for interactions (`open` before interactions, `close` after). -- Every manual `agent-device open` must have a matching `agent-device close` before the agent finishes, using the same `--session`, `--platform`, `--udid`, and `--state-dir` flags. -- Use `keyboard dismiss` for iOS keyboard dismissal; it may tap safe native controls such as `Done` but must not fall back to system back navigation. + +- Process execution goes through `src/utils/exec.ts` (`runCmd`, `runCmdStreaming`, `runCmdSync`, + `runCmdBackground`, `runCmdDetached`). Do not import raw `spawn`/`spawnSync` elsewhere — extend an + exec helper instead. Plain `.mjs` packaging fixtures that cannot import TS helpers keep + child-process usage local and prefer `execFile`/`execFileSync`. +- Interactions use the daemon session flow: `open` before, `close` after. +- `keyboard dismiss` is the iOS keyboard dismissal path. It may tap safe native controls such as + `Done`, but must not fall back to system back navigation. - Do not remove shared snapshot/session model behavior without full migration. -- Command/device support must come from `src/core/capabilities.ts`. -- Apple-family target changes must keep `src/kernel/device.ts`, `src/core/capabilities.ts`, `src/core/dispatch-resolve.ts`, `src/platforms/apple/core/devices.ts`, and `src/platforms/apple/core/runner/runner-xctestrun.ts` in sync. -- iOS simulator-set scoping is iOS-specific: do not let `iosSimulatorDeviceSet` hide the host macOS desktop target when `--platform macos` or `--target desktop` is requested. -- If Swift runner code changes, run `pnpm build:xcuitest`. -- Use `inferFillText` from `src/daemon/action-utils.ts` and `uniqueStrings` from - `src/kernel/collections.ts`. -- Use `evaluateIsPredicate` from `src/selectors/predicates.ts` for assertion logic. - -## Logs Contract -- Logs backend/source of truth is `src/daemon/app-log.ts`. -- `session.ts` should orchestrate only (start/stop/path/doctor/mark), not duplicate backend logic. -- App logs are distinct from runner/platform output. Keep app/device log capture in `app.log`; Apple runner and `xcodebuild` subprocess output belongs in the session-scoped `runner.log`. -- Preserve external grep/tail workflow in docs/skills. - -## Diagnostics & Errors -- Diagnostics source of truth: `src/utils/diagnostics.ts` - - `withDiagnosticsScope`, `updateDiagnosticsScope`, `emitDiagnostic`, `withDiagnosticTimer`, `flushDiagnosticsToSessionFile` -- Request diagnostics belong in `sessions//requests/.ndjson` once the effective session is resolved. The top-level daemon log is for daemon lifecycle/startup and pre-session failures. -- Session artifact paths are centralized in `src/daemon/session-store.ts`; do not hand-build session log paths in handlers. -- Do not add ad-hoc stderr/file logging where diagnostics helpers apply. -- Normalize user-facing failures via `src/kernel/errors.ts` (`normalizeError`). -- Failure payload contract: `code`, `message`, `hint`, `diagnosticId`, `logPath`, `details`. -- User-facing errors should be short and actionable: say what failed, why when known, and how to recover. Put recovery steps in `hint` when the action is not obvious, for example restart/retry, use plain screenshot when AX state is unavailable, navigate with coordinates, or inspect logs. -- If an interaction unexpectedly takes 5+ seconds, inspect the relevant daemon log before attributing it to the app. Check the session `--state-dir` `daemon.log` or the failure `logPath` for runner restart, stale session recovery, AX failure, transport retry, or command timeout evidence. -- Preserve `hint`, `diagnosticId`, `logPath` when wrapping/rethrowing errors. -- `--debug` is canonical; `--verbose` is backward-compatible alias. -- Keep redaction centralized in diagnostics helpers. - -## Optional Optimizations -- Treat optional optimization calls such as cache/preflight/probe requests as best-effort unless the feature contract says they are required. If an optimization fails, times out, returns non-OK, or returns an unusable shape, prefer falling back to the existing required command path. -- Keep optimization timeouts shorter than the underlying operation timeout. A preflight should not consume the full budget for a later upload or command. - -## React Native Verification -- After changing runtime code exercised through `bin/agent-device.mjs` or the daemon, run `pnpm build` and `pnpm clean:daemon` before manual device verification so snapshots use current `dist` output. -- Before any local Android device verification from source, run `pnpm build`, `pnpm build:android`, and `pnpm clean:daemon`. `build:android` refreshes and verifies both bundled Android helper artifacts for the current package version. -- Android verification must prove the helper path is active before the test run. Capture `snapshot -i --json` and require `androidSnapshot.backend` to be `android-helper` with `helperVersion` equal to `package.json`'s version. A stock UIAutomator fallback is not valid verification unless the fallback itself is the behavior under test. -- For repo-owned `Agent Device Tester` verification, use `examples/test-app/README.md` as the source of truth for simulator, physical-device, Metro/dev-client, and app-surface verification steps. Do not treat an already installed `com.callstack.agentdevicelab` as sufficient unless the README's Metro/dev-build and `snapshot -i` checks prove the expected app surface is running. -- For Android RN/Expo/dev-client apps connected to any local Metro port, `adb reverse tcp: tcp:` is harmless and should be run before opening the app or URL on the emulator/device. -- In sandboxed agent environments, run manual `agent-device` CLI verification that starts the daemon outside the sandbox with escalation. The daemon binds localhost, and sandboxed runs can fail before any product code executes with `listen EPERM: operation not permitted 127.0.0.1` or repeated `Failed to start daemon`/metadata cleanup messages. Do not spend time debugging those as agent-device regressions; rerun the same command with escalation. Unit tests, typecheck, lint, and build can stay sandboxed unless they need platform devices or network/listener access. +- Apple-family target changes keep `src/kernel/device.ts`, `src/core/capabilities.ts`, + `src/core/dispatch-resolve.ts`, `src/platforms/apple/core/devices.ts`, and + `src/platforms/apple/core/runner/runner-xctestrun.ts` in sync. +- iOS simulator-set scoping is iOS-specific: `iosSimulatorDeviceSet` must not hide the host macOS + desktop target when `--platform macos` or `--target desktop` is requested. +- Use `inferFillText` (`src/daemon/action-utils.ts`), `uniqueStrings` (`src/kernel/collections.ts`), + and `evaluateIsPredicate` (`src/selectors/predicates.ts`) rather than reimplementing them. +- Do not update `skills/**/SKILL.md` for command behavior or workflow guidance unless the user asks. + Skills are thin routers to versioned CLI help; they must not carry behavior details. + +## Scope & shape + +- Keep changes to one command family or module group unless the task explicitly crosses boundaries. + If scope expands, stop and confirm. Preserve daemon session semantics and platform behavior. +- Do not inspect both iOS and Android paths unless the task is explicitly cross-platform. +- Prefer composition at platform boundaries: public aliases normalize into shared primitives, and + providers contribute transport/device bindings instead of cloning interaction runtimes. +- Use `unknown` only at trust boundaries — parsed JSON, daemon/runtime payloads, catch values, + generic I/O, parser callbacks. Once validated, narrow to a domain type instead of carrying + `unknown` through internal helper and formatter signatures. +- Before finalizing, do one tightening pass over touched and adjacent areas: drop obsolete code, + redundant tests, stale helpers/fixtures, and duplication the change made unnecessary. +- Name durable module concepts with `CONTEXT.md` vocabulary. Do not coin parallel names across docs, + tests, and code. + +Module size is about agent context safety, and the unit is questions, not lines: a file should answer +one question so `rg` → read-whole-file stays one cheap bounded read. + +- tripwires: target ≤300 LOC per implementation file; past 500, extract before adding behavior; past + 1,000 is architecture debt unless it is generated data or a fixture snapshot. Tests are not exempt. +- name files by the domain concept they answer (`runner-cache.ts`, `interaction-touch-response.ts`), + not by layer leftovers (`utils2.ts`, `common.ts` accretion). +- colocate machine-readable claims with the code they describe — coverage manifests beside contract + tests, registry cells beside enforcement pointers, decision comments at the decision site. Agents + navigate by claims, not directory listings. +- test files mirror source topology 1:1; when a source module splits, split its test file in the same + PR. A 3,000-line family aggregation makes every fixture lookup a whole-file read. + `interaction.test.ts` and platform `index.test.ts` predate this rule and shrink opportunistically — + do not add to them. +- shared fixtures are named exports in a sibling fixtures module (see + `test/integration/interaction-contract/fixtures.ts`), never inline literals repeated per test. +- long guidance/data tables live behind focused modules, not beside parser/runtime logic. +- barrels only at package boundaries. Legacy internal barrels are gated for removal (`CONTEXT.md`). +- extract when it improves locality for a concept callers already need, not to hit a line count. +- `src/daemon/handlers/session.ts` and `src/platforms/apple/core/apps.ts` are already over budget. + Extract the Apple-family/macOS-specific helpers before adding behavior to either. + +## Toolchain gotchas + +- `pnpm` only. Do not add or restore `package-lock.json`. ESLint/Prettier are gone — the lint/format + stack is OXC (`.oxlintrc.json`, `.oxfmtrc.json`). Read `.oxlintrc.json` before treating lint output + as a source-level bug. +- Daemon state: packaged installs use `~/.agent-device`; source checkouts use worktree-scoped dirs + under `~/.agent-device/dev/-`. Inspect with `pnpm daemon:state-dir`, override + with `--state-dir`/`AGENT_DEVICE_STATE_DIR`, prune with `pnpm clean:daemon --prune-dev`. Daemons + are isolated per worktree; **devices are not** — target different devices for concurrent worktrees. +- Node ≥22. Prefer built-ins (`fetch`, Web Streams, `AbortSignal.timeout`) over compatibility + wrappers unless the surrounding code needs a lower-level transport. +- Emit with `tsdown` (Rolldown), typecheck with TypeScript 7 via `tsc`. Declaration generation uses + the TS7 native executable and is stricter than a plain typecheck: if it fails, inspect + `tsconfig.lib.json` (it needs an explicit `rootDir: "./src"`) and `tsdown.config.ts` first, and run + `pnpm check:tooling` for any build-tooling edit. +- Prefer the aggregate `package.json` scripts; they encode the expected validation bundles better + than ad hoc command lists. + +## Apple runner seams + +The OS-agnostic XCTest runner lives under `src/platforms/apple/core/runner/`. Keep dependency +direction clean: transport below client/session behavior, shared command/error contracts in the +runner contract module, xctestrun preparation/build/cache isolated from request execution. For +connect errors, retry policy, or command typing, start in +`src/platforms/apple/core/runner/runner-contract.ts` before touching client/transport files. + +## Diagnostics, errors, logs + +- Diagnostics source of truth: `src/utils/diagnostics.ts` (`withDiagnosticsScope`, + `updateDiagnosticsScope`, `emitDiagnostic`, `withDiagnosticTimer`, `flushDiagnosticsToSessionFile`). + No ad-hoc stderr/file logging where these apply; redaction stays centralized here. +- Request diagnostics belong in `sessions//requests/.ndjson`. The + top-level daemon log is for lifecycle/startup and pre-session failures. Session artifact paths come + from `src/daemon/session-store.ts` — do not hand-build them in handlers. +- Logs backend: `src/daemon/app-log.ts`. `session.ts` orchestrates only (start/stop/path/doctor/mark) + and must not duplicate backend logic. App/device logs stay in `app.log`; Apple runner and + `xcodebuild` subprocess output belongs in the session-scoped `runner.log`. Preserve the external + grep/tail workflow documented in help/skills. +- Normalize user-facing failures via `normalizeError` (`src/kernel/errors.ts`). Payload contract: + `code`, `message`, `hint`, `diagnosticId`, `logPath`, `details`. Preserve `hint`, `diagnosticId`, + and `logPath` when wrapping or rethrowing. Errors say what failed, why when known, and how to + recover — recovery steps go in `hint` when the action is not obvious. +- `--debug` is canonical; `--verbose` is a backward-compatible alias. +- An interaction that unexpectedly takes 5+ seconds is a daemon-log question, not an app question: + check the session `daemon.log` or the failure `logPath` for runner restart, stale session recovery, + AX failure, transport retry, or command timeout evidence. +- Optional optimizations (cache/preflight/probe) are best-effort unless the feature contract says + otherwise: on failure, timeout, non-OK, or unusable shape, fall back to the required command path. + Keep their timeouts shorter than the operation they precede. + +## Selector system -## Known environment traps (do not debug these as regressions) -- First `node` exec right after the dev-signed Apple runner launches can block ~19s at 0% CPU (Gatekeeper re-verification). It poisons back-to-back CLI wall-clock timing; absorb with a throwaway `node -e 0` or measure in-process/daemon-side. -- A leftover session holding the device fails every subsequent command instantly with `DEVICE_IN_USE` naming the owner; the hint's `close --session` guidance is the fix, not daemon debugging. -- Contention flakes: `request-handler-catalog` ("specialized daemon routes...") and the doctor provider scenario time out under host load. Protocol before believing a regression: rerun in isolation AND reproduce on plain `origin/main` under the same load. A changing failure set that passes in isolation is contention, not your change. - -## Manual Device Session Hygiene -- Treat every manually opened `agent-device` session as a resource that must be closed, including exploratory sessions and failed verification attempts. -- For experiments, use a purpose-specific session name and, when practical, an isolated `--state-dir` under `/private/tmp` when you need cleanup isolation beyond the current worktree's default daemon. -- Keep track of each opened session in the working notes. Before final response, close each one with the same flags used to open it. -- If `close` or a later command is blocked by stale daemon metadata, inspect running processes first with `ps -ax | rg "agent-device|xcodebuild test-without-building"`. Stop only exact stale PIDs that belong to the verification run, then run `pnpm clean:daemon`. -- If cleanup cannot be completed, report the remaining session name, state dir, process IDs, and metadata paths as a blocker. - -## Selector System Rules - Interaction commands (`click`, `fill`, `get`, `is`) and `wait` accept selectors and `@ref`. -- Pipeline: **parse -> resolve -> act -> record selectorChain -> re-resolve as a divergence suggestion on replay failure**. -- Keep selector parsing, matching, and resolution in `src/selectors/`. -- Call `buildSelectorChainForNode` after resolving target nodes. -- New element-targeting interactions must support selector + `@ref` and record `selectorChain`, so `collectReplaySelectorCandidates` (`src/daemon/handlers/session-replay-heal.ts`) can surface it as a ranked suggestion in a replay divergence report (`session-replay-divergence.ts`). ADR 0012 retired `--update`'s silent rewrite-on-heal; there is no automated write path to hook into. -- New selector keys remain centralized in `src/selectors/parse.ts`. -- New `is` predicates belong in `evaluateIsPredicate`. -- On macOS, snapshot rects are absolute in window space. Point-based runner interactions must translate through the interaction root frame; do not assume app-origin `(0,0)` coordinates. -- Prefer selector or `@ref` interactions over raw x/y commands in tests and docs, especially on macOS where window position can vary across runs. - -## Shared Test Utilities -- Before writing a new test, inspect `src/__tests__/test-utils/index.ts` and search for existing factories, fixtures, and mocked binaries with `rg -n "export .*make|export .*DEVICE|withMocked" src/__tests__/test-utils`. -- Use the test-utils barrel for imports and prefer named shared fixtures over inlining new `DeviceInfo`, `SessionState`, snapshot, store, or mocked-binary objects. -- Do not duplicate session/store/device helpers when a shared helper already exists; if a helper is missing, add it near the concept it serves and export it through the barrel. - -## Testing Matrix -- For code changes, run `pnpm check:affected --base origin/main --run` by default (`--json` for a machine-readable plan without execution). It delegates affected Vitest selection to `vitest related` and derives the remaining gates from repository sources of truth; GitHub CI stays authoritative. See `docs/agents/testing.md`. -- Docs/skills only: no tests required unless a more specific rule below applies. -- CLI help/guidance changes in `src/cli/parser/cli-help.ts` or `src/cli-schema/`: run - `pnpm exec vitest run src/cli/parser/__tests__ src/cli-schema/command-schema-guards.test.ts`. -- SkillGym prompt/assertion changes: run `pnpm test:skillgym:case `; the script builds local CLI help first. For broad validation, use `pnpm test:skillgym`; append `-- --tag fixture-smoke` or `-- --tag skill-guidance` when validating one suite group. -- Non-TS, no behavior impact: no tests unless requested. -- Keep tests behavioral; do not assert shapes or cases TypeScript already proves. -- Any TS change: `pnpm typecheck` or `pnpm check:quick`. -- Fallow CI failures: reproduce with `pnpm check:fallow --base origin/main` instead of manually estimating complexity/dead-code impact. -- Test-only DI seam CI failures: the workflow enforces this; do not add optional `typeof` DI params in production code. -- Tooling/config change (`package.json`, `tsconfig*.json`, `.oxlintrc.json`, `.oxfmtrc.json`): `pnpm check:tooling`. -- Daemon handler/shared module change: `pnpm check:unit`. -- Platform/device-response change (anything emitting `platform`/`appleOs` on the wire, or shaping a daemon response): also run `pnpm test:integration:provider` and `pnpm test:coverage` — both exercise the `provider-integration` project (incl. the apple-platform-output leak guard); `pnpm check:unit` alone does NOT. Internal `apple` must never reach a command response — project through `publicPlatformString`. -- iOS runner/Swift change: `pnpm build:xcuitest`. -- Cross-platform behavior change: run `pnpm test:integration`. -- Any change in: `src/`, `test/`, `skills/`: `pnpm format`. - -## PR Readiness Checklist -- Static gates first: required checks from **Testing Matrix** pass, `pnpm check:fallow --base origin/main` is clean when code quality/dead-code risk is relevant, CI guards are green, and no conflict markers or unmerged paths remain. -- Do not report a PR as CI-green from a local unit-only run alone: use `pnpm test:unit` for the repo unit bundle, or `vitest run --project unit-core --project subprocess-stub` when invoking Vitest directly. The **Integration Tests** and **Coverage** jobs run the `provider-integration` project, so verify green on the actual PR head across those jobs, not just unit. -- Command-surface changes preserve CLI, Node.js, daemon, MCP, help, docs, and SkillGym coverage where that surface is affected. Do not duplicate command contracts across layers. -- Device-facing behavior is not merge-ready until it has real simulator/emulator/device evidence for the changed path. Fixture-backed tests can prove contracts, but they do not replace a live run that creates or observes the artifact/state the feature claims to handle. -- If live verification is blocked, state the blocker, exact command or device needed, and downgrade the PR to residual risk instead of calling it ready. -- Runtime output must stay agent-friendly: compact defaults, top offenders first for diagnostics/perf, bounded arrays in JSON, artifact paths for large raw data, and progressive lookup for deeper detail. -- Before final response or PR handoff, close every manual `agent-device` session opened during verification and report any cleanup that could not be completed. -- Reviewers should check sibling PR ordering, hidden behavior changes, docs/help impact, and whether the tightening pass removed obsolete code/tests introduced or made unnecessary by the change. - -## PR Review Checklist -- Review against the linked issue, not only the diff. State the issue's motivating behavior and verify the PR fixes that behavior directly. -- Check relevant ADRs before reviewing architecture, routing, command-surface, platform-boundary, diagnostics, or testing-strategy changes. Treat ADR conflicts as review findings unless the PR updates/supersedes the ADR explicitly. -- Read issue dependency notes such as `Blocked by: ...`, linked PRs, and sibling branches before judging correctness. If a PR should be stacked on another branch, call out the base/sequence problem before reviewing details. -- Trace the real production route from command surface through daemon/request routing to the platform backend. Tests that mock away the router or exercise only a helper do not prove the shipped path. -- For each key regression test, identify what deletion, revert, or old implementation would make it fail. If reverting the implementation still passes, the test is vacuous and must be fixed. -- Check for hidden behavior changes separately from intended refactors, especially output shape, warning/error propagation, artifact paths, and fallback/retry tiers. -- Verify that tests cover the issue's motivating failure, not just the new abstraction or shared helper. Prefer before/after evidence when an external reviewer or issue reports a concrete divergence. -- Treat green CI as necessary but insufficient for device-facing or routing-sensitive work. Require live simulator/emulator/device evidence where the changed path depends on platform behavior. - -## Common Mistakes -- Adding command logic to `src/daemon.ts` instead of handlers. -- Adding capability checks outside `src/core/capabilities.ts`. -- Inlining `is` predicate logic in handlers. -- Returning non-normalized user-facing errors. -- Duplicating logs backend logic in handlers instead of `src/daemon/app-log.ts`. -- Growing `src/daemon/handlers/session.ts` or `src/platforms/apple/core/apps.ts` further without extracting Apple-family/macOS-specific helpers first. -- Reintroducing an npm lockfile or assuming ESLint/Prettier still exist in this repo. -- Changing `tsconfig.lib.json`/build tooling without running `pnpm check:tooling`; declaration generation is stricter than a plain typecheck. - -## Docs & Skills -- Versioned CLI help is the agent-facing source of truth. Put workflow guidance/help topics in - `src/cli/parser/cli-help.ts`, shared flag contracts in `src/contracts/cli-flags.ts`, flag - definitions in `src/commands/cli-grammar/`, command-specific schema/help metadata with the owning - command family, and assertions near the focused CLI parser/help tests. -- Keep parser schema and help rendering separate: parser/help rendering lives in `src/cli/parser/`, - while command schema metadata is derived from command metadata, command family declarations, and - the schema-only merge path in `src/cli-schema/command-overrides.ts`. -- Before planning device automation commands, read `agent-device help workflow`; then read topic help such as `debugging`, `react-native`, `react-devtools`, `physical-device`, `macos`, or `dogfood` when relevant. This is required even when local agent skills are unavailable. -- Skills are thin routers. Keep `skills/**/SKILL.md` focused on when to use the skill, version gating, which `agent-device help ` page to read, and a short default loop. Do not duplicate full CLI manuals in skills. -- For behavior/CLI surface changes, update help/metadata, README or `website/docs/**` when user-facing, and a SkillGym case in `test/skillgym/suites/agent-device-smoke-suite.ts` when command-planning guidance changes. -- Do not update `skills/**/SKILL.md` for command behavior or workflow guidance unless the user explicitly asks; skills must route to versioned CLI help instead of carrying behavior details. -- Keep SkillGym cases behavioral and command-planning oriented. Prefer prompts that assert the user-visible contract and expected command family over brittle exact output, but forbid known bad patterns. -- Use `pnpm test:skillgym:case ` for focused SkillGym validation; it runs the environment guard and builds local CLI help before `skillgym run`. -- Run SkillGym broad validation with `pnpm test:skillgym`; append v0.8 filters such as `-- --tag fixture-smoke` for focused suite groups. -- In final summaries, state whether docs/skills were updated; if not, explain why. - -## When Blocked -- If blocked by network/device/auth/permissions, stop and report: - - blocker - - why it blocks completion - - exact next command/action needed to unblock - -## Finding Source Owners -- Do not turn this file into a source tree map. For implementation work, identify owner modules from the durable registries and then follow imports/tests from there. -- Command identity and projection: search command descriptors and command contracts first with `rg -n "|CommandDescriptor|defineCommand" src/core/command-descriptor src/command-catalog.ts src/commands`. -- Daemon routing and policy: start with `src/daemon/daemon-command-registry.ts`, then trace to the named handler/request module with `rg -n "|route|policy" src/daemon`. -- Platform behavior and capabilities: start with `src/core/capabilities.ts` and the relevant platform under `src/platforms/`; use `rg`, not broad directory reads. -- CLI help and command-planning guidance: start with `src/cli/parser/cli-help.ts` and - `src/commands/cli-grammar/`; for command-specific schema, search - `rg -n "helpDescription|summary|supportedFlags|allowedFlags" src/commands src/cli/parser src/cli-schema`, - and check `SCHEMA_ONLY_CLI_COMMAND_SCHEMAS` in `src/cli-schema/command-overrides.ts` for - schema-only CLI commands (`cdp`, `auth`, `connect`, `proxy`, `react-devtools`, `web`). - -## Pull Requests -- Before opening PR: ensure no conflict markers/unmerged paths. -- Commit messages and PR titles should use conventional prefixes such as `feat:`, `fix:`, `chore:`, `perf:`, `refactor:`, `docs:`, `test:`, `build:`, or `ci:` as appropriate. -- Do not use bracketed automation prefixes such as `[codex]` or similar bot tags in commit messages or PR titles. -- Open a ready-for-review PR by default. Use a draft PR only when the user explicitly asks for one or the work is intentionally incomplete. -- PR body must be short, reviewer-oriented, and include: - - `## Summary`: describe the user/API behavior, not the implementation file tour. Lead with what changed for operators, clients, command authors, or platform behavior. Use a compact before/after when it clarifies the workflow or bug fix. For new or changed public APIs, include 1-3 concrete CLI/Node/MCP examples that reviewers can scan. Include `Closes #123` when applicable. - - `## Validation`: summarize meaningful evidence in concise prose or bullets. Prefer scenario names, manual device/browser evidence, changed screenshots, CI status, and notable failures/retries with their outcome. Avoid command accounting for routine local gates; mention an exact command only when it is unusual, manually reproducible evidence, or necessary to explain a residual risk. For docs-only changes, say why runtime validation is not applicable instead of writing a command checklist. -- Call out real tradeoffs, known gaps, or follow-ups explicitly; omit boilerplate when there are none. -- Include touched-file count and note if scope expanded beyond initial command family. - -## Priority Order -- When guidance conflicts, apply in this order: **Hard Rules -> Scope & Changes -> Testing Matrix -> style/preferences**. +- Pipeline: **parse → resolve → act → record selectorChain → re-resolve as a divergence suggestion on + replay failure.** Call `buildSelectorChainForNode` after resolving target nodes. +- New element-targeting interactions must support selector + `@ref` and record `selectorChain` so + `collectReplaySelectorCandidates` (`src/daemon/handlers/session-replay-heal.ts`) can rank it in a + divergence report (`session-replay-divergence.ts`). ADR 0012 retired `--update`'s silent + rewrite-on-heal; there is no automated write path to hook into. +- New selector keys stay centralized in `src/selectors/parse.ts`; new `is` predicates belong in + `evaluateIsPredicate`. +- On macOS, snapshot rects are absolute in window space. Point-based runner interactions translate + through the interaction root frame — do not assume app-origin `(0,0)`. Prefer selector or `@ref` + over raw x/y in tests and docs, especially on macOS where window position varies across runs. + +## Known environment traps (do not debug these as regressions) + +- The first `node` exec right after the dev-signed Apple runner launches can block ~19s at 0% CPU + (Gatekeeper re-verification). It poisons back-to-back CLI wall-clock timing; absorb it with a + throwaway `node -e 0`, or measure in-process/daemon-side. +- A leftover session holding the device fails every subsequent command instantly with + `DEVICE_IN_USE` naming the owner. The hint's `close --session` guidance is the fix, not daemon + debugging. +- Contention flakes: `request-handler-catalog` ("specialized daemon routes...") and the doctor + provider scenario time out under host load. Before believing a regression, rerun in isolation AND + reproduce on plain `origin/main` under the same load. A changing failure set that passes in + isolation is contention, not your change. + +## Docs & skills + +- Before adding guidance, examples, schemas, or command metadata anywhere, decide which layer owns + it: the command surface, CLI grammar, CLI help, MCP projection, or daemon runtime. Picking the + layer after writing is how the same contract ends up duplicated across two of them. + `docs/agents/cli-flags.md` walks the layers for the flag case. +- Decide docs impact with the change, not after. For behavior/CLI-surface changes: update + help/metadata, README or `website/docs/**` when user-facing, and a SkillGym case in + `test/skillgym/suites/agent-device-smoke-suite.ts` when command-planning guidance changes. +- Keep SkillGym cases behavioral and command-planning oriented: assert the user-visible contract and + expected command family, forbid known bad patterns, avoid brittle exact output. +- State in the final summary whether docs/skills were updated, and why not if they weren't. + +When guidance conflicts, Hard Rules win, then scope, then testing, then style. diff --git a/CONTEXT.md b/CONTEXT.md index 8f13ed54c0..0b6009bf9d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,89 +1,217 @@ # Agent Device Domain Context +Durable vocabulary for this repo. Use these names in code, tests, issue titles, and architecture +notes rather than coining parallel ones. You rarely need the whole file — jump to the section your +task touches: + +- [Sessions, targets, devices](#sessions-targets-devices) +- [Command surface & routing](#command-surface--routing) +- [Interaction, refs, and guarantees](#interaction-refs-and-guarantees) +- [Gestures & touch](#gestures--touch) +- [Snapshots & capture](#snapshots--capture) +- [Recording & replay](#recording--replay) +- [Maestro compatibility](#maestro-compatibility) +- [Providers, cloud, and the test harness](#providers-cloud-and-the-test-harness) +- [Architecture](#architecture-perfect-shape-refactor-completed-2026-07) — the two-registry end state +- [Selector capture reliability contract](#selector-capture-reliability-contract) — invariants any capture refactor must preserve +- [Testing principles](#testing-principles) + ## Terms -- Provider-backed integration scenario: device-free integration test that runs the real daemon request path and replaces only external device or host tool execution. -- Provider: request-scoped adapter interface for external device, runner, or host tool execution. -- Cloud WebDriver runtime: package-shaped `ProviderDeviceRuntime` implementation that maps a - cloud-owned Appium/WebDriver session into agent-device lease, inventory, install, interactor, and - release hooks without adding provider-specific branches to daemon routing. Cloud WebDriver - adapters must expose explicit command capabilities because snapshots come from Appium page source - rather than agent-device native iOS runner or Android helper backends. -- CloudArtifact: provider-hosted session output such as video, Appium logs, device logs, automation - logs, or provider dashboard links. Cloud artifacts stay under the `cloudArtifacts` response field - so they do not collide with daemon-managed local/downloadable `artifacts`. -- DaemonArtifactType: optional semantic category supplied by the command or adapter that owns a - daemon-managed downloadable artifact, such as `screenshot`, `screen-recording`, or `trace-log`. - Finalization and inventory code must preserve this value when present, not infer it from - filenames, fields, or MIME types. Missing artifact types must not prevent artifact registration. - The type documents known values while allowing provider or command owners to introduce more - specific strings. -- Provider transcript: exact record of provider calls used when a test must verify platform command translation. -- Scenario transcript: command-level integration flow that describes user-visible behavior through daemon commands. -- In-process provider scenario harness: integration runner that invokes the daemon request handler directly without opening an HTTP listener. -- HTTP contract test: narrow test that verifies JSON-RPC transport, auth, and response finalization over the daemon HTTP boundary. -- Daemon RPC protocol version: integer advertised by daemon/proxy `/health` and checked by remote clients before HTTP JSON-RPC; bump only for breaking transport/request/response compatibility across the remote daemon boundary. +### Sessions, targets, devices + - Interactor: semantic interface between command dispatch and platform behavior. - Platform module: platform-specific implementation behind the Interactor. - Target: selected automation destination, such as mobile, tv, or desktop. - Modality: broad supported device family, such as mobile, tv, or desktop. - Session: daemon-owned state for a selected target and opened app or surface. -- Script recording: opt-in session mode armed before actions so a persisted `.ad` can carry portable - action inputs and recording-time target identity evidence. It is distinct from screen/video recording. -- Recorded input parameterization: explicit fill authoring contract that sends literal text only to the - live interaction while the recorder stores `${VAR}` before any durable recording/event/publication - boundary. The caller owns the uppercase variable name; no selector or field-name heuristic infers - sensitivity. Replay resolves the placeholder immediately before dispatch and preserves the authored - placeholder if that run is recorded again. -- Open-to-destination script: self-contained `.ad` script with exactly one initial `open`, a destination - guard after its last app-state mutation, no `close`, and an app session left active for subsequent work. - Avoid: replay (artifact noun), fragment (reserved for lifecycle-free composition), partial script. -- Destination guard: portable selector-targeted `wait` near the end of an open-to-destination script that - confirms a landmark on the ready destination screen before replay hands the live session to its caller. -- Recording backend: daemon-internal module interface selected per recording target that owns platform recording validation, output path policy, start/stop execution, and record-only cleanup below the daemon recording lifecycle. -- Device lease: logical remote ownership of one selected device for a - tenant/run/client and lease provider, separate from platform helper process - locking. -- Device key: stable provider-scoped device identity used for lease contention, - such as a simulator UDID, physical device id, or provider inventory id. -- Lease provider: remote connection source that routes and owns a device lease, - such as `proxy`, cloud bridge, or `limrun`. -- Runner/process lease: backend helper mutual-exclusion guard for platform - runners or tools; it is not the remote client ownership boundary. +- Device lease: logical remote ownership of one selected device for a tenant/run/client and lease + provider, separate from platform helper process locking. +- Device key: stable provider-scoped device identity used for lease contention, such as a simulator + UDID, physical device id, or provider inventory id. +- Lease provider: remote connection source that routes and owns a device lease, such as `proxy`, + cloud bridge, or `limrun`. +- Runner/process lease: backend helper mutual-exclusion guard for platform runners or tools; it is + not the remote client ownership boundary. - Host process primitive: low-level host PID helpers in `src/utils/host-process.ts` for liveness, start-time/command reads, process listing, process-tree expansion, PID de-duplication, and best-effort signaling. It must not own domain cleanup policy such as browser ownership markers, runner lease reclamation, daemon takeover checks, or app-log PID metadata verification. -- Command surface: catalog of public command identity, interface exposure, adapter policy, and shared command metadata across CLI, Node.js, MCP, and batch entrypoints. -- Daemon command registry: daemon-side source of truth for command route ownership and request-policy traits, including admission exemptions, session locking, selector validation, replay-scoped actions, recording invalidation, Android dialog guards, and request provider device resolution. -- Runner command traits: per-command-type classification for iOS/macOS runner lifecycle behavior, distinct from the public command surface and daemon command registry. The Swift runner traits classify interaction, read-only, and runner-lifecycle axes for XCTest execution; Swift resolves the alert command as read-only only for its `get` action. The TypeScript runner command traits classify daemon-side runner send/recovery policy such as read-only retry routing, readiness probes, and recent-healthy-mutation preflight skips; the TypeScript table is command-type keyed and currently classifies alert as read-only for daemon retry policy. Each side keeps one source of truth keyed by runner command type. -- Coordinate-first resolved element activation: iOS/macOS runner interaction pattern where a selector or text query resolves the semantic `XCUIElement`, then activation uses the element's resolved center coordinate when a frame is available. This keeps target selection semantic while avoiding `XCUIElement.tap()` post-action element re-resolution after normal navigation. tvOS remains focus/remote-driven. -- Interaction dispatch path: one concrete route an interaction command takes to the device (runtime selector/ref resolution, direct iOS selector, native ref via web clickRef, coordinate, maestro non-hittable fallback). Every path classifies every guarantee in the ADR 0011 registry. -- Gesture plan: typed, platform-neutral normalization of one- or two-contact gesture intent into bounded pointer trajectories. Contact topology is separate from motion; two-contact intent remains pan/pinch/rotate/transform even when native injection shares one executor. See ADR 0013. -- Android planned-touch executor: Android-local adapter seam that accepts `AndroidTouchPlan`—the - platform-neutral `GesturePlan` plus Android's stationary long-press plan—and selects the paired + +### Command surface & routing + +- Command surface: catalog of public command identity, interface exposure, adapter policy, and + shared command metadata across CLI, Node.js, MCP, and batch entrypoints. +- Daemon command registry: daemon-side source of truth for command route ownership and + request-policy traits, including admission exemptions, session locking, selector validation, + replay-scoped actions, recording invalidation, Android dialog guards, and request provider device + resolution. +- Runner command traits: per-command-type classification for iOS/macOS runner lifecycle behavior, + distinct from the public command surface and daemon command registry. The Swift runner traits + classify interaction, read-only, and runner-lifecycle axes for XCTest execution; Swift resolves the + alert command as read-only only for its `get` action. The TypeScript runner command traits classify + daemon-side runner send/recovery policy such as read-only retry routing, readiness probes, and + recent-healthy-mutation preflight skips; the TypeScript table is command-type keyed and currently + classifies alert as read-only for daemon retry policy. Each side keeps one source of truth keyed by + runner command type. +- Daemon RPC protocol version: integer advertised by daemon/proxy `/health` and checked by remote + clients before HTTP JSON-RPC; bump only for breaking transport/request/response compatibility + across the remote daemon boundary. + +### Interaction, refs, and guarantees + +- Interaction dispatch path: one concrete route an interaction command takes to the device (runtime + selector/ref resolution, direct iOS selector, native ref via web clickRef, coordinate, maestro + non-hittable fallback). Every path classifies every guarantee in the ADR 0011 registry. +- Coordinate-first resolved element activation: iOS/macOS runner interaction pattern where a selector + or text query resolves the semantic `XCUIElement`, then activation uses the element's resolved + center coordinate when a frame is available. This keeps target selection semantic while avoiding + `XCUIElement.tap()` post-action element re-resolution after normal navigation. tvOS remains + focus/remote-driven. +- Guarantee cell: one (dispatch path, guarantee) entry in `src/contracts/interaction-guarantees.ts`, + classified as runtime/runner/delegated/inapplicable/waived. Completeness is a compile error; + honesty is gate-tested. +- Owned waiver: a `gap:`-prefixed waived cell carrying a `trackingIssue` URL. Waivers are diffable + debt with an owner, never folklore. +- Delegation-on-error: a fast path falling back to the runtime path on semantic failure shapes. It + closes failure-side guarantee cells only — never success-path parity. +- Parity table: golden JSON fixture under `contracts/fixtures/` consumed by both vitest and the + runner's gated Swift tests, so a cross-language rule (e.g. tap-point policy) cannot drift silently. + Change the rule only via the table. +- Coverage manifest: `CONTRACT_COVERAGE` export beside each interaction contract test file claiming + which matrix cells it proves; the coverage gate requires every enforced/delegated cell to be + claimed and rejects overclaims of waived cells. +- Ref frame (ADR 0014): the session's single authorization namespace for mutation `@ref`s, kept + separate from the latest operational observation (`session.snapshot`). It owns a frozen epoch (the + `refsGeneration` the client received), an immutable source tree, a lifecycle state + (`active`/`expired`), and an issuance scope (`all` for a complete snapshot, or the bounded set of + ref bodies a partial publication emitted). Owned solely by `src/daemon/ref-frame.ts`. A complete + snapshot activates an `all` frame; `find`/settled diff/replay divergence activate a bounded partial + frame that supersedes the prior one; internal read captures never activate or reindex it. +- Frame expiry seam (ADR 0014): every mutating leaf calls `expireRefFrame` synchronously, immediately + before the device op that may change element identity (after all pre-action guards), so a + post-dispatch failure still leaves the frame expired — there is no success-only rollback. Ref + resolution binds `@eN` against the frame's source tree, so an Android freshness (or any read-only) + capture cannot retarget an admitted ref by positional coincidence; a fresh capture's coordinates are + adopted only when its node's local identity matches. +- Mutation admission (ADR 0014): a ref mutation is admitted only against an active frame whose epoch + and issuance scope authorize the ref (`admitRefMutation`, order-sensitive reasons + `ref_frame_expired` → `ref_generation_mismatch` → `plain_ref_requires_complete_frame` → + `ref_not_issued`). Rejections carry `details.reason` and name the lifetime failure. A ref-oriented + sequence that performs several mutations must re-observe (snapshot), consume an honestly issued + settled ref in pinned form, or use selectors. Read-only ref consumers stay fail-open with a + staleness warning while the frame retains the ref's evidence. +- Ref generation pin: optional `~s` suffix on an @ref carrying the snapshot generation it was + minted from. Accepted as input everywhere, emitted by no tree output (snapshot token budget), + auto-appended by the MCP layer, stripped and ignored by replay. +- Settled observation: opt-in (`--settle`) post-action payload on press/click/fill/longpress — the + quiet-window stable loop re-captures until the UI settles, and the response carries the diff vs the + pre-action tree (changed lines only, added lines with fresh refs, `refsGeneration` when the settled + tree was stored). Best-effort: never fails the action; `settled: false` plus a hint on never-quiet + content. +- Resolution disclosure (ADR 0012 decision 2): additive `resolution` field on + press/click/fill/longpress responses discloses how the acting path resolved its target — + `runtime`/`unique` or `runtime`/`disambiguated` (with `matchCount`/`winnerDiagnostic`/`tiebreak`/ + up-to-5 `alternatives`) on the daemon tree, `ref`/`exact` for a resolved `@ref` (runtime-ref and + native-ref), `ref`/`label-fallback` when runtime-ref recovered a stale `@ref` via its recorded + trailing label, or `direct-ios`/`not-observed` on the XCTest fast path; absent entirely on the + coordinate path and on dispatches whose runner actually executed the maestro non-hittable + coordinate fallback (permission alone keeps the direct path's `not-observed`). Pre-action + diagnostics only: `winnerDiagnostic`/`alternatives` entries carry an opaque, non-`@` + `diagnosticRef` that is never ref-issued, never MCP-pinned, and cannot be reused as an `@ref` + target — a fresh snapshot/find is required before acting on an alternative. + +### Gestures & touch + +- Gesture plan: typed, platform-neutral normalization of one- or two-contact gesture intent into + bounded pointer trajectories. Contact topology is separate from motion; two-contact intent remains + pan/pinch/rotate/transform even when native injection shares one executor. See ADR 0013. +- Android planned-touch executor: Android-local adapter seam that accepts `AndroidTouchPlan` — the + platform-neutral `GesturePlan` plus Android's stationary long-press plan — and selects the paired provider-native touch/viewport adapter or bundled instrumentation-helper adapter. Scroll and long-press retain their command semantics and only share physical touch execution through this seam. Helper long-press executes its absolute stationary path without a viewport probe; provider long-press receives its paired provider-owned viewport. See ADR 0013. -- Multi-touch geometry: the internal initial span and angle plus centroid translation, scale, and rotation used to build both contact trajectories. Geometry is viewport-aware and fails early when the requested motion cannot fit; it is not a public tuning surface. -- Maestro program: source-preserving typed representation of supported Maestro YAML. It is interpreted directly through the compatibility runtime port and never lowered through generic replay action strings. See ADR 0015. -- Maestro observation generation: explicit compatibility-engine state identifying evidence captured since the most recent mutation. Queries may share semantic evidence within one generation; every mutation attempt invalidates it before dispatch. Interaction geometry is action-local: unique exact iOS selectors resolve and tap atomically in XCTest, while coordinate dispatch uses a fresh target snapshot. Rectangles are never shared across command boundaries. -- Guarantee cell: one (dispatch path, guarantee) entry in `src/contracts/interaction-guarantees.ts`, classified as runtime/runner/delegated/inapplicable/waived. Completeness is a compile error; honesty is gate-tested. -- Owned waiver: a `gap:`-prefixed waived cell carrying a `trackingIssue` URL. Waivers are diffable debt with an owner, never folklore. -- Parity table: golden JSON fixture under `contracts/fixtures/` consumed by both vitest and the runner's gated Swift tests, so a cross-language rule (e.g. tap-point policy) cannot drift silently. Change the rule only via the table. -- Coverage manifest: `CONTRACT_COVERAGE` export beside each interaction contract test file claiming which matrix cells it proves; the coverage gate requires every enforced/delegated cell to be claimed and rejects overclaims of waived cells. -- Delegation-on-error: a fast path falling back to the runtime path on semantic failure shapes. It closes failure-side guarantee cells only — never success-path parity. -- Ref generation pin: optional `~s` suffix on an @ref carrying the snapshot generation it was minted from. Accepted as input everywhere, emitted by no tree output (snapshot token budget), auto-appended by the MCP layer, stripped and ignored by replay. -- Ref frame (ADR 0014): the session's single authorization namespace for mutation `@ref`s, kept separate from the latest operational observation (`session.snapshot`). It owns a frozen epoch (the `refsGeneration` the client received), an immutable source tree, a lifecycle state (`active`/`expired`), and an issuance scope (`all` for a complete snapshot, or the bounded set of ref bodies a partial publication emitted). Owned solely by `src/daemon/ref-frame.ts`. A complete snapshot activates an `all` frame; `find`/settled diff/replay divergence activate a bounded partial frame that supersedes the prior one; internal read captures never activate or reindex it. -- Frame expiry seam (ADR 0014): every mutating leaf calls `expireRefFrame` synchronously, immediately before the device op that may change element identity (after all pre-action guards), so a post-dispatch failure still leaves the frame expired — there is no success-only rollback. Ref resolution binds `@eN` against the frame's source tree, so an Android freshness (or any read-only) capture cannot retarget an admitted ref by positional coincidence; a fresh capture's coordinates are adopted only when its node's local identity matches. -- Mutation admission (ADR 0014): a ref mutation is admitted only against an active frame whose epoch and issuance scope authorize the ref (`admitRefMutation`, order-sensitive reasons `ref_frame_expired` → `ref_generation_mismatch` → `plain_ref_requires_complete_frame` → `ref_not_issued`). Rejections carry `details.reason` and name the lifetime failure. A ref-oriented sequence that performs several mutations must re-observe (snapshot), consume an honestly issued settled ref in pinned form, or use selectors. Read-only ref consumers stay fail-open with a staleness warning while the frame retains the ref's evidence. -- Settled observation: opt-in (`--settle`) post-action payload on press/click/fill/longpress — the quiet-window stable loop re-captures until the UI settles, and the response carries the diff vs the pre-action tree (changed lines only, added lines with fresh refs, `refsGeneration` when the settled tree was stored). Best-effort: never fails the action; `settled: false` plus a hint on never-quiet content. -- Snapshot capture plan: per-strategy ordered chain of iOS snapshot capture backends (recursive tree, query sweep, private AX) run by one plan runner under a shared wall-clock budget; recovery ordering is declared data, never a per-call-site branch. -- Snapshot quality verdict: structured outcome (state, backend, reason code, effective depth, collapsed leaves) computed once by the plan runner and shipped with every planned snapshot payload; the daemon and CLI render it instead of re-deriving degradation from node shapes. -- iOS WebView semantic presentation: the interactive snapshot projection that recognizes XCTest's typed `WebView` root and WebKit's `Other -> StaticText` wrapper pairs. It keeps raw diagnostics unchanged, presents ordinary wrapper text as `StaticText`, and presents wrappers carrying WebKit's numeric HTML heading level as `Heading`. -- AX-unavailable target invalidation: iOS/macOS runner behavior where a root accessibility snapshot failure such as `kAXErrorIllegalArgument` marks the cached `XCUIApplication` target handle suspect. The runner fails closed for degraded interactive snapshots, clears the cached target, and lets the next command reacquire the app through normal activation. -- Resolution disclosure (ADR 0012 decision 2): additive `resolution` field on press/click/fill/longpress responses discloses how the acting path resolved its target — `runtime`/`unique` or `runtime`/`disambiguated` (with `matchCount`/`winnerDiagnostic`/`tiebreak`/up-to-5 `alternatives`) on the daemon tree, `ref`/`exact` for a resolved `@ref` (runtime-ref and native-ref), `ref`/`label-fallback` when runtime-ref recovered a stale `@ref` via its recorded trailing label, or `direct-ios`/`not-observed` on the XCTest fast path; absent entirely on the coordinate path and on dispatches whose runner actually executed the maestro non-hittable coordinate fallback (permission alone keeps the direct path's `not-observed`). Pre-action diagnostics only: `winnerDiagnostic`/`alternatives` entries carry an opaque, non-`@` `diagnosticRef` that is never ref-issued, never MCP-pinned, and cannot be reused as an `@ref` target — a fresh snapshot/find is required before acting on an alternative. +- Multi-touch geometry: the internal initial span and angle plus centroid translation, scale, and + rotation used to build both contact trajectories. Geometry is viewport-aware and fails early when + the requested motion cannot fit; it is not a public tuning surface. + +### Snapshots & capture + +- Snapshot capture plan: per-strategy ordered chain of iOS snapshot capture backends (recursive tree, + query sweep, private AX) run by one plan runner under a shared wall-clock budget; recovery ordering + is declared data, never a per-call-site branch. +- Snapshot quality verdict: structured outcome (state, backend, reason code, effective depth, + collapsed leaves) computed once by the plan runner and shipped with every planned snapshot payload; + the daemon and CLI render it instead of re-deriving degradation from node shapes. +- iOS WebView semantic presentation: the interactive snapshot projection that recognizes XCTest's + typed `WebView` root and WebKit's `Other -> StaticText` wrapper pairs. It keeps raw diagnostics + unchanged, presents ordinary wrapper text as `StaticText`, and presents wrappers carrying WebKit's + numeric HTML heading level as `Heading`. +- AX-unavailable target invalidation: iOS/macOS runner behavior where a root accessibility snapshot + failure such as `kAXErrorIllegalArgument` marks the cached `XCUIApplication` target handle suspect. + The runner fails closed for degraded interactive snapshots, clears the cached target, and lets the + next command reacquire the app through normal activation. + +### Recording & replay + +- Script recording: opt-in session mode armed before actions so a persisted `.ad` can carry portable + action inputs and recording-time target identity evidence. It is distinct from screen/video + recording. +- Recorded input parameterization: explicit fill authoring contract that sends literal text only to + the live interaction while the recorder stores `${VAR}` before any durable + recording/event/publication boundary. The caller owns the uppercase variable name; no selector or + field-name heuristic infers sensitivity. Replay resolves the placeholder immediately before dispatch + and preserves the authored placeholder if that run is recorded again. +- Open-to-destination script: self-contained `.ad` script with exactly one initial `open`, a + destination guard after its last app-state mutation, no `close`, and an app session left active for + subsequent work. Avoid: replay (artifact noun), fragment (reserved for lifecycle-free composition), + partial script. +- Destination guard: portable selector-targeted `wait` near the end of an open-to-destination script + that confirms a landmark on the ready destination screen before replay hands the live session to + its caller. +- Recording backend: daemon-internal module interface selected per recording target that owns + platform recording validation, output path policy, start/stop execution, and record-only cleanup + below the daemon recording lifecycle. + +### Maestro compatibility + +- Maestro program: source-preserving typed representation of supported Maestro YAML. It is + interpreted directly through the compatibility runtime port and never lowered through generic + replay action strings. See ADR 0015. +- Maestro observation generation: explicit compatibility-engine state identifying evidence captured + since the most recent mutation. Queries may share semantic evidence within one generation; every + mutation attempt invalidates it before dispatch. Interaction geometry is action-local: unique exact + iOS selectors resolve and tap atomically in XCTest, while coordinate dispatch uses a fresh target + snapshot. Rectangles are never shared across command boundaries. + +### Providers, cloud, and the test harness + +- Provider: request-scoped adapter interface for external device, runner, or host tool execution. +- Provider-backed integration scenario: device-free integration test that runs the real daemon + request path and replaces only external device or host tool execution. +- Cloud WebDriver runtime: package-shaped `ProviderDeviceRuntime` implementation that maps a + cloud-owned Appium/WebDriver session into agent-device lease, inventory, install, interactor, and + release hooks without adding provider-specific branches to daemon routing. Cloud WebDriver adapters + must expose explicit command capabilities because snapshots come from Appium page source rather + than agent-device native iOS runner or Android helper backends. +- CloudArtifact: provider-hosted session output such as video, Appium logs, device logs, automation + logs, or provider dashboard links. Cloud artifacts stay under the `cloudArtifacts` response field + so they do not collide with daemon-managed local/downloadable `artifacts`. +- DaemonArtifactType: optional semantic category supplied by the command or adapter that owns a + daemon-managed downloadable artifact, such as `screenshot`, `screen-recording`, or `trace-log`. + Finalization and inventory code must preserve this value when present, not infer it from filenames, + fields, or MIME types. Missing artifact types must not prevent artifact registration. The type + documents known values while allowing provider or command owners to introduce more specific + strings. +- Provider transcript: exact record of provider calls used when a test must verify platform command + translation. +- Scenario transcript: command-level integration flow that describes user-visible behavior through + daemon commands. +- In-process provider scenario harness: integration runner that invokes the daemon request handler + directly without opening an HTTP listener. +- HTTP contract test: narrow test that verifies JSON-RPC transport, auth, and response finalization + over the daemon HTTP boundary. ## Architecture (perfect-shape refactor, completed 2026-07) @@ -102,36 +230,33 @@ The perfect-shape refactor is complete and merged. Its end-state: traits, and platform dispatch command set are _derived_ by parity-tested projection. Command families still own surface metadata/CLI schema in `src/commands/**`, but descriptor/catalog coherence guards prevent surface names from drifting; system command facets now project their - simple Node client command methods. Closed public Node-client result contracts are narrowed - through `CommandResultMap`; action/backend-dependent methods remain explicitly broad until their - public response projections are reconciled. See - [Node client result types](docs/node-client-result-types.md). One - `PlatformPlugin` per platform family (`src/core/platform-plugin/`) stops core/daemon from branching - on platform, with the Apple plugin the first instance. See - [ADR 0008](docs/adr/0008-command-descriptor-registry.md). -- Typed result spine. Per-command typed results replaced the ad-hoc `Record`-typed returns across - the daemon/dispatch path; errors gained machine-readable `retriable`/`supportedOn` signals on - `DaemonError` (#939). Error-system conventions live in - [ADR 0010](docs/adr/0010-error-system.md). + simple Node client command methods. Closed public Node-client result contracts are narrowed through + `CommandResultMap`; action/backend-dependent methods remain explicitly broad until their public + response projections are reconciled. See + [Node client result types](docs/node-client-result-types.md). One `PlatformPlugin` per platform + family (`src/core/platform-plugin/`) stops core/daemon from branching on platform, with the Apple + plugin the first instance. See [ADR 0008](docs/adr/0008-command-descriptor-registry.md). +- Typed result spine. Per-command typed results replaced the ad-hoc `Record`-typed returns across the + daemon/dispatch path; errors gained machine-readable `retriable`/`supportedOn` signals on + `DaemonError` (#939). Error-system conventions live in [ADR 0010](docs/adr/0010-error-system.md). - Apple platform model. Internally `Platform` is `apple` (plus `android`/`linux`/`web`) with an - `appleOs` discriminant (`ios | ipados | tvos | watchos | visionos | macos`); the shared Apple - engine lives under `src/platforms/apple/core/` with per-OS leaves under - `src/platforms/apple/os//`. The public wire stays non-breaking: `PUBLIC_PLATFORMS` - (`src/kernel/device.ts`) still emits `ios`/`macos` leaf output. See - [ADR 0009](docs/adr/0009-apple-platform-consolidation.md). + `appleOs` discriminant (`ios | ipados | tvos | watchos | visionos | macos`); the shared Apple engine + lives under `src/platforms/apple/core/` with per-OS leaves under `src/platforms/apple/os//`. + The public wire stays non-breaking: `PUBLIC_PLATFORMS` (`src/kernel/device.ts`) still emits + `ios`/`macos` leaf output. See [ADR 0009](docs/adr/0009-apple-platform-consolidation.md). - Folder DAG + layering lint. `scripts/layering/check.ts` enforces two different scopes in CI. - GLOBALLY, across every production source file, it enforces the R1-R3 move rules - (kernel-sink, commands-floor, platforms-seam) and rejects all production static value-import - cycles. Separately, it ranks an explicit target spine — as rank groups, lowest (kernel sink) to - highest, where `A ◄ B` means B may not be outranked by A (the back-edge order the gate rejects), NOT - that every displayed import exists: + GLOBALLY, across every production source file, it enforces the R1-R3 move rules (kernel-sink, + commands-floor, platforms-seam) and rejects all production static value-import cycles. Separately, + it ranks an explicit target spine — as rank groups, lowest (kernel sink) to highest, where `A ◄ B` + means B may not be outranked by A (the back-edge order the gate rejects), NOT that every displayed + import exists: `kernel ◄ { contracts, request, selectors, platforms } ◄ core ◄ { commands, cli-schema } ◄ { client, daemon-server } ◄ daemon-client ◄ cli` — and rejects every back-edge within it. Root entrypoints and peripheral zones (`mcp`, `compat`, - `remote`, `metro`, `replay`, `recording`, `snapshot`, `screenshot-diff`, `cloud-webdriver`, - `sdk`, `utils`) are deliberately unranked (`UNRANKED_ZONES` in `scripts/layering/model.ts`): - they still obey R1-R4, but the gate asserts no total back-edge order over them. It is not a - claim that every folder is arranged in one DAG. `model.test.ts` guards that no new zone escapes - this classification silently. + `remote`, `metro`, `replay`, `recording`, `snapshot`, `screenshot-diff`, `cloud-webdriver`, `sdk`, + `utils`) are deliberately unranked (`UNRANKED_ZONES` in `scripts/layering/model.ts`): they still + obey R1-R4, but the gate asserts no total back-edge order over them. It is not a claim that every + folder is arranged in one DAG. `model.test.ts` guards that no new zone escapes this classification + silently. - Agent-cost. Responses carry a cost block and MCP `outputSchema`, rendered through a leveled `ResponseView`. @@ -156,23 +281,23 @@ the observable freshness and failure semantics below before any runtime refactor simple `id` selectors because label/text/value reads need snapshot disambiguation. - Regular selector reads remain capture-backed. `@ref`s resolve against the authorized ref frame's source tree (ADR 0014), not whatever now sits at that index in a newer observation; selector - `get`/`is`/`find`/`wait` capture through the backend. `find` and `wait` - polling must bypass the 750 ms snapshot cache. The cache is also bypassed while Android freshness - recovery or post-gesture stabilization is active. + `get`/`is`/`find`/`wait` capture through the backend. `find` and `wait` polling must bypass the + 750 ms snapshot cache. The cache is also bypassed while Android freshness recovery or post-gesture + stabilization is active. - Sparse snapshot quality verdicts are observable failures. Sparse captures must not replace `session.snapshot`, and selector routes should report the sparse verdict instead of treating a root-only or sparse tree as an empty UI. -- iOS sparse and AX failures are not proof of empty UI. Regular visible snapshots can recover - through the capture plan; raw and strict paths preserve failure. `runnerFatal` invalidates the - cached target and must never refresh healthy mutation recency. -- Android helper reuse must not become snapshot result caching. Freshness is short lived, marked - only after navigation-sensitive actions, compared against broad route-safe baselines, and not - learned from scoped, depth-limited, interactive, or ref-refresh snapshots. +- iOS sparse and AX failures are not proof of empty UI. Regular visible snapshots can recover through + the capture plan; raw and strict paths preserve failure. `runnerFatal` invalidates the cached target + and must never refresh healthy mutation recency. +- Android helper reuse must not become snapshot result caching. Freshness is short lived, marked only + after navigation-sensitive actions, compared against broad route-safe baselines, and not learned + from scoped, depth-limited, interactive, or ref-refresh snapshots. - Pending interaction outcome retry runs before post-gesture stabilization. Android freshness then composes when needed. Stabilization applies after swipe, scroll, gesture, or an explicit flag, and disables direct iOS selector shortcuts while pending. -- `setSessionSnapshot` is the centralized session snapshot mutation path. Sparse captures do not - write back, and empty `@ref`-scoped snapshot output must not replace the stored session snapshot. +- `setSessionSnapshot` is the centralized session snapshot mutation path. Sparse captures do not write + back, and empty `@ref`-scoped snapshot output must not replace the stored session snapshot. - Maestro target matching remains snapshot-based and policy-owned. Coordinate dispatch always uses a fresh target snapshot. A unique exact iOS match may instead reuse bound same-generation semantic evidence and dispatch through XCTest's atomic selector tap; structured live-selector failures return @@ -195,8 +320,14 @@ Evidence: [ADR 0002](docs/adr/0002-persistent-platform-helper-sessions.md), ## Testing Principles - Provider-backed integration scenarios should exercise the public daemon path whenever practical. -- Prefer the in-process provider scenario harness for broad scenarios; keep HTTP contract tests narrow and transport-specific. -- Provider seams sit below platform modules so integration tests still cover platform command translation. +- Prefer the in-process provider scenario harness for broad scenarios; keep HTTP contract tests narrow + and transport-specific. +- Provider seams sit below platform modules so integration tests still cover platform command + translation. - Provider transcripts are for exact external command contracts. -- Scenario transcripts are for broad, user-rooted workflows that should replace mocked handler unit tests. -- Unit tests stay for pure logic, parser matrices, selector matching, capabilities, and important edge cases. +- Scenario transcripts are for broad, user-rooted workflows that should replace mocked handler unit + tests. +- Unit tests stay for pure logic, parser matrices, selector matching, capabilities, and important edge + cases. + +Gate selection, speed rules, and shared fixtures live in [docs/agents/testing.md](docs/agents/testing.md). diff --git a/docs/agents/cli-flags.md b/docs/agents/cli-flags.md new file mode 100644 index 0000000000..7a4245fb80 --- /dev/null +++ b/docs/agents/cli-flags.md @@ -0,0 +1,42 @@ +# Adding a CLI Flag + +A new flag touches only the layers that need to understand it. Stop at the layer where it stops +mattering — threading it further is the common failure, not stopping too early. + +1. `src/contracts/cli-flags.ts`: add to `CliFlags`; add the definition to the matching + `src/commands/cli-grammar/flag-definitions-*.ts` owner and the relevant group in `flag-groups.ts` + (for example `SNAPSHOT_FLAGS`). Then update the command family metadata/schema that exposes the + flag; find the owner with + `rg -n "|supportedFlags|allowedFlags" src/commands src/cli-schema src/cli/parser`. For + schema-only CLI commands (`cdp`, `auth`, `connect`, `proxy`, `react-devtools`, `web`) the owner is + `SCHEMA_ONLY_CLI_COMMAND_SCHEMAS` in `src/cli-schema/command-overrides.ts`. +2. `src/commands/cli-grammar/*`: read the CLI flag into command input. +3. `src/commands/command-projection.ts` and command-family projection helpers: write the input into + the daemon request only if the flag affects daemon execution. +4. `src/commands/*-command-contracts.ts`: add to the command input schema only if the option should + be available through Node.js or MCP as structured input. +5. `src/client/client-types.ts`: update the public typed client option only when the Node.js + interface exposes it. +6. `src/client/client-normalizers.ts`: update daemon flag normalization only when the request still + needs a public-to-internal translation. +7. `src/daemon/context.ts` and `src/core/dispatch-context.ts`: add the field only when it flows into + platform dispatch. +8. Handler/platform modules: thread the option only after the command surface, grammar, and + projection prove it belongs there. +9. `scripts/integration-progress-model.ts`: classify the flag (device-observable vs + intentionally-outside). The architecture-progress gate fails CI on unclassified public flags. +10. If the flag changes interaction semantics, revisit the affected cells in + `src/contracts/interaction-guarantees.ts` (scope with `appliesTo` when the flag exists only on + some commands). + +Command-only flags (like `find --first`) that never reach the platform layer usually stop at +steps 1-3, plus step 9. + +## Where CLI help and schema live + +- Long help prose: `src/cli/parser/cli-help.ts`. Flag definitions: `src/commands/cli-grammar/`. +- Command-specific usage/flag metadata lives with the command family metadata that owns the command. +- Parser/help *rendering* stays in `src/cli/parser/`; command schema metadata is derived from command + metadata, family declarations, and the schema-only merge path in + `src/cli-schema/command-overrides.ts`. Keep the two separate. +- Locating an owner: `rg -n "helpDescription|summary|supportedFlags|allowedFlags" src/commands src/cli/parser src/cli-schema`. diff --git a/docs/agents/device-verification.md b/docs/agents/device-verification.md new file mode 100644 index 0000000000..804843a5dc --- /dev/null +++ b/docs/agents/device-verification.md @@ -0,0 +1,53 @@ +# Manual Device Verification + +Read this before running `agent-device` by hand against a simulator, emulator, or physical device. + +## Before the run: defeat staleness + +Dev-loop staleness has three layers, and each produces a convincing false negative. + +- After changing runtime code reached through `bin/agent-device.mjs` or the daemon: `pnpm build`, + then `pnpm clean:daemon` — the daemon does not self-reload. +- Before any Android verification from source: `pnpm build`, `pnpm build:android`, `pnpm clean:daemon`. + `build:android` refreshes and verifies both bundled Android helper artifacts for the current + package version. +- `shutdown` deliberately HANDS OFF a healthy simulator runner. The adopted runner keeps serving the + old Swift binary until you kill its process or the source fingerprint changes, so "my change did + nothing" measured against an adopted runner is a classic false negative. If Swift runner code + changed, run `pnpm build:xcuitest`. + +## Prove the path under test was actually active + +- Android: capture `snapshot -i --json` and require `androidSnapshot.backend` to be `android-helper` + with `helperVersion` equal to `package.json`'s version. A stock UIAutomator fallback is not valid + verification unless the fallback itself is the behavior under test. +- For repo-owned `Agent Device Tester` work, `examples/test-app/README.md` is the source of truth for + simulator, physical-device, Metro/dev-client, and app-surface steps. An already-installed + `com.callstack.agentdevicelab` is not sufficient — the README's Metro/dev-build and `snapshot -i` + checks must prove the expected app surface is running. +- For Android RN/Expo/dev-client apps on any local Metro port, `adb reverse tcp: tcp:` is + harmless and should be run before opening the app or URL. + +## Session hygiene + +Every manually opened session is a resource that must be closed — including exploratory sessions and +failed verification attempts. + +- Every `agent-device open` needs a matching `close` with the same `--session`, `--platform`, + `--udid`, and `--state-dir` before the agent finishes. +- Use a purpose-specific session name for experiments, and an isolated `--state-dir` under + `/private/tmp` when you need cleanup isolation beyond the current worktree's default daemon. +- Track opened sessions in working notes; close each one before the final response. +- If `close` is blocked by stale daemon metadata, inspect processes first with + `ps -ax | rg "agent-device|xcodebuild test-without-building"`. Stop only exact stale PIDs belonging + to this verification run, then `pnpm clean:daemon`. +- If cleanup cannot be completed, report the remaining session name, state dir, PIDs, and metadata + paths as a blocker. + +## Sandboxed environments + +Start the daemon outside the sandbox with escalation. The daemon binds localhost, and sandboxed runs +fail before any product code executes, with `listen EPERM: operation not permitted 127.0.0.1` or +repeated `Failed to start daemon`/metadata cleanup messages. Those are not agent-device regressions — +rerun with escalation. Unit tests, typecheck, lint, and build can stay sandboxed unless they need +devices or listener access. diff --git a/docs/agents/domain.md b/docs/agents/domain.md index 6ddbcdc3ee..26cf80f05e 100644 --- a/docs/agents/domain.md +++ b/docs/agents/domain.md @@ -1,13 +1,11 @@ # Domain Docs -This is a single-context repo. +Single-context repo. Before architecture, diagnosis, TDD, triage, PRD, or roadmap work, read +`CONTEXT.md` for domain vocabulary and the capture-reliability contract, plus the relevant ADRs in +`docs/adr/`. -Before architecture, diagnosis, TDD, triage, PRD, or roadmap work, read: +Use `CONTEXT.md` vocabulary in issue titles, refactor proposals, test names, and architecture notes. +If a proposed change contradicts an ADR, say so explicitly and explain why the decision should be +reopened. -- `CONTEXT.md` for domain vocabulary, test strategy terms, and architecture language. -- Relevant ADRs in `docs/adr/` for accepted architecture decisions. -- This `docs/agents/` directory for issue-tracker and triage-label conventions. -- `docs/agents/web-backend.md` before changing web automation backend setup or diagnostics. -- `docs/agents/testing.md` for maintainer-only test lane notes such as the live web smoke. - -Use the vocabulary from `CONTEXT.md` in issue titles, refactor proposals, test names, and architecture notes. If a proposed change contradicts an ADR, call that out explicitly and explain why the decision should be reopened. +`AGENTS.md` routes to the rest of this directory by task type. diff --git a/docs/agents/pull-requests.md b/docs/agents/pull-requests.md new file mode 100644 index 0000000000..1af3b1116f --- /dev/null +++ b/docs/agents/pull-requests.md @@ -0,0 +1,60 @@ +# Pull Requests + +## Readiness + +- Static gates first: required checks pass, `pnpm check:fallow --base origin/main` is clean when + code-quality/dead-code risk is relevant, CI guards are green, and no conflict markers or unmerged + paths remain. +- A local unit-only run is not CI-green. Use `pnpm test:unit` for the repo unit bundle, or + `vitest run --project unit-core --project subprocess-stub` when invoking Vitest directly. The + **Integration Tests** and **Coverage** jobs run the `provider-integration` project — verify those + green on the actual PR head. +- Device-facing behavior is not merge-ready without real simulator/emulator/device evidence for the + changed path. Fixture-backed tests prove contracts; they do not replace a live run that creates or + observes the artifact/state the feature claims to handle. If live verification is blocked, state + the blocker and the exact command/device needed, and downgrade the PR to residual risk rather than + calling it ready. +- Command-surface changes preserve CLI, Node.js, daemon, MCP, help, docs, and SkillGym coverage + where that surface is affected, without duplicating command contracts across layers. +- Runtime output stays agent-friendly: compact defaults, top offenders first for diagnostics/perf, + bounded arrays in JSON, artifact paths for large raw data, progressive lookup for deeper detail. +- Close every manual `agent-device` session opened during verification + (`docs/agents/device-verification.md`) and report any cleanup that could not be completed. + +## PR body + +Conventional commit prefixes (`feat:`, `fix:`, `chore:`, `perf:`, `refactor:`, `docs:`, `test:`, +`build:`, `ci:`). No bracketed bot tags like `[codex]`. Ready-for-review by default; draft only when +asked or when the work is intentionally incomplete. + +- `## Summary`: user/API behavior, not an implementation file tour. Lead with what changed for + operators, clients, command authors, or platform behavior. A compact before/after helps when it + clarifies the workflow or bug fix. For new or changed public APIs, include 1-3 concrete CLI/Node/MCP + examples a reviewer can scan. `Closes #123` when applicable. +- `## Validation`: meaningful evidence in concise prose — scenario names, manual device/browser + evidence, changed screenshots, CI status, notable failures/retries and their outcome. Avoid command + accounting for routine local gates; name an exact command only when it is unusual, manually + reproducible evidence, or needed to explain a residual risk. For docs-only changes, say why runtime + validation does not apply instead of writing a command checklist. +- Call out real tradeoffs, known gaps, and follow-ups; omit boilerplate when there are none. +- Note touched-file count and whether scope expanded beyond the initial command family. + +## Reviewing + +- Review against the linked issue, not only the diff. State the issue's motivating behavior and + verify the PR fixes *that*. +- Check relevant ADRs before reviewing architecture, routing, command-surface, platform-boundary, + diagnostics, or testing-strategy changes. An ADR conflict is a review finding unless the PR updates + or supersedes the ADR explicitly. +- Read dependency notes (`Blocked by: ...`, linked PRs, sibling branches) before judging correctness. + A base/sequence problem outranks detail review. +- Trace the real production route from command surface through daemon/request routing to the platform + backend. Tests that mock away the router, or exercise only a helper, do not prove the shipped path. +- For each key regression test, identify what deletion or revert would make it fail. If reverting the + implementation still passes, the test is vacuous. +- Check for hidden behavior changes separately from intended refactors: output shape, + warning/error propagation, artifact paths, fallback/retry tiers. +- Verify tests cover the issue's motivating failure, not just the new abstraction. Prefer + before/after evidence when an issue reports a concrete divergence. +- Green CI is necessary but insufficient for device-facing or routing-sensitive work. +- Check whether the tightening pass removed code/tests the change made obsolete. diff --git a/docs/agents/testing.md b/docs/agents/testing.md index e27159726f..b36969283d 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -1,5 +1,52 @@ # Testing Notes +## Which gates a change needs + +Default for code changes: `pnpm check:affected --base origin/main --run`. It derives the gate set +from repository sources of truth, so prefer it over interpreting the table below by hand. GitHub CI +stays authoritative. + +The mapping it encodes, for when you need to run a gate directly or reason about coverage: + +| Change | Gate | +| --- | --- | +| Any TypeScript | `pnpm typecheck` or `pnpm check:quick` | +| Daemon handler / shared module | `pnpm check:unit` | +| Tooling/config (`package.json`, `tsconfig*.json`, `.oxlintrc.json`, `.oxfmtrc.json`) | `pnpm check:tooling` | +| Platform/device response — anything emitting `platform`/`appleOs` on the wire, or shaping a daemon response | `pnpm test:integration:provider` **and** `pnpm test:coverage` | +| Cross-platform behavior | `pnpm test:integration` | +| iOS runner / Swift | `pnpm build:xcuitest` | +| CLI help/guidance (`src/cli/parser/cli-help.ts`, `src/cli-schema/`) | `pnpm exec vitest run src/cli/parser/__tests__ src/cli-schema/command-schema-guards.test.ts` | +| SkillGym prompts/assertions | `pnpm test:skillgym:case ` (broad: `pnpm test:skillgym`, filter with `-- --tag fixture-smoke` or `-- --tag skill-guidance`) | +| Anything in `src/`, `test/`, `skills/` | `pnpm format` | + +Two traps worth naming: + +- The platform/device-response row is the one agents miss. `pnpm check:unit` does **not** exercise the + `provider-integration` project, and that project holds the apple-platform-output leak guard. + Internal `apple` must never reach a command response — project through `publicPlatformString`. +- Fallow CI failures reproduce with `pnpm check:fallow --base origin/main`. Do not estimate + complexity or dead-code impact by hand. + +Docs/skills-only and non-TS changes with no behavior impact need no tests. Test-only DI seam CI +failures are enforced by the workflow — do not add optional `typeof` DI params to production code to +satisfy a test. + +## Shared test utilities + +Before writing a new test, inspect `src/__tests__/test-utils/index.ts`: +`rg -n "export .*make|export .*DEVICE|withMocked" src/__tests__/test-utils`. Import through the +barrel and prefer named shared fixtures over inlining new `DeviceInfo`, `SessionState`, snapshot, +store, or mocked-binary objects. If a helper is missing, add it near the concept it serves and export +it through the barrel. + +Keep tests behavioral. Do not assert shapes or cases TypeScript already proves. + +Test through public interfaces where practical, and do not add unrelated production exports solely +to make a test easier — widening the public surface for a test is a product change, and the exports +outlive the test that motivated them. If a seam is genuinely missing, add it as a real one rather +than as a test affordance (the workflow separately forbids test-only `typeof` DI params). + ## Affected-check selector (`pnpm check:affected`) `pnpm check:affected --base ` derives which local checks a diff needs, so @@ -44,8 +91,12 @@ sides of a rename are classified (a moved file cannot look docs-only by its destination alone). Anything the selector cannot classify — unknown, ambiguous, workflow/tooling, or -a change to the selector's own sources (including the `AGENTS.md` Testing -Matrix) — **fails open to the full check set**. +a change to the selector's own sources — **fails open to the full check set**. +That includes this file: the Testing Matrix above is the prose the ownership +rules mirror, so `docs/agents/testing.md` is selector-owning +(`SELECTOR_OWNING_DOCS` in `scripts/check-affected/model.ts`) and outranks the +docs-only short-circuit its path would otherwise take. If the matrix moves +again, move that entry with it. The plan documents the rule and changed path behind every selected check. Model and catalog live under `scripts/check-affected/`; the derivation is guarded diff --git a/scripts/check-affected/checks.ts b/scripts/check-affected/checks.ts index 9051daf1b2..5e2dc5da3c 100644 --- a/scripts/check-affected/checks.ts +++ b/scripts/check-affected/checks.ts @@ -145,9 +145,10 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [ id: 'skillgym', label: 'SkillGym command-planning suite', kind: { type: 'script', script: 'test:skillgym' }, - // No GitHub workflow runs SkillGym; per the AGENTS.md testing matrix it is - // a local-only gate (`pnpm test:skillgym`). Keep it locally runnable rather - // than claiming a CI job that does not exist and silently skipping it. + // No GitHub workflow runs SkillGym; per the Testing Matrix in + // docs/agents/testing.md it is a local-only gate (`pnpm test:skillgym`). + // Keep it locally runnable rather than claiming a CI job that does not + // exist and silently skipping it. ciJobs: [], localRunnable: true, }, diff --git a/scripts/check-affected/model.test.ts b/scripts/check-affected/model.test.ts index 2621601f85..ed73554fef 100644 --- a/scripts/check-affected/model.test.ts +++ b/scripts/check-affected/model.test.ts @@ -135,7 +135,9 @@ test('workflow/tooling and selector-owning changes fail open', () => { plan(['scripts/check-affected/model.ts']).failOpenReasons[0]?.rule, 'selector-owning', ); - assert.equal(plan(['AGENTS.md']).failOpenReasons[0]?.rule, 'selector-owning'); + // The Testing Matrix lives here; a matrix edit must outrank the docs-only + // short-circuit that its `docs/` path would otherwise take. + assert.equal(plan(['docs/agents/testing.md']).failOpenReasons[0]?.rule, 'selector-owning'); }); test('a fail-open path in a mixed changeset forces the full set', () => { diff --git a/scripts/check-affected/model.ts b/scripts/check-affected/model.ts index a40d73c32f..71e71c1356 100644 --- a/scripts/check-affected/model.ts +++ b/scripts/check-affected/model.ts @@ -106,9 +106,17 @@ const ROOT_TOOLING = new Set([ '.npmrc', ]); +// Prose that specifies this selector's own behavior — the Testing Matrix these +// ownership rules mirror. It is docs by path, but editing it can invalidate the +// derivation below, and the selector cannot tell whether it did. Keep this in +// sync when the matrix moves; the docs short-circuit would otherwise treat it as +// inert Markdown. +const SELECTOR_OWNING_DOCS = new Set(['docs/agents/testing.md']); + function isSelectorOwning(file: string): boolean { return ( - file === 'AGENTS.md' || (file.startsWith('scripts/check-affected/') && !file.endsWith('.md')) + SELECTOR_OWNING_DOCS.has(file) || + (file.startsWith('scripts/check-affected/') && !file.endsWith('.md')) ); } @@ -221,7 +229,8 @@ const nodeIntegrationOwnership: OwnershipRule = ({ file }) => : []; // SkillGym validates skill guidance (`skills/`) and owns its harness -// (`test/skillgym/`); AGENTS.md routes skill-prompt/assertion changes here. +// (`test/skillgym/`); the Testing Matrix in docs/agents/testing.md routes +// skill-prompt/assertion changes here. const skillgymOwnership: OwnershipRule = ({ file, underSkills }) => underSkills || file.startsWith('test/skillgym/') ? [