Conversation
…t exec internals First step of the platform-seam work (#1490 W3 prerequisite / #1478 P6a). 13 of the ~22 modules naming `ExecResult` live under `src/platforms/`, and every one of them wanted only the type: the shape of a finished host command, with no runtime behavior at all. Because the type was declared beside the implementation in `src/utils/exec.ts`, each of those modules imported root code to describe a value it already held — a dependency no platform package could ever satisfy, for nothing. `ExecResult` moves to `@agent-device/contracts/platform`, beside `PlatformPlugin` and the rest of the platform-family vocabulary. The running stays in `src/utils/exec.ts`: process spawning, argument policy, timeouts and redaction are host policy the root composition owns, and #1490 keeps `exec` root-side deliberately. This splits the vocabulary from the policy rather than relocating the policy. Type-only change, no re-export shim at the old path (#1490's rule): every consumer now names the contracts subpath, and `exec.ts` imports the type it implements against. What this does NOT do, stated so the next step is not mistaken for done: 45 platform modules still import `utils/exec.ts` for BEHAVIOR — runCmd (98 references), requireExecSuccess (45), withCommandExecutorOverride (32), execFailureDetails (30), whichCmd (24), runCmdBackground (21) and a long tail. That is genuine capability coupling and needs the narrow injected-capability treatment W1c used for limrun, not a type move. Gates: typecheck, lint, format, check:layering, and check:affected all green. Layering unmoved — R6 at 7, R9 at 47, R11 at 10 packages / 32 subpaths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117wfrvC6MRDJUWdRBErNEF
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
P1 — this reopens a deferred architecture path without its required trigger. #1490 records W3 as “not now” and says it stops there. #1478 records P6 DEFER: P6a is not pursued, and P6b is deferred until a concrete platform/provider contribution or cleanup incident earns it. This PR explicitly calls itself the first W3/P6a prerequisite, but names no new trigger, shrinks no R6/R9/layering measure, and leaves the 45 behavioral Please revert/close this, or first record and link a concrete trigger plus the narrowly scoped seam decision it earns. The mechanical type move is otherwise complete; it changes no runtime/device behavior, so live-device evidence is inapplicable. |
|
P1 accepted on the substance. You are right, and it contradicts my own analysis earlier in this session — I concluded platform packaging was "not now" and that the reviving condition is a forcing function rather than a tidiness argument, then framed this PR as a W3/P6a prerequisite anyway. No trigger, so the framing was wrong. Your specific charge checks out against my own measurements: R6 stayed 7, R9 stayed 47, R11 stayed 10 packages / 32 subpaths. No gate-measurable improvement, and the 45 behavioral Two things before a decision, neither of them a defence of the framing:
Either outcome is fine by me. If it closes, it closes; the diff is mechanical and costs nothing to redo once something actually earns the seam. If it stays open, it needs the trigger recorded and linked exactly as you say, and the description rewritten to stop claiming a prerequisite for work that is deferred. Holding further pushes until that is settled. Generated by Claude Code |
|
Closing per your P1 — the requester agrees. No trigger exists, so the justification did not survive: R6 stayed 7, R9 stayed 47, R11 stayed 10 packages / 32 subpaths, and the 45 behavioral The diff is mechanical and costs nothing to redo when something actually earns the seam — a platform/provider contribution or a cleanup incident, per #1490 and #1478's own conditions. Recording what was learned so it is not re-derived:
Nothing further pushed to the branch. Generated by Claude Code |
|
|
Correcting one line in my close comment: I said "nothing further pushed to the branch", and I have now pushed This PR stays closed — the push does not reopen it and is not a request to reconsider. The commit is the Generated by Claude Code |
First step of the platform-seam work — the #1490 W3 / #1478 P6a prerequisite. Type-only, no behavior change.
The problem
ExecResult—{ stdout, stderr, exitCode, stdoutBuffer? }— was declared beside the implementation insrc/utils/exec.ts. 13 of the ~22 modules that name it live undersrc/platforms/, and every one of them wanted only the type: the shape of a finished host command, with no runtime behavior at all.So platform modules imported root code to describe a value they already held. For a type that erases at compile time.
The change
ExecResultmoves to@agent-device/contracts/platform, besidePlatformPluginand the rest of the platform-family vocabulary.The running stays in
src/utils/exec.ts— process spawning, argument policy, timeouts, redaction are host policy the root composition owns, and #1490 keepsexecroot-side deliberately. This splits the vocabulary from the policy rather than relocating the policy.Per #1490's rules: no re-export shim at the old path. Every consumer names the contracts subpath, and
exec.tsimports the type it implements against.What this is not
check:layeringreports zero value-import cycles before and after, and R4 would have rejected one. This was a layering/packaging blocker.@agent-device/contractsis"private": true— an internal workspace package bundled into the single published artifact. Nothing is exposed to consumers.utils/exec.tsfor behavior —runCmd(98 references),requireExecSuccess(45),withCommandExecutorOverride(32),execFailureDetails(30),whichCmd(24),runCmdBackground(21), and a long tail. That is genuine capability coupling and needs the narrow injected-capability treatment W1c used for limrun, not a type move. It is the single biggest remaining blocker to platform packages, and it is the next slice, not this one.Why it is worth landing on its own
It is the one part of the exec coupling that is pure cost with no design question attached: 13 modules paying a dependency for nothing. Separating it means the behavioral port that follows is reviewed on its own merits, with the noise already gone.
Validation
pnpm typecheck,lint,format:check,check:layering,check:production-exports, andVITEST_MAX_WORKERS=2 pnpm check:affected --runall green atdac1cf7.Layering unmoved — R6 at 7, R9 at 47, R11 at 10 packages / 32 subpaths.
check:production-exportsis byte-identical to themainbaseline (10 issues, 10 suppressed).One number I have not fully attributed: the size report shows a reduction — JS raw −3.5 kB, gzip −1.3 kB, tarball −9.8 kB, unpacked −32 kB. A type-only move should be ≈0, so flagging rather than claiming a cause. The plausible mechanism is that modules which imported only
type ExecResultfromexec.tsnow have no edge to it at all, soexec.jsstops being pulled into chunks that never needed it — but I have not verified that, and I would rather say so than assert it. A reduction cannot regress behavior, and the packaged-CLI and bundle-size gates cover the artifact; happy to dig in if the number matters to review.