ADR-110: Observe-and-file is the dominant pattern
Status: Proposed Date: 2026-04-26 Related: ADR-067 (playbooks-unified-primitive), ADR-083 (defense-in-depth-rule-promotion), ADR-090 (ai-as-scaffold-pipelines-as-runtime), ADR-104 (dry-run-on-history-adaptive-gate), ADR-107 (gates-as-playbook-primitives), ADR-109 (type-claude-step-and-observer-field)
Context
ADR-109 introduced two shapes for runtime-driven Claude calls: type: claude (Claude is the step) and observer: (Claude watches a step). Both are real, but they are not equally common.
The dominant usage shape is observer: + automatic warp-ticket filing on anomaly. Most playbooks are structural — ansible, shell, kubectl, terraform — and the bulk of Claude's value sits beside them, watching for things that look wrong and queueing them for human or agent attention later. type: claude as a step is rare: only when the structured verdict is itself the deliverable (the dry-run gate verdict, the e2e summary, the context-aware hard-assert).
Without naming this explicitly, every playbook author re-derives the question of which shape to reach for, and the easy answer ("just make it a type: claude step") leads to expensive, slow, hard-to-audit playbooks. The 2026-04-26 deploy postmortem (warp #321) made the case concrete: each of the five fix iterations would have been caught by an observer reading ansible output and filing a warp ticket — not by a type: claude step blocking the fire.
The work to land the directive + the surrounding ergonomics is tracked in warp #336 (sub-tickets r1 through r7).
Decision
observer.on_anomaly: file-warp-ticket is the first-class directive of the observer: clause introduced in ADR-109. The runtime, on observer verdict of "anomaly", POSTs a warp item to the warp API tagged for later review rather than blocking the fire. Most Claude calls in playbooks should be observers using this directive.
The auto-filed ticket carries provenance for downstream queries: tags: [observer:claude, "discovered_during:<run_id>", "parent_playbook:<id>"] plus a repo: derived from the parent playbook's target_repo input so tickets land where they belong, plus a priority: and description: derived from the observer's structured output.
The default model for observers is claude-haiku-4-5@<pinned-version> per the feedback_token_thrift_routine_agents_haiku.md memory rule. Observers are routine-shaped, watching-not-deciding work; Haiku covers them at roughly $0.001 per call. A 20-playbook fleet with 5 fires per playbook per day and one observer per playbook costs ~$0.10/day = ~$3/month — negligible, which is the load-bearing point: observe-and-file scales without cost panic.
What counts as an "anomaly" is per-playbook and per-prompt. Examples that motivated the directive:
- An ansible task fails outright.
- An ansible task reports
ok=N changed=Nbut stderr matches a configured regex (ERROR,FATAL, specific warning patterns). - A shell exits 0 but stdout does not match the expected pattern.
- A
compose pullsucceeds but the resulting image SHA does not match what was just merged (the warp #321 postmortem class).
Consequences
What becomes easier.
- Playbook authors default to the right shape. The convention doc + the validator hint ("this looks like a watching step — consider
observer.on_anomaly:") pull authors away from over-usingtype: claude. - Steady-state Claude cost is bounded and predictable. Observers default to Haiku; a fleet of 20 playbooks costs roughly $3/month in observer calls. Cost panic stops being a reason to skip observation.
- Tickets accumulate where decisions get made —
/board— instead of in operator chat scrollback. Filteringtags ~ observer:claude(sub-ticket r5) gives the operator the auto-filed queue in one view. - The "observer would have caught it" counterfactual stops being theoretical. The r7 sub-ticket back-fills the warp #321 5-fix saga as the regression corpus for the
observe-and-file.md@v1prompt; the prompt then ships with proof that it catches every fix iteration that sequence produced.
What becomes harder.
- Authors must reason about what counts as an anomaly per playbook. A too-tight definition misses real failures; a too-loose one creates ticket storms. The mitigation lives in the prompt corpus + per-author convention.
- Ticket-storm risk is real. A flaky upstream that fires the same anomaly on every fire produces 100 identical tickets. The mitigation is sliding-window dedupe (sub-ticket r6, sliding 1h, same shape as the warp #318 activity-card dedupe pattern) plus a warp-API rate limit on
?source=observerposts (sub-ticket r2). - Observer tickets must auto-archive on resolution. The token-thrift rule from warp #258 applies: open observer tickets count against the
/boardcognitive budget; resolved ones must drop off, not accumulate.
What we sign up to maintain.
- The
observe-and-file.mdprompt + its regression corpus (the warp #321 5-fix saga as v1 fixture). - The dedupe window logic + the
?source=observerrate limit on the warp API. - The 3-way decision tree convention doc (sub-ticket r4) — when to use
observer:vstype: claudevstype: pause. This is the playbook-author-facing surface that pulls authors toward the right default.
Alternatives considered
- Make
on_anomaly: file-warp-ticketopt-in viatype: claudeinstead of a defaultobserver:directive. Rejected: that pushes authors toward the heavier, more expensive primitive for the common case. The whole point of the dominance claim is the cheap, side-channel shape carries the bulk of the load. - Use Sonnet for observers (consistency with
type: claude). Rejected: observer work is routine-shaped — read structural output, classify against a fixed prompt. Thefeedback_token_thrift_routine_agents_haiku.mdmemory rule names this case explicitly. Sonnet would 30x the cost for no observable verdict-quality lift on this prompt class. - Skip auto-filing — surface anomalies on
/operateand let the operator decide. Rejected: the operator is intentionally hands-off (CLAUDE.md "the owner is busy"); anomaly review IS what the queue is for. Filing the ticket is the queue entry; the operator reviews/boardfiltered byobserver:claudeon their own cadence. - One observer per playbook, fixed at the playbook level. Rejected: different steps in the same playbook need different anomaly definitions (a
compose pullstep's anomaly is "SHA mismatch"; an ansible step's anomaly is "exit 0 with no hosts matched"). Per-stepobserver:is the right granularity.
Supersedes: none Superseded by: none yet