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
Every agent-device invocation — --version included — eagerly evaluates 365 modules through src/cli.ts. Measured on main at 5ba4ac7 with pnpm depgraph (value edges only, so type-only and await import edges are excluded); the number matches the eager-closure gate's own reading of src/cli.ts exactly.
It is the outlier by a wide margin. Across the 239 entries the gate measures:
These are not three unrelated wins. They are one defect class: a module that needs vocabulary — a constant list, a predicate over it — reaches it through a runtime barrel and pays for the runtime. Wave 3 (#2335) made this both visible and fixable: the vocabulary now has package homes, and the subpath-per-file rule gives each one a cheap door.
Dominator analysis from src/cli.ts — modules that stop loading if that one module goes lazy:
module
dominates
commands/family/registry.ts
118
commands/interaction/index.ts
49
commands/interaction/metadata.ts
43
commands/interaction/runtime/gestures.ts
40
cli/connection/provider-policy.ts
32
packages/provider-webdriver/src/index.ts
31
Steps, in order
Each is independently landable and independently measurable.
A — one import line, −40 modules (365 → 325)
src/commands/interaction/metadata.ts:42 imports SCROLL_INPUT_DIRECTIONS from ./runtime/gestures.ts. Every other import in that file is vocabulary (@agent-device/contracts/*, @agent-device/selectors); this one line is the odd one out, and it drags 40 modules of gesture runtime into every CLI run.
gestures.ts does not even own the constant — it re-exports it from @agent-device/contracts/scroll-gesture, with a comment above saying the vocabulary lives there "so the public API can declare ScrollOptions without depending on this command runtime". metadata.ts already imports from that exact module on line 21.
Fix: fold the specifier into the existing contracts import. One line, no behaviour change.
B — one subpath, −31 modules (325 → 294)
src/cli/connection/provider-policy.ts imports CLOUD_WEBDRIVER_PROVIDERS (a constant map), isCloudWebDriverProviderName (a pure predicate over it) and one type from @agent-device/provider-webdriver — whose only export is ".", so the whole WebDriver runtime loads to answer "is this string a known provider name".
packages/provider-webdriver/src/providers.ts has zero imports: a pure leaf. It predates the subpath-per-file rule the wave established.
Fix: publish ./providers pointing straight at src/providers.ts and point provider-policy.ts at it.
C — family registry facet split, −82 further (294 → 212)
src/commands/family/registry.ts value-imports all twelve command families to build one array, and dominates 118 modules. Its seven non-test consumers each want a different facet — CLI schemas, CLI readers, metadata, definitions, output formatters, daemon writers — and each pays for all twelve families' runtime.
Unlike A and B this is a design task, not a mechanical one, and should not be started assuming otherwise. family/types.ts:31 states the coupling directly: "What a command file authors: metadata plus run; the facet derives the executable from them." Separating them means the registry aggregates metadata eagerly and resolves run at dispatch. Split this out into its own issue if the design grows past one PR.
Expected result
after
modules
vs today
today
365
—
A
325
−40
A + B
294
−71
A + B + C
212
−153 (−42%)
The module counts above are measured. The wall-clock effect is not: today's size report reads --version 27 ms and --help ~79 ms, and module count is a proxy for parse-and-evaluate cost, not a linear one. Each step should report the size-report delta it actually produced rather than predicting one.
Validation
The eager-closure gate already enforces no-growth per entry against the merge-base, so each step verifies itself and the improvement shows up in the gate's own numbers. No baseline edits, no APPROVED_OVER_CEILING row, and no new suppressions should be needed by any step — if one appears to be, that is a signal the step is doing something other than what is described here.
Evaluated and not scheduled
The daemon code-signature cost (~620 → ~1,461 modules walked, ~2.9 ms → ~7.5 ms cached, landed with refactor(daemon): move four pure leaves to their kits #2347) is a real startup regression but a different mechanism — a workspace source walk in walkDaemonCodeGraph, not an import closure. Nothing here touches it; it wants its own issue.
packages/platform-android/src/mechanics.ts (177) and the platform-apple façades (100–120) are the next-largest closures, but they are not on the CLI's hot path; the composition root keeps them behind src/platform-runtime.ts (47).
A lint rule banning vocabulary→runtime edges: only one instance of the smell exists today (step A), so a gate would be enforcement ahead of evidence.
Purpose
Every
agent-deviceinvocation —--versionincluded — eagerly evaluates 365 modules throughsrc/cli.ts. Measured onmainat5ba4ac7withpnpm depgraph(value edges only, so type-only andawait importedges are excluded); the number matches the eager-closure gate's own reading ofsrc/cli.tsexactly.It is the outlier by a wide margin. Across the 239 entries the gate measures:
src/cli.ts)platform-android/src/mechanics.ts)Where those 365 live:
commands92,contracts65,provider-webdriver25,host-kit24,cli19,core18,kernel17,selectors15,snapshot14,ad-script13.These are not three unrelated wins. They are one defect class: a module that needs vocabulary — a constant list, a predicate over it — reaches it through a runtime barrel and pays for the runtime. Wave 3 (#2335) made this both visible and fixable: the vocabulary now has package homes, and the subpath-per-file rule gives each one a cheap door.
Dominator analysis from
src/cli.ts— modules that stop loading if that one module goes lazy:commands/family/registry.tscommands/interaction/index.tscommands/interaction/metadata.tscommands/interaction/runtime/gestures.tscli/connection/provider-policy.tspackages/provider-webdriver/src/index.tsSteps, in order
Each is independently landable and independently measurable.
A — one import line, −40 modules (365 → 325)
src/commands/interaction/metadata.ts:42importsSCROLL_INPUT_DIRECTIONSfrom./runtime/gestures.ts. Every other import in that file is vocabulary (@agent-device/contracts/*,@agent-device/selectors); this one line is the odd one out, and it drags 40 modules of gesture runtime into every CLI run.gestures.tsdoes not even own the constant — it re-exports it from@agent-device/contracts/scroll-gesture, with a comment above saying the vocabulary lives there "so the public API can declareScrollOptionswithout depending on this command runtime".metadata.tsalready imports from that exact module on line 21.Fix: fold the specifier into the existing contracts import. One line, no behaviour change.
B — one subpath, −31 modules (325 → 294)
src/cli/connection/provider-policy.tsimportsCLOUD_WEBDRIVER_PROVIDERS(a constant map),isCloudWebDriverProviderName(a pure predicate over it) and one type from@agent-device/provider-webdriver— whose only export is".", so the whole WebDriver runtime loads to answer "is this string a known provider name".packages/provider-webdriver/src/providers.tshas zero imports: a pure leaf. It predates the subpath-per-file rule the wave established.Fix: publish
./providerspointing straight atsrc/providers.tsand pointprovider-policy.tsat it.C — family registry facet split, −82 further (294 → 212)
src/commands/family/registry.tsvalue-imports all twelve command families to build one array, and dominates 118 modules. Its seven non-test consumers each want a different facet — CLI schemas, CLI readers, metadata, definitions, output formatters, daemon writers — and each pays for all twelve families' runtime.Unlike A and B this is a design task, not a mechanical one, and should not be started assuming otherwise.
family/types.ts:31states the coupling directly: "What a command file authors: metadata plusrun; the facet derives the executable from them." Separating them means the registry aggregates metadata eagerly and resolvesrunat dispatch. Split this out into its own issue if the design grows past one PR.Expected result
The module counts above are measured. The wall-clock effect is not: today's size report reads
--version27 ms and--help~79 ms, and module count is a proxy for parse-and-evaluate cost, not a linear one. Each step should report the size-report delta it actually produced rather than predicting one.Validation
The eager-closure gate already enforces no-growth per entry against the merge-base, so each step verifies itself and the improvement shows up in the gate's own numbers. No baseline edits, no
APPROVED_OVER_CEILINGrow, and no new suppressions should be needed by any step — if one appears to be, that is a signal the step is doing something other than what is described here.Evaluated and not scheduled
walkDaemonCodeGraph, not an import closure. Nothing here touches it; it wants its own issue.packages/platform-android/src/mechanics.ts(177) and theplatform-applefaçades (100–120) are the next-largest closures, but they are not on the CLI's hot path; the composition root keeps them behindsrc/platform-runtime.ts(47).🤖 Generated with Claude Code
https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz