Files
callstack__agent-device/docs/adr/0003-daemon-command-registry.md
Michał Pierzchała b6128c0088 docs: retire plans/perfect-shape.md — roadmap complete (#1003)
* docs: retire plans/perfect-shape.md — roadmap complete

The perfect-shape roadmap (two-registry thesis: CommandDescriptor +
PlatformPlugin, typed-result spine, folder DAG + layering lint, agent-cost,
and the Apple apple+appleOs platform model with a non-breaking leaf wire) is
substantively complete and merged. Per its own §5 retirement note, the durable
decisions now live in ADR-0008 (command descriptor) and ADR-0009 (Apple/AppleOS),
and current-state terms in CONTEXT.md; this removes the last plan file.

- Delete plans/perfect-shape.md (plans/ is now empty and gone).
- CONTEXT.md: add "Architecture (perfect-shape refactor, completed 2026-07)"
  end-state summary plus a "Deferred / next-minor" note (Phase 2c client-types
  narrowing, b.3 recording/providers facets, strict DAG back-edge inversion,
  legacy alias drops) so nothing is lost.
- Repoint every remaining perfect-shape.md/§ reference (ADRs 0003/0008/0009,
  ci.yml, scripts/layering/check.ts, and the platform-plugin/apple comments)
  to ADR-0008/0009 or CONTEXT.md. No dangling references remain.

Docs/comment-only; tsc, oxlint, oxfmt, and the layering DAG check all pass.

* docs: repoint dangling perfect-shape section refs before retiring the roadmap

Removing plans/perfect-shape.md left three comments citing bare section numbers
with no surviving target. The rationales are already inlined, so drop the numbers
(and point the do-not-flatten note at the durable ADR):
- src/platforms/apple/plugin.ts: `(§7)` -> "do-not-flatten; see docs/adr/0009".
- src/core/interactors/register-builtins.ts: "the §5.1 ... sketch" -> "an ... sketch".
- scripts/layering/check.ts: drop `(§5.5 ...)`, keep the inline "re-export barrels only".
2026-07-02 07:19:37 +02:00

5.5 KiB

ADR 0003: Daemon Command Registry

Status

Accepted

Context

Daemon request handling depends on command traits that are not part of the public command surface: which handler route owns a command, whether tenant lease admission applies, whether session execution should lock, whether selector validation applies, whether replay can run an action in the current session scope, whether invalid recordings block the request, whether Android blocking-dialog recovery applies, and how request-scoped providers resolve a device.

Those traits used to be spread across src/command-catalog.ts, request-policy modules, and handler-local coverage tables. That made src/command-catalog.ts carry daemon-only behavior next to public command identity, and it required duplicate command sets to stay aligned by convention.

Decision

Keep public command identity in src/command-catalog.ts and public input/output contracts in src/commands/**.

Add src/daemon/daemon-command-registry.ts as the daemon-side source of truth for command route ownership and daemon request-policy traits. Request modules consume predicate functions from the registry instead of recreating command string sets. Handler modules own execution logic only; they do not export duplicate coverage tables to prove route membership.

The daemon registry is internal-only. It must not define CLI grammar, Node.js client options, MCP schemas, user-facing help, or platform capability support. Those remain owned by the command contract, projection, help, and capability modules.

Alternatives Considered

  • Keep daemon groups in src/command-catalog.ts: this keeps one command-name file, but it mixes public command identity with daemon runtime policy and makes the catalog grow for internal-only routing decisions.
  • Keep handler-local coverage tables: this makes each handler self-describing, but creates a second route membership source that can drift from the router and request-policy modules.
  • Put route checks directly in request modules: this is locally simple, but scatters command classification across admission, locking, provider scoping, replay, recording, and generic dispatch.

Consequences

Adding or moving a daemon-handled command requires updating the daemon command registry with its route and request-policy traits. The registry tests pin the trait decisions, while provider-backed integration scenarios verify important request-policy behavior through the real daemon request path.

The registry file is intentionally a dense internal contract. Its interface should stay small: callers ask daemon-policy questions through named predicates rather than reading or mutating command sets.

AGENTS.md should contain only the operating rule and relevant file pointers for agents. This ADR owns the rationale so future changes do not need to infer it from agent instructions.

Update (2026-06): single-declaration / derivation model

A later proposal (the CommandDescriptor direction, now ADR 0008) unifies a command's declarations so the public catalog, capability matrix, CLI/MCP projections, batch allowlist, and this daemon registry are derived from one registration site, to remove the cross-table drift that several of these surfaces are kept aligned against by convention.

This ADR's decision stands. Its boundary is about ownership and the predicate interface, not about the physical file a trait is typed in. "Separate source of truth" means separately owned and exposed through named predicates — a property that survives a projected/derived backing table. A derived daemon registry is therefore permitted only if it preserves all of the following invariants:

  1. Daemon-owned declaration. Route and request-policy traits are declared in a daemon-owned facet (under src/daemon/) and composed into the registration — never inlined as fields on the public command contract in src/commands/** or src/command-catalog.ts. Co-locating a registration call is fine; co-locating ownership of daemon policy in the public surface is the contamination this ADR rejected (see Alternatives, "Keep daemon groups in src/command-catalog.ts").
  2. Predicate interface unchanged. Consumers keep asking daemon-policy questions through the named predicates (getDaemonCommandRoute, isLeaseAdmissionExempt, shouldLockSessionExecution, …). The daemon registry remains their sole exposer; derivation changes how the backing table is built, not how it is read.
  3. No leakage into public projections. The catalog/CLI/MCP/help/capability projections must be type-prevented from reading daemon-only traits, and the daemon registry must still not define CLI grammar, Node.js options, MCP schemas, user-facing help, or capability support.
  4. One declaration per concern, enforced by types. The single registration site must make a missing or duplicated daemon trait a compile error — replacing today's "aligned by convention". This is the structural improvement that justifies derivation over a separately hand-authored table.

A single flat public descriptor whose daemon fields leak into public views is not permitted — that is the "collapse daemon policy into a public command registry" failure this ADR exists to prevent. Compose facets owned by their domains; derive the registry from them. Until that derivation lands and is pinned by the registry tests, the hand-authored src/daemon/daemon-command-registry.ts remains the source of truth and the operating rule in AGENTS.md is unchanged.