ADR-123: Templates as generator, AI as gap-filler (build pipeline shift)
Status: Proposed Date: 2026-04-29 Related: ADR-067 (playbooks-as-unified-primitive), ADR-081 (gy-* canon — and 2026-04-28 amendment), ADR-085 (typed I/O pipeline blocks), ADR-093 (manifest-driven project tools), ADR-111 (broker — model routing), ADR-117 (module guidelines)
Context
Five gy-queue MVP build attempts — cloud_brave_amber, light_grain_stone, haven_willow_wind, heath_noble_ridge, pond_leaf_falcon, cliff_quartz_north — burned ~$150 of opus spend across the Build phase and produced zero shippable code. Each run hit the 600s sub-agent watchdog or drifted on TDD-from-scratch, and each rerun cost roughly the same again because the pipeline treats every build as a from-scratch generation problem.
The diagnosis is structural, not tactical. Today's build pipeline calls AI as the code generator — Build phase is opus running TDD against an empty repo, writing 80%+ of the resulting code. The first four bytes of every gy-queue attempt are the same package main boilerplate the previous attempt also paid opus to emit. Cost per build is $60-100, determinism is low (opus drift between runs producing different module shapes), verifiability is poor (an opus stream isn't review-clean), reproducibility is absent (the same brief produces different code each invocation), and resumability is nil (a failed slot is opaque AI reasoning rather than a Go compile error pointing at line 47).
The fleet has, in parallel, been building the primitives that make the inversion possible. ADR-085 typed I/O blocks gave us composable units. ADR-067 playbooks-as-unified-primitive gave us templated step shapes. ADR-081 (with its 2026-04-28 amendment) made the gy-* canon a templated set of UI primitives instead of a hand-rolled component zoo. gyrum-go accumulated templated backend patterns. gyrum-pipelines accumulated templated runner shapes. ADR-093 manifest-driven project tools showed declarative substrate works at the repo scaffold level. The build pipeline is the last layer that hasn't caught up with the rest of the substrate.
The operator named the principle directly on 2026-04-29: "code templates and libraries are the majority; AI does as little as it needs; this produces a templatable repeatable process." This ADR records that principle and the architectural shift it implies.
Decision
Inversion of generation pressure. Templates plus libraries are the generator. AI is a gap-filler for the irreducible 20%. The build pipeline shifts from one large opus-driven Build step to six phases with a deterministic generator at the centre and scoped AI calls at the edges.
The six phases:
- Phase A — Configuration (AI, ~$5-10). Discovery → personas, MVP scope, branding, blueprint, spec. Output is a structured spec document, not code.
- Phase B — Spec linter (deterministic, <1s). Pre-flight invariant checks:
href→ route resolution, field references match types, slot coverage is total. Catches structural bugs before any AI spend in C/D. - Phase C — Template expansion (deterministic, $0, seconds). Read spec → emit ~80% of repo via the
gyrum-templateslibrary. CRUD handlers,sqlcstores, Svelte pages, auth scaffold, Dockerfile,release.yml, OpenAPI document. No model call; no judgement; no drift between runs. - Phase D — Slot fill (scoped AI, ~$5-15). Each template emits
// GYRUM_SLOT: namemarkers for the irreducible 20% that genuinely needs AI judgement. One scoped per-slot model call replaces each marker. Parallelisable. Regression-corpus-tested per slot name. - Phase E — Validate (deterministic + scoped AI on failures, ~$1-3).
go build,go test,npm run build, Playwright. Failures trigger targeted slot re-fills against the failing slot, not a whole-pipeline retry. - Phase F — Retro (AI synthesis, ~$2). Lessons learned, finding-journal updates, slot-corpus delta proposals.
Total cost envelope: $15-30/build (vs $80-150 today). Determinism: high (Phase C is bit-identical between runs given a stable spec). Reproducibility: high (re-running a build from the same spec produces the same code modulo Phase D slot bodies). Reviewability: high (Phase C diff against previous run is structural; Phase D diff is scoped to slot bodies a reviewer can read).
Slot grammar (the contract between layers)
The contract that makes Phase D scoped instead of free-form:
// GYRUM_SLOT: claim_atomic_sql
// intent: SELECT FOR UPDATE SKIP LOCKED filtered by tenant + capability
// inputs: ctx, tenant_id string, capabilities []string
// output: *Job or sql.ErrNoRows
// constraints: postgres only; row-level lock; no race window
job, err := h.store.ClaimNext(r.Context(), tenant.ID, req.Capabilities)
A slot:
- Names itself (
claim_atomic_sql) — globally unique within a template family. - Declares
intent— what the body should achieve, in one sentence. - Declares
inputsandoutput— typed, so the surrounding generated code compiles before the slot is filled. - Declares
constraints— invariants the AI must respect (DB engine, lock semantics, error shape). - Has a paired regression-corpus entry:
(intent, expected output shape)so a slot fill is verifiable against the prior canonical fill.
The AI's only job in Phase D is to replace the slot body. It never edits surrounding code, never imports new packages without a declared imports: slot field, and never widens the slot's contract. The slot is the airlock.
Migration phases (one ticket each)
| Phase | Ticket | Work |
|---|---|---|
| 1 | warp#576 | Extract gyrum-labs/gyrum-templates repo from inline pipeline templates. |
| 2 | warp#577 | Slot grammar + parser package at cmd/server/pipeline/slot/. |
| 3 | warp#578 | Pipeline shift: replace BuildStep with TemplateExpandStep + SlotFillStep. |
| 4 | warp#579 | SpecLintStep pre-flight (Phase B). |
| 5 | (follow) | Retire opus-driven TDD step from gy-queue + gy-agent build playbooks. |
| 6 | (follow) | Backfill regression corpus for the founding slot families. |
Total elapsed: 3-5 days of focused work.
Pipeline flow
flowchart TD
A[Phase A — Configuration<br/>AI, ~$5-10<br/>spec doc] --> B
B[Phase B — Spec linter<br/>deterministic, <1s<br/>fail fast on invariants] --> C
B -->|lint fail| A
C[Phase C — Template expansion<br/>deterministic, $0<br/>~80% of repo + slot markers] --> D
D[Phase D — Slot fill<br/>scoped AI, ~$5-15<br/>parallel per-slot, regression-tested] --> E
E[Phase E — Validate<br/>build/test/playwright<br/>~$1-3 on failures] --> F
E -->|targeted slot fail| D
F[Phase F — Retro<br/>AI synthesis, ~$2<br/>lessons + finding journal]
Consequences
What becomes easier:
- Build cost drops from $80-150 to $15-30 per attempt. A failed run costs the slot delta, not the whole pipeline.
- Determinism is restored. A spec → repo run is bit-identical in Phase C; only slot bodies vary.
- Review is structural again. Phase C diffs read like infrastructure-as-code; Phase D diffs are scoped to one function body each.
- New systems inherit the templated shape for free: gy-agent, gy-factory, every future MVP build picks up the same generator.
- Failure isolation. A bad slot fill triggers a re-fill against that slot only, not a 600s opus restart.
- Slot regression-corpus becomes a living asset — a slot that drifts on a model bump shows up immediately in the corpus diff.
What becomes harder:
- The fleet now operates a
gyrum-templatesrepo as a first-class artefact (warp#576). Templates need their own versioning, release cadence, and regression tests against representative specs. ADR-117 module guidelines extends to covertemplate-authoring@v1. - Phase B spec linter is a new failure mode for spec authors. A spec that lints today may not lint tomorrow as new invariants are added; spec authoring becomes structurally constrained.
- Slot grammar is a new mini-language. New template authors must learn it, and the parser at
cmd/server/pipeline/slot/becomes a load-bearing component (warp#577). - The retired opus-driven Build step is non-trivial to remove cleanly — running playbooks reference it. Migration ticket (Phase 5) must coexist with the new pipeline for one release before the old step is deleted.
- Slot regression-corpus must be backfilled for every founding slot before the new pipeline is the default. Until backfill, slot fills are ungated against drift.
What we sign up to operate:
gyrum-templatesreleases (versioned, semver, regression-tested against fixture specs).- Slot regression corpus under
gyrum-knowledge-base(per ADR-116 RAG indexing). - Phase B spec-linter rules (tracked as module guideline
spec-linting@v1). - A
template-driftcron (sibling to the ADR-109 nightly drift cron) that re-runs the slot corpus against pinned models and files anomalies as warp tickets per ADR-110.
Alternatives considered
- Keep opus-driven Build, add caching/checkpointing. Tactical-only. Caching lowers per-run cost but doesn't address determinism, reviewability, or reproducibility. Drift between runs remains; review still reads model-emission noise. Loses against template expansion on every axis except author effort.
- Hand-write the templates without a slot grammar. Templates without slots produce monolithic output that an AI cannot edit safely without re-emitting the whole file (the failure mode
feedback_scaffold_additive_only.mdalready names). Slot grammar is the airlock that makes scoped AI calls scoped. - Use a generic codegen tool (e.g. plop, hygen, openapi-generator). Each covers one slice (UI scaffolds, OpenAPI clients) but none compose across the gy-* canon, the playbook-as-step shape, and the slot grammar this ADR requires. The fleet's primitives (ADR-085 typed I/O, ADR-067 playbooks, ADR-081 gy-* canon) are the right substrate; an external tool would re-invent half of them and diverge from the rest.
- Skip Phase B (spec linter) — let Phase E catch invariant violations via build/test failures. Cheaper to author. Wrong tradeoff: by Phase E we've already paid for Phase D slot fills against a structurally invalid spec. Phase B is <1s and pre-AI-spend; the asymmetry is decisive.
Supersedes: none (extends ADR-067 and ADR-085 by naming how the pipeline generates) Superseded by: