Skip to content

Deterministic-first

Deterministic by default; LLM-orchestrated where it has to be.

That’s the rule. This page is the long form.

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 call sdlc 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>.md and each SKILL.md references 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 grep does.

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.

Not everything reduces. The skills that survive are the ones whose work is genuinely judgment-shaped:

WorkflowWhy it stays LLM-shaped
TriageIs this backlog item a real task, a duplicate, or noise?
Readiness verificationA linter flags missing H2s; only a model judges a hand-wavy AC list.
DefinitionWalking a human through filling a half-baked task’s gaps.
PR review triageThree review comments — which need action, which a reply, which a dismissal?
Semantic driftThis 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.

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.

  • Harness-agnostic — the projection contract that keeps one op reachable from every door.
  • Ops reference — every registered path, generated from the registry.