Skip to content

refactor(daemon): move CLI-shared daemon modules out of the daemon package and gate CLI reachability (R78) - #3625

Merged
thymikee merged 2 commits into
refactor/daemon-wire-types-to-contractsfrom
refactor/daemon-client-deps-to-contracts
Oct 15, 2026
Merged

thymikee merged 2 commits into
refactor/daemon-wire-types-to-contractsfrom
refactor/daemon-client-deps-to-contracts

Conversation

@thymikee

@thymikee thymikee commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

L1b of the daemon single-entrypoint stack, on top of #3616: the CLI process reaches no module of @agent-device/daemon, and R78 now proves it by reachability instead of a folder ban.

  • Modules moved out of the daemon (git moves; tests travel with them where the test fixtures allow):
    • daemon-contracts: the device-claim cluster (record, store, inspection, reboot, settlement, claims), session-artifact-paths (with the managed-path check), daemon-stop, and the claim reconciler + owner-scoped composer + recovery platform scope.
    • capture-kit: the app-log durable resource, its store and recovery, and the app-log file policy (they sit with the other durable captures).
    • kernel/device: decodeDeviceIdentity (was in capture-kit), so daemon-contracts does not depend on capture-kit for claim records.
    • DaemonCommandRoute is read from command-registry.
  • device release --stale composes from root platform runtimes (src/platform-runtime-claim-recovery.ts) and reaches no daemon module. src/cli/commands/device-release.ts is gone.
  • R78 (daemon-client-entry) is now reach-only: a breadth-first walk over static and dynamic edges from src/bin.ts/src/cli.ts that fails on any reached module outside the client-side roots that imports the daemon package, naming the chain. R82 and R78 share one walker (scripts/layering/process-reach.ts). The folder ban over src/cli.ts, src/cli/, src/client/, src/remote/ and src/daemon-client/ is the client-side entry in LOGICAL_MODULE_POLICIES, which refactor(daemon): move wire request and response types out of the daemon package #3616 introduced for src/daemon-client/. The tests plant type-only, value and dynamic imports on every root, the exact laundering chain cli.ts → cli/commands/device.ts → platform-runtime-claim-recovery.ts → daemon, a type-only hop that must not count, and a stale-entry case. On the first run the walk also found platform-runtime-app-log-output.ts/-process.ts importing daemon helpers; both moved too.
  • ADR 0032/0033 stop describing R78 as recording edges, and the AGENTS.md session-artifact-paths pointer follows the move.

Subpath exports

Package Before After
daemon 118 107
daemon-contracts 12 17 (device-claims, device-claims-fixtures, device-claim-recovery, daemon-stop, session-artifact-paths)
capture-kit 80 83 (app-log-durable-resource, app-log-resource-recovery, app-log-files)
kernel, host-kit, test-kit 22 / 20 / 18 unchanged

Net: 210 → 207. The device-claim cluster is one explicit-name entry (device-claims) instead of seven files exported separately. Two judgment calls to review:

  • daemon-stop keeps its own subpath. It lazily imports daemon-registration-owner, and its test mocks that seam, so merging it into the owner module (or re-exporting it from one) would either make a static cycle or break the test.
  • The reconciler lives in daemon-contracts (ADR 0033 records why: the daemon and the daemonless release both run it, and ADR 0019 section 1 keeps the transaction with the shared code), which depends on capture-kit. The reverse (capture-kit → daemon-contracts) is a TypeScript project-reference cycle through command-registry → selectors → capture-kit, and there is no package above both. The dependency stays acyclic; if the layering intent is "contracts below capture", the reconciler needs a new package, which I did not create.

Eager closure

scripts/__tests__/eager-closure-budgets.test.ts passes with no growth on any existing entry. Three new entries carry APPROVED_OVER_CEILING rows (app-log-durable-resource.ts, device-claim-recovery.ts, device-claims.fixtures.ts): each is code moved verbatim from a daemon entry of the same weight, and only the daemon and the lazily loaded device release --stale path import them. The first draft widened capture-kit's root barrel (32 → 63 modules) and daemon-resolution; both were reverted to separate entries for that reason. Four tests that need test-kit stay under packages/daemon (test-kit depends on daemon-contracts, and capture-kit/daemon-contracts tests cannot import it): the claim, settlement, reconciliation and app-log-files tests exercise the moved modules through their public entries.

Diff size

