Deterministic-first
Deterministic by default; LLM-orchestrated where it has to be.
That’s the rule. This page is the long form.
The cost asymmetry
Section titled “The cost asymmetry”An LLM call is non-deterministic, expensive, and slow relative to a typed op. 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 is the difference between a tool that feels crisp and a chatbot pretending to be one.
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 adapter’s historical
behavior). 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 canonical early example was status-enum drift: each task skill once
duplicated the valid status: values as prose, and the prose drifted.
The enum moved into solutions/ontological/lib/model/entities/task/schema.json and the
validator enforces it; the skills reference it by path.
That pressure ran to completion in the
op-substrate full sweep. Every remaining capability moved
out of the old plugin/scripts/ and plugin/validators/ directories
into solutions/ontological/lib/ as a registry op, and every caller flipped to
sdlc <path…>. Both legacy directories are deleted; 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.