Deterministic-first
Deterministic by default; LLM-orchestrated where it has to be.
The cost asymmetry
Section titled “The cost asymmetry”A schema check runs in milliseconds, gives the same answer every time, costs nothing per invocation, and produces a precise error you can act on. A model call costs cents-to-dollars, takes seconds, and produces prose you re-read on every run to be sure it didn’t drift. Multiplied across hundreds of checks per workflow, that asymmetry decides whether the tool feels crisp.
What “deterministic-first” looks like in practice
Section titled “What “deterministic-first” looks like in practice”The shape is “LLM as a thin orchestration layer over many small deterministic ops.” Concretely:
- Schemas, not validation prose. Every entity type has a
schema.json. Skills don’t re-explain the contract; they callsdlc entities validate <file>and gate on its exit code. - Ops, not procedural prose. Creating a task is
sdlc task create, not “tell the model the template and hope.” The op mints the id and enforces frontmatter shape and defaults. - Conventions as referenceable docs. Prose two skills would share
(commit-message format, branch naming) is factored into
solutions/ontological/conventions/<topic>.mdand eachSKILL.mdreferences it. One source of truth. - State on disk. Task status lives in the file’s frontmatter. The LLM
doesn’t track which task is in progress —
git grepdoes.
One op, one output contract
Section titled “One op, one output contract”Every op’s canonical result is its Zod output object — the single
machine shape every door shares. A --output text|json|jsonl parameter
projects that object; no op has a stdout-only truth
(D-H7FS-op-substrate-surface §4). Two op classes cross two projections,
and the grid is total:
text json / jsonl ┌──────────────────────────┬──────────────────────────┐ sync │ cli.render(output, io) │ the validated output │ (request/ │ writes deterministic │ object, sorted keys, │ response) │ lines/tables; may end in │ one document │ │ a terminal marker │ │ ├──────────────────────────┼──────────────────────────┤ streaming │ progress lines as events │ JSONL — one object per │ (emits │ occur, then a terminal │ event, then the terminal │ events) │ line │ result object │ └──────────────────────────┴──────────────────────────┘Defaults are op-declared, never TTY-sniffed. An op that supplies a
cli.render(output, io) hook defaults to text — so sdlc task sort
emits its lines with no flags, parity for line-oriented consumers. An op
without a render hook defaults to json. The default is fixed per op, so
skills and CI see a deterministic surface (solutions/ontological/cli/registry_adapter.ts,
defaultOutputMode).
The render hook is also where an op shapes legacy exit semantics: its
return value, when a number, becomes the CLI exit code — letting a “FAIL”
shape exit non-zero without throwing (CliRender in
solutions/ontological/lib/registry.ts). Streaming is a descriptor property, not a
flag; a streaming op under --output json buffers to its terminal
object, a sync op under --output jsonl emits its single object as one
line.
Failures are typed. An op throws an OpError carrying a code from a
canonical taxonomy; each adapter maps the code to its native failure (CLI
exit code, MCP error, HTTP status). The exit-code table lives in the
registry — GENERIC through MODEL_ERROR, plus a SERVICE_ERROR tier
for lifecycle failures (OP_ERROR_EXIT_CODES in
solutions/ontological/lib/registry.ts). An OpError may also carry a verbatim marker
(CAS-FAILED ref=…) so a legacy deterministic-failure line survives the
move into an op byte-for-byte.
Where the LLM still belongs
Section titled “Where the LLM still belongs”Not everything reduces. The skills that survive are the ones whose work is genuinely judgment-shaped:
| Workflow | Why it stays LLM-shaped |
|---|---|
| Triage | Is this backlog item a real task, a duplicate, or noise? |
| Readiness verification | A linter flags missing H2s; only a model judges a hand-wavy AC list. |
| Definition | Walking a human through filling a half-baked task’s gaps. |
| PR review triage | Three review comments — which need action, which a reply, which a dismissal? |
| Semantic drift | This site’s Ops reference is generated, but this page is hand-written — a model checks the prose still matches the code. |
A skill calls ops; it is never an op (D-0007 §3). The substrate stays
LLM-agnostic so the same capability is reachable from a shell, CI, or a
non-Claude agent.
The migration pressure, realized
Section titled “The migration pressure, realized”Patterns that start as LLM prose migrate downward as they stabilize. The
task status: enum lives in
solutions/ontological/lib/model/entities/task/schema.json and the
validator enforces it; skills reference it by path instead of restating
the values.
That pressure ran to completion in the
op-substrate full sweep: every capability lives in
solutions/ontological/lib/ as a registry op reached via sdlc <path…>,
and the registry is the deterministic surface. When you find a “the
model has to remember to do X” pattern that survives five runs unchanged,
that’s evidence it wants to become an op — declare it once, get every
door free.
See also
Section titled “See also”- Harness-agnostic — the projection contract that keeps one op reachable from every door.
- Ops reference — every registered path, generated from the registry.