Skip to content

refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5) - #1555

Draft
thymikee wants to merge 21 commits into
mainfrom
p5/extract-ad-replay
Draft

refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5)#1555
thymikee wants to merge 21 commits into
mainfrom
p5/extract-ad-replay

Conversation

@thymikee

@thymikee thymikee commented Aug 2, 2026

Copy link
Copy Markdown
Member

refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5)

Implements P5 per the approved design of record (proposal, binding amendment), built as staged, individually-gated commits on one branch, cutting over atomically in this PR. Step 2 of the approved sequence (behavior pinning) merged separately as #1552 and is this PR's regression net — none of those tests' assertions changed anywhere in this branch. All four findings from the first review are addressed in commits 281f33599..947b90ca3; the response comment maps them one-by-one.

The façade — complete, no deviations

packages/ad-replay exports exactly inspectAdReplay, runAdReplay, formatReplaySuccessMessage, and the types their signatures reference (AdReplayManifest, AdReplayDigestFlags, AdReplayRunOutcome, AdReplayStepOutcome, AdReplayStepFailure, AdReplayStepRuntime, and the ReplaySelectorPort family). Package exports map is exactly ".", and a new exact exported-symbol gate in scripts/layering/package-boundaries.test.ts pins the named list (plant-verified: a stray export fails the gate with a clear diff).

  • Parsing/planning/digest/resume are behind the entrypoints: inspectAdReplay(sourcePath) returns the manifest including planDigest and resolveEntryIndex(...); the digest/resume internals (plan-digest.ts, resume.ts) are package-private. Resume validation deliberately stays eager in prepareReplayPlan call order — moving it later would let a rejected --from mutate coordinator state before failing (traced ordering hazard, avoided).
  • Target verification runs inside the engine step loop: verifyAndDispatchStep in the package drives the verify-then-dispatch sequence, calling narrow daemon capabilities (beginTargetVerification, captureObservation, classifyTarget, dispatchStep, buildRecordedUnverifiableFailure, buildTargetBindingFailure, buildPostDispatchTargetBindingFailure, handleActionFailure). The four policy functions are package-private. The per-step order — including the subtlety that post-dispatch mismatch divergences use the pre-step artifact snapshot — is preserved exactly (mapped before/after in the stage report).
  • Neutral outcomes, no generic: AdReplayStepRuntime has no type parameter. The engine returns tagged neutral outcomes (AdReplayRunOutcome/AdReplayStepFailure: kind, message, artifact paths, evidence values). The daemon adapter keeps a side-map (lastResponse closure in createAdReplayStepRuntime) so the client-visible DaemonResponse is the literal same object as pre-refactor — no wire type crosses the boundary in either direction (the only DaemonResponse mentions inside the package are comments stating its absence).

Shared vocabulary went to its owner

Per the review's "proper shared owner" alternative, measured per symbol: the identity vocabulary (annotationLocalIdentity, matchesLocalIdentity, matchesAncestryPrefix, LocalIdentity, identityFieldMismatches, firstAncestryMismatch) and classifyTargetBindingMatch (+ vars.ts) moved to packages/ad-script — they interpret .ad/TargetAnnotationV1 semantics and are consumed by recording-side root code (and, for vars, the Maestro path). session-replay-report-action.ts / session-replay-suggestion-ranking.ts measured as root-only consumers and moved back to the daemon (undoing an over-move). ad-script's façade additions are covered by its existing boundary row.

The selector port

Three operations (readSelectorExpression, resolveRecordedTarget — same-alternative winner+domain invariant implemented in the production adapter, lifted verbatim; buildSelectorCandidates), trafficking only in strings, kernel snapshot values, and tagged unions. Production adapter: src/daemon/replay-selector-port.ts. In-memory adapter: src/__tests__/test-utils/in-memory-replay-selector-port.ts — now honors ReplaySelectorCandidateOptions.nodes with production's exact shared-ID drop semantics. Contract suite: 9 cells × 2 adapters (18 tests), including the shared-ID demotion cell (counterfactual: ignoring nodes fails the in-memory leg while production stays green). Two AST-needing helpers (resolveReplaySuggestionCandidate, readReplaySelectorDisplayValue) are daemon-side plain exports beside the adapter — provably inexpressible through the port without leaking the AST.

Invalid-backend rejection restored

prepareReplayPlan rejects any non-maestro replayBackend with the byte-identical INVALID_ARGS message from main, before any inspection or session work. Handler-level regression test proves zero step dispatch (counterfactual: without the guard, the script executed); a companion test pins that maestro still routes.

What moved / stayed (final)

  • Package: step loop + verify-and-dispatch, inspect/manifest, digest/resume internals, verification policy, selector-port type family.
  • ad-script: annotation identity vocabulary, binding classification, ${VAR} vars module (shared with the Maestro path).
  • Daemon (root): request admission, invalid-backend gate, P4b coordinator (sole transaction owner, ownership test untouched), capture/dispatch/publication/artifacts, wire builders consuming neutral evidence values, Maestro dispatch and format.ts routing (above both engines), report-action + suggestion-ranking, sanitizeIdentity/describeCandidate (pinned by snapshot-lines), target-identity-node.ts/target-evidence-tree.ts (shared with dispatch/recording).

Rebase: #1554's keep-session absorbed into the engine (head e6cbe6b76)

After #1554 merged, this branch rebased onto main and folded its terminal-lifecycle policy into the P5 architecture rather than keeping a parallel decider: resolveSuppressedTerminalCloseIndex/countExecutedReplayActions unified with the engine's repair terminal-close predicate inside the step loop (one OR'd suppression condition, keepSession || runtime.isRepairArmed(), checked dynamically after armStep so a first-time --save-script arm is visible); AdReplayRunRequest gained one field (keepSession); the daemon's session-replay-terminal-lifecycle.ts module is deleted (no duplicate isExecutableReplayAction anywhere); the SessionStore postcondition stays daemon-side. #1554's six unit tests pass unchanged end-to-end, plus five new package-internal runAdReplay tests cover the unified policy directly. The replayed count is now a per-dispatched-step counter (fixing the old approximation that over-counted nested-replay markers).

Decomposition: the daemon adapter is now four cohesive modules

session-replay-runtime.ts went from ~1100 lines (post-rebase) to 242 — thin orchestration only. Extracted along its natural seams: session-replay-runtime-engine-adapter.ts (473: the runtime-bag capabilities, build*Failure implementations, side-map mechanics), session-replay-runtime-plan.ts (261: backend validation, manifest inspection, resume-index resolution, Maestro routing), session-replay-runtime-session.ts (219: session preparation, repair preflight, save-script arming). Coordinator construction stays solely in the orchestrator — the ownership test passes untouched, no allowlist changes.

Two load-bearing ordering invariants discovered during the extraction are now pinned by counterfactual-verified tests: post-dispatch mismatch divergences report the pre-step artifact snapshot (engine test; counterfactual red showed the failed dispatch's own artifacts leaking in), and a rejected --from/--plan-digest never reaches prepareReplaySession's coordinator-mutating writes (plan test; counterfactual red showed pendingRecordAndHeal being cleared before the failure).

Gates (re-run at every stage; latest full chain at head e6cbe6b76)

typecheck / lint / format:check / check:layering (53 tests incl. the new exact-symbol gate) / check:replay-compat (10 mined scripts, 6 tags, 12 digest-pinned entries) / fallow — green. Full vitest: 5351/5352 at the last full run with the only failures being the documented contention-timeout class (isolate-rerun green; the two pid-liveness assertion races are fixed separately in #1556). Fallow baseline: one surgical 8-line addition for the relocated in-memory adapter; full regen deliberately rejected (would silently drop 12 unrelated stale entries).

Live evidence (exact head 947b90ca3, round 2)

Standard suites:

Leg Scenario Result Time
Android (Pixel_7_CI, Release APK, android-helper 0.20.3 probe-verified) checkout-form-android.ad PASS 23.7s
Android gesture-lab-android.ad PASS 12.3s
iOS (iPhone 17 Pro sim) gesture-lab.ad PASS 31.0s
iOS checkout-form.ad BLOCKED — #1542 (known; one clean attempt; Android twin passes, engine exonerated) 15.1s

Extended evidence (Android, constructed via the CLI's own record/replay loop; verbatim log in artifacts):

  • target-v1 + save-script: recorded flow with open --save-script; saved script carries # agent-device:target-v1 {"id":"refresh-metrics","role":"button",...,"verification":"verified"}. (Finding: annotations require arming at openclose --save-script alone yields selector chains without target-v1 evidence, per session-open-surface.ts arming semantics.)
  • Verification green path: replay of the annotated script — 6 steps, clean pass.
  • Divergence red path: replay against the wrong screen → REPLAY_DIVERGENCE, classification selector-miss (matchCount 0) — recorded target evidence did not verify, with a record-and-heal repair suggestion including --from 3 --plan-digest 8ff7b932….
  • Resume: replay --from 3 --plan-digest <hash> after correcting the screen — Replayed 2 steps in 0.8s, completed cleanly.
  • Save-script repair: full record-and-heal loop — armed diverging replay, live-corrected via a blessed @ref, resumed, explicit close --save-script committed the repair; repaired script ends with # agent-device:heal-complete, carries fresh "verification":"verified" annotations for every step, and replays green (6 steps, 4.0s). Behavioral observation (informational, consistent with the repair-transaction commit boundary): an armed session's --from resume does not auto-run a trailing scripted bare close; the explicit close commits.

Artifacts: /private/tmp/ad-p5-live-artifacts-r2/ (28 files incl. the verbatim command log, original/divergence/repaired scripts, per-attempt replay trees). Round-1 artifacts at the pre-review head remain in /private/tmp/ad-p5-live-artifacts/.

Residual risks

Generated by Claude Code

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-1555/

Built to branch gh-pages at 2026-08-03 07:31 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.93 MB 1.94 MB +7.2 kB
JS gzip 619.2 kB 621.1 kB +1.9 kB
npm tarball 738.9 kB 740.9 kB +1.9 kB
npm unpacked 2.59 MB 2.60 MB +7.3 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.7 ms 27.1 ms -0.5 ms
CLI --help 66.1 ms 65.5 ms -0.7 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/session.js +6.7 kB +1.8 kB
dist/src/internal/daemon.js -9 B -4 B
dist/src/selector-runtime.js 0 B -4 B
dist/src/interaction.js 0 B -3 B
dist/src/viewport-dimension.js 0 B -1 B

@thymikee

thymikee commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Review verdict: request changes / not ready at 62cde8db9.

  • [P1] Preserve invalid-backend rejection. Before this extraction, parseReplayInput rejected any .ad request whose replayBackend was set to an unknown value with INVALID_ARGS. The new native path falls through resolveReplayFormat and calls inspectAdReplay, which receives no flags; the remaining advisory parse in device selection catches and discards the error. A raw .ad replay with replayBackend: unknown`` can now execute. Restore validation in the authoritative replay path and add a real handler/router regression test proving no step dispatch occurs.

  • [P1] Complete the binding façade instead of documenting deviations. The approved P5 amendment requires the package root to expose only inspectAdReplay and runAdReplay, with parsing, variables, planning, digest/resume, verification, divergence, and neutral outcomes private. packages/ad-replay/src/index.ts instead exports broad vars/digest/identity/verification/ranking/selector policy, and root handlers import those directly. That preserves the ownership smear P5 exists to remove. Move those consumers behind the two entrypoints (or keep genuinely shared recording vocabulary in its proper shared owner), then add an exact exported-symbol shape gate—not only an exports-subpath gate.

  • [P1] Do not smuggle daemon wire failures through a generic. AdReplayStepRuntime<TResponse> is instantiated as AdReplayStepRuntime<DaemonResponse>, and runAdReplay returns {ok:false,response:TResponse}. Hiding the type parameter does not make the outcome neutral: opaque daemon wire/error data crosses and returns through the engine. Replace it with explicit neutral tagged execution/failure outcomes and map them to DaemonResponse only in the daemon adapter; parsing/planning/digest/resume must also occur behind runAdReplay per the accepted façade.

  • [P2] Make the second selector adapter conform. The in-memory adapter ignores ReplaySelectorCandidateOptions, including nodes, while the production adapter uses it for shared-ID demotion. The dual-adapter contract only tests a unique ID, so it cannot prove the binding amendment's shared-ID-demotion cell. Make both adapters honor the same contract and add a shared-ID case that drops the ID candidate on both.

The selector port direction, package dependency direction, coordinator ownership, and current CI are otherwise clean. The PR should remain draft: #1478 still requires both exact-head live replay suites plus target-v1 verification, divergence, resume, and save-script repair evidence; the iOS checkout leg is currently blocked by #1542. No ready-for-human label until the code findings and live-readiness blockers are cleared.

thymikee added a commit that referenced this pull request Aug 2, 2026
thymikee added a commit that referenced this pull request Aug 2, 2026
thymikee added a commit that referenced this pull request Aug 2, 2026
…inspectAdReplay (#1555 review)

P1 "do not smuggle daemon wire failures through a generic": drop the
TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and
handleActionFailure now return neutral tagged AdReplayStepOutcome/
AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay
returns a neutral completed/failed AdReplayRunOutcome. The engine never
holds or returns a DaemonResponse. The daemon adapter
(createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire
response in a local side-map as it builds each neutral outcome, and
runReplayScriptFile reads it back once runAdReplay reports which step
failed, so the final response is byte-identical to before this split.

P1 "parsing/planning/digest/resume must also occur behind runAdReplay":
relocate computeReplayPlanDigest's call site and the --from/--plan-digest
resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's
manifest as planDigest and a resolveEntryIndex closure. Neither is a new
top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing
is preserved exactly (still called eagerly in prepareReplayPlan, before
prepareReplaySession's coordinator-mutating side effects) since moving
resume validation to run inside runAdReplay itself would let a rejected
--from request mutate coordinator/session state first -- a real ordering
hazard, not just a cosmetic one.

computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex
leave the ad-replay façade; request-router-repair-expired.test.ts and
prepareReplayPlan read the digest/resume result off the manifest instead.
thymikee added a commit that referenced this pull request Aug 2, 2026
…replay façade (#1555 review)

P1 "complete the binding façade instead of documenting deviations":
classifyTargetBindingMatch never had a real consumer reachable through
inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time
self-check in session-target-evidence.ts and its replay-time
classification wrapper in session-replay-target-classification.ts) are
daemon files that imported it directly. It interprets TargetAnnotationV1
evidence semantics shared beyond the engine, so it moves to
packages/ad-script alongside target-annotation-identity.ts (new
target-annotation-classification.ts + its test), and both daemon call
sites now import it from there instead of @agent-device/ad-replay.

One deviation remains and is reported rather than papered over per the
review's own instruction: the four target-verification policy functions
(planPreDispatchTargetVerification, planPostResolutionTargetVerification,
deriveReplayTargetGuardMismatchEvidence,
deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type
family stay exported. Their sole caller,
session-replay-target-verification.ts, interleaves these pure decisions
with daemon-only async work (capture, SessionStore, coordinator/resume
stamping, wire shaping) that must stay outside the engine by design;
moving their call sites to live only behind runAdReplay would require
restructuring that whole orchestration into new fine-grained
AdReplayStepRuntime capabilities, which is out of scope for this pass.
See packages/ad-replay/src/index.ts's header comment for the full
reasoning.

P1 "add the reviewer-required exact exported-symbol gate": adds
readNamedExports (scripts/layering/package-boundaries.ts), a small
parser over a façade's `export { .. } from`, `export type { .. } from`,
and direct-declaration forms, and pins @agent-device/ad-replay's exact
21-symbol export list in package-boundaries.test.ts. Plant-verified: a
stray `export const` addition failed the assertion; removed it and the
gate went green again.
thymikee added a commit that referenced this pull request Aug 2, 2026
…#1555 review)

Moves the verify-then-dispatch decision flow into packages/ad-replay's
step loop so the four target-verification policy functions
(plan{PostResolution,PreDispatch}TargetVerification,
derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become
engine-private and leave the ad-replay façade. The daemon
(session-replay-target-verification.ts) shrinks to the narrow
AdReplayStepRuntime capabilities the engine drives: routing
(beginTargetVerification), capture (captureObservation), classification
(classifyTarget), dispatch (dispatchStep), and wire-building
(buildRecordedUnverifiableFailure, buildTargetBindingFailure,
buildPostDispatchTargetBindingFailure). Wire output and replay-compat
stay byte-identical; the exact-symbol façade gate is updated to the
shrunken export list.
@thymikee

thymikee commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Re-review verdict: still request changes at 947b90ca3.

Resolved since 62cde8db9: unknown replay backends are rejected before dispatch; both selector adapters now honor shared-ID demotion; runAdReplay drives the production verification loop and coordinator ownership remains singular.

Remaining blockers:

  • [P1] Enforce the approved two-entrypoint boundary. packages/ad-replay/src/index.ts still exports a broad symbol set (including formatReplaySuccessMessage and selector/runtime types), and the new exact-symbol gate pins those extras instead of the approved inspectAdReplay / runAdReplay façade. Planning/variable assembly also remains daemon-side in session-replay-runtime.ts, though P5 assigns it to the engine.
  • [P1] Keep daemon wire details out of the engine. The adapter passes response.error.details verbatim as Record<string, unknown> and engine policy reads it to derive mismatch evidence. Replace this with an explicit neutral tagged mismatch payload built by the daemon adapter.
  • [P1] Preserve the terminal-executable seam from feat: keep replay session active on request #1554 before integration. This head still tests terminal close against the raw final action index and returns actions.length - entryIndex; trailing non-executable replay markers can therefore dispatch a repair close, and skipped actions are counted. Rebase/sequence with feat: keep replay session active on request #1554 and retain its last-executable-action semantics and postcondition.
  • [P1] Restore the exact-head quality gate. Fallow fails on new dispatchStep / prepareReplayPlan complexity and the 231-line runtime adapter; this is owner-action, not infrastructure. Split by domain question rather than baselining the new debt.

The prior live evidence is for 62cde8db9, not this head. After code blockers clear, run the required exact-head target-v1/divergence/resume/save-script suites; iOS remains explicitly blocked by #1542. Keep the PR draft and do not apply ready-for-human.

@thymikee

thymikee commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

All four findings addressed at head 947b90ca3 (commits 281f33599..947b90ca3), with round-2 live evidence at the exact head — point by point:

P1 invalid-backend rejection — restored at the authoritative point (prepareReplayPlan entry, before inspection or any session work), byte-identical INVALID_ARGS message vs main. Handler-level regression test proves zero step dispatch (counterfactual: with the guard removed, the script executed — expected true to be false); a companion test pins that maestro still routes.

P1 complete the façade — done, no deviations remain. Shared vocabulary went to its proper owner (packages/ad-script): the annotation identity functions, classifyTargetBindingMatch, and vars (shared with the Maestro path); report-action/ranking measured as root-only and moved back to the daemon. Everything else is package-private; parsing/planning/digest/resume run behind the entrypoints (inspectAdReplay's manifest carries planDigest/resolveEntryIndex; resume validation stays eager by design — moving it inside runAdReplay would let a rejected --from mutate coordinator state before failing). The requested exact exported-symbol gate now pins the named export list in package-boundaries.test.ts (plant-verified: a stray export fails with a clear diff).

P1 no wire smuggling — the TResponse generic is gone. The engine returns neutral tagged outcomes; mismatch evidence crosses as values into daemon-side build*Failure capabilities; the daemon adapter keeps a side-map so the client-visible DaemonResponse is the literal same object as before (replay-compat green; the only DaemonResponse mentions inside the package are comments stating its absence). Target verification itself now runs inside the engine step loop via narrow capture/classify/dispatch capabilities, with the per-step order — including the pre-step artifact-snapshot subtlety on post-dispatch mismatches — mapped before/after and preserved.

P2 second adapter conformance — the in-memory adapter honors ReplaySelectorCandidateOptions.nodes with production's exact shared-ID drop semantics (read from build.ts, not guessed); a shared-ID cell now runs on both adapters (9 cells × 2 = 18 green). Counterfactual: reverting the in-memory demotion fails its leg while production stays green.

Live-readiness — round-2 evidence at 947b90ca3 (PR body has the tables; artifacts in /private/tmp/ad-p5-live-artifacts-r2/ incl. a verbatim command log): both standard suites re-ran (Android 2/2; iOS gesture-lab PASS; checkout leg blocked by #1542, one clean attempt, Android twin passing), plus the four named behaviors constructed live via the CLI's own loop — target-v1 annotations recorded and quoted, green-path verification, a real selector-miss divergence with its record-and-heal suggestion, --from/--plan-digest resume, and the full repair loop ending in a # agent-device:heal-complete script that replays green. One informational observation from the repair leg: an armed session's --from resume does not auto-run a trailing scripted bare close (the explicit close --save-script commits) — consistent with the repair-transaction commit boundary, reported rather than worked around.

Remaining ready-for-human blocker: #1542 (iOS checkout leg). The PR stays draft until that's fixed or the corpus is repaired without weakening coverage.

Generated by Claude Code

@thymikee
thymikee marked this pull request as ready for review August 2, 2026 16:34
@thymikee
thymikee marked this pull request as draft August 2, 2026 16:39
@thymikee
thymikee force-pushed the p5/extract-ad-replay branch from 5a4f4c2 to e6cbe6b Compare August 2, 2026 17:26
thymikee added a commit that referenced this pull request Aug 2, 2026
thymikee added a commit that referenced this pull request Aug 2, 2026
thymikee added a commit that referenced this pull request Aug 2, 2026
…inspectAdReplay (#1555 review)

P1 "do not smuggle daemon wire failures through a generic": drop the
TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and
handleActionFailure now return neutral tagged AdReplayStepOutcome/
AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay
returns a neutral completed/failed AdReplayRunOutcome. The engine never
holds or returns a DaemonResponse. The daemon adapter
(createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire
response in a local side-map as it builds each neutral outcome, and
runReplayScriptFile reads it back once runAdReplay reports which step
failed, so the final response is byte-identical to before this split.

P1 "parsing/planning/digest/resume must also occur behind runAdReplay":
relocate computeReplayPlanDigest's call site and the --from/--plan-digest
resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's
manifest as planDigest and a resolveEntryIndex closure. Neither is a new
top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing
is preserved exactly (still called eagerly in prepareReplayPlan, before
prepareReplaySession's coordinator-mutating side effects) since moving
resume validation to run inside runAdReplay itself would let a rejected
--from request mutate coordinator/session state first -- a real ordering
hazard, not just a cosmetic one.

computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex
leave the ad-replay façade; request-router-repair-expired.test.ts and
prepareReplayPlan read the digest/resume result off the manifest instead.
thymikee added a commit that referenced this pull request Aug 2, 2026
…replay façade (#1555 review)

P1 "complete the binding façade instead of documenting deviations":
classifyTargetBindingMatch never had a real consumer reachable through
inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time
self-check in session-target-evidence.ts and its replay-time
classification wrapper in session-replay-target-classification.ts) are
daemon files that imported it directly. It interprets TargetAnnotationV1
evidence semantics shared beyond the engine, so it moves to
packages/ad-script alongside target-annotation-identity.ts (new
target-annotation-classification.ts + its test), and both daemon call
sites now import it from there instead of @agent-device/ad-replay.

One deviation remains and is reported rather than papered over per the
review's own instruction: the four target-verification policy functions
(planPreDispatchTargetVerification, planPostResolutionTargetVerification,
deriveReplayTargetGuardMismatchEvidence,
deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type
family stay exported. Their sole caller,
session-replay-target-verification.ts, interleaves these pure decisions
with daemon-only async work (capture, SessionStore, coordinator/resume
stamping, wire shaping) that must stay outside the engine by design;
moving their call sites to live only behind runAdReplay would require
restructuring that whole orchestration into new fine-grained
AdReplayStepRuntime capabilities, which is out of scope for this pass.
See packages/ad-replay/src/index.ts's header comment for the full
reasoning.

P1 "add the reviewer-required exact exported-symbol gate": adds
readNamedExports (scripts/layering/package-boundaries.ts), a small
parser over a façade's `export { .. } from`, `export type { .. } from`,
and direct-declaration forms, and pins @agent-device/ad-replay's exact
21-symbol export list in package-boundaries.test.ts. Plant-verified: a
stray `export const` addition failed the assertion; removed it and the
gate went green again.
thymikee added a commit that referenced this pull request Aug 2, 2026
…#1555 review)

Moves the verify-then-dispatch decision flow into packages/ad-replay's
step loop so the four target-verification policy functions
(plan{PostResolution,PreDispatch}TargetVerification,
derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become
engine-private and leave the ad-replay façade. The daemon
(session-replay-target-verification.ts) shrinks to the narrow
AdReplayStepRuntime capabilities the engine drives: routing
(beginTargetVerification), capture (captureObservation), classification
(classifyTarget), dispatch (dispatchStep), and wire-building
(buildRecordedUnverifiableFailure, buildTargetBindingFailure,
buildPostDispatchTargetBindingFailure). Wire output and replay-compat
stay byte-identical; the exact-symbol façade gate is updated to the
shrunken export list.
thymikee added a commit that referenced this pull request Aug 2, 2026
…les (#1555)

Splits the ~1096-line replay runtime into cohesive pieces, keeping
session-replay-runtime.ts as thin orchestration (~240 LOC):

- session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime
  adapter (createAdReplayStepRuntime, the build*Failure capability
  implementations, and the lastResponse/lastObservation side-map
  mechanics), extracted verbatim.
- session-replay-runtime-plan.ts: extended with the plan-side helpers
  (validateReplayBackendFlag, inspectReplayPlanManifest,
  resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay)
  alongside the buildReplayMetadataFlags helper already there —
  buildReplayMetadataFlags is now module-private since its one caller
  moved into the same file. Also introduces ReplayScriptFileParams,
  named here (instead of derived via Parameters<typeof
  runReplayScriptFile>) so routeMaestroReplay can reference the shape
  without importing back from session-replay-runtime.ts.
- session-replay-runtime-session.ts (new): session preparation
  (prepareReplaySession and its coordinator arming/repair-preflight
  helpers), extracted verbatim.

Coordinator ownership is unchanged: createReplayCoordinator is still
constructed only in session-replay-runtime.ts, matching
replay-coordinator-ownership.test.ts's allowlist as-is — every
extracted module receives the already-constructed ReplayCoordinator as
a parameter. Pure move; no behavior change.
thymikee added a commit that referenced this pull request Aug 2, 2026
…tion (#1555)

Two invariants found during the P5 decomposition pass now have direct
counterfactual-verified coverage:

- packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a
  post-dispatch target-binding mismatch (dispatchWithGuard) must report
  the accumulated PRE-step artifact snapshot it was called with, never
  the artifacts the failed dispatch itself produced. Verified red by
  swapping the buildPostDispatchTargetBindingFailure call to
  outcome.artifactPaths.

- src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a
  rejected --from/--plan-digest resume must never reach
  prepareReplaySession's coordinator-mutating writes (the R2 ordering
  invariant) — a pre-armed repair transaction and corrective-resume
  watermark are asserted byte-for-byte unchanged after rejection.
  Verified red by calling prepareReplaySession before honoring the
  plan-validation rejection.
@thymikee

thymikee commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Resync complete at head e6cbe6b76, all CI green — requesting re-review.

Since the redraft: rebased onto main absorbing #1554 (its terminal-lifecycle policy folded INTO the engine step loop, unified with the repair terminal-close predicate as one suppression condition; the daemon module deleted; all six #1554 unit tests pass unchanged end-to-end, plus five new package-internal runAdReplay tests on the unified policy). Then the maintainer-approved decomposition: session-replay-runtime.ts is now 242 lines of orchestration over three extracted modules (engine adapter 473, plan 261, session prep 219); coordinator construction unmoved, ownership test untouched. Both load-bearing ordering invariants found during the extraction are pinned with counterfactual-verified tests: pre-step artifact snapshot on post-dispatch mismatches (red showed the failed dispatch's artifacts leaking), and rejected --from never reaching coordinator-mutating writes (red showed pendingRecordAndHeal cleared before failure).

Round-3 live evidence at exact head e6cbe6b76 (artifacts /private/tmp/ad-p5-live-artifacts-r3/): Android suite 2/2 (checkout 20 steps 12.7s, gesture-lab 32 steps 20.3s, helper backend/version probe-verified); iOS gesture-lab 31 steps PASS; iOS checkout blocked by still-open #1542 (one clean attempt; partial fix for it is up separately as #1559 with the remaining stabilization defect awaiting a design decision). NEW: keep-session live check--keep-session replay ran 19/20 steps (authored terminal close suppressed), session verifiably alive afterward with populated app state, explicit close clean — proving #1554's feature rides the extracted engine. Divergence/resume spot-check: mutated script → REPLAY_DIVERGENCE selector-miss with repair hint, live correction, --from 12 --plan-digest resume completed 9 steps green.

Prior review findings all addressed in 281f33599..947b90ca3 (see the point-by-point comment above); the façade carries zero deviations with the exact-symbol gate enforcing it. Remaining ready-for-human blocker: #1542's second defect (post-gesture stabilization semantics — options documented in #1559 for a maintainer decision).

Generated by Claude Code

@thymikee

thymikee commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Review findings at e6cbe6b:\n\n- [P1] Enforce the accepted two-entrypoint facade. The approved P5 amendment says packages/ad-replay exports only inspectAdReplay and runAdReplay; the current facade additionally exports formatReplaySuccessMessage, runtime/outcome types, and the full selector-port family, and the new exact-symbol gate blesses that widening. The gate also ignores export-star declarations, so it can miss future widening. Keep success formatting daemon-side, hide internal capability/selector types behind entrypoint signatures, and make the gate reject every export form.\n- [P1] Translate wire failures before the engine boundary. AdReplayDispatchOutcome carries details as a generic unknown-valued record, the daemon adapter assigns response.error.details verbatim, and engine policy parses that bag. This is still daemon wire projection crossing into the engine despite the PR's neutral-outcomes/no-generic claim. Narrow each mismatch in the adapter into explicit tagged evidence values.\n- [P1] Move variable semantics/planning behind the replay entrypoint. The daemon still assembles ReplayVarScope, interpolates actions in invokeReplayAction, and independently interpolates target verification. P5 assigns variables and planning to ad-replay; leaving these paths daemon-owned preserves duplicated orchestration/semantics.\n- [P1] Keep this draft pending exact-head live evidence. The PR body's live corpus is for 947b90c, not current head e6cbe6b, and the prescribed iOS checkout leg remains blocked by #1542. Re-run the full required corpus on the corrected exact head before readiness.\n\nThe #1554 terminal-close fold-in and current CI checks look sound, but the accepted P5 boundary/readiness gates are not yet met.

thymikee added a commit that referenced this pull request Aug 3, 2026
… P1)

packages/ad-replay/src/index.ts now exports exactly two value symbols,
inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage
(presentation) moves beside its one caller in session-replay-runtime.ts, and
every type a root daemon file needs is derived structurally off the two
entrypoints in the one new src/daemon/ad-replay-facade-types.ts module
instead of being named off the façade.

scripts/layering/package-boundaries.ts's readNamedExports is rewritten on
oxc-parser's own static-export table instead of a regex, so it can no longer
silently miss a widening export form: a bare `export *` re-export or an
`export default` now throws (an un-enumerable, and therefore un-pinnable,
export), while `export * as ns` and every other enumerable form is still
counted. The pinned exact-symbol assertion in package-boundaries.test.ts is
narrowed to ['inspectAdReplay', 'runAdReplay'].
thymikee added a commit that referenced this pull request Aug 3, 2026
…1555 review P1)

AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried
a generic `details: Record<string, unknown> | undefined` bag straight off
the wire response — a daemon wire projection crossing into the engine even
though the outcome itself was already a neutral type. The daemon adapter
(session-replay-runtime-engine-adapter.ts) now narrows that bag into the
typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes
(observed identity, expected/observed structural denotation, ancestry
entries, match count) before returning the outcome; the unknown-parsing
readers move there with the wire-reading responsibility they always were.
target-verification.ts's deriveReplayTargetGuardMismatchEvidence/
deriveWaitLandmarkMismatchEvidence now consume only the typed values — no
`unknown`-valued record type remains on any engine-crossing signature.
thymikee added a commit that referenced this pull request Aug 3, 2026
…1555 review P1)

The daemon assembled the `${VAR}` scope (buildPreparedReplayScope) and
interpolated actions at two independent call sites: dispatch's own
(invokeReplayAction) and target verification's separate one
(resolveTargetVerificationEntry) — duplicated orchestration the P5 design
assigns to the engine.

runAdReplay's request now carries the raw scope INPUTS (varSources: plain
builtins/file/shell/cli-env data, plus actionLines/actionSourcePaths/
resolvedPath for interpolation-error location) instead of a built scope; the
engine builds the scope and resolves each action exactly once per step,
handing the RESOLVED action to dispatchStep/beginTargetVerification while
every other capability still receives the ORIGINAL recorded action (a
target-binding divergence reports the recorded selector, never an expanded
${VAR}). This is the one resolution site now — session-replay-action-runtime.ts's
invokeReplayAction and session-replay-target-verification.ts's
resolveTargetVerificationEntry no longer hold a scope or call
resolveReplayAction themselves.

Scrub-value collection (collectReplayScrubbableVarValues, for divergence-report
redaction) is kept single-sourced in the engine too: it's computed from the
engine's own live scope and threaded to each build-failure/handleActionFailure
capability as an explicit scrubVars argument, rather than the daemon
recomputing it from a second scope object (which would have gone stale,
since expandedBuiltinNames tracking now only happens engine-side).

The Maestro replay path's own daemon-side vars usage is unrelated (a
different engine) and is out of scope here.
thymikee added 20 commits August 3, 2026 09:12
…s/ad-replay

Stage A of the #1478 P5 extraction: vars, plan-digest (+canonical-json,
sole consumer), the target-identity classification core, report-action,
and suggestion-ranking move verbatim; imports updated. The package facade
temporarily re-exports the moved symbols so root consumers keep compiling;
a later stage narrows it to inspectAdReplay/runAdReplay only.
…inspectAdReplay (#1555 review)

P1 "do not smuggle daemon wire failures through a generic": drop the
TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and
handleActionFailure now return neutral tagged AdReplayStepOutcome/
AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay
returns a neutral completed/failed AdReplayRunOutcome. The engine never
holds or returns a DaemonResponse. The daemon adapter
(createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire
response in a local side-map as it builds each neutral outcome, and
runReplayScriptFile reads it back once runAdReplay reports which step
failed, so the final response is byte-identical to before this split.

P1 "parsing/planning/digest/resume must also occur behind runAdReplay":
relocate computeReplayPlanDigest's call site and the --from/--plan-digest
resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's
manifest as planDigest and a resolveEntryIndex closure. Neither is a new
top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing
is preserved exactly (still called eagerly in prepareReplayPlan, before
prepareReplaySession's coordinator-mutating side effects) since moving
resume validation to run inside runAdReplay itself would let a rejected
--from request mutate coordinator/session state first -- a real ordering
hazard, not just a cosmetic one.

computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex
leave the ad-replay façade; request-router-repair-expired.test.ts and
prepareReplayPlan read the digest/resume result off the manifest instead.
…replay façade (#1555 review)

P1 "complete the binding façade instead of documenting deviations":
classifyTargetBindingMatch never had a real consumer reachable through
inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time
self-check in session-target-evidence.ts and its replay-time
classification wrapper in session-replay-target-classification.ts) are
daemon files that imported it directly. It interprets TargetAnnotationV1
evidence semantics shared beyond the engine, so it moves to
packages/ad-script alongside target-annotation-identity.ts (new
target-annotation-classification.ts + its test), and both daemon call
sites now import it from there instead of @agent-device/ad-replay.

One deviation remains and is reported rather than papered over per the
review's own instruction: the four target-verification policy functions
(planPreDispatchTargetVerification, planPostResolutionTargetVerification,
deriveReplayTargetGuardMismatchEvidence,
deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type
family stay exported. Their sole caller,
session-replay-target-verification.ts, interleaves these pure decisions
with daemon-only async work (capture, SessionStore, coordinator/resume
stamping, wire shaping) that must stay outside the engine by design;
moving their call sites to live only behind runAdReplay would require
restructuring that whole orchestration into new fine-grained
AdReplayStepRuntime capabilities, which is out of scope for this pass.
See packages/ad-replay/src/index.ts's header comment for the full
reasoning.

P1 "add the reviewer-required exact exported-symbol gate": adds
readNamedExports (scripts/layering/package-boundaries.ts), a small
parser over a façade's `export { .. } from`, `export type { .. } from`,
and direct-declaration forms, and pins @agent-device/ad-replay's exact
21-symbol export list in package-boundaries.test.ts. Plant-verified: a
stray `export const` addition failed the assertion; removed it and the
gate went green again.
…#1555 review)

Moves the verify-then-dispatch decision flow into packages/ad-replay's
step loop so the four target-verification policy functions
(plan{PostResolution,PreDispatch}TargetVerification,
derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become
engine-private and leave the ad-replay façade. The daemon
(session-replay-target-verification.ts) shrinks to the narrow
AdReplayStepRuntime capabilities the engine drives: routing
(beginTargetVerification), capture (captureObservation), classification
(classifyTarget), dispatch (dispatchStep), and wire-building
(buildRecordedUnverifiableFailure, buildTargetBindingFailure,
buildPostDispatchTargetBindingFailure). Wire output and replay-compat
stay byte-identical; the exact-symbol façade gate is updated to the
shrunken export list.
… into the ad-replay engine

Rebasing p5/extract-ad-replay onto main pulled in #1554's --keep-session
feature, which had grown its own daemon-side terminal-close-suppression
predicate (session-replay-terminal-lifecycle.ts's
resolveSuppressedTerminalCloseIndex/countExecutedReplayActions) independently
of this branch's own engine-side one (step-loop.ts's
isRepairArmedTerminalCloseAction). Both are the same decision family — replay
--keep-session and an active --save-script repair now share ONE structural
resolution (resolveSuppressedTerminalCloseIndex, generalized to "terminal
among EXECUTABLE actions" rather than the old physical-last-index check) and
one suppression check inside runAdReplay, gated on keepSession OR
runtime.isRepairArmed(). AdReplayRunRequest grew a keepSession field; the
neutral 'replayed' count in AdReplayRunOutcome is now computed inline in the
loop instead of the daemon's old actions.length - entryIndex approximation.

requireLiveSessionForKeepSession (the --keep-session live-session
postcondition) stays daemon-side, inlined into session-replay-runtime.ts,
since it inspects SessionStore state the engine never sees. The daemon-only
session-replay-terminal-lifecycle.ts this arrived with is deleted entirely —
its isExecutableReplayAction was a duplicate of the engine's own.

runReplayScriptFile's Maestro-format routing (including the new --keep-session
Maestro rejection) was extracted into routeMaestroReplay to keep the function
under fallow's complexity threshold after re-threading keepSession through it.

Added packages/ad-replay/src/internal/__tests__/step-loop.test.ts covering the
unified suppression decision (both keepSession and repair-armed) directly
against runAdReplay, including the terminal-among-executable-actions case with
a trailing nested replay marker. The daemon-level integration tests (6 tests
in session-replay-terminal-lifecycle.test.ts, exercising the same behavior
through runReplayScriptFile) and the SDK provider-scenario test
(active-session-script-publication.test.ts) needed no changes and pass
unmodified.
…les (#1555)

Splits the ~1096-line replay runtime into cohesive pieces, keeping
session-replay-runtime.ts as thin orchestration (~240 LOC):

- session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime
  adapter (createAdReplayStepRuntime, the build*Failure capability
  implementations, and the lastResponse/lastObservation side-map
  mechanics), extracted verbatim.
- session-replay-runtime-plan.ts: extended with the plan-side helpers
  (validateReplayBackendFlag, inspectReplayPlanManifest,
  resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay)
  alongside the buildReplayMetadataFlags helper already there —
  buildReplayMetadataFlags is now module-private since its one caller
  moved into the same file. Also introduces ReplayScriptFileParams,
  named here (instead of derived via Parameters<typeof
  runReplayScriptFile>) so routeMaestroReplay can reference the shape
  without importing back from session-replay-runtime.ts.
- session-replay-runtime-session.ts (new): session preparation
  (prepareReplaySession and its coordinator arming/repair-preflight
  helpers), extracted verbatim.

Coordinator ownership is unchanged: createReplayCoordinator is still
constructed only in session-replay-runtime.ts, matching
replay-coordinator-ownership.test.ts's allowlist as-is — every
extracted module receives the already-constructed ReplayCoordinator as
a parameter. Pure move; no behavior change.
…tion (#1555)

Two invariants found during the P5 decomposition pass now have direct
counterfactual-verified coverage:

- packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a
  post-dispatch target-binding mismatch (dispatchWithGuard) must report
  the accumulated PRE-step artifact snapshot it was called with, never
  the artifacts the failed dispatch itself produced. Verified red by
  swapping the buildPostDispatchTargetBindingFailure call to
  outcome.artifactPaths.

- src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a
  rejected --from/--plan-digest resume must never reach
  prepareReplaySession's coordinator-mutating writes (the R2 ordering
  invariant) — a pre-armed repair transaction and corrective-resume
  watermark are asserted byte-for-byte unchanged after rejection.
  Verified red by calling prepareReplaySession before honoring the
  plan-validation rejection.
… P1)

packages/ad-replay/src/index.ts now exports exactly two value symbols,
inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage
(presentation) moves beside its one caller in session-replay-runtime.ts, and
every type a root daemon file needs is derived structurally off the two
entrypoints in the one new src/daemon/ad-replay-facade-types.ts module
instead of being named off the façade.

scripts/layering/package-boundaries.ts's readNamedExports is rewritten on
oxc-parser's own static-export table instead of a regex, so it can no longer
silently miss a widening export form: a bare `export *` re-export or an
`export default` now throws (an un-enumerable, and therefore un-pinnable,
export), while `export * as ns` and every other enumerable form is still
counted. The pinned exact-symbol assertion in package-boundaries.test.ts is
narrowed to ['inspectAdReplay', 'runAdReplay'].
…1555 review P1)

AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried
a generic `details: Record<string, unknown> | undefined` bag straight off
the wire response — a daemon wire projection crossing into the engine even
though the outcome itself was already a neutral type. The daemon adapter
(session-replay-runtime-engine-adapter.ts) now narrows that bag into the
typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes
(observed identity, expected/observed structural denotation, ancestry
entries, match count) before returning the outcome; the unknown-parsing
readers move there with the wire-reading responsibility they always were.
target-verification.ts's deriveReplayTargetGuardMismatchEvidence/
deriveWaitLandmarkMismatchEvidence now consume only the typed values — no
`unknown`-valued record type remains on any engine-crossing signature.
…1555 review P1)

The daemon assembled the `${VAR}` scope (buildPreparedReplayScope) and
interpolated actions at two independent call sites: dispatch's own
(invokeReplayAction) and target verification's separate one
(resolveTargetVerificationEntry) — duplicated orchestration the P5 design
assigns to the engine.

runAdReplay's request now carries the raw scope INPUTS (varSources: plain
builtins/file/shell/cli-env data, plus actionLines/actionSourcePaths/
resolvedPath for interpolation-error location) instead of a built scope; the
engine builds the scope and resolves each action exactly once per step,
handing the RESOLVED action to dispatchStep/beginTargetVerification while
every other capability still receives the ORIGINAL recorded action (a
target-binding divergence reports the recorded selector, never an expanded
${VAR}). This is the one resolution site now — session-replay-action-runtime.ts's
invokeReplayAction and session-replay-target-verification.ts's
resolveTargetVerificationEntry no longer hold a scope or call
resolveReplayAction themselves.

Scrub-value collection (collectReplayScrubbableVarValues, for divergence-report
redaction) is kept single-sourced in the engine too: it's computed from the
engine's own live scope and threaded to each build-failure/handleActionFailure
capability as an explicit scrubVars argument, rather than the daemon
recomputing it from a second scope object (which would have gone stale,
since expandedBuiltinNames tracking now only happens engine-side).

The Maestro replay path's own daemon-side vars usage is unrelated (a
different engine) and is out of scope here.
@thymikee
thymikee force-pushed the p5/extract-ad-replay branch from 717b0e4 to a554fae Compare August 3, 2026 07:18
CodeQL flagged the interpolation regex's fallback group as js/polynomial-redos
once vars.ts moved into packages/ (library-input classification): every
${NAME:- prefix of an unclosed input rescanned to end-of-string, quadratic
overall — 1,857 ms measured on 20k repetitions of '${A:-['. Replaced with a
single-pass scanner; failed fallback scans emit their span verbatim and resume
after it (escape-pair alignment is identical from every candidate start inside
the span, so no later candidate can terminate where the failed scan could not).
Equivalence: 200k-trial differential fuzz against the retired regex over the
adversarial alphabet, zero mismatches; both adversarial shapes now resolve in
1-2 ms.
@thymikee

thymikee commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

All four findings from the latest review addressed; branch reconciled onto main; all CI green at head bfa4b04a6; full corpus re-run at this exact head.

P1 façadepackages/ad-replay/src/index.ts exports exactly inspectAdReplay and runAdReplay: two value symbols, zero type exports. formatReplaySuccessMessage is daemon-private again. Root derives every type structurally from the entrypoint signatures in one module (src/daemon/ad-replay-facade-types.ts: Parameters<typeof runAdReplay>[1], ReturnType<typeof inspectAdReplay>, the port family off AdReplayStepRuntime['port']). The gate is rewritten on oxc-parser's static-export table and rejects every export form — bare export * and default exports throw; plant-verified (Missing expected exception... /export \* from/ when the rejection was disabled).

P1 wire translationAdReplayDispatchOutcome no longer carries a details: Record<string, unknown> bag: the adapter narrows response.error.details into explicit tagged evidence (AdReplayGuardMismatchEvidence / AdReplayLandmarkMismatchEvidence — typed identity/ancestry/structural values); the unknown-parsing readers moved out of the package; engine policy consumes typed values only. The only Record<string, unknown> mentions left in the package are comments documenting its absence.

P1 variables/planning behind the entrypoint — the daemon passes plain varSources data on the run request; the engine builds the scope and resolves each action exactly once, handing resolved actions to dispatchStep/verification. invokeReplayAction and target verification no longer interpolate; scrub-value collection is engine-computed and threaded as an explicit argument (single source, no stale second scope). Maestro's own vars usage untouched, as scoped.

Reconcile + one new finding fixed en route — rebased onto main absorbing #1558/#1559 (zero conflicts; android-lifecycle's armed-at-open scenario passes against the engine, proving the #1558 gate composes). CodeQL then flagged the ${VAR:-fallback} interpolation regex (js/polynomial-redos — surfaced by vars.ts's move into packages/; 1,857 ms measured on 20k adversarial repetitions). Replaced with a single-pass linear scanner: 200k-trial differential fuzz against the retired regex over the adversarial alphabet, zero mismatches; adversarial inputs now 1–2 ms; passthrough + adversarial regression tests added.

P1 exact-head live evidence at bfa4b04a6 (artifacts /private/tmp/ad-p5-live-artifacts-final/): Android suite 2/2 (checkout 20 steps, gesture-lab 32; helper backend/version probe-verified); iOS gesture-lab 31 steps PASS; iOS checkout-form fails on #1542 defect 2 only (both known manifestations reproduced across two runs — stale-but-fast AX at the step-11 click, AX-deferral wedge — pre-existing, documented in #1559, unrelated to this branch). Extended set at this head: target-v1 annotation quoted; recorded-script replay green; wrong-screen REPLAY_DIVERGENCE with repair hint; --from/--plan-digest resume green; --keep-session suppression with live-session proof and clean explicit close; unarmed close --save-script rejects with the #1558 recovery hint (compose proof); and a live ${VAR} interpolation leg proving the new scanner's substitution and fallback paths in actual device state (field-name → 'Ada Lovelace', field-email → 'fallback@example.com').

Remaining readiness blocker is unchanged and external to this branch: #1542 defect 2 (post-gesture stabilization semantics — options awaiting a maintainer decision in #1559's body). Everything else in the ready-gate is now green at the exact head.

Generated by Claude Code

@thymikee

thymikee commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Re-review at bfa4b04: the three code P1s are resolved. The package facade now exports only inspectAdReplay/runAdReplay with an AST-based gate that rejects unbounded/default exports; wire error.details is narrowed into typed mismatch evidence in the daemon adapter; and variable scope/interpolation now runs once inside runAdReplay. Exact-head CI is green, and no new code finding was found.\n\nReadiness remains blocked:\n\n- [P1 validation] The prescribed iOS checkout leg is still red on #1542 defect 2. #1559 merged only the first defect and explicitly left stale-AX stabilization unresolved, so P5 step 7 is not satisfied. Keep this draft and do not apply ready-for-human until that corpus is green (or the accepted gate is deliberately changed without weakening coverage).\n- [P1 evidence] Preserve the claimed exact-head extended evidence. /private/tmp/ad-p5-live-artifacts-final/ contains the suite JUnit files and three recorded scripts, but no command log/results substantiating the claimed target-v1 replay, divergence, resume, repair, keep-session, and variable-execution legs. Attach/preserve those outputs or put verifiable result excerpts in the PR before readiness.\n\nAlso refresh the PR body: it still describes the old expanded facade and older validation heads, contradicting the current implementation and latest response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant