You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Continue ADR 0019 beyond the adoption
checkpoint until the daemon holds no platform behavior: every device command consumes
request-bound platform runtimes, cross-cutting platform policy lives behind owned facets, and src/daemon/ leaves the R3 platforms-seam allowlist. Migration follows the amendment (ADR 0019
sections 8–10): move-dominated size accounting, tiered evidence, one bind per handler,
evidence-gated startup recovery, two-phase shutdown.
This is the canonical tracker. Denominators and per-unit budgets are recorded here before each
wave starts; a wave's units are not split into worker issues before that.
Start condition: satisfied — the ADR 0019 amendment merged in #1738 (73b54c15c).
Current state — 2026-08-20 (re-derived at 06d27de4d)
Derived from main, not from the hand-written wave plan. Since the previous checkpoint the whole
Wave 4 stack landed on default-branch main: get/read#1877 (494eb52f6), is#1883 (inside #1877), and wait#1875 (17bdca76c). The read dispatch-alias descriptor was deleted rather
than migrated, which is why legacy drops by four while device-runtime gains three.
Platform execution mode
Previous (5bdc1554f)
Now (06d27de4d)
none
17
17
inventory
1
1
device-runtime
20
23
legacy
39
35
The parametrized cutover table has 23 rows. Production daemon → platform dependencies are 61 total / 42 value edges (from 62 / 42 at the previous checkpoint; the snapshot start record
was 64 / 45). Capability buckets remaining: 19. The terminal gate is therefore not satisfied
and this tracker stays open.
hover was unassigned. It is a genuine interaction command with a capability bucket, and the
Wave 5 canonical list below omitted it. It is now recorded on Wave 5, which is why that row reads
15 rather than 14.
The 8 residue descriptors carry no capability bucket. They are legacy-by-delegation: the CLI
route dispatches another command that executes the platform work, and the mode-dominance gate in __tests__/platform-execution-cli-route.test.ts is what keeps them legacy. Most should
reclassify to none as their delegates migrate rather than need units of their own. That is an
analysis task, not a migration; nothing may reclassify before its delegate lands.
Remaining 61 daemon → platform edges, by owning unit
Owner
Edges
Concentration
perf / trace (blocked)
14
session-perf.ts (4), session-perf-legacy.ts, session-native-perf.ts, session-perf-xctrace.ts, types.ts perf fields, part of session-teardown.ts
Wave 4 find remains legacy and deferred. refactor: migrate get to the request-bound device runtime #1877 owns its get text leg and the focus unit
below owns its focus leg; the direct find type execution path still requires the Wave 5 type unit before an atomic R35 cutover is honest. R35 stays reserved.
Wave 4 shared capture debt remains and is still unassigned: the 10 production files under src/daemon/snapshot-presentation/ios/, android-snapshot-freshness.ts, android-snapshot-timeout-evidence.ts, screenshot-overlay-android.ts, and their
observation/interaction call sites. They remain live because non-snapshot consumers still depend
on them. This is the one remaining block of work that is file-disjoint from the descriptor
units, so it is the natural parallel track; it must be assigned before Wave 5 migrates the
freshness call sites in selector-capture-runtime.ts / deferred-interaction-outcome.ts.
Wave 5 interaction descriptors and all Wave 6 closure units/residue remain.
Before starting another unit, post its exact denominator, parity source, facet owner, expected
deletions, evidence tier, and size budget here as required below. This checkpoint does not
authorize an implicit multi-command migration.
local affected gate green; exact-head lanes running
Immediate critical path: land the focus unit, then type, then the deferred Wave 4 find
R35 cutover — focus clears the first of find's two blockers. Do not start another descriptor
unit in parallel: every unit touches registry.ts, runtime-command-cutover-table.ts, contracts/platform-runtime-operations.ts and each platform-*/src/runtime.ts, so two in flight
collide on the same four files. The Wave 4 shared capture debt is the one parallel-safe track.
Denominator is derived, not hand-listed
The wave lists below are a plan; the completion denominator is derived and re-derived per wave
from two sources, so a command cannot be silently orphaned:
the descriptor registry — every descriptor whose explicit platformExecution discriminator is legacy (wave 0 made the discriminator exhaustive: none | legacy | inventory | device-runtime, with no default; an undeclared discriminator is now rejected);
and
the platform dependency graph — every productionsrc/daemon/** module with any dependency
edge on src/platforms/** or a concrete @agent-device/platform-* package: static, dynamic, re-export, or type-only. Scope matches the layering scanner's existing exclusion
of *.test.ts and __tests__/: daemon test files importing concrete platform implementations
are outside the denominator and outside unit budgets (a unit rewrites the tests of the
production modules it migrates as part of normal cutover work, but pre-existing
integration-style test imports are not migration debt). R3 tolerates dynamic and type-only
edges by design, and legacy daemon behavior reaches platforms through exactly those edges
(e.g. device-ready.ts), so the R3 seam list is not a valid proxy for this graph.
Units are recorded by canonical descriptor name, with dispatch aliases, the public/internal
pairing where they differ (public install-from-source ↔ internal install_source, which owns the
daemon route), the exact handler owner file, and the cutover-gate row. Aliases (read → get, metrics → perf) are surfaced on their canonical unit and are never separate units.
Terminal gate (closes this tracker): a dedicated planted-red layering rule rejecting every
dependency edge — static, dynamic, re-export, and type-only — from production src/daemon/**
modules (test files excluded, matching the layering scanner's scope) to src/platforms/** and to
concrete @agent-device/platform-* packages (daemon types come from contracts only); zero
descriptor capability buckets; zero descriptors with platformExecution: legacy; and src/daemon/ removed from the R3 platforms-seam allowlist. The daemon platform branch/tag
count reaches its floor rather than a mandated zero: per the accepted end-state ("ideally zero,
practically minimal"), remaining occurrences must each be enumerated and justified as
identifier-only — a platform name flowing through as opaque data or a display label, never
selecting behavior. Non-device daemon commands are outside the denominator because their
discriminator is none, not because a hand-written list omits them.
Locked constraints (from the amendment — summarized, ADR text is authoritative)
Move-dominated: root src/ shrinks as packages grow; net shipped-size growth is exceptional and
itemized. Every unit reports root-bytes-removed vs package-bytes-added plus the checkpoint's four
size metrics against the 44c298d7f baseline and its stack base.
Two evidence tiers: request-scoped units (facts + operations + parity cell table + cutover gate)
vs durable-resource units (full ADR §4–5 lifecycle evidence). Importing durable machinery
promotes the tier.
One parametrized cutover gate; the four per-command policy files fold into it in wave 0a.
Facts replace descriptor capability buckets; each unit deletes its bucket + requireCommandSupported wiring with the legacy adapter.
One bind per handler for new units; side-effect-free facts inspection for
admission/capabilities/doctor; one neutral defineUse; preferred operations require a recorded
measurement.
Cross-cutting facets land with their first consuming unit and have one contract owner.
R3 platforms-seam allowlist narrows in the same unit that removes an area's last platform
import; the list never grows.
Waves
Wave 0 — structural substrate trim (behavior-neutral only)
Pure relocation/gate work; a change that alters any production admission, cleanup, or hint
behavior does not belong here.
Make the platformExecution discriminator explicit and exhaustive: none | legacy | inventory | device-runtime, remove the { kind: 'legacy' } default at registry entry, and
annotate every non-device descriptor none. Metadata-only; a registry gate rejects
undeclared discriminators. This is what makes the derived denominator machine-readable.
Parametrized runtime-command-cutover gate; fold device-inventory/logs-runtime/ network-runtime/record-runtime cutover-policy files into its table; one planted-red for
the mechanism.
Absorb the androidAdbExecutor DI slot in request-handler-chain.ts into the existing
provider-resolution mechanism, with wiring-equivalence tests.
Gateway facts inspection is not a wave-0 item: landing it with zero consumers would violate
the dead-code-clean substrate rule (and trip the fallow production-exports gate). It lands inside
the boot unit — its first consumer.
The record/network handlers keep their admission-use double binds. They were compliant when
they merged; ADR §9 retires the idiom for new units and permits — but does not require — a
retrofit as its own reviewed unit. Retrofitting two working handlers is consistency polish, not
denominator work, and stays off this tracker's critical path.
Wave 1 — request-scoped device lifecycle units (ordered)
Order is a dependency chain, not a preference: boot lands the shared readiness facet the others
consume.
boot — lands the device-readiness facet: ensureReady (covers simulator/emulator boot
AND physical-device readiness, matching the legacy ensureDeviceReady breadth), headless
boot as a fact, stopped-AVD-placeholder resolution moved to the Android inventory source,
Apple runner-cache prewarm becomes platform-internal keep-hot policy (ADR 0002).
shutdown — shutdownTarget operation. Eligibility comes from inspecting the shutdownTarget availability fact only; the handler never calls ensureReady, and an
already-stopped target returns success immediately (legacy target-shutdown.ts parity —
the migration must not boot a target in order to shut it down).
apps — required use: ensureReady + listApps (complete use listed in the descriptor;
readiness is not hidden inside another operation).
appstate — required use: ensureReady + appState; sessionless path included in the
denominator.
Deletions per unit: matching branches in session-inventory.ts, session-state.ts, device-ready.ts, target-shutdown.ts, session-doctor-app.ts; capability buckets.
Wave 2 — perf (durable tier)
Current status: not started; still blocked on the sequencing decision below.
Canonical descriptors: perf (whole descriptor, including its native-perf sub-surface routed via
observability; metrics alias surfaces here) and trace.
The deprecated aggregate forms (perf bare/sample/metrics) cannot remain an action-selected
legacy path inside a migrated perf descriptor — that would be a dual platform-execution path and
fail ADR 0019 §6. Default resolution: sequence this unit after the next major (#1370) deletes
the deprecated forms. That is the zero-machinery path — deletion retires their platform coupling
per §8, and the surviving narrowed surface migrates as one ordinary descriptor unit. Waves 3–4 do
not depend on perf and proceed in the meantime; the cost is only that the SessionState perf-field
cleanup waits.
Escalation only if the major slips and the perf coupling becomes blocking: a preparatory
behavior-neutral descriptor split (deprecated forms onto a separate internal legacy descriptor,
public surface unchanged) is acceptable only with wire-identity proof — the client sends the
full request including command: 'perf' over agent_device.command, so the wire command, session
events, and saved recorded actions must be byte-identical before and after (gate #1717/#1432 is
the proof vehicle), and the terminal gate then waits on the major deleting the split descriptor.
The surface narrowing (#1731) is already merged, so the denominator is the narrowed surface.
Facets: one perf facet with distinct operations (frames, memory sample, native profile/trace),
facts-declared availability; reuses capture-kit for durable captures.
Deletions: SessionState.applePerf + nativePerf.android (R7 shrink in-unit), session-perf-xctrace.ts, session-native-perf.ts platform exclusivity, the apple_perf + android_native_perf teardown steps, the 12 branch sites in session-perf.ts.
Canonical descriptors: install, reinstall, push, install_source (internal; owns the daemon
route — the public install-from-source surface and its capability bucket are recorded on this
unit); then open, prepare, close, and runtime (the runtime-hints setter — it shares the
runtime-hints facet this wave lands).
Facets: install/deploy (the session-deploy.ts dispatch table moves fully behind it), open-target
resolution/classification, runtime-hints (neutral key/value contract), close semantics.
Cross-cutting landed here: test-IME as durable device state with marker-based recovery (startup
orphan restore moves onto the evidence-gated pattern); macOS close-time alert dismissal inside
the close unit; two-phase gateway shutdown lands with the first unit that deletes a
daemon-runtime platform shutdown call.
Current status: complete except find. snapshot#1779, diff#1847, viewport#1864, screenshot#1878, get/read#1877, is#1883 and wait#1875 have all landed on main. find is the only Wave 4 descriptor left. Its get text leg moved in #1877 and its focus leg
moves in the Wave 5 focus unit; R35 waits on the Wave 5 type unit.
Canonical descriptors: snapshot, find, get (alias read), is, wait, screenshot, viewport, diff.
Facets: capture, snapshot-freshness (Android staleness policy generalized to a hook),
system-modal probe, overlay source-rect resolution.
Post-refactor: migrate snapshot to device runtime #1779 correction: the shared presentation/freshness/overlay work below did not move with
the command-atomic snapshot descriptor cutover because remaining observation/interaction
consumers still own it. It remains open Wave 4 debt and must be assigned explicitly before those
consumers migrate.
iOS presentation rules (src/daemon/snapshot-presentation/ios/, originally 6 production files) relocate inside the
snapshot unit, not as a standalone move: the files depend on root-only helpers (React Native
overlay policy, scroll-indicator helpers, daemon tree helpers) whose owners must be decided
first — snapshot-domain vocabulary moves to contracts/kernel or a snapshot domain package; platform-apple retains no root imports. No generic presentation-registration API while Apple is
the only rule contributor: the rules sit behind the Apple capture implementation until a second
platform earns the seam (no-platform-common rule).
Current status: prerequisite refactors merged; migration in progress, focus first.
Prework merged first as platform-neutral refactors: #1691 (touch orchestration split), #1656
(selector structural stages into typed policy).
Canonical descriptors: click, fill, type, press, longpress, gesture, scroll, swipe, back, home, focus, hover, orientation, tv-remote, keyboard. hover was
added by the 2026-08-20 re-derivation: it holds a capability bucket and is a point-addressed touch
command, so the derived denominator surfaced it even though the original hand-written list omitted
it.
Order within the wave is a dependency chain where one exists: focus → type unblocks the
deferred Wave 4 find R35 cutover, because both of find's remaining direct execution paths are
those two commands.
Facets: interaction dispatch; system-chrome guard + foreground-escape detection (cross-cutting,
consumed by generic dispatch); tap-outcome corroboration; direct-selector fast path as the one
measured preferred operation. Full ADR 0011 guarantee-matrix obligations per path.
Deletions: platform sites in interaction-touch.ts (densest file), interaction-* handlers, android-system-dialog.ts, direct-ios-selector.ts, selector-runtime.ts branches, androidBlockingDialogGuard registry flag. Android snapshot-helper ownership moves fully into
the Android platform module here (its daemon-shutdown reset call moves onto the two-phase stop).
Wave 6 — closure (reaches the terminal gate)
Current status: not started.
Canonical descriptors with capability buckets not owned by earlier waves: audio, clipboard, trigger-app-event, alert, settings, react-native, app-switcher. Each gets a normal unit
(most are small request-scoped facets; audio already receives an injected backend and may be
mostly bucket deletion).
Named units the derived denominator surfaces even though they carry no capability bucket:
doctor — legacy session-route descriptor consuming platform behavior: per-family toolchain
checks (session-doctor-toolchain.ts dispatch, session-doctor-android.ts, session-doctor-web.ts A-class handlers) become platform-contributed check lists behind a
doctor facet; inventory-failure aggregation already consumes the migrated inventory gateway.
capabilities — legacy session-route descriptor projecting support information; migrates
onto side-effect-free facts inspection as its data source. Deliberately last among command
units: it projects the union of every migrated command's facts, so it flips when the facts
surface is complete.
Daemon-runtime residue, each with a named owner:
Apple runner-lease state-dir configuration (setRunnerLeaseOwnerStateDir) — moves into the
Apple module's process-lifetime configuration (two-phase shutdown participant).
Web orphan recovery (cleanupWebBrowserOrphansForDaemonStartup, cleanupManagedAgentBrowserOrphans) — moves onto evidence-gated startup recovery under the
web module.
iOS simulator runner detach/stop (detachIosSimulatorRunnerSessionsForShutdown, stopAllIosRunnerSessions) — becomes the Apple module's two-phase shutdown implementation.
Re-derive the denominator; enumerate and burn down any src/daemon/** platform dependency
edge (static, dynamic, re-export, or type-only) not deleted by earlier waves
(request-lock-policy.ts switches → runtime-declared lock metadata; request-platform-providers.ts roster → generic registration). The wave-0 discriminator
pass will also classify commands the hand-listed waves don't name (replay, test, events, debug, web, …) — whatever it marks legacy lands here if no earlier wave
claimed it.
Flip the terminal gate: land the all-edge-kinds daemon layering rule with its planted-red
case, remove src/daemon/ from the R3 platforms-seam allowlist, and assert zero capability
buckets and zero platformExecution: legacy descriptors. If the wave-2 descriptor split was
taken, this item is blocked on the next major deleting the deprecated perf descriptor.
Independent cleanups (off the critical path)
Platform knowledge without platform imports — not denominator work; do whenever convenient:
daemon-client-timeout.ts runs Apple-runner pkill patterns and emits Apple hint text on every
local timeout regardless of session platform (string-level knowledge; src/daemon/client/ is
already outside the R3 seam). User-facing hint change; review with before/after fixtures.
Optional record/network retrofit onto single-bind facts inspection (permitted by ADR §9 as
its own reviewed unit; not required).
Validation per unit
pnpm check:affected --run; pnpm check for substrate/architecture changes.
The parametrized cutover gate row, fact-coverage scenarios for every denominator cell, and the
tier-appropriate evidence (parity cell table, or full durable lifecycle evidence).
Size report: four checkpoint metrics vs baseline + stack base, root-bytes-removed vs
package-bytes-added, itemized justification for any net growth.
Before each wave
Record here: exact canonical descriptor list (with aliases, public/internal pairings, handler
owner files), denominator cells, the parity source (legacy behavior at which commit), facet
contract owner, expected deletions, and the unit budgets. No wave 2+ unit starts before wave 1 has
landed at least one unit proving the request-scoped tier's trimmed evidence in practice.
Outcome
Continue ADR 0019 beyond the adoption
checkpoint until the daemon holds no platform behavior: every device command consumes
request-bound platform runtimes, cross-cutting platform policy lives behind owned facets, and
src/daemon/leaves the R3 platforms-seam allowlist. Migration follows the amendment (ADR 0019sections 8–10): move-dominated size accounting, tiered evidence, one bind per handler,
evidence-gated startup recovery, two-phase shutdown.
This is the canonical tracker. Denominators and per-unit budgets are recorded here before each
wave starts; a wave's units are not split into worker issues before that.
Start condition: satisfied — the ADR 0019 amendment merged in #1738 (
73b54c15c).Current state — 2026-08-20 (re-derived at
06d27de4d)Derived from
main, not from the hand-written wave plan. Since the previous checkpoint the wholeWave 4 stack landed on default-branch
main:get/read#1877 (494eb52f6),is#1883 (inside#1877), and
wait#1875 (17bdca76c). Thereaddispatch-alias descriptor was deleted ratherthan migrated, which is why
legacydrops by four whiledevice-runtimegains three.5bdc1554f)06d27de4d)noneinventorydevice-runtimelegacyThe parametrized cutover table has 23 rows. Production daemon → platform dependencies are
61 total / 42 value edges (from 62 / 42 at the previous checkpoint; the snapshot start record
was 64 / 45). Capability buckets remaining: 19. The terminal gate is therefore not satisfied
and this tracker stays open.
Completed:
bootrefactor: route boot through readiness runtime #1747,appsrefactor: route apps through request runtime #1756,appstaterefactor: route appstate through platform runtime #1755,shutdownrefactor: route shutdown through device runtime #1757.open/prepare/close/runtimerefactor: route application lifecycle through runtime facts #1759.find:snapshotrefactor: migrate snapshot to device runtime #1779,diffrefactor: migrate diff to request-bound runtime #1847,
viewportrefactor: migrate viewport to request runtime #1864,screenshotrefactor: migrate screenshot to request-bound runtime #1878,get/readrefactor: migrate get to the request-bound device runtime #1877 (R36, and thereadentrydeleted),
isrefactor: migrate is to the request-bound device runtime #1883 (R37),waitrefactor: migrate wait to request-bound runtime #1875 (R38). refactor(daemon): one capture-input builder and one admit-then-bind step #1876 separately landed the behavior-neutralshared capture-input/admit-then-bind consolidation and split
handlers/find.ts.Remaining 35
legacydescriptors, by ownerperf,tracefindclickfilltypepresslongpressgesturescrollswipebackhomefocushoverorientationtv-remotekeyboardaudioclipboardtrigger-app-eventalertsettingsreact-nativeapp-switcherdoctorcapabilitiesbatchdaemondebugeventsreplaytestwebreact-devtoolsTwo corrections this re-derivation forced:
hoverwas unassigned. It is a genuine interaction command with a capability bucket, and theWave 5 canonical list below omitted it. It is now recorded on Wave 5, which is why that row reads
15 rather than 14.
route dispatches another command that executes the platform work, and the mode-dominance gate in
__tests__/platform-execution-cli-route.test.tsis what keeps themlegacy. Most shouldreclassify to
noneas their delegates migrate rather than need units of their own. That is ananalysis task, not a migration; nothing may reclassify before its delegate lands.
Remaining 61 daemon → platform edges, by owning unit
perf/trace(blocked)session-perf.ts(4),session-perf-legacy.ts,session-native-perf.ts,session-perf-xctrace.ts,types.tsperf fields, part ofsession-teardown.tsandroid-system-dialog.ts(3),interaction-android-escape.ts,interaction-touch-reference-frame.ts,interaction-touch-response.ts,interaction-runtime.ts,selector-runtime.tsdoctorsession-doctor-{android,web,toolchain}.ts,session-doctor.tsalert/audiosnapshot-alert.ts(3),session-audio.ts,audio-probe.tsrequest-platform-providers.ts(6),server/daemon-runtime.ts(4),device-ready.ts(3),snapshot-session.ts,apple-runner-options.ts,request-router.ts,request-recording-health.ts,session-device-utils.ts,install-source-resolution.ts,session.ts,session-test-infrastructure.tsRemaining critical-path work
perf/traceremains sequenced after the next-major deletion in Next major: scheduled breaking cleanups (legacy batch-step shape, deprecated rotate client surface, client-types narrowing) #1370 unless thetracker records the approved split escalation.
findremains legacy and deferred. refactor: migrate get to the request-bound device runtime #1877 owns itsget textleg and thefocusunitbelow owns its
focusleg; the directfind typeexecution path still requires the Wave 5typeunit before an atomic R35 cutover is honest. R35 stays reserved.src/daemon/snapshot-presentation/ios/,android-snapshot-freshness.ts,android-snapshot-timeout-evidence.ts,screenshot-overlay-android.ts, and theirobservation/interaction call sites. They remain live because non-snapshot consumers still depend
on them. This is the one remaining block of work that is file-disjoint from the descriptor
units, so it is the natural parallel track; it must be assigned before Wave 5 migrates the
freshness call sites in
selector-capture-runtime.ts/deferred-interaction-outcome.ts.deletions, evidence tier, and size budget here as required below. This checkpoint does not
authorize an implicit multi-command migration.
Open critical-path PRs at this checkpoint:
focus(Wave 5, unit 1)main·06d27de4dImmediate critical path: land the
focusunit, thentype, then the deferred Wave 4findR35 cutover —
focusclears the first offind's two blockers. Do not start another descriptorunit in parallel: every unit touches
registry.ts,runtime-command-cutover-table.ts,contracts/platform-runtime-operations.tsand eachplatform-*/src/runtime.ts, so two in flightcollide on the same four files. The Wave 4 shared capture debt is the one parallel-safe track.
Denominator is derived, not hand-listed
The wave lists below are a plan; the completion denominator is derived and re-derived per wave
from two sources, so a command cannot be silently orphaned:
platformExecutiondiscriminator islegacy(wave 0 made the discriminator exhaustive:none | legacy | inventory | device-runtime, with no default; an undeclared discriminator is now rejected);and
src/daemon/**module with any dependencyedge on
src/platforms/**or a concrete@agent-device/platform-*package: static,dynamic, re-export, or type-only. Scope matches the layering scanner's existing exclusion
of
*.test.tsand__tests__/: daemon test files importing concrete platform implementationsare outside the denominator and outside unit budgets (a unit rewrites the tests of the
production modules it migrates as part of normal cutover work, but pre-existing
integration-style test imports are not migration debt). R3 tolerates dynamic and type-only
edges by design, and legacy daemon behavior reaches platforms through exactly those edges
(e.g.
device-ready.ts), so the R3 seam list is not a valid proxy for this graph.Units are recorded by canonical descriptor name, with dispatch aliases, the public/internal
pairing where they differ (public
install-from-source↔ internalinstall_source, which owns thedaemon route), the exact handler owner file, and the cutover-gate row. Aliases (
read→get,metrics→perf) are surfaced on their canonical unit and are never separate units.Terminal gate (closes this tracker): a dedicated planted-red layering rule rejecting every
dependency edge — static, dynamic, re-export, and type-only — from production
src/daemon/**modules (test files excluded, matching the layering scanner's scope) to
src/platforms/**and toconcrete
@agent-device/platform-*packages (daemon types come from contracts only); zerodescriptor capability buckets; zero descriptors with
platformExecution: legacy; andsrc/daemon/removed from the R3 platforms-seam allowlist. The daemon platform branch/tagcount reaches its floor rather than a mandated zero: per the accepted end-state ("ideally zero,
practically minimal"), remaining occurrences must each be enumerated and justified as
identifier-only — a platform name flowing through as opaque data or a display label, never
selecting behavior. Non-device daemon commands are outside the denominator because their
discriminator is
none, not because a hand-written list omits them.Locked constraints (from the amendment — summarized, ADR text is authoritative)
src/shrinks as packages grow; net shipped-size growth is exceptional anditemized. Every unit reports root-bytes-removed vs package-bytes-added plus the checkpoint's four
size metrics against the
44c298d7fbaseline and its stack base.merges before its command's unit (perf's narrowing, refactor: narrow perf API to actionable evidence #1731, is merged).
vs durable-resource units (full ADR §4–5 lifecycle evidence). Importing durable machinery
promotes the tier.
requireCommandSupportedwiring with the legacy adapter.admission/capabilities/doctor; one neutral
defineUse; preferred operations require a recordedmeasurement.
import; the list never grows.
Waves
Wave 0 — structural substrate trim (behavior-neutral only)
Pure relocation/gate work; a change that alters any production admission, cleanup, or hint
behavior does not belong here.
platformExecutiondiscriminator explicit and exhaustive:none | legacy | inventory | device-runtime, remove the{ kind: 'legacy' }default at registry entry, andannotate every non-device descriptor
none. Metadata-only; a registry gate rejectsundeclared discriminators. This is what makes the derived denominator machine-readable.
device-inventory/logs-runtime/network-runtime/record-runtimecutover-policy files into its table; one planted-red forthe mechanism.
defineUse(remove per-domain currying wrappers).androidAdbExecutorDI slot inrequest-handler-chain.tsinto the existingprovider-resolution mechanism, with wiring-equivalence tests.
Gateway facts inspection is not a wave-0 item: landing it with zero consumers would violate
the dead-code-clean substrate rule (and trip the fallow production-exports gate). It lands inside
the
bootunit — its first consumer.The
record/networkhandlers keep their admission-use double binds. They were compliant whenthey merged; ADR §9 retires the idiom for new units and permits — but does not require — a
retrofit as its own reviewed unit. Retrofitting two working handlers is consistency polish, not
denominator work, and stays off this tracker's critical path.
Wave 1 — request-scoped device lifecycle units (ordered)
Order is a dependency chain, not a preference:
bootlands the shared readiness facet the othersconsume.
boot— lands the device-readiness facet:ensureReady(covers simulator/emulator bootAND physical-device readiness, matching the legacy
ensureDeviceReadybreadth), headlessboot as a fact, stopped-AVD-placeholder resolution moved to the Android inventory source,
Apple runner-cache prewarm becomes platform-internal keep-hot policy (ADR 0002).
shutdown—shutdownTargetoperation. Eligibility comes from inspecting theshutdownTargetavailability fact only; the handler never callsensureReady, and analready-stopped target returns success immediately (legacy
target-shutdown.tsparity —the migration must not boot a target in order to shut it down).
apps— required use:ensureReady+listApps(complete use listed in the descriptor;readiness is not hidden inside another operation).
appstate— required use:ensureReady+appState; sessionless path included in thedenominator.
Deletions per unit: matching branches in
session-inventory.ts,session-state.ts,device-ready.ts,target-shutdown.ts,session-doctor-app.ts; capability buckets.Wave 2 — perf (durable tier)
Current status: not started; still blocked on the sequencing decision below.
Canonical descriptors:
perf(whole descriptor, including its native-perf sub-surface routed viaobservability;
metricsalias surfaces here) andtrace.The deprecated aggregate forms (
perfbare/sample/metrics) cannot remain an action-selectedlegacy path inside a migrated
perfdescriptor — that would be a dual platform-execution path andfail ADR 0019 §6. Default resolution: sequence this unit after the next major (#1370) deletes
the deprecated forms. That is the zero-machinery path — deletion retires their platform coupling
per §8, and the surviving narrowed surface migrates as one ordinary descriptor unit. Waves 3–4 do
not depend on perf and proceed in the meantime; the cost is only that the
SessionStateperf-fieldcleanup waits.
Escalation only if the major slips and the perf coupling becomes blocking: a preparatory
behavior-neutral descriptor split (deprecated forms onto a separate internal legacy descriptor,
public surface unchanged) is acceptable only with wire-identity proof — the client sends the
full request including
command: 'perf'overagent_device.command, so the wire command, sessionevents, and saved recorded actions must be byte-identical before and after (gate #1717/#1432 is
the proof vehicle), and the terminal gate then waits on the major deleting the split descriptor.
The surface narrowing (#1731) is already merged, so the denominator is the narrowed surface.
facts-declared availability; reuses capture-kit for durable captures.
SessionState.applePerf+nativePerf.android(R7 shrink in-unit),session-perf-xctrace.ts,session-native-perf.tsplatform exclusivity, theapple_perf+android_native_perfteardown steps, the 12 branch sites insession-perf.ts.Wave 3 — install family, then open/close
Current status: complete via #1758 and #1759.
Canonical descriptors:
install,reinstall,push,install_source(internal; owns the daemonroute — the public
install-from-sourcesurface and its capability bucket are recorded on thisunit); then
open,prepare,close, andruntime(the runtime-hints setter — it shares theruntime-hints facet this wave lands).
session-deploy.tsdispatch table moves fully behind it), open-targetresolution/classification, runtime-hints (neutral key/value contract), close semantics.
orphan restore moves onto the evidence-gated pattern); macOS close-time alert dismissal inside
the close unit; two-phase gateway shutdown lands with the first unit that deletes a
daemon-runtime platform shutdown call.
session-open*.tsbranch sites,runtime-hints.tsdispatch,android_imeteardownstep,
restoreOrphanedAndroidTestImeOnDaemonStartupdaemon wiring.Wave 4 — observation
Current status: complete except
find.snapshot#1779,diff#1847,viewport#1864,screenshot#1878,get/read#1877,is#1883 andwait#1875 have all landed onmain.findis the only Wave 4 descriptor left. Itsget textleg moved in #1877 and itsfocuslegmoves in the Wave 5
focusunit; R35 waits on the Wave 5typeunit.Canonical descriptors:
snapshot,find,get(aliasread),is,wait,screenshot,viewport,diff.system-modal probe, overlay source-rect resolution.
the command-atomic
snapshotdescriptor cutover because remaining observation/interactionconsumers still own it. It remains open Wave 4 debt and must be assigned explicitly before those
consumers migrate.
src/daemon/snapshot-presentation/ios/, originally 6 production files) relocate inside thesnapshot unit, not as a standalone move: the files depend on root-only helpers (React Native
overlay policy, scroll-indicator helpers, daemon tree helpers) whose owners must be decided
first — snapshot-domain vocabulary moves to contracts/kernel or a snapshot domain package;
platform-appleretains no root imports. No generic presentation-registration API while Apple isthe only rule contributor: the rules sit behind the Apple capture implementation until a second
platform earns the seam (no-platform-common rule).
android-snapshot-freshness.ts,android-snapshot-timeout-evidence.ts,screenshot-overlay-android.ts, freshness call sites inselector-capture-runtime.ts/deferred-interaction-outcome.ts,snapshot-runtime.tsbranches.Wave 5 — interaction (largest)
Current status: prerequisite refactors merged; migration in progress,
focusfirst.Prework merged first as platform-neutral refactors: #1691 (touch orchestration split), #1656
(selector structural stages into typed policy).
Canonical descriptors:
click,fill,type,press,longpress,gesture,scroll,swipe,back,home,focus,hover,orientation,tv-remote,keyboard.hoverwasadded by the 2026-08-20 re-derivation: it holds a capability bucket and is a point-addressed touch
command, so the derived denominator surfaced it even though the original hand-written list omitted
it.
Order within the wave is a dependency chain where one exists:
focus→typeunblocks thedeferred Wave 4
findR35 cutover, because both of find's remaining direct execution paths arethose two commands.
consumed by generic dispatch); tap-outcome corroboration; direct-selector fast path as the one
measured
preferredoperation. Full ADR 0011 guarantee-matrix obligations per path.interaction-touch.ts(densest file),interaction-*handlers,android-system-dialog.ts,direct-ios-selector.ts,selector-runtime.tsbranches,androidBlockingDialogGuardregistry flag. Android snapshot-helper ownership moves fully intothe Android platform module here (its daemon-shutdown reset call moves onto the two-phase stop).
Wave 6 — closure (reaches the terminal gate)
Current status: not started.
Canonical descriptors with capability buckets not owned by earlier waves:
audio,clipboard,trigger-app-event,alert,settings,react-native,app-switcher. Each gets a normal unit(most are small request-scoped facets;
audioalready receives an injected backend and may bemostly bucket deletion).
Named units the derived denominator surfaces even though they carry no capability bucket:
doctor— legacy session-route descriptor consuming platform behavior: per-family toolchainchecks (
session-doctor-toolchain.tsdispatch,session-doctor-android.ts,session-doctor-web.tsA-class handlers) become platform-contributed check lists behind adoctor facet; inventory-failure aggregation already consumes the migrated inventory gateway.
capabilities— legacy session-route descriptor projecting support information; migratesonto side-effect-free facts inspection as its data source. Deliberately last among command
units: it projects the union of every migrated command's facts, so it flips when the facts
surface is complete.
Daemon-runtime residue, each with a named owner:
setRunnerLeaseOwnerStateDir) — moves into theApple module's process-lifetime configuration (two-phase shutdown participant).
cleanupWebBrowserOrphansForDaemonStartup,cleanupManagedAgentBrowserOrphans) — moves onto evidence-gated startup recovery under theweb module.
detachIosSimulatorRunnerSessionsForShutdown,stopAllIosRunnerSessions) — becomes the Apple module's two-phase shutdown implementation.src/daemon/**platform dependencyedge (static, dynamic, re-export, or type-only) not deleted by earlier waves
(
request-lock-policy.tsswitches → runtime-declared lock metadata;request-platform-providers.tsroster → generic registration). The wave-0 discriminatorpass will also classify commands the hand-listed waves don't name (
replay,test,events,debug,web, …) — whatever it markslegacylands here if no earlier waveclaimed it.
case, remove
src/daemon/from the R3 platforms-seam allowlist, and assert zero capabilitybuckets and zero
platformExecution: legacydescriptors. If the wave-2 descriptor split wastaken, this item is blocked on the next major deleting the deprecated perf descriptor.
Independent cleanups (off the critical path)
Platform knowledge without platform imports — not denominator work; do whenever convenient:
daemon-client-timeout.tsruns Apple-runner pkill patterns and emits Apple hint text on everylocal timeout regardless of session platform (string-level knowledge;
src/daemon/client/isalready outside the R3 seam). User-facing hint change; review with before/after fixtures.
record/networkretrofit onto single-bind facts inspection (permitted by ADR §9 asits own reviewed unit; not required).
Validation per unit
pnpm check:affected --run;pnpm checkfor substrate/architecture changes.tier-appropriate evidence (parity cell table, or full durable lifecycle evidence).
package-bytes-added, itemized justification for any net growth.
Before each wave
Record here: exact canonical descriptor list (with aliases, public/internal pairings, handler
owner files), denominator cells, the parity source (legacy behavior at which commit), facet
contract owner, expected deletions, and the unit budgets. No wave 2+ unit starts before wave 1 has
landed at least one unit proving the request-scoped tier's trimmed evidence in practice.