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
Extend the existing pnpm workspace (which already contains website) into an internal modular monorepo: new packages/* members with enforced public interfaces (exports maps + workspace:* deps), extracted incrementally, bottom-up along the layering spine. A deep package-specifier import becomes a resolution error, and a repo-relative bypass becomes a gate failure. The published surface stays exactly one npm artifact (agent-device); every new internal packages/* member is "private": true, while the root package remains publishable.
Maintainer decision (thymikee, 2026-07-29): the workspace is the target state, superseding the "workspaces only when earned" stance in docs/daemon-modularity-proposal.md — by that document's own trigger list, the third trigger (repeated boundary regressions that physical package resolution would prevent more reliably) is already met: 72 static imports reach past the PlatformPlugin seam, the R6 type-inversion ledger started at 61, and the largest type-cycle SCC spans 102 files, all accumulated while the zone gates were in force.
Extraction follows the layering dependency order. When a zone leaves src/, delete only the layering-lint rows whose guarantee is physically subsumed by package resolution; semantic, ownership, cycle, and package-interface policies remain. A package boundary enforces an already-earned module interface — it does not by itself make the interface deep or earn a new port.
Tracking
W-pre — relocate kernel stragglers (no workspace yet)
W0 — workspace scaffold + @agent-device/kernel pilot
W1a — @agent-device/contracts + split the ProviderDeviceRuntime contract from root runtime coordination
Root src/ may import packages. A package may never import root src/ — enforced by package resolution plus a cheap lint rejecting escapes from packages/.
Every consumer outside the owning package uses its package specifier. Relative imports into packages/*/src are forbidden even from root source/tests; otherwise exports can be bypassed. The sole exception is an R8 zero-dep closure importing the exact source target named by that package's exports map.
Every internal package-to-package dependency is declared as workspace:*. Root build-time links to private internal packages stay out of the packed runtime dependency manifest. Do not add TypeScript paths wildcards that resolve package internals and bypass exports.
A module moves only when its entire remaining dependency set is packages / npm deps / node builtins. No compatibility shims, no re-export barrels left at the old path — atomic cutover, same rule as refactor(daemon): extract replay, Maestro, and replay-test modules #1478's internal-interface policy.
No common / foundation / utils package, ever. A shared package is named by domain, and every file in it has a package consumer at the time it moves.
Export shape follows module shape, not source-file count. Leaf vocabulary packages may expose consumer-earned domain subpaths such as @agent-device/kernel/errors; deep modules (provider-webdriver, provider-limrun, maestro, replay-test, ad-replay) export only their façade at .. No mega-barrels and no automatic export for every source file.
tsdown bundles @agent-device/* (noExternal) into the single published artifact; lazy platform loading is preserved. The packed agent-device manifest must contain zero @agent-device/* runtime dependencies because those packages are private and unpublished. Publishing any package separately is a separate future decision.
TypeScript project references per package; root typecheck becomes tsc -b.
R8 zero-dep jobs cannot resolve workspace package specifiers. If such a job genuinely needs extracted code, it may use the sole relative-import exception above; the gate derives permitted targets from exports and rejects every non-exported/internal target. Otherwise make that job install dependencies.
One worker, one branch, one PR per brief. Rebase from the dependency's merged commit.
Worker briefs
W-pre — relocate kernel stragglers
Goal: kernel's file set is its public API before it becomes a package.
Audit (importer counts across src/scripts/test): errors 493, device 264, snapshot 208, contracts 81 — plus collections 4, rect 9, redaction 5, daemon-error 3, all coherent kernel vocabulary. Three files drifted into the sink because R1 pushes shared code down, not because they are foundational:
audio-probe-support.ts (4 importers, all platform/plugin code) — feature policy ("which devices support host audio probing"); move beside its consumers (contracts or platforms).
observability-redaction.ts (1 importer) — colocate with its consumer or fold into contracts.
input-validation.ts (3 importers) — same treatment.
Done: the three files live outside src/kernel/; kernel has zero internal imports (already true — R1); all gates green; no behavior change.
W0 — scaffold + @agent-device/kernel pilot
Goal: prove the whole toolchain on the module the spine already certified as dependency-free.
Audit the three low-level helpers webdriver shares with existing consumers — utils/timeouts.ts, utils/xml.ts, and utils/bounds.ts — and move only those whose domain placement is honest. utils/version.ts stays root-side: it owns filesystem/project-root lookup, so provider package factories receive the resolved client version as an immutable value. utils/exec.ts also stays root-side.
Codemod every ../kernel/x.ts import to @agent-device/kernel/x repo-wide. ⚠️ This is the one large-conflict change of the initiative — land in a quiet window (after fix: publish replay refs after response projection #1489/P1 merges, before P2 branches).
Gate wiring in the same PR: layering drops only kernel rows physically replaced by resolution and gains package-to-root, cross-package-relative-import, undeclared-workspace-dependency, and export-target checks; check:affected gains packages/ ownership (today those paths fail open as unknown); fallow entries; vitest/oxlint/oxfmt/coverage cover packages/; Testing Matrix row updated.
Done: packaged-CLI and export-subpath smokes green; emitted JavaScript contains no unresolved @agent-device/* specifiers; the packed manifest has zero @agent-device/* runtime dependencies; size report ≈ 0 delta; tests assert package-specifier, ordinary repo-relative, cross-package-relative, and R8-relative deep-import rules; pnpm check green.
STOP if: tsdown cannot preserve the single artifact or lazy platform loading; emitted or packed output retains an internal package dependency; any old-path re-export is needed; a zero-dep script needs a package specifier or a non-exported source target.
Goal: the shared-vocabulary zone (already "declared below its consumers" per #1405/#1435) becomes a package, and the provider interface becomes clean enough to live in it.
Audited blocker: src/provider-device-runtime.ts imports types from daemon/request-platform-providers.ts and platforms/apple/core/runner/runner-provider.ts — the interface has downward type-deps into daemon and Apple internals. Invert them (kin to the remaining R6 baseline entries; shrink that ledger where the fix overlaps).
Split the value/type-only ProviderDeviceRuntime contract and its neutral option/result values into packages/contracts. Keep AsyncLocalStorage, the active-runtime registry, request scoping, provider composition, and test-isolation mutation in root composition code; the contracts package must not acquire ambient mutable runtime coordination.
Move src/contracts/** with consumer-earned domain subpaths. platforms/permission-utils.ts moves here only if the inversion audit confirms it is shared vocabulary rather than WebDriver/platform policy.
Done: contracts package has imports only from @agent-device/kernel/node and contains no mutable runtime registry/composition; R6 baseline does not grow (shrinks where touched); only physically superseded layering rows are deleted; all parity/coverage gates green.
STOP if: an inversion requires a runtime (non-type) dependency moving down, or DaemonRequest would need to enter contracts.
W1b — @agent-device/provider-webdriver
Goal: first product leaf. BrowserStack + AWS Device Farm adapters behind the packaged interface.
Audited deps: kernel (4 files), contracts (9 files), ProviderDeviceRuntime, the W0-audited utils/{timeouts,xml,bounds} placements, root-supplied version value, oneutils/exec.ts call site, and platforms/permission-utils.ts (in contracts after W1a only if confirmed as shared vocabulary).
Refactor the single exec call site to receive a narrow host-command capability through the package factory rather than importing root exec or widening ProviderDeviceRuntime. The root production adapter delegates to src/utils/exec.ts; the package's deterministic adapter runs the same focused contract.
Root provider composition targets the package while preserving current startup/load behavior. Cloud WebDriver is statically imported today; do not claim or require new laziness in this PR. Preserve Limrun's existing conditional dynamic load independently.
Current evidence is not only 17 colocated unit-test lines: provider scenarios already include roughly 1,159 lines across cloud-webdriver-provider-adapters.test.ts and cloud-webdriver-runtime.test.ts, plus surface tests. Move those tests with their subject where appropriate, identify behavior gaps at the package façade, and add provider-transcript coverage only for unpinned adapter contracts. Replace superseded implementation-coupled tests rather than layering duplicates.
Done: package exports only . and imports only kernel/contracts/npm/node; existing provider-scenario behavior remains covered through the package façade; missing adapter contracts gain provider transcripts; cloud connect flows remain green in provider-integration.
W1c — limrun seam (blocker discovered in audit; no package yet)
Goal: remove limrun's deep imports so it can move without widening the provider contract.
src/providers/limrun/ currently imports core/interactors/android.ts, platforms/android/adb-executor.ts, platforms/android/device-input-state.ts, and utils/exec.ts. Define a narrow dependency object at the createLimrunRuntime package-factory seam for the demonstrated Android interactor/ADB/reverse operations and host commands. Root composition supplies adapters backed by the existing Android platform and src/utils/exec.ts; a deterministic in-memory adapter exercises the same contract. These capabilities are construction dependencies of the Limrun implementation, not new methods on ProviderDeviceRuntime.
Done: limrun's imports reduce to kernel/contracts/@limrun/api/factory-injected capabilities; ProviderDeviceRuntime does not widen; live limrun session evidence (device-verification doc applies — this is device-facing).
STOP if: the seam needs a universal command dispatcher, exposes raw platform modules, or widens ProviderDeviceRuntime beyond what its existing consumers demonstrably need.
W1d — @agent-device/provider-limrun
Goal: atomically move the W1c-clean Limrun implementation behind one package façade.
Cut root composition over to the package and delete the old path without a re-export.
Move or replace tests so they exercise the package façade and its injected capability contracts, not implementation files.
Preserve conditional loading when LIMRUN_API_KEY is absent and preserve provider/device behavior when present.
Done: package exports only ., imports only kernel/contracts/@limrun/api/node, and has zero root src/ imports; provider scenarios plus live Limrun evidence pass; packed output and startup behavior remain compatible.
After W0 and W1a establish the workspace and shared vocabulary, amend #1478 so P2/P3/P5 land directly in packages/{maestro,replay-test,ad-replay}; do not first create src/<module>/ and move it again. W1b/W1c/W1d are sibling provider work and do not block this replay chain. Each brief's Done gains "lands as a workspace package; zero root imports; only . is exported." The package graph's acyclicity makes P5's engine-out-of-the-SCC requirement non-optional. If both engines need the same snapshot/selector values, that earns a focused addition to @agent-device/contracts — never a new grab-bag.
W3 — platforms
If #1478's P6 checkpoint defers platform modularity, record "not now" here and stop. If it proceeds, complete P6a/P6b first, then make a fresh package go/no-go from the resulting logical seams. Packaging must not be used to design those seams. A "go" additionally requires open capability unions, plugin-contributed capability matrices, and verified per-package native asset resolution. Not scheduled here.
Explicitly out
@agent-device/host — rejected; only W0-audited low-level helpers may move to kernel, while version/project-root policy and exec stay in root composition.
screenshot-diff package — dropped from the schedule (may join opportunistically later; its OCR path has a separate pending decision).
metro — deferred: imports daemon/session-paths.ts and the companion tunnel; needs its own seam brief before it can satisfy the no-shims rule.
Publishing packages to npm individually.
STOP conditions (initiative-wide)
Published artifact or packed manifest loses byte/behavior/dependency parity; a move needs a compatibility re-export at the old path; a package needs a root src/ import; a zero-dep script needs a non-exported package source; tsdown cannot keep one artifact + lazy platform loading; CI wall-clock regresses materially; a shared package would be created without a named domain and current consumers.
Success metrics
Deep import of a non-exported package file = resolution error, pinned by test.
Layering zone table shrinks monotonically with each extraction; R6/R9 never grow.
Size report neutral through W1; packaged-CLI + export-subpath smokes stay green throughout.
No internal package outside the seven named through W2 (kernel, contracts, provider-webdriver, provider-limrun, maestro, replay-test, ad-replay); package count is a ceiling, not a success target.
Zero re-export shims; zero packages/ → src/ imports.
W1b closes the cloud-webdriver test gap (2,631 prod LOC / 17 test lines → transcript-covered).
Validation
Every worker: pnpm check:affected --run. W0 and W1a run pnpm check because they rewire the workspace, type graph, and gates; every package cutover (W1b, W1d, and #1478 P2/P3/P5) also runs pnpm check because path topology and published bundling change broadly. W1b/W1c/W1d additionally run provider-integration and the applicable live device/cloud evidence per docs/agents/device-verification.md. CI stays authoritative.
docs/daemon-modularity-proposal.md §"Conditional workspace layout" — the package constraints adopted here (private: true, workspace:*, single artifact, no foundation dumps); its "only when earned" stance is superseded by this issue
Outcome
Extend the existing pnpm workspace (which already contains
website) into an internal modular monorepo: newpackages/*members with enforced public interfaces (exportsmaps +workspace:*deps), extracted incrementally, bottom-up along the layering spine. A deep package-specifier import becomes a resolution error, and a repo-relative bypass becomes a gate failure. The published surface stays exactly one npm artifact (agent-device); every new internalpackages/*member is"private": true, while the root package remains publishable.Maintainer decision (thymikee, 2026-07-29): the workspace is the target state, superseding the "workspaces only when earned" stance in
docs/daemon-modularity-proposal.md— by that document's own trigger list, the third trigger (repeated boundary regressions that physical package resolution would prevent more reliably) is already met: 72 static imports reach past the PlatformPlugin seam, the R6 type-inversion ledger started at 61, and the largest type-cycle SCC spans 102 files, all accumulated while the zone gates were in force.Extraction follows the layering dependency order. When a zone leaves
src/, delete only the layering-lint rows whose guarantee is physically subsumed by package resolution; semantic, ownership, cycle, and package-interface policies remain. A package boundary enforces an already-earned module interface — it does not by itself make the interface deep or earn a new port.Tracking
@agent-device/kernelpilot@agent-device/contracts+ split theProviderDeviceRuntimecontract from root runtime coordination@agent-device/provider-webdriver@agent-device/provider-limrunpackages/maestro,packages/replay-test,packages/ad-replay; W1b/W1c/W1d do not block that replay chainLocked rules (every PR)
src/may import packages. A package may never import rootsrc/— enforced by package resolution plus a cheap lint rejecting escapes frompackages/.packages/*/srcare forbidden even from root source/tests; otherwiseexportscan be bypassed. The sole exception is an R8 zero-dep closure importing the exact source target named by that package'sexportsmap.workspace:*. Root build-time links to private internal packages stay out of the packed runtime dependency manifest. Do not add TypeScriptpathswildcards that resolve package internals and bypassexports.common/foundation/utilspackage, ever. A shared package is named by domain, and every file in it has a package consumer at the time it moves.@agent-device/kernel/errors; deep modules (provider-webdriver,provider-limrun,maestro,replay-test,ad-replay) export only their façade at.. No mega-barrels and no automatic export for every source file.@agent-device/*(noExternal) into the single published artifact; lazy platform loading is preserved. The packedagent-devicemanifest must contain zero@agent-device/*runtime dependencies because those packages are private and unpublished. Publishing any package separately is a separate future decision.tsc -b.exportsand rejects every non-exported/internal target. Otherwise make that job install dependencies.Worker briefs
W-pre — relocate kernel stragglers
Goal: kernel's file set is its public API before it becomes a package.
Audit (importer counts across
src/scripts/test):errors493,device264,snapshot208,contracts81 — pluscollections4,rect9,redaction5,daemon-error3, all coherent kernel vocabulary. Three files drifted into the sink because R1 pushes shared code down, not because they are foundational:audio-probe-support.ts(4 importers, all platform/plugin code) — feature policy ("which devices support host audio probing"); move beside its consumers (contracts or platforms).observability-redaction.ts(1 importer) — colocate with its consumer or fold intocontracts.input-validation.ts(3 importers) — same treatment.Done: the three files live outside
src/kernel/; kernel has zero internal imports (already true — R1); all gates green; no behavior change.W0 — scaffold +
@agent-device/kernelpilotGoal: prove the whole toolchain on the module the spine already certified as dependency-free.
pnpm-workspace.yamlgainspackages/*.packages/kernel/:package.json(private: true, explicit consumer-earned subpathexports), compositetsconfig.json.utils/timeouts.ts,utils/xml.ts, andutils/bounds.ts— and move only those whose domain placement is honest.utils/version.tsstays root-side: it owns filesystem/project-root lookup, so provider package factories receive the resolved client version as an immutable value.utils/exec.tsalso stays root-side.../kernel/x.tsimport to@agent-device/kernel/xrepo-wide.check:affectedgainspackages/ownership (today those paths fail open as unknown); fallow entries; vitest/oxlint/oxfmt/coverage coverpackages/; Testing Matrix row updated.Done: packaged-CLI and export-subpath smokes green; emitted JavaScript contains no unresolved
@agent-device/*specifiers; the packed manifest has zero@agent-device/*runtime dependencies; size report ≈ 0 delta; tests assert package-specifier, ordinary repo-relative, cross-package-relative, and R8-relative deep-import rules;pnpm checkgreen.STOP if: tsdown cannot preserve the single artifact or lazy platform loading; emitted or packed output retains an internal package dependency; any old-path re-export is needed; a zero-dep script needs a package specifier or a non-exported source target.
W1a —
@agent-device/contracts+ProviderDeviceRuntimeinversionGoal: the shared-vocabulary zone (already "declared below its consumers" per #1405/#1435) becomes a package, and the provider interface becomes clean enough to live in it.
src/provider-device-runtime.tsimports types fromdaemon/request-platform-providers.tsandplatforms/apple/core/runner/runner-provider.ts— the interface has downward type-deps into daemon and Apple internals. Invert them (kin to the remaining R6 baseline entries; shrink that ledger where the fix overlaps).ProviderDeviceRuntimecontract and its neutral option/result values intopackages/contracts. KeepAsyncLocalStorage, the active-runtime registry, request scoping, provider composition, and test-isolation mutation in root composition code; the contracts package must not acquire ambient mutable runtime coordination.src/contracts/**with consumer-earned domain subpaths.platforms/permission-utils.tsmoves here only if the inversion audit confirms it is shared vocabulary rather than WebDriver/platform policy.Done: contracts package has imports only from
@agent-device/kernel/node and contains no mutable runtime registry/composition; R6 baseline does not grow (shrinks where touched); only physically superseded layering rows are deleted; all parity/coverage gates green.STOP if: an inversion requires a runtime (non-type) dependency moving down, or
DaemonRequestwould need to enter contracts.W1b —
@agent-device/provider-webdriverGoal: first product leaf. BrowserStack + AWS Device Farm adapters behind the packaged interface.
ProviderDeviceRuntime, the W0-auditedutils/{timeouts,xml,bounds}placements, root-supplied version value, oneutils/exec.tscall site, andplatforms/permission-utils.ts(in contracts after W1a only if confirmed as shared vocabulary).ProviderDeviceRuntime. The root production adapter delegates tosrc/utils/exec.ts; the package's deterministic adapter runs the same focused contract.cloud-webdriver-provider-adapters.test.tsandcloud-webdriver-runtime.test.ts, plus surface tests. Move those tests with their subject where appropriate, identify behavior gaps at the package façade, and add provider-transcript coverage only for unpinned adapter contracts. Replace superseded implementation-coupled tests rather than layering duplicates.Done: package exports only
.and imports only kernel/contracts/npm/node; existing provider-scenario behavior remains covered through the package façade; missing adapter contracts gain provider transcripts; cloud connect flows remain green in provider-integration.W1c — limrun seam (blocker discovered in audit; no package yet)
Goal: remove limrun's deep imports so it can move without widening the provider contract.
src/providers/limrun/currently importscore/interactors/android.ts,platforms/android/adb-executor.ts,platforms/android/device-input-state.ts, andutils/exec.ts. Define a narrow dependency object at thecreateLimrunRuntimepackage-factory seam for the demonstrated Android interactor/ADB/reverse operations and host commands. Root composition supplies adapters backed by the existing Android platform andsrc/utils/exec.ts; a deterministic in-memory adapter exercises the same contract. These capabilities are construction dependencies of the Limrun implementation, not new methods onProviderDeviceRuntime.Done: limrun's imports reduce to kernel/contracts/
@limrun/api/factory-injected capabilities;ProviderDeviceRuntimedoes not widen; live limrun session evidence (device-verification doc applies — this is device-facing).STOP if: the seam needs a universal command dispatcher, exposes raw platform modules, or widens
ProviderDeviceRuntimebeyond what its existing consumers demonstrably need.W1d —
@agent-device/provider-limrunGoal: atomically move the W1c-clean Limrun implementation behind one package façade.
LIMRUN_API_KEYis absent and preserve provider/device behavior when present.Done: package exports only
., imports only kernel/contracts/@limrun/api/node, and has zero rootsrc/imports; provider scenarios plus live Limrun evidence pass; packed output and startup behavior remain compatible.W2 — #1478 phases land as packages
After W0 and W1a establish the workspace and shared vocabulary, amend #1478 so P2/P3/P5 land directly in
packages/{maestro,replay-test,ad-replay}; do not first createsrc/<module>/and move it again. W1b/W1c/W1d are sibling provider work and do not block this replay chain. Each brief's Done gains "lands as a workspace package; zero root imports; only.is exported." The package graph's acyclicity makes P5's engine-out-of-the-SCC requirement non-optional. If both engines need the same snapshot/selector values, that earns a focused addition to@agent-device/contracts— never a new grab-bag.W3 — platforms
If #1478's P6 checkpoint defers platform modularity, record "not now" here and stop. If it proceeds, complete P6a/P6b first, then make a fresh package go/no-go from the resulting logical seams. Packaging must not be used to design those seams. A "go" additionally requires open capability unions, plugin-contributed capability matrices, and verified per-package native asset resolution. Not scheduled here.
Explicitly out
@agent-device/host— rejected; only W0-audited low-level helpers may move to kernel, while version/project-root policy and exec stay in root composition.screenshot-diffpackage — dropped from the schedule (may join opportunistically later; its OCR path has a separate pending decision).metro— deferred: importsdaemon/session-paths.tsand the companion tunnel; needs its own seam brief before it can satisfy the no-shims rule.STOP conditions (initiative-wide)
Published artifact or packed manifest loses byte/behavior/dependency parity; a move needs a compatibility re-export at the old path; a package needs a root
src/import; a zero-dep script needs a non-exported package source; tsdown cannot keep one artifact + lazy platform loading; CI wall-clock regresses materially; a shared package would be created without a named domain and current consumers.Success metrics
packages/ → src/imports.Validation
Every worker:
pnpm check:affected --run. W0 and W1a runpnpm checkbecause they rewire the workspace, type graph, and gates; every package cutover (W1b, W1d, and #1478 P2/P3/P5) also runspnpm checkbecause path topology and published bundling change broadly. W1b/W1c/W1d additionally run provider-integration and the applicable live device/cloud evidence perdocs/agents/device-verification.md. CI stays authoritative.References
docs/daemon-modularity-proposal.md§"Conditional workspace layout" — the package constraints adopted here (private: true,workspace:*, single artifact, no foundation dumps); its "only when earned" stance is superseded by this issue