118 files, +744 −582 over #3616: over the 1,000-line budget. Most of it is moves counted twice (rename detection does not pair the merged or split files: the kernel decode move is +85/−90, the claim reconciler, owner recovery and scope moves another ~300) and import rewrites across the daemon tests. If you want this under budget I would split the kernel/capture-kit moves (decodeDeviceIdentity, app-log durable resource and file policy) into a lower layer.

Validation

pnpm check:affected --run passes on 3ce8ac06c (L1b) and on 824077b38 (#3616), both rebased on origin/main e649bc0; pnpm check:daemon-wire-compat passes on both. Earlier L1b attempts failed on the eager-closure budget, production-exports and fallow (all fixed above) and twice on tmpdir-leaks-model while two other threads' gates ran on this host; that check passes alone twice and in the final runs. A fresh-context review of the first L1b draft found only stale path references (fixed); the maintainer review of this shape asked for the shared walker and the policy entry, both done.

No device-facing behavior changes; device release --stale runs the same reconciler code, composed from the same root gateway.

🤖 Generated with Claude Code

@thymikee
thymikee added this pull request to stack #3626 October 14, 2026 18:27
@thymikee thymikee changed the title refactor/daemon client deps to contracts refactor(daemon): move CLI-shared daemon modules out of the daemon package and gate CLI reachability (R78) Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-15 08:14 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 118 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/daemon-contracts/src/session-artifact-paths.ts
Comment thread docs/adr/0033-collocation-decision-table.md Outdated
Comment thread packages/daemon-contracts/src/recovery-platform-scope.ts
Comment thread scripts/layering/daemon-client-entry.ts Outdated
Comment thread packages/daemon-contracts/src/device-claim-recovery.ts
Comment thread scripts/layering/daemon-client-entry.test.ts Outdated
Comment thread docs/adr/0032-layering-graph-engine.md Outdated
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.25 MB 5.25 MB -301 B
Package (unpacked) 5.25 MB 5.25 MB -301 B
Package (download) 1.58 MB 1.58 MB -123 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 18.7 ms 18.1 ms -0.6 ms
CLI --help 49.6 ms 50.9 ms +1.2 ms

@thymikee

Copy link
Copy Markdown
Member Author

The reach walk in R78 duplicates R82's walk, so this needs one change before merge. The other code looks fine at 6b96029, and CI is green (43 checks, 0 failing, including check:layering and the eager-closure budget test). I did not run check:layering or typecheck locally, so "the CLI reaches no daemon module" rests on that CI result, and I did not recount the subpath exports or check removed subpaths for leftover importers beyond what typecheck proves.

reachedDaemonImports in daemon-client-entry.ts copies the walk in daemon-process-reach.ts almost line for line. It has the same stale-entry refusal, the same outgoing map with the edge.typeOnly && !edge.dynamic filter, the same cameFrom BFS and the same chain rebuild. R78 differs only in its denied set (a prefix, not named modules) and in stopping at daemon edges. That makes two copies of one rule, so a fix to edge semantics has to land twice and the two can drift. Please extract one walker in scripts/layering, for example reachWithChains(entries, edges, { stopAt }), that returns the cameFrom map, a chain renderer and the stale-entry check. Then R82 and the R78 reach half become two declarations on it. Every "process entry must not load module set" rule should go through that one walker, and check:layering should stay green with the existing R78 and R82 tests unchanged.

Should daemon-contracts own the device-claim reconciler at all? It now depends on capture-kit at runtime because the reconciler drives app-log, audio-probe and perf-capture recovery, and it holds a process-lifetime PlatformRequestScope. Those are not on-disk or wire contracts. Would it be smaller to keep daemon-contracts to the claim record, store, inspection, paths, reboot and settlement, plus session-artifact-paths and daemon-stop, and let the root own the reconciler beside createClaimRecoveryGateway in src/platform-runtime-claim-recovery.ts? The daemon dep would change from claimRecoveryGateway: ClaimRecoveryGatewayFactory to a root-composed composeClaimReconciler(scope): DeviceClaimReconciler, which the three daemon sites call. That drops the daemon-contracts to capture-kit edge, the device-claim-recovery subpath and its APPROVED_OVER_CEILING row. Daemon tests that build reconcilers through device-claims-fixtures would need a test-kit fixture or stay root-owned, and a one-line ADR 0033 row should say whether runtime recovery may live in daemon-contracts. Production size is about +110 and behavior is unchanged, so this is an ownership question: if you considered this split and rejected it, a short reason is enough.

On the open review threads, the realpath skip for a missing dirname (P1) and emitDiagnostic returning early without a scope (P2) both exist on the base commit and are moved verbatim here. They are fine as separate fixes. Two lower-priority threads still apply: the missing dynamic-import case in the folder-ban tests, and the ADR 0032 "flat ban" wording. These do not apply, so please resolve them: ADR 0033 and type-only edges (the ADR describes the folder ban, which does cover type-only edges), require()/createRequire (no production use exists, and this is a limit of the shared import parser, not R78), and OwnerScopedClaimRecovery missing from the facade (no consumer names it).

thymikee and others added 2 commits October 15, 2026 08:13
…ckage

The device-claim record, store, inspection and settlement, the session
artifact paths and the stop path now live in daemon-contracts, which the CLI
and the daemon both read. The claim reconciler, its owner-scoped composer and
the recovery platform scope sit beside them; the app-log durable resource,
its recovery and the app-log file policy sit in capture-kit with the other
durable captures. The device identity decode moves to kernel, so no contracts
module needs capture-kit for it. `device release --stale` and the platform
runtimes it composes reach no daemon module, and `DaemonCommandRoute` is read
from command-registry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
R78 walks static and dynamic edges from src/bin.ts and src/cli.ts and fails
on any reached module outside the client-side roots that imports the daemon
package, naming the chain. R82 and R78 share one walker, process-reach.ts.
The client-side policy in LOGICAL_MODULE_POLICIES now covers the CLI, typed
client, remote transports and daemon client. The two moved entries that carry
the durable-capture machinery and the claim fixtures entry are approved over
the new-entry ceiling, and the ADRs say what the rules do now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the refactor/daemon-client-deps-to-contracts branch from 6b96029 to 3ce8ac0 Compare October 15, 2026 06:26
@thymikee

Copy link
Copy Markdown
Member Author

Addressed at 3ce8ac06c:

  • One walker. Extracted scripts/layering/process-reach.ts (walkProcessReach with a stopAt predicate, a chain renderer and staleReachModules). R82 and the R78 reach half are now declarations on it; the R82 tests are unchanged and pass. The R78 folder ban moved into the client-side entry of LOGICAL_MODULE_POLICIES, so R78 is reach-only and reports nothing the policy already reports.
  • Reconciler ownership. I kept it in daemon-contracts, with the reason added as a row in ADR 0033. Both the daemon (at open and startup) and the daemonless device release --stale run it, and ADR 0019 section 1 gives the daemon-side shape the recovery transaction (what to rebuild from, in what order, when to dispose) with root composition supplying only the gateway. Moving the transaction to root would reverse that and rename the injected claimRecoveryGateway dependency across about forty test fixtures. The cost you name is real: daemon-contracts depends on capture-kit, and the reverse is a project-reference cycle through selectors. If you prefer the root-owned reconciler I will do it as a follow-up layer.
  • Threads. The missing dynamic-import case and the ADR 0032 wording are fixed; the realpath and diagnostics-scope threads are pre-existing behavior moved verbatim (not ticketed yet); the other three are resolved with reasons in-thread.
  • pnpm check:affected --run passes on this tip and on refactor(daemon): move wire request and response types out of the daemon package #3616.

@thymikee

Copy link
Copy Markdown
Member Author

The code at 3ce8ac0 looks good to me. The earlier findings from 6b96029 (#3625 (comment)) are fixed: ADR 0033 now separates the client-side direct-edge policy from R78's reach hop, the dynamic-import test runs for each of the five client-side roots, and the ADR 0032 note says where the flat ban lives.

Not blocking: the new device-claim-recovery.ts row in docs/adr/0033-collocation-decision-table.md:148 cites ADR 0019 section 1, but the rule that root composition supplies only the gateway is in section 10, and ADR 0019:850-851 still names the old packages/daemon path for claim-recovery-gateway.ts, so you can cite section 10 and update that path to daemon-contracts, or leave it.

I reviewed only the new changes in 3ce8ac0 and did not run check:layering, typecheck or the R78 and R82 tests myself, so "the real tree passes R78" rests on your check:affected run. All 14 jobs that did not pass were cancelled and none failed, so CI shows no failing step, and the changes touch only scripts/layering, the eager-closure budget table and ADR text. Please rerun CI on 3ce8ac0 so the checks produce a result before merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee
thymikee merged commit 7e097a0 into main Oct 15, 2026
36 of 50 checks passed
@thymikee
thymikee deleted the refactor/daemon-client-deps-to-contracts branch October 15, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant