Decisions

ADR-110: Observe-and-file is the dominant pattern

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.

#110

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=N but stderr matches a configured regex (ERROR, FATAL, specific warning patterns).
  • A shell exits 0 but stdout does not match the expected pattern.
  • A compose pull succeeds 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-using type: 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. Filtering tags ~ 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@v1 prompt; 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=observer posts (sub-ticket r2).
  • Observer tickets must auto-archive on resolution. The token-thrift rule from warp #258 applies: open observer tickets count against the /board cognitive budget; resolved ones must drop off, not accumulate.

What we sign up to maintain.

  • The observe-and-file.md prompt + its regression corpus (the warp #321 5-fix saga as v1 fixture).
  • The dedupe window logic + the ?source=observer rate limit on the warp API.
  • The 3-way decision tree convention doc (sub-ticket r4) — when to use observer: vs type: claude vs type: pause. This is the playbook-author-facing surface that pulls authors toward the right default.

Alternatives considered

  • Make on_anomaly: file-warp-ticket opt-in via type: claude instead of a default observer: 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. The feedback_token_thrift_routine_agents_haiku.md memory 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 /operate and 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 /board filtered by observer:claude on 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 pull step's anomaly is "SHA mismatch"; an ansible step's anomaly is "exit 0 with no hosts matched"). Per-step observer: is the right granularity.

Supersedes: none Superseded by: none yet