Skip to content
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,10 @@ This repo encodes invariants as self-declaring gates. The correct response to a

## Selector System Rules
- Interaction commands (`click`, `fill`, `get`, `is`) and `wait` accept selectors and `@ref`.
- Pipeline: **parse -> resolve -> act -> record selectorChain -> heal on replay**.
- 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`, record `selectorChain`, and hook replay healing (`healReplayAction` in `session.ts` + selector helpers in `session-replay-heal.ts`).
- 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.
Expand Down
14 changes: 11 additions & 3 deletions docs/adr/0012-interactive-replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +441,18 @@ indexing, so repeated source lines are distinguished by their plan index.

Every divergence includes `resume: { allowed, from, reason?, planDigest }`. `planDigest` is SHA-256 over
the canonical fully expanded plan, including each action's command, normalized inputs, control shape,
platform-conditioned expansion, and source provenance. A resume requires both `--from N` and
platform-conditioned expansion, and source provenance. Concretely "normalized inputs" bind each action's
positionals/flags, its execution-affecting `runtime` hints, and its `target-v1` identity annotation
(decision 3 — verification consumes it pre-action, so a changed annotation is execution-affecting); and
"platform-conditioned expansion" binds the EFFECTIVE resolved `platform`/`target` the run invokes with (CLI
flag over script metadata), never the raw script metadata, so a digest computed for one target is not
reusable against another. Deliberately EXCLUDED are native `.ad` `${VAR}` VALUES: substitution happens
after planning, so changing only late-bound values keeps the same digest. Maestro environment substitution
instead happens during compatibility parsing and can change action inputs, includes, or control expansion,
so it can change the digest. A resume requires both `--from N` and
`--plan-digest <planDigest>` from the report. The daemon rebuilds the current plan and rejects
`INVALID_ARGS` before any action when its digest differs, so edits, include changes, or environment-driven
expansion cannot silently retarget ordinal N. `allowed: false` explains why no resume is safe; its digest
`INVALID_ARGS` before any action when its digest differs, so edits or parse-time expansion cannot silently
retarget ordinal N. `allowed: false` explains why no resume is safe; its digest
is still diagnostic, not an authorization to bypass preflight.

Resume does not reconstruct execution state. For `N > 1`, preflight must reject with `INVALID_ARGS` when
Expand Down
2 changes: 1 addition & 1 deletion docs/maestro-compat-debt-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This map summarizes the Maestro compatibility surface after the lane 6 audit. LO
| Input and focus | `replay-flow.ts` input coalescing, `interactions.ts` input/erase/paste/pressKey slices, `runtime.ts`, daemon Maestro fallback in `interaction-touch.ts` | ~350 LOC plus shared daemon flags | Coalesces `tapOn` + `inputText` + `pressKey Enter` into `wait`/`fill`/enter for likely text-entry selectors, leaves focused-field input as `type`, maps `eraseText` to backspaces, falls back from keyboard enter to newline typing, and allows Maestro non-hittable coordinate fallback for tap selectors. | Uses native `fill`, `type`, `keyboard`, `click`, replay variable substitution, and daemon touch fallback flags. | A native "focus then fill" replay primitive and focused-field clear/erase operation would remove most parser heuristics while improving `.ad` flows too. | Text-entry detection is name-based; focused-field commands depend on existing device focus; non-hittable coordinate fallback is intentionally compatibility-only and can mask poor selectors. | Moderate: parser coalescing tests in `replay-flow.test.ts`; daemon fallback covered in `interaction.test.ts`; no broad provider guard for focused erase/paste. | Converge on native focus/fill/clear semantics. Keep Maestro's optional/non-hittable quirks as compat flags. | Lane 8: native input primitive |
| Assertions and waits | `runtime-assertions.ts`, wait/assert slices of `interactions.ts`, `runtime-flow.ts` visible condition path | ~450 LOC | Polls raw snapshots for `assertVisible`, adds a terminal grace capture, treats `assertNotVisible` as stable hidden after repeated samples or timeout, computes animation stability from snapshot signatures, maps `extendedWaitUntil`, and waits briefly for `runFlow.when.visible` while keeping `notVisible` point-in-time. | Uses native `snapshot`, `wait`, `is`, visible predicates, reference frames, replay blocks, and timeout helpers. | A shared waiter/poller utility with explicit grace, stable-hidden, raw-snapshot, and timeout policies would let native `wait` and Maestro share mechanics without sharing defaults. | Timeout and stability semantics are subtle; raw full snapshots are expensive; making `notVisible` wait would change cleanup-flow behavior. | Good: `runtime-assertions.test.ts` covers deadline/hidden edge cases; `runtime-flow.test.ts` covers visible wait and immediate notVisible. | Extract generic polling/stability helpers; keep Maestro timing constants and condition semantics local. | Lane 8: waiter convergence |
| Scroll, swipe, and geometry | `points.ts`, `runtime-geometry.ts`, geometry/scroll/swipe slices of `interactions.ts` and `runtime-interactions.ts` | ~500 LOC | Parses absolute and percentage points, converts authored percentage swipes through the shared core frame-bounds helper, caches reference frames in replay scope, delegates directional screen swipes to core gesture presets, biases tap points for large text containers and bottom tabs, loops `scrollUntilVisible` with `wait`/`find` probes, and maps `swipe.label`. | Uses native `click`, `gesture`, `scroll`, `swipe`, `find`, `wait`, touch reference frames, and raw snapshots. | Target-derived swipes and frame caching could move into native gesture planning when non-Maestro callers need them. | Stale or missing raw snapshot frames break percent gestures; target-derived swipe geometry and tap bias remain compatibility heuristics. | Good: `runtime-geometry.test.ts`, `runtime-interactions.test.ts`; the provider guard asserts fresh snapshots and exact authored Android percentage coordinates. | Keep authored coordinates lossless and route semantic gestures through core planners. Extract target geometry or frame caching only with a second native caller. | Lane 8 or 9: gesture planner |
| Flow control, `runFlow`, retry, and hooks | `flow-control.ts`, `runtime-flow.ts`, hook flattening in `replay-flow.ts` | ~700 LOC | Handles `onFlowStart`/`onFlowComplete`, file and inline `runFlow`, per-block env, static platform gates, limited `when.true` boolean/platform expressions, runtime visible/notVisible gates through `SessionAction.replayControl`, parse-expanded `repeat.times`, and runtime `retry` via replay retry blocks. | Depends on replay block runtime (`invokeReplayActionBlock`, `invokeReplayRetryBlock`), replay vars/env, typed `SessionReplayControl`, and native snapshot visibility resolution. | Flow block execution is now mostly converged. A native replay block AST would only be worth it if native `.ad` syntax also needs conditional blocks, deterministic repeats, or faithful nested line reporting. | `repeat.times` materializes actions with a guardrail; nested line numbers are lossy; expression support is intentionally tiny; visible conditions are snapshot-dependent; compat flow controls are intentionally in-memory and rejected for `replay -u`. | Strong: `replay-flow.test.ts` covers hooks/runFlow/env/repeat/retry/platform gates/expressions; `runtime-flow.test.ts` covers runtime condition behavior; daemon replay tests guard execution and `replay -u` rejection; PR #620 covers provider retry/runFlow path. | Keep `replayControl` as the shared runtime boundary. Do not add a native block AST unless native replay gains its own block syntax; keep Maestro expression grammar and unsupported `repeat.while` local until native runtime has loop semantics. | Lane 7 complete / revisit only with native block syntax |
| Flow control, `runFlow`, retry, and hooks | `flow-control.ts`, `runtime-flow.ts`, hook flattening in `replay-flow.ts` | ~700 LOC | Handles `onFlowStart`/`onFlowComplete`, file and inline `runFlow`, per-block env, static platform gates, limited `when.true` boolean/platform expressions, runtime visible/notVisible gates through `SessionAction.replayControl`, parse-expanded `repeat.times`, and runtime `retry` via replay retry blocks. | Depends on replay block runtime (`invokeReplayActionBlock`, `invokeReplayRetryBlock`), replay vars/env, typed `SessionReplayControl`, and native snapshot visibility resolution. | Flow block execution is now mostly converged. A native replay block AST would only be worth it if native `.ad` syntax also needs conditional blocks, deterministic repeats, or faithful nested line reporting. | `repeat.times` materializes actions with a guardrail; nested line numbers are lossy; expression support is intentionally tiny; visible conditions are snapshot-dependent; compat flow controls are intentionally in-memory (a `retry`/`runFlow.when` block is one entry in the replay-resume executable plan, never individually addressable by `replay --from`, and ADR 0012 migration step 5's resume preflight rejects skipping into or resuming at one). | Strong: `replay-flow.test.ts` covers hooks/runFlow/env/repeat/retry/platform gates/expressions; `runtime-flow.test.ts` covers runtime condition behavior; daemon replay tests guard execution and resume's control-flow rejection; PR #620 covers provider retry/runFlow path. | Keep `replayControl` as the shared runtime boundary. Do not add a native block AST unless native replay gains its own block syntax; keep Maestro expression grammar and unsupported `repeat.while` local until native runtime has loop semantics. | Lane 7 complete / revisit only with native block syntax |
| `runScript` | `run-script.ts`, `runtime.ts` runScript branch | 229 LOC plus router branch | Executes trusted flow-local JavaScript with `node:vm`, exposes env values, `output`, `json`, and synchronous-looking `http.post` through a timeout-bounded child Node process; exports output as `output.<key>` replay variables. | Uses replay variable scope, `runCmdSync`, and `AppError`; there is no native `.ad` command equivalent. | Shared env/output variable plumbing could converge, but script execution should not become native without a separate security model and product decision. | High security and determinism risk: `node:vm` is not a sandbox, scripts can make network requests, async support is intentionally narrow, and output key rules are compatibility-specific. | Partial: parser/order/env/path behavior covered in `replay-flow.test.ts`; docs describe trust/security limits; no focused execution tests for `http.post`, `json`, or output validation. | Keep compat-local. Add focused execution tests before expanding helpers; do not expose as native command until sandboxing and trust model are explicit. | Lane 9: runScript guard tests |
| Suite discovery and test artifacts | `src/daemon/handlers/session-test-discovery.ts`, `session-test-suite.ts`, `session-test-artifacts.ts`, replay grammar/backend plumbing | ~120 Maestro-specific LOC in daemon/test path | When `--maestro`/backend is selected, discovers `.ad`, `.yaml`, and `.yml`, allows untyped YAML through platform filtering, runs through native replay test suite, and preserves original Maestro flow filenames in artifacts. | Native test suite runner, replay backend selection, session lifecycle, artifact materialization, and CLI `--maestro` flag. | Mostly converged already. The remaining improvement is making replay backend extension/filter policy data-driven so future backends avoid daemon conditionals. | Low. Main risk is accidentally including non-Maestro YAML when backend is set, or changing platform-filter behavior for untyped YAML. | Good: `session-test-discovery.test.ts`, `session-test-suite.test.ts`, `session-test-artifacts.test.ts`; PR #620 provider suite runs a Maestro YAML test. | Keep small daemon hook for now; extract backend discovery policy only if another replay backend appears. | Lane 6 complete / no follow-up unless backend count grows |
| Docs and support matrix | `support-matrix.ts`, `website/docs/docs/replay-e2e.md`, CLI flag/help tests, issue tracker references | 42 LOC source matrix plus docs | Maintains supported/unsupported capability lists, formats CLI help copy, links tracker/new issue URLs, and keeps replay docs synced with the source matrix. | CLI flag definitions/help rendering and website docs. | Generate or embed the support list from one source in docs/help to remove manual prose drift. | Medium user-facing risk: stale docs can imply unsupported parity or hide known gaps. | Good: `support-matrix.test.ts` asserts CLI help and docs stay synced with the shared matrix. | Keep `support-matrix.ts` as source of truth; update it with every behavior change and mirror only explanatory context in docs. | Lane 6 complete / ongoing docs hygiene |
Expand Down
4 changes: 3 additions & 1 deletion scripts/integration-progress-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ function summarizeProviderScenarioFlagCoverage(files) {
['restart', 'logs clear --restart workflow'],
['networkInclude', 'network dump include modes', ['include']],
['noRecord', 'action recording suppression'],
['replayUpdate', 'selector-healing replay update', ['update']],
['replayUpdate', 'retired --update no-op replays without rewriting (ADR 0012)', ['update']],
['replayEnv', 'replay/test variable injection', ['env']],
['replayFrom', 'replay resume skips completed steps (ADR 0012)', ['resumeFrom']],
['replayPlanDigest', 'replay resume plan-digest preflight binding', ['resumePlanDigest']],
['failFast', 'test suite stops after first failure'],
['timeoutMs', 'wait/test timeout flags'],
['retries', 'test suite retry budget flows through request path'],
Expand Down
10 changes: 9 additions & 1 deletion src/__tests__/cli-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ test('help workflow prints agent workflow topic and skips daemon dispatch', asyn
assert.match(result.stdout, /agent-device help workflow/);
assert.match(result.stdout, /Core loop:/);
assert.match(result.stdout, /Do not use CSS selectors/);
assert.match(result.stdout, /Native \.ad interpolation is late-bound after planning/);
assert.match(
result.stdout,
/Maestro environment substitution occurs during compatibility parsing/,
);
});

test('help workflow preserves known device workaround guidance', async () => {
Expand Down Expand Up @@ -114,7 +119,10 @@ test('help workflow documents the selector disambiguation policy (#1037)', async
assert.match(result.stdout, /deepest node first/);
assert.match(result.stdout, /then smallest on-screen area/);
assert.match(result.stdout, /Selector did not resolve uniquely/);
assert.match(result.stdout, /replay and replay-heal apply the same depth-then-area policy/);
assert.match(
result.stdout,
/replay's suggestion re-resolution.*applies the same depth-then-area policy/,
);
assert.match(result.stdout, /targetHittable: false/);
});

Expand Down
Loading
Loading