Skip to content

refactor(maestro): shape the replay backend as a create(host) plugin - #3623

Merged
thymikee merged 5 commits into
mainfrom
refactor/maestro-replay-backend-plugin
Oct 15, 2026
Merged

thymikee merged 5 commits into
mainfrom
refactor/maestro-replay-backend-plugin

Conversation

@thymikee

@thymikee thymikee commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Second half of the #3377 replay-backend prep. Builds on #3622 (merged); based on main. Behavior, wire shapes, help text and error wording are unchanged; the backend still resolves once per process (ADR 0015).

  • The Maestro adapter moved from replay-port into packages/maestro/src/replay-backend/ and ships as ReplayBackendPlugin = { create(host) } (@agent-device/maestro/replay-backend), with an engine-free declaration (.../replay-backend-declaration).
  • ReplayBackendHost (built in replay-backend-host.ts) is the only way the adapter reaches process state or daemon policy: trace append, builtin vars, script-source read, target-device resolution, request signal, and the divergence-failure builder from refactor(replay-port): own the replay backend contract in contracts #3622.
  • The registry loads the plugin through a function-scoped import and calls create(host) once. replay-backend-declarations.ts holds the declaration table, so the registry's eager closure (and every daemon route that loads it) does not grow. Both tables are mapped over ReplayBackendId, so a declaration or plugin under another backend's id fails to type-check.
  • help maestro reads its topic from the declaration, so cli-help no longer imports the engine.
  • Failure tests now drive the plugin through getReplayBackend('maestro'); two warnings checks became unit tests.
  • Dropped the unused ./daemon-runtime-port export.
  • Only the two replay-port files above name @agent-device/maestro in production (scripts/fuzz still does).

32 files touched. Sign-off needed: the APPROVED_OVER_CEILING row for replay-maestro-backend.ts names thymikee as owner, so it needs his approval.

Refs #3377.

Validation

Tested SHA: 36d06fa (pnpm check:affected --run).

  • pnpm check:affected --base <layer-1 branch> --run: all runnable checks passed.
  • pnpm check:quick and focused vitest (replay-port, maestro, daemon sessions, CLI help/MCP, startup closure, closure budgets) passed.
  • Diff against the layer-1 branch: +607/−314. A -M90% stat shows more because several moved files are 80–91% similar after the host rewrite.
  • Coverage, provider integration and maestro-differential are CI-authoritative.

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 32 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/__tests__/eager-closure-budgets.ts">

<violation number="1" location="scripts/__tests__/eager-closure-budgets.ts:140">
P3: This allowance row is authored in the same PR that uses it, and the gate only checks that an `owner` string exists. Please record the owner's sign-off before merge, for example in the PR approval, since the diff itself does not show it.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

issue: '#3377',
reason:
'The Maestro replay backend plugin IS the engine behind `replay --maestro`; the registry reaches it only through a function-scoped import(), and no startup hub evaluates it (cli-startup-import-closure pins that).',
owner: 'thymikee',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This allowance row is authored in the same PR that uses it, and the gate only checks that an owner string exists. Please record the owner's sign-off before merge, for example in the PR approval, since the diff itself does not show it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/__tests__/eager-closure-budgets.ts, line 140:

<comment>This allowance row is authored in the same PR that uses it, and the gate only checks that an `owner` string exists. Please record the owner's sign-off before merge, for example in the PR approval, since the diff itself does not show it.</comment>

<file context>
@@ -132,7 +132,14 @@ export const NEW_ENTRY_CEILINGS: Readonly<Record<EntryCategory, number>> = Objec
+    issue: '#3377',
+    reason:
+      'The Maestro replay backend plugin IS the engine behind `replay --maestro`; the registry reaches it only through a function-scoped import(), and no startup hub evaluates it (cli-startup-import-closure pins that).',
+    owner: 'thymikee',
+  },
+});
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code change: maintainer approval of this PR is the sign-off for the APPROVED_OVER_CEILING row. Leaving this thread open so thymikee sees it.

@thymikee

Copy link
Copy Markdown
Member Author

The design holds at 5352eef, and one small test fix is needed before merge: the first inline thread below, where a test name promises suggestNodes but the test never calls it. CI is still pending: all 17 lanes were queued or running and none had failed. The lanes this diff touches are Typecheck & Package, Repo Guards and Integration/Smoke, and they decide whether it can merge. I read the diff but did not run the test suites or the closure-budget and layering gates, and I did not measure the eager-closure size of replay-maestro-backend.ts. I also did not check the CLI startup closure numerically after cli-help moved to replay-port/replay-backend-declarations, though reading the imports it should shrink. I checked the moved tests by reading the diff, not with git diff -M90%, because the PR's own rewrite leaves them 57-97% similar.

Not blocking: the host builds resolveTargetDevice (replay-backend-host.ts#L24) with a Maestro rule (appTarget is used only on iOS), and replay-device-selection.ts#L66 applies the same rule when it pre-binds the device. They agree today, so a second backend could make them diverge; a one-line doc on the host member is enough for now, or a declared policy both sites read later. Also, declaredUrl() in maestro-support-matrix.test.ts#L11 reads the URLs from the help body, so the two help assertions at lines 39-40 cannot fail; pinning the literals or exporting the constants would fix that. Take or leave both.

Three inline threads on this PR still apply. The first is the test name that promises suggestNodes. The daemon test at lines 342-475 covers the route end to end, so either assert the suggestion source or drop it from the name. The second is the lost first-event ordering check, where toHaveBeenNthCalledWith(1, ...) restores it. The third is the APPROVED_OVER_CEILING owner row. It needs no code change, because the maintainer's merge approval discharges it.

Before merge, the CI lanes above need to go green and thymikee needs to approve the APPROVED_OVER_CEILING row. After that, retarget the PR to main once #3622 merges. No conflicts.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.25 MB 5.25 MB +950 B
Package (unpacked) 5.25 MB 5.25 MB +950 B
Package (download) 1.58 MB 1.58 MB +450 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.9 ms 25.6 ms -0.3 ms
CLI --help 75.1 ms 77.6 ms +2.5 ms

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-15 09:04 UTC

@thymikee
thymikee force-pushed the refactor/maestro-replay-backend-plugin branch from 5352eef to be07f5b Compare October 14, 2026 18:39
@thymikee

Copy link
Copy Markdown
Member Author

Both non-blocking notes taken in be07f5b: the host's resolveTargetDevice now documents that it applies the Maestro device-binding rule (the app target steers selection on iOS only), the same rule replay-device-selection.ts applies; and maestro-support-matrix.test.ts pins the ADR and issue URL literals, so the two help assertions can fail. The test-name thread, the first-event ordering thread and the closure-reason claim are fixed too; the owner row is left open for sign-off.

@thymikee
thymikee force-pushed the refactor/maestro-replay-backend-plugin branch from be07f5b to a12c6ab Compare October 14, 2026 18:57
@thymikee

Copy link
Copy Markdown
Member Author

The Maestro replay backend now has the create(host) plugin shape. The code at a12c6ab is clean: I found no new problems since the earlier review (#3623 (comment)), and the findings from 5352eef are fixed. I read the code. I did not run the unit suites, the eager-closure budget gate, the layering gate or the startup-closure test, and I did not measure the eager-closure size of replay-maestro-backend.ts, so the over-ceiling margin is unverified. I would like those results before this counts as proven.

The Smoke Tests lane failed in the iOS runner XCTest RunnerTests.testNonReadCommandStillLaunchesANotRunningSessionApp, because the fixture /tmp/agent-device-inject-tap-recorded-failure-for-testing was missing. This looks unrelated: the diff from e59514f to a12c6ab has no Swift, runner or Apple files, and the PR only changes Maestro replay packaging. I judged this only from that lack of overlap. I did not compare it with runs of the same test on main. Please rerun the lane.

One question on simplicity. Nothing reads ReplayBackendDeclaration.apiVersion, and only provider plugins validate theirs (src/plugins/manifest.ts:63). The registry never reads ReplayBackendPlugin.declaration, because help uses the separate REPLAY_BACKEND_DECLARATIONS table. Should apiVersion and plugin.declaration wait for the installed-plugin loader that would check them? Or should the registry be the one reader: derive the help table from the plugins' declarations and assert that declaration.id and backend.id match the registry key? Either way, the id and the declaration would no longer be registered in parallel with nothing keeping them in step.

Of the open review threads, the cubic-dev-ai P3 on scripts/tests/eager-closure-budgets.ts:136-141 still holds (#3623 (comment)). The owner row is self-authored, so it needs no code change, only the owner's approval. The reason text is now true, because isLazyEngineModule covers the plugin subpath. The other two threads are fixed at this head and can be resolved: the failure-suggestion test now compares against collectMaestroFailureSuggestions and asserts a non-empty result (#3623 (comment)), and the observer test uses toHaveBeenNthCalledWith(1, ...) (#3623 (comment)).

No conflicts are known. To merge, the Smoke Tests lane needs a green rerun, the owner must approve the APPROVED_OVER_CEILING row, and the PR must be retargeted to main after #3622 merges.

@thymikee
thymikee added this pull request to stack #3630 October 15, 2026 05:58
Base automatically changed from refactor/maestro-replay-backend-plugin-shape to main October 15, 2026 05:58
@thymikee
thymikee force-pushed the refactor/maestro-replay-backend-plugin branch from a12c6ab to 85d588e Compare October 15, 2026 05:58
@thymikee

Copy link
Copy Markdown
Member Author

At 85d588e the code looks good, but I cannot call it ready: the question from my earlier review (a12c6ab) is still open. All 40 checks pass at 85d588e, including the unit, layering, eager-closure and startup-closure gates that were unverified before. The Smoke Tests failure from a12c6ab is gone, and there are no conflicts. The two Cubic test threads on the failure suggestions and the start-event order are fixed at this head.

Is one registration per backend id enough here? REPLAY_BACKEND_DECLARATIONS (https://github.com/callstack/agent-device/blob/85d588e/packages/replay-port/src/daemon-port/replay-backend-declarations.ts#L11) and BUILTIN_REPLAY_BACKEND_PLUGINS (https://github.com/callstack/agent-device/blob/85d588e/packages/replay-port/src/daemon-port/replay-backend-registry.ts#L33) are two id-keyed tables for one backend. Help reads the first. The registry reads the second and never reads plugin.declaration. Nothing in production checks that the ids and apiVersion agree, so a second backend could show help that does not describe the engine that runs. You could keep { declaration, load } in the registry, let help read from it, and drop ReplayBackendPlugin.declaration. Or you could drop declaration and apiVersion from the plugin type until an installed-plugin loader exists. Is that loader in scope for #3377? If not, please delete the unread fields now.

Not blocking, and you can take or leave it: the duplicated tables above also mean only one identity test for maestro links them.

The open Cubic thread on the APPROVED_OVER_CEILING row in eager-closure-budgets.ts (#3623 (comment)) still applies. It needs no code change, only maintainer sign-off.

I ran no tests locally and read no CI logs; the gate results are from the 40/40 check summary. I did not measure the eager-closure size of replay-maestro-backend.ts, so the over-ceiling margin rests on that gate passing. How help maestro and replay --maestro behave through the CLI is inferred from source and CI, not from a live run. Before merge, answer the registration question above, and then the maintainer needs to approve the over-ceiling row.

…ts id, drop unread fields

Both id-keyed tables are mapped over ReplayBackendId, so a declaration or plugin registered
under another backend's id fails to type-check. The tables stay apart so the daemon routes that
load backends do not evaluate help text. ReplayBackendPlugin.declaration and
ReplayBackendDeclaration.apiVersion had no reader; the installed-plugin loader that would check
them is not part of #3377 yet.
@thymikee

Copy link
Copy Markdown
Member Author

At 85d588e this branch now conflicts with main. Please rebase and resolve the conflicts. The code verdict from my earlier review of 85d588e is unchanged, and the question about one registration per backend id is still open.

@thymikee
thymikee force-pushed the refactor/maestro-replay-backend-plugin branch from 85d588e to 36d06fa Compare October 15, 2026 08:29
@thymikee

thymikee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Registration question answered in a0be641 and the head is rebased onto main at 36d06fa (#3622 merged).

The installed-plugin loader is not in scope for #3377 yet, so the unread fields are gone: ReplayBackendPlugin is now { create(host) } and ReplayBackendDeclaration drops apiVersion.

I tried the single { declaration, load } table first. The eager-closure gate rejected it: the registry would statically import the declaration (and the support-matrix text behind it), which adds 2 modules to every daemon route that loads backends (session-replay-command, native-command, test-command, session-test-source-discovery). So the two tables stay apart, but both are now mapped over ReplayBackendId ({ [TId in ReplayBackendId]: ReplayBackendDeclaration<TId> } and () => Promise<ReplayBackendPlugin<TId>>). A declaration or plugin registered under another backend's id no longer type-checks, so help cannot describe a different engine than the one its id loads. The identity test now compares against the declaration subpath directly.

The rebase also dropped the three #3616 APPROVED_OVER_CEILING rows, because the merge-base now carries those entries and the stale-row gate requires removing them. pnpm check:affected --run passes at 36d06fa. The owner-sign-off thread on the Maestro row stays open for the maintainer's approval.

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at 36d06fa. The question about one registration per backend id is settled: both tables are now mapped over ReplayBackendId, so a mismatched id fails to type-check, and the two-table split is justified by the eager-closure gate. The rebase left no conflicts, and the three removed #3616 approval rows are the ones main already carries.

Smoke Tests is still running, not failed, and this update does not touch the device or runner routes it exercises. The open Cubic P3 thread on the #3377 approval row still holds, but it is a sign-off, not a code defect.

Not blocking: one doc-comment line at replay-backend-registry.ts#L10 is about 150 characters and could be rewrapped.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee
thymikee merged commit 8c01a37 into main Oct 15, 2026
25 checks passed
@thymikee
thymikee deleted the refactor/maestro-replay-backend-plugin branch October 15, 2026 09:03
thymikee added a commit that referenced this pull request Oct 15, 2026
#3623 put the entry on main, so the merge-base now carries it and the
no-growth rule governs it; the stale-row check asks the next PR to remove it.
thymikee added a commit that referenced this pull request Oct 15, 2026
…e runtime gate (#3631)

* refactor(interaction): move the moving-target decision policy into the runtime gate

MovingTargetBaseline stays in contracts beside CommandSessionRecord; the
window and sample-gap constants, the decision union, and
movingTargetGateDecision move into the runtime gate, their only consumer.
The decision tests move with them, unchanged.

* chore(gates): drop the @agent-device/contracts/moving-target-gate subpath

The module is gone; remove its exports-map entry and its line in the
contracts export snapshot.

* docs(interaction): scope the moving-target gate header to resolved targets and any dispatch

* chore(gates): drop the replay-maestro-backend over-ceiling approval

#3623 put the entry on main, so the merge-base now carries it and the
no-growth rule governs it; the stale-row check asks the next PR to remove it.
thymikee added a commit that referenced this pull request Oct 15, 2026
…rule the runner runs (#3632)

* refactor(apple): reduce SnapshotPresentedSurface to the element-type rule the runner runs

The runner's only caller classifies a RawAXNode acquisition, which carries no
UIKit class name, so the class-aware branch never ran in production. Keep the
element-type vocabulary, the strict Element(<digits>) parse, and the
acquisition entry point; the Swift parity test now checks every golden case
against classBlindExpected. The TypeScript rule and its two-column parity test
are unchanged.

* chore(gates): drop the replay-maestro-backend over-ceiling approval

#3623 put the entry on main, so the merge-base now carries it and the
no-growth rule governs it; the stale-row check asks the next PR to remove it.

* docs(contracts): attribute the AX role prefix to accessibility roles, not the runner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant