Op-substrate surface: path[] command paths, umbrella nouns, defineService, render hook
Status: open/accepted
Summary
Section titled “Summary”- A command is a 2–3 segment kebab path (
path: string[], hard cap 3), replacing the flat{noun, verb}descriptor. Depth-3 only where a group has ≥2 members; singleton groups are banned. - The surface lands at 11 visible nouns
(
task backlog milestone standard entities commit report docs quality project dashboard) and 4 hidden (plugin pr gate lease); nouns added by later decisions (config,orchestrate,reference,term) follow the same rules. Entity CRUD is untouched; the 18 remaining script capabilities get final homes (notablytask inflight,project scan {todos,candidates},quality baseline {capture,diff,prune}). defineServiceis a registry sibling todefineOpfor long-running capabilities (dashboard, lease heartbeat-loop): same path-keyed index and help surface, lifecycle dispatch (start/stop?/list?) instead of request/response. Resolves D-0007’s open question.- One output contract (§4): every op’s canonical result is its zod output object; a single
--output text|json|jsonlparameter shapes the projection over two op classes (sync, streaming). Defaults are op-declared: a descriptor-suppliedcli.render(output, io)hook is the text projection and makes text the default (parity for line-oriented consumers liketask nextandquality run --log); without one, json — today’s adapter behavior. - Adapter projection is a pure function of the path: CLI segments verbatim; MCP tool name =
underscore-join (forward-only, uniqueness enforced by project-check, not reversibility); HTTP
route =
/<seg>/<seg>[/<seg>]with method by verb class. - This amends D-0007-deterministic-op-substrate (§2 descriptor shape) and resolves two of its open questions (long-running ops; skill-local triage). Migration sequencing lives in the op-substrate full-sweep milestone.
Context
Section titled “Context”D-0007 fixed the architecture — entity-oriented lib, op registry, generated adapters — and T-0010/T-0014 landed the registry and the generated CLI. The remaining sweep (promote every capability, codemod callers, delete shims) needed a final command surface. A first flat proposal added 13 new top-level nouns; the human asked for a more elegant shape with nesting where it pays.
The surface below came out of an adversarial redesign: four competing
designs (umbrella grouping, strict-flat, domain-ontology, caller-
ergonomics), three hostile critics per design, a three-judge panel
(registry-maintainer, skill-author, MCP-consumer personas — unanimous for
umbrella), synthesis with grafts, then a refutation round and a plan
re-audit. Five contested calls were arbitrated by the human on
2026-06-05. The working plan with full tables is
docs/plans/op-substrate-full-sweep.md (Rev 2).
Decision
Section titled “Decision”1. Command paths
Section titled “1. Command paths”OpDescriptor.path: string[] — 2 or 3 kebab segments, each
KEBAB-validated, 3 a hard cap. Registry key = path.join(' ')
(byte-identical for existing 2-level ops). A {noun, verb} export
without path is normalized to path: [noun, verb], so existing
descriptors compile unedited. noun = path[0], verb = path.at(-1),
groups = path.slice(1, -1) become derived accessors.
Nesting rule: a group exists only where the domain genuinely nests AND
the group has ≥2 members. Today exactly three qualify: project scan {todos, candidates}, quality baseline {capture, diff, prune}, and the
lease two-axis (lease task {claim, acquire, reacquire, fence, transition, archive}, lease op claim).
2. The surface
Section titled “2. The surface”| Noun | Verbs (* hidden) | Notes |
|---|---|---|
| task | create, update, next, inflight, preview-id, probe-state, resolve, check-claims*, close-commit*, gap-report*, lint-state-origin*, parse-touchpoints*, resolve-touchpoints*, scan-placeholders* | inflight re-homed from count_inflight_tasks; next owns pickup order (D-Q2WR-task-pickup-order); the task-state lefthook gate sits on its entity |
| backlog | create, capture, preview-id, update | entity CRUD; the last LEGACY_NOUNS shim |
| milestone | create | entity CRUD |
| standard | create, update, supersede, preview-id | entity CRUD |
| entities | validate, audit*, migrate*, check-identifiers* | all four register via the explicit @lib/model/ops barrel import at CLI bootstrap; validate absorbs the validate_frontmatter contract |
| commit / report | create, get-schema / render, get-schema | unchanged sibling parity |
| docs | generate, build-subsites | noun renamed from index by D-B4CA-term-entity-and-generated-glossary; lib→skill inversion fixed |
| quality | run, detect, baseline {capture, diff, prune} | baseline visible; each mode its own contract |
| project | setup, doctor, cleanup, scan {todos, candidates}, detect-worktree-init, preflight-worktree, teardown-worktree | the consuming-project hub |
| dashboard | start, stop, list | top-level: list is machine-global, nesting under project would lie |
| plugin* | info, resolve, install-permissions | hidden meta-noun; long names free |
| pr* | classify | cursor ledger travels with the op |
| gate* | skill-prose, worktree-scope, markdown-fixtures | lefthook invokes the CLI directly |
| lease* | task {claim, acquire, reacquire, fence, transition, archive}, op claim, heartbeat, release, inspect, list, parse-footer, reconcile, migrate, heartbeat-loop | two-axis depth-3 preserved; heartbeat (single-shot op) distinct from heartbeat-loop (service) |
Nouns added by later decisions (config, orchestrate, reference,
term) follow the same rules; sdlc --help is the authoritative roster.
Naming principles, in priority order:
- Nest only where the domain nests, and only with ≥2 members.
- Names truthful about direction and action.
- An op routes to the noun its data is about.
- Keep learned symmetries intact — when sibling nouns share a verb a
user has already memorized (
commit get-schema/report get-schema), rename both or neither; entity CRUD stays untouched. - Human-vs-machine asymmetry — brevity is a budget spent on human-typed
verbs; machine-only (hidden) ops prefer fully explicit names
(
plugin install-permissions) because no human types them. A permission to be verbose for clarity, not a length mandate. - Preserve real domain distinctions over flattening.
3. defineService
Section titled “3. defineService”defineService({ path, summary, hidden?, lifecycle: { start, stop?, list? } }) — kind: 'service' brand, registered into the same
path-keyed index, validated by a parallel validator (services carry no
input/output/handler), dispatched by a lifecycle dispatcher that bypasses
invokeOp’s output parsing (a non-returning loop would hang it). Service
modules live at services/<svc>/<name>-service.ts, found by a second
walk traversal. Lifecycle failures map to a SERVICE_ERROR exit tier
(already-running, stale pidfile, port-in-use, SIGTERM-to-dead-pid).
4. Output contract
Section titled “4. Output contract”Canonical shape. Every op’s result is its zod output object — the
single machine shape all doors share. Everything below is a projection
of that object; no op has a stdout-only truth.
Two op classes × two projections:
text | json / jsonl | |
|---|---|---|
| sync (request/response) | the op’s cli.render(output, io) writes deterministic lines/tables; may end in a terminal marker line (PR: <url>, CLAIMED task=…) | the validated output object, sorted keys, one document |
| streaming (emits events while running) | progress lines as events occur, then a deterministic terminal line | JSONL — one event object per line, then the terminal result object |
One parameter shapes it: --output text|json|jsonl (--json kept
as an alias for --output json). Rules:
- Defaults are op-declared, never TTY-sniffed: an op with a render
hook defaults to
text(parity —sdlc task nextemits its lines with no flags); an op without one defaults tojson(today’s adapter behavior). Deterministic for skills and CI. - Streaming is an op property, not a flag — declared on the
descriptor (
quality run --log,lease heartbeat-loop, dashboard logs). A streaming op under--output jsonbuffers and emits only the terminal object; a sync op under--output jsonlemits its single object as one line. The 2×2 is total: every combination is defined. - stderr carries diagnostics and legacy failure routing where a
ported contract requires it (
quality runFAIL lines); exit codes come from the OpError/SERVICE_ERROR tables, with the render hook able to shape legacy exit semantics. - All three modes are live in the CLI adapter (
quality runis the first streaming op); the MCP/HTTP adapters, when built, consume the same contract.
Behavior-lock goldens — the migration’s parity instrument: before a
script moves, capture its exact stdout/stderr/exit for fixed inputs as
committed fixtures (the “golden”); after the move, a test spawns the CLI
(bun run cli/sdlc.ts <path…>) and asserts byte-identical output.
Spawning the CLI rather than importing the module means the golden
survives the relocation unchanged — it locks behavior across the move.
5. Adapter projection
Section titled “5. Adapter projection”One path, three doors, all pure functions of the path:
- CLI: segments verbatim; interior segments are description-only group subcommands.
- MCP tool name: underscore-join with intra-segment hyphens preserved
(
project_scan_todos,gate_worktree-scope). A deterministic forward map — not reversible from the string; uniqueness is enforced by a project-check forbidding any two paths whose join collides. - HTTP route:
/<seg>/<seg>[/<seg>]; method by verb class (create/update/start/claim/… → POST; get-schema/audit/list/scan-*/… → GET; unenumerated defaults to POST). hiddensuppresses help/manifest listing only; every hidden op resolves when addressed directly (lefthook and external agents reach them).
6. Arbitrated calls (human, 2026-06-05)
Section titled “6. Arbitrated calls (human, 2026-06-05)”| Question | Resolution |
|---|---|
| Gate family split | Entity affinity — task lint-state-origin on task; only the two non-entity gates under gate |
| quality baseline sub-modes | Depth-3 leaves, each with its own zod contract — no --action discriminated union |
| Top-level count | dashboard stays top-level (11 visible) — list is machine-global |
| entities wiring | Explicit barrel import at composeNouns; T-U72C keeps the walk-root question |
| validate_frontmatter | Absorb the full contract into entities validate (3-tier resolution, exit-1-on-fail, text render); delete the shim; no retained adapters |
- Predictability with a scannable front door. Eleven visible nouns,
each a clean domain, beats both the 13-new-noun flat sprawl and a
deep tree. The judge panel’s guessing-game tests drove the re-homings
(
task inflightfixed the consensus worst guess). - The machinery cost is honest and one-time. The path[] change, hidden rendering, second walk, and render hook are a real generator rewrite — priced into one blocking task (T-NV49-op-path-substrate) with byte-identical help snapshots as its rollback gate, rather than smeared across ports.
- Renames are nearly free right now. The codemod task rewrites every
caller anyway, so fixing existing names (
count_inflight_tasks→task inflight) costs one more mapping row, not a migration of its own.
Consequences
Section titled “Consequences”- Amends D-0007-deterministic-op-substrate: the §2 descriptor’s
{noun, verb}becomespath: string[]; §2’s “registry owns the exit table” gains the SERVICE_ERROR tier; the §5 adapter list gains the projection contract above. D-0007’s open questions on long-running ops and skill-local triage are resolved here. The code shipped via T-NV49-op-path-substrate. - Binds T-902K:
taskis established as the lifecycle-verb home — the futuretask transitionsits flat beside create/update/next/ inflight, notask lifecyclesubgroup. - Binds the MCP/HTTP adapters (when built) to the projection rules in §5 — tool names and routes are derived, never hand-assigned.
- End state (reached):
plugin/scripts/andplugin/validators/are deleted; lefthook invokes the CLI directly;LEGACY_NOUNSholds onlybacklog. - Migration sequencing, task breakdown, and risk controls live in the
op-substrate full-sweep milestone (M-0003, closed) and
docs/plans/op-substrate-full-sweep.md— this decision fixes the surface and machinery contract, not the order of work.