ADR-107: Gates as playbook primitives
Status: Proposed Date: 2026-04-26 Related: ADR-067 (playbooks-unified-primitive), ADR-068 (playbook-runtime), ADR-090 (ai-as-scaffold-pipelines-as-runtime), ADR-092 (host-project-separation), ADR-104 (dry-run-on-history-adaptive-gate)
Numbering note. Slots 105 and 106 are claimed by in-flight ADR tickets (warp #317 e2e gate; warp #327 warp-02 cutover) but have not yet landed as files. Per the README "never reuse a number" rule those slots remain reserved for those decisions when they land; this ADR takes the next free file slot — 107.
Context
The warp deploy saga of 2026-04-26 (see postmortem, warp #321) was a fix-then-fire-then-discover loop with Claude in the loop the whole time. Five iterations, no progress against the structural problem; the eventual fix was to provision a fresh host and cut over.
Out of the post-incident review the operator surfaced the load-bearing constraint:
"a lot of these things should be structural in the playbook, remember we want to hand out from claude at some point the running"
That is the constraint this ADR exists to honour. Today's safety net is built out of three substrates that all assume Claude is watching:
- Claude's heuristics — pattern-recognising "this deploy has failed too many times", suggesting alternatives.
- Runtime features — special-cased rejection logic baked into the playbook executor (e.g. "refuse to fire if recent fires are red").
- UI affordances — operator spotting a red badge on
/operateand intervening manually.
All three vanish the moment Claude isn't the loop driver. An operator firing a playbook from a fresh terminal, or a cron trigger, or a webhook from an external system, gets none of the safety. This is the gap that today's incident lived in: every gate the team has built so far has assumed an attentive Claude session.
The four open gate tickets surfaced today (#313 dry-run-on-history, #317 playbook-e2e, #323 hard-assert verify, #324 iteration ceiling) all started life as proposed runtime features. If they ship in that shape, the safety net stays Claude-shaped.
ADR-067 already established that playbooks are the unified primitive of the system. ADR-090 established that AI is scaffolding and pipelines are the runtime target. This ADR follows the line both of those drew: gates should not be a special category outside the primitive — they should be expressed in the primitive, so they survive the handoff away from Claude.
Decision
Every safety mechanism in the gyrum factory MUST graduate into one of two playbook primitives:
- A reusable step that any playbook can
import_tasks, ship underroles/gates/tasks/(e.g.verify_image_sha,dry_run_self,verify_terminal_assert,pre_check_recent_health). - A meta-playbook that orchestrates other playbook fires, declared
kind: orchestration, fired by the same runtime as any other playbook (e.g.fire-against-throwaway.yaml,watch-playbook-health.yaml,escalate-on-fix-loop.yaml).
The runtime stays minimal: it executes a playbook, streams events, reports state. It does not bake gate-logic in. Gate-logic baked into the runtime cannot be inspected, audited, or replaced by anyone who can read YAML — the moment such a gate misfires, the only fix path is a runtime PR.
The four open gate tickets are reshaped against this rule before implementation:
| Today's framing | Reshaped as |
|---|---|
| warp #313 — runtime intercepts fires when recent reds | Playbook step pre_check: dry-run-self.yaml. The "any-red-in-last-5" condition becomes input (min_recent_greens: 5), not a runtime check. |
| warp #317 — CI fires playbooks against throwaway hosts | Meta-playbook fire-against-throwaway.yaml. gyrum-review-pr invokes gyrum-fire-playbook fire-against-throwaway --input target_playbook=<id>; playbook authors don't need to know about CI. |
| warp #323 — runtime rejects release_flows lacking a hard-assert | Authoring rule enforced by gyrum-validate-playbook on PR review + a standard step verify_terminal_assert.yaml shipped in the role library so authors can drop it in. |
| warp #324 — runtime tracks fix-count + escalates | Meta-playbook watch-playbook-health.yaml runs on cron, scans recent fires, fires escalate-on-fix-loop.yaml when the iteration count crosses N=3 within an hour. |
The implementation tranche — step library extraction, meta-playbook library, and the four migrations — is tracked in warp #329. The acceptance criterion is the "operator-without-Claude test" (r8 of that epic): fire deploy-warp from a clean session with no Claude loop running, watch every gate fire and every fail-path trigger correctly.
Consequences
What becomes easier.
- Gates are inspectable as YAML. An operator (or a fresh agent) can
cat roles/gates/tasks/verify_image_sha.ymland see the contract; today the same logic lives in compiled runtime code that requires source-tree spelunking. - Gates compose. A new playbook author writes
import_tasks: roles/gates/verify_image_sha.ymlrather than re-implementing SHA verification or hoping the runtime catches the omission. - Gates fire without Claude. Cron triggers, webhook fires, operator-from-terminal fires — all get the same safety net the Claude-driven session gets, because the safety lives in the same primitive that ran the work.
- New gates ship faster. A new step is a YAML file in
roles/gates/tasks/; a new meta-playbook is a YAML file underplaybooks/. No runtime PR, no executor recompile, no coordination with the runtime owner. - The rule-promotion engine (ADR-083) has a place to land its outputs: a recurring finding promotes to a step in
roles/gates/, which any playbook can then opt into.
What becomes harder.
- Discoverability. A runtime feature is implicit (every fire gets it for free); a step is opt-in (every playbook author must include it). The mitigation lives in
gyrum-validate-playbook: akind: release_flowplaybook lackingverify_terminal_assertfails validation, and the validator owns the "you forgot the gate" surface. - Versioning. A step shipped in the role library has a version; playbooks pin against that version; upgrading a step requires playbooks to opt in. The runtime-feature alternative ships once and applies to all fires retroactively; the step-library alternative trades that for inspectability.
- Meta-playbooks introduce a second tier. Today a playbook fires hosts; a meta-playbook fires playbooks. The runtime needs to handle both shapes (today's
kind: orchestrationalready covers it; the work is in the validator and the UI surfacing). - No central kill-switch. A runtime feature can be disabled by editing one config; a step lives in N playbooks and disabling it requires N edits. The mitigation is the
bypass: trueinput on each gate step plus an audit log entry naming the bypass — same shape as the existing--skip-smokeand--skip-dry-runoverrides.
What we sign up to maintain.
- The role library
roles/gates/. Every step is documented with its inputs, side effects, and the failure-mode class it catches. Same shape as_template.mdfor ADRs. - The meta-playbook conventions.
kind: orchestrationplaybooks must declare the playbooks they fire (so the dependency graph is statically inspectable), and the runtime's "max recursion depth" guard applies (a meta-playbook firing itself is a finite loop with operator escalation, not a stack-blower). - The validator rules.
gyrum-validate-playbookowns enforcing "release_flow must end in a hard-assert step" and similar shape rules; failing those rules is agyrum-review-prblock. - The migration of the four open gate tickets. Each one is reshaped into the step-or-meta form before implementation, even though the runtime form would ship faster. The faster path leaves us in the same hole this incident exposed.
Alternatives considered
- Bake the gates into the runtime. Considered and rejected — this is the today shape and is exactly what produced the warp #321 incident. Runtime gates fail invisibly when Claude isn't watching, and they cannot be inspected by anyone who can read YAML.
- Express gates as Claude prompts in CLAUDE.md. Rejected for the same root reason — prompts only fire when Claude is the loop driver. The whole point of this ADR is to make the safety survive the handoff away from Claude.
- Express gates as wrapper-script logic in
~/.local/bin/gyrum-*. Rejected because the wrappers are not always on the call path. Cron fires, webhook fires, and operator-from-fresh-terminal fires can hit the runtime directly. The gate must live on the same path the work lives on; today that path is the playbook. - Express gates as code in a third substrate (e.g. a sidecar service that watches the runtime). Rejected on YAGNI and on substrate count. We already have two substrates — playbooks and runtime — and adding a third is more surface area to maintain than it saves. The two existing primitives are sufficient if we use them.
- Ship the four open gate tickets in their original runtime-feature shape and migrate later. Considered on shipping cadence. Rejected because "migrate later" has a poor track record in this codebase, and because every runtime gate that ships has to be deprecated AND its replacement built — twice the work for no savings on calendar time.
Supersedes: none Superseded by: leave blank until a later ADR reverses this one
References
- ADR-067 — playbooks-unified-primitive (the primitive these gates live in)
- ADR-068 — playbook-runtime (the executor that stays minimal)
- ADR-090 — ai-as-scaffold-pipelines-as-runtime (the line this ADR continues)
- ADR-104 — dry-run-on-history-adaptive-gate (the first gate to migrate; warp #313)
- warp #313 — dry-run gate, reshaped as a step
- warp #317 — playbook-e2e gate, reshaped as a meta-playbook
- warp #323 — hard-assert, reshaped as step + validator rule
- warp #324 — iteration ceiling, reshaped as meta-playbook
- warp #321 — postmortem (the motivating case)
- warp #327 — warp-02 cutover (the first orchestrator playbook that exemplifies the meta-playbook pattern)
- warp #329 — implementation epic (this ADR is r1)
- postmortem 2026-04-26 — the writeup of the saga that motivated the principle