callstack / callstack/agent-device
refactor: cut the daemon's commands-closure leak and its last root hubs (successor to #2278 and #2335)
- Dominant language
- TypeScript
- Stars
- 4.6k
- Forks
- 299
- Avg merge
- 10h 14m
- Merged PRs (30d)
- 537
Description
## Purpose
Remove the last structural coupling that makes the daemon expensive to change and impossible to package, and stop it paying for the whole command pipeline it never calls. Successor to #2278 (residual semantic coupling, closed) and #2335 (decoupling wave 3, closed). Those waves moved code into packages and classified what remained; this one cuts the two things measurement shows are still load-bearing.
Measured at `9aa6465768` with the gate's own model (`pnpm depgraph` plus a throwaway probe against `scripts/layering/model.ts`): 1,684 production files, 8,689 edges, 38 zones, R4 = 0, R5 = 0, R6 = 4.
Two findings drive the sequence:
1. **The daemon's eager value closure contains 121 `src/commands/**` files** (717 files total in the closure). One value edge is responsible for 48 of them: `src/runtime.ts` → `./commands/index.ts`. Five daemon files import `src/runtime.ts`.
2. **Daemon → `src/` outside the daemon is 51 file pairs** (42 root flat files, 7 `src/core/**`, 2 `src/cli-schema/**`), down from 154 edges into 67 files at the wave-3 baseline. Twelve of those pairs are one file, `src/provider-device-runtime.ts`, and ten of its twelve importers need exactly one symbol, `isActiveProviderDevice`.
Fan-in is not cost. Churn over the last 400 commits: `src/backend.ts` 53, `src/provider-device-runtime.ts` 19, `src/runtime.ts` 6, `src/runtime-contract.ts` 5, `src/io.ts` 4. `runtime-contract.ts` has 27 importers and is nearly frozen; it is not a target.
## Children
Wave A (independent):
- #2540 — `refactor(daemon): stop importing the commands barrel through src/runtime.ts`. The closure leak; the only child that changes what the daemon loads.
- #2541 — `refactor(runtime): own provider-device admission behind a typed capability`. 12 pairs → 0.
- #2542 — `chore(gates): classify root hubs, dynamic edges, and the root-to-daemon entry`. Makes the residue in this issue visible and non-regrowing.
Wave B (after Wave A):
- #2543 — `refactor(cli-schema): orient the schema layer below commands`. 5 value back-edges, 7 type-only.
- #2544 — `refactor(daemon): extract the maestro adapter and durable-capture admission subtrees`. Blocked on where `daemon-request` lives.
Wave B is not scheduled while a Wave A branch is open on the same files; `src/commands/index.ts` (#2540) and `cli-schema/command-schema.ts` (#2543) overlap, and #2542 owns the R76 inventory both #2541 and #2544 want to edit.
## Completion conditions
This umbrella closes when every child is closed and:
- The daemon's value closure reaches `src/commands` only through `src/runtime-command-surface.ts`, contains only the `capture`, `selectors`, and `interactions` runtime families (`src/commands/*/runtime/**`) plus their shared runtime helpers, and includes no command facet, CLI reader, output formatter, or the all-family barrel. Measured at `775163bb35`: 34 files, 6,368 LOC, down from 121 files, 19,659 LOC. Zero was the original wording; it is unreachable while the daemon executes those three families in-process, and relocating the runtime directories would only change the count, not what loads (decided 2026-09-14 on #2540).
- Daemon → `src/` outside `src/daemon/` is 0 file pairs except the edges classified composition-essential or daemon-policy-essential by the R76 inventory.
- `pnpm depgraph` reports the same R4/R5 = 0 and R6 does not grow past its ratchet.
## Explicitly not scheduled
- **Packaging the daemon.** Inbound is already only ~20 non-daemon files and no `packages/**` file imports `src/**`, but packaging it before the closure cut and the hub ports just relocates the eager-closure problem. #2469 must also land first: the eager-closure budget cannot currently tolerate a module split.
- **Moving `src/backend.ts` or `src/runtime-contract.ts`.** Both are type-only surfaces (value closure of `backend.ts` is one file) with churn of 53 and 5 commits per 400. Relocating a near-frozen type surface is a rename, not decoupling.
- **Growing `@agent-device/contracts`** as a destination for `src/core/**` without a separate measurement of its own churn. At 18k LOC and 185 files it is the second hub in the repo.
- **Collocation-only file moves, generic hook bags, and new gate detectors that do not name a measured edge.** Per #2531's rules: fix-shaped, not detector-shaped.
## Reproduction
`pnpm depgraph --out /tmp/depgraph.json`, or a throwaway `scripts/layering/.probe.ts` over `listSourceFiles()` + `resolveImportEdges()` restricted to `isProductionSourceFile`, computing per-bucket outbound pairs, edge kinds (`typeOnly`/`dynamic`), and value-only transitive closure from `src/daemon/**`. Delete the probe after.
Contributor guide
Research direction
Start with `pnpm depgraph --out /tmp/depgraph.json` and the measurements in `scripts/layering/model.ts`; inspect the Wave A child issues #2540–#2542 before changing anything. Track the daemon value closure and outbound pairs from `src/daemon/**`, and treat the listed completion conditions and R4/R5/R6 gate results as done.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, build-system, devtools
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100