ADR-169: Auto-swarm dispatch + auto-merge gating — the queue is the dispatcher; the verification spec is the merge gate
Status: Proposed Date: 2026-05-07
Related: warp#1782 (parent EPIC: self-healing fleet), warp#1783 (amendment: self-healing rides the existing agent-executor queue — eliminates the dispatcher), ADR-167 (sister: structured failure event schema), ADR-168 (sister: auto-diagnosis library shape), ADR-164 / warp#1735 (ticket category axis — drives dispatch eligibility), ADR-166 (precedent: ai-research splits along an executor-isolation boundary; the queue is the seam), warp#1762 (ai-research executor isolation — the substrate this composes with), warp#1763 (warp as fleet CI queue — the substrate this composes with), ADR-115 (principle-aware reviewers — the gate that runs even on auto-merged PRs), ADR-126 (precedent: contract ADRs land before consumer code)
TL;DR
Per warp#1783 the self-healing EPIC has no dispatcher: the auto-filed warp ticket rides the existing agent-executor queue, and any agent in the pool picks it up via the normal warp-ready → warp-claim → ship-PR loop. This ADR locks the thinner-than-original-warp#1782-v1 contract for what category value the auto-filer chooses, what makes a fix-PR eligible for auto-merge, what makes it ineligible (every destructive op, every first-occurrence shape), and how the operator disables a misbehaving library entry mid-fire. The dispatch layer is one rule (file ticket with the right category); the auto-merge layer is one gate (verification spec matches + tests pass + diff stays within bounds). Everything else is the existing queue + the existing review.
Context
The original warp#1782 Layer 4 framing called for a dedicated "auto-swarm dispatch" component that would consume tickets, fire staff agents, and orchestrate the fix-PR end-to-end. The amendment (warp#1783) collapsed that layer entirely. The reason is structural: every component the dispatcher would have built ALREADY EXISTS as the agent-executor queue.
Today, when an operator files a warp ticket, the standard flow is:
- Operator runs
warp-add(or YAML frontmatter on a markdown file). - The ticket lands as
status: readyon the queue with the appropriatecategoryper ADR-164. - Any agent on any host running
warp-readysees the ticket. - The agent atomically claims via
warp-claim, ships a PR viagyrum-start-work→gyrum-complete-pr. - The PR rides
gyrum-review-pr's 3-persona panel (per ADR-115) and the standard structural-checks gate. - On merge, the ticket auto-closes via the existing
Closes warp#NNNPR-body convention.
Every step is in production. Today's session generated several auto-filed tickets (doc-debt rollups, postgres-password-sync gate audit, etc.) that flowed exactly this way without any new infrastructure.
The detector (per ADR-167's events stream + ADR-168's matched rules) just needs to file a warp ticket with the right category and the right body. The queue handles dispatch. The agent handles ship. The review gate handles correctness. The merge handles deploy. Done.
What this ADR locks is therefore narrow: (a) which category the detector picks per library entry; (b) what makes the resulting fix-PR eligible for the operator's auto-merge button (vs. a regular operator-clicks-merge step); (c) what's NEVER auto-mergeable; (d) how the operator disables a runaway library entry. The original "auto-swarm dispatcher" code path is simply not built — its replacement is "use the queue."
Decision
The detector files an auto-detected warp ticket whose category is taken from the library entry's proposed_fix.category (per ADR-168 §1). The existing agent-executor queue dispatches; no new dispatcher exists. A fix-PR is auto-mergeable iff (a) the library entry's approval_mode.current is auto; (b) the PR's diff matches the entry's verification_spec (paths in allowlist; line count under cap; required commands pass); (c) ADR-115's principle-aware review still passes; AND (d) the PR's underlying op is not in the closed list of destructive operations. Otherwise the PR routes through the operator's normal merge click. Operator kill-switch is the <name>.disabled.yaml rename per ADR-168 §3.
The shape has six parts: dispatch (what category the detector files), auto-merge eligibility (the verification spec gate), the destructive-op allowlist that overrides everything, the operator kill-switch contract, the safety boundaries (what's NEVER auto-fired), and the verification dogfood (the system observes itself observing).
1. Dispatch — the queue is the dispatcher
The detector's only dispatch action is filing a warp ticket via the existing POST /api/v1/items endpoint. The ticket's fields:
kind: ticket
title: "[auto-detected] {{ rule.name }} — {{ context }}"
description: "{{ rule.proposed_fix.ticket_template | rendered }}"
priority: {{ rule.priority | default("med") }}
category: {{ rule.proposed_fix.category }}
repo: {{ event.repo | default(rule.default_repo) }}
tags:
- auto-detected
- "rule:{{ rule.name }}"
- "rule-version:{{ rule.version }}"
- "approval-mode:{{ rule.approval_mode.current }}"
type: bug
Three points lock the dispatch contract:
categoryselection is per-rule, defined in ADR-168'sproposed_fix.categoryfield. The closed set is ADR-164's{warning, approval-needed, incident}:category: warning— observation-shaped. Detector found a soft signal worth tracking; no agent-claim action; auto-resolves when the condition clears. Used for low-priority drift-class entries (e.g. "GitHub Actions budget at 78%").category: approval-needed— the dominant case. Detector found a fix-shaped match; agent picks up after operator approval (operator clicks "approve" on the ticket; status flips toready; queue dispatches). This is the primary path whileapproval_mode.currentisrequiredorrequired-after-N.category: incident— service-down-shaped. Auto-resolved only when the underlying service is healthy AND a post-mortem is filed. Never auto-merge-eligible (see §3 below).category: work— used implicitly whenapproval_mode.currentisautoAND the fix-shape is non-destructive. Filed asstatus: readyimmediately; the existing queue dispatches without operator approval. The auto-merge gate (§2) is what makes this safe.
tags: auto-detectedis the provenance marker warp#1783 requires. Every auto-filed ticket is distinguishable from operator-filed by a single tag query; the audit + retro surface stays clean.Per-event-id idempotency — the detector uses
(rule.name, primary-event-id-in-window)as the dedupe key. Filing the same ticket twice for the same firing-window is rejected at the queue'sPOST /api/v1/itemsendpoint with a 409 + the existing item's ID returned. The detector's debounce per ADR-168 §1 makes this rare; the queue-side guard makes it correct.
The detector does NOT fire agents. It does NOT poll for ticket completion. It does NOT manage worktrees. Every one of those was a layer the original warp#1782 Layer 4 would have built; the queue substrate per warp#1783 does all of it for free.
2. Auto-merge eligibility
A fix-PR auto-merges iff ALL FIVE conditions hold. Any failure routes the PR to the operator's normal merge surface (operator clicks merge on gyrum-complete-pr or in the GitHub UI).
- Library entry is
approval_mode.current = auto. Per ADR-168 §1, this is the per-entry trust state, gated behind operator-signed-off promotion. Noauto, no auto-merge. - PR title or body references the auto-filed ticket via
Closes warp#NNN. The agent that picked up the ticket follows the standardgyrum-complete-prflow which adds this. Without the back-link, the auto-merge gate cannot find the library entry and falls back to manual review. - PR diff matches the library entry's
verification_spec(per ADR-168 §1):- Every changed file's path is in
verification_spec.expected_diff_paths(allowlist; the validator at PR-author time AND the auto-merge gate at merge time both enforce). - Total changed-line count ≤
verification_spec.expected_diff_max_lines(capped at 500 by the validator per ADR-168 §5). - Every
verification_spec.must_passcommand exits 0 against the merged tree.
- Every changed file's path is in
gyrum-review-prreturns SUCCESS. The 3-persona principle-aware panel (per ADR-115) runs on every PR regardless of provenance. An auto-filed-ticket fix-PR that fails review goes back to the operator the same way any other PR does.- The fix is not in the destructive-op allowlist (§3 below). This is the load-bearing safety boundary; even an
autolibrary entry whoseverification_specmatches CANNOT auto-merge a destructive op.
When all five hold, gyrum-complete-pr auto-merges via the existing --merge (not --squash) strategy. The merged PR's Closes warp#NNN body line auto-closes the auto-filed ticket via the existing close-on-merge convention. The fleet sees: detector → ticket → agent → PR → auto-merge → ticket-closed, with the operator's involvement bounded to (a) initial library-entry promotion to auto and (b) the kill-switch surface (§4).
When any of the five fails, the PR remains open and routes to the operator's normal merge surface. ADR-115's persona reviewers post their findings; the operator clicks merge after review per the existing flow. The library entry's provenance.successful_fires is NOT incremented — the auto-merge eligibility gate is the structural enforcement that "successful fire" means "shipped end-to-end without operator merge intervention."
3. Destructive-op allowlist — auto-merge is NEVER eligible
The closed list of operations that can NEVER auto-merge regardless of library state:
- Database mutations — any diff touching files matching
**/migrations/**,**/sql/**,**/schema/*.sql,**/*.sql. Schema changes are irreversible-shaped. - Production secrets / credentials — any diff touching
**/vault/*,**/.env*,**/credentials*,**/secret*,**/*token*. Even when the change is "rotate", operator click is mandatory. - DNS / networking config —
**/dns/*,**/cloudflare/*,**/nginx*.conf,**/Caddyfile,**/traefik*.yaml. A DNS bug can take the fleet offline; operator click is mandatory. - Destroy / delete commands in playbooks — any diff that adds a line matching
^[+].*(?:terraform destroy|kubectl delete|docker compose down -v|gh repo delete|gh release delete|DELETE FROM|DROP TABLE|TRUNCATE). Pattern-match at the auto-merge gate; the closed list is the same shape as ADR-107's destructive-pattern matcher. - Branch protection / CODEOWNERS /
.github/workflows/**— anything that would change the gate set itself. The system can propose changes to the gates; the operator must click them in. - Money-spending / quota-affecting operations —
**/billing*,**/quota*, anything ininfrastructure/cloud-init/that provisions paid resources.
Pattern-matching is at the auto-merge gate (a gyrum-can-auto-merge <pr> script that returns 0/1). The list is the same shape as the existing ADR-107 destructive-pattern allowlist that gates type: claude step authorship; reusing that matcher (or its successor per the ADR-107 surface) is the consistent move.
category: incident tickets are NEVER auto-merge-eligible regardless of any other state. Per warp#1782's pitfalls list: incidents are operator-pageable; the fix-PR is operator-merged. The incident category is the structural marker; the destructive-op allowlist is the file-level marker; both fail closed.
4. Operator kill-switch — disable a runaway library entry mid-fire
The operator's emergency surface is the <name>.disabled.yaml rename per ADR-168 §3:
cd gyrum-knowledge-base
git mv auto-diagnosis/atomic-deploy-regex-404.yaml \
auto-diagnosis/atomic-deploy-regex-404.disabled.yaml
git commit -m "chore(auto-diagnosis): disable atomic-deploy-regex-404 — false-positive storm"
git push
The detector picks up the rename within one hot-reload cycle (≤30s). The disabled rule no longer matches events; in-flight auto-filed tickets that are already on the queue continue to run their lifecycle (the kill-switch is a forward gate, not a retroactive cancel). The operator who pushes the disable commit MAY also warp-cancel the open auto-filed tickets via the existing surface; the kill switch and the cancel are independent operations.
The kill switch is git-pushed in the operator's normal devtools flow. There is no UI button; there is no out-of-band override. This is intentional: the kill switch IS git, so the audit trail (who disabled, when, with what commit message) is the same shape as every other change.
A weekly curation cron (Phase 5 of warp#1782) summarises *.disabled.yaml entries with disable-age > 7 days and files an approval-needed ticket per orphan: "still relevant, or delete?". This is the structural defence against forgotten kill-switches per ADR-168 §6.
5. Safety boundaries — what's NEVER auto-fired
warp#1782's pitfall list names six safety boundaries; this ADR locks them:
- First-occurrence-of-a-new-shape MUST NOT auto-fire. Per ADR-168, every new library entry starts at
approval_mode: required; promotion toautorequires N≥3 successful fires + operator signoff. The validator at PR-author time enforces this — a PR that introduces a new entry directly atautois rejected. - Destructive / irreversible / financial / identity operations are NEVER auto-merged. Per §3 above; the closed list is the structural enforcement.
category: incidentis NEVER auto-merged. Service-down conditions are operator-pageable per ADR-164; the fix-PR is operator-clicked.- The library is NEVER bulk-modified by an agent. Edits to
gyrum-knowledge-base/auto-diagnosis/**MUST come from operator-authored or operator-approved PRs. The persona reviewers refuse PRs that add multiple library entries in one diff (split-and-resubmit per the existing single-concern rule). - The detector is NEVER given write access to the events stream. The detector reads
GET /api/v1/events; it does not POST. Mutating the stream from the detector would create a feedback loop the auditor cannot follow. - Fan-out across the fleet on a single match is NEVER allowed. When the same library entry's trigger matches across N projects simultaneously (e.g. a global PAT outage), the detector files ONE ticket per project (still per-context), not one fan-out ticket. The dispatch is per-context; cross-project correlation is a future detector primitive.
6. Verification dogfood — the system observes itself observing
The detector's own failures emit failure_events per ADR-167. Examples:
- Detector cannot reach the events endpoint →
tool=detector, step=fetch-events, error_signature=connection-refused:warp-events. - Detector parses an invalid YAML on hot-reload →
tool=detector, step=hot-reload, error_signature=yaml-parse-error:auto-diagnosis/<name>.yaml. - Auto-merge gate fails-closed on a non-allowlisted path →
tool=detector, step=auto-merge-gate, error_signature=path-not-in-allowlist. - Library entry's fixture mismatch detected at startup →
tool=detector, step=startup, error_signature=fixture-mismatch:<rule-name>.
The detector emits these events to the same stream it reads. A future library entry (e.g. detector-down-no-events-in-window) consumes them. This is the recursive dogfood per warp#1782: the system observing itself observing. The contract here is that the detector emits its failures via the SAME shared library every other tool uses (gyrum-go/failure-event per ADR-167 §2); no special "internal" surface.
The end-to-end test the warp#1783-revised Phase 4 builds (S4-NEW.1) exercises exactly this loop: stub a synthetic failure event → detector matches a library rule → ticket auto-files → agent picks up → ships PR → auto-merges → ticket closes. Pure integration test, no new code beyond the test harness; the substrate exists.
Consequences
Wins
- No dispatcher to build, maintain, or operate. The queue + the existing agent-executor pool are the dispatcher. The original Layer 4 in warp#1782 becomes zero net new code.
- Auto-merge is a verification gate, not a magic merge button. The five conditions in §2 are AND-composed; failing any one routes to the operator's normal flow. The "auto" path is opt-in per library entry, gated by trust accumulation per ADR-168.
- Destructive-op allowlist fails closed. Even a misbehaving library entry whose
verification_specmatches cannot auto-merge a database migration, a DNS change, or aterraform destroy. The closed list is the structural enforcement, identical in shape to ADR-107's existing destructive-pattern matcher. - Operator kill-switch is git-shaped. No UI surface, no special override, no out-of-band trust mechanism.
git mv <name>.yaml <name>.disabled.yaml && git pushis the recovery path; the audit trail is the commit message; the recovery latency is ≤30s + push latency. - Composes with existing substrate. ADR-166 (warp#1762) executor isolation: the agent that picks up the auto-filed ticket runs on operator hardware with the right credentials. warp#1763 warp-as-CI-queue: when a CI runner observes its own pipeline failure, the same events-stream → ticket → pickup loop fires. Both compositions are zero-code; they ride the same queue.
- Recursive dogfood is structural. The detector's own failures emit events; future library entries match those events; the system improves its own observability over time.
Costs
- Auto-merge gate is one new script (
gyrum-can-auto-merge <pr>). Estimated 100-150 LOC: parse the PR'sCloses warp#NNN, fetch the auto-filed ticket's library-entry tag, fetch the entry'sverification_spec, run the path + line + must-pass checks, run the destructive-op pattern match. Reuses the YAML reader from the validator per ADR-168 §5; reuses the destructive-op pattern from ADR-107. - Two new closed lists to maintain — the destructive-op file-pattern list and the destructive-op pattern-match list (in §3). Both are the same shape as ADR-107's existing matcher and should ideally be the same file (a follow-up consolidation PR after ADR-107's matcher relocates per ADR-110's gate-registry pattern).
gyrum-can-auto-mergeruns synchronously at merge time. Estimated 5-10s of wall-clock (YAML parse + git diff + must-pass commands). Mitigated by the gate running AFTERgyrum-review-pr(which already takes minutes); the marginal latency is negligible.- Operator must understand the kill-switch path. "git mv to disable a rule" is a small documentation burden; mitigated by the kill-switch being part of every library entry's PR-body checklist (the entry's PR explicitly notes the kill-switch surface).
Risks
- Auto-merge gate has a bug; an unsafe diff slips through. Defence-in-depth: ADR-115's persona reviewers run on every PR regardless of provenance; a diff that the auto-merge gate erroneously approved still has to pass the persona panel. The gates compose: the structural matcher is the first failure mode; the persona review is the second. Both have to fail for an unsafe merge.
- Library entry hand-edited to bypass the verification spec. A curator could in principle write a
verification_specso loose it auto-approves anything. Mitigated by ADR-168 §5's validator:expected_diff_max_lines ≤ 500andexpected_diff_pathsmandatory; persona reviewers (per ADR-115) refuseverification_specblocks that look pathologically loose. - Auto-merge cascade across the fleet. A single library entry's trigger matches in 10 projects simultaneously; ten PRs auto-merge in parallel; one of them is wrong; the bug ships everywhere. Mitigated by §5's "fan-out NEVER allowed" boundary AND by the per-context dispatch (one ticket per context, agent picks up sequentially per project, not in parallel). An operator who sees fire-2 succeed and fire-3 fail can disable the rule via the kill-switch before fire-4.
- Operator-signoff on
autopromotion is gameable. A persona could in principle promote a rule toautowithout genuine evidence. Mitigated by ADR-115's principle-aware reviewers being instructed to refuse promotion PRs without an explicitsuccessful_fires ≥ Nevidence chain in the PR body — and byprovenance.reviewer_signoffrecording the operator's handle, not the persona's, onautopromotions.
Mitigations
- AND-composed eligibility — five conditions in §2 must ALL hold. Removing any one would weaken the gate; adding new conditions is the only safe direction.
- Closed destructive-op list in §3 fails closed. New file patterns can be added (one-line PR); they are never removed without an ADR amendment.
- Operator-only kill switch — git push to
gyrum-knowledge-base. No UI surface that could be hit by a misclick or a hostile agent. - Verification dogfood — the detector emits its own failures; misbehaviour surfaces in the same observability surface every other tool uses.
- Composition with ADR-115 — every auto-merged PR still rides the principle-aware persona panel. A bug in the auto-merge gate cannot bypass the review gate.
- Forward-only kill switch — disabling a rule does not retroactively cancel in-flight tickets. This is intentional: agents that have already claimed work continue; the operator can additionally
warp-cancelif needed; the two surfaces are independent and compose.
Alternatives considered
Build a dedicated dispatcher (the original warp#1782 Layer 4)
Stand up a separate service that consumes auto-filed tickets, fires staff agents in dedicated worktrees, manages PR lifecycle, and merges on success. Rejected per warp#1783: the agent-executor queue already does all of this for operator-filed tickets; building a parallel dispatcher for auto-filed tickets would reproduce the work, double the operational surface, and create an asymmetry where auto-filed and operator-filed work flow through different code paths. The "different paths" failure mode is the one warp#1783 named explicitly: agents shouldn't know or care who filed; they pick ready work.
Auto-merge gate as a GHA workflow (instead of gyrum-can-auto-merge)
Implement the auto-merge gate as a GitHub Actions workflow that runs on every PR labelled auto-detected. Rejected: GHA workflows live in .github/workflows/** which is in the destructive-op file-pattern list — the gate would protect itself from edits via itself, a circular dependency that breaks the bootstrap. A devtools-shipped script (gyrum-can-auto-merge) is the consistent shape with the existing review/complete-pr surface.
No closed destructive-op list; rely on persona reviewers to catch unsafe diffs
Drop §3's closed list; trust ADR-115's principle-aware reviewers to catch every unsafe diff. Rejected: persona judgment is non-deterministic — a rare-shape destructive op might slip past three personas who are trained on common patterns. The closed list is deterministic and fails closed; the persona review is non-deterministic and stochastic-failing. Defence in depth requires both. (Same shape as why ADR-107's destructive-pattern matcher exists alongside type: pause operator confirmations.)
Operator kill-switch as a UI button on the library entry
Add a "disable" button in the warp UI that toggles the entry's enabled: false field. Rejected: introduces a UI-only state mutation that's not trivially auditable from git log; introduces a permission surface (who can press the button) that doesn't exist today; introduces a recovery path (operator presses re-enable; library reader needs to know to refresh) that diverges from the standard "PR + push" flow. Git-shaped kill-switch is consistent with the rest of the curation surface.
Auto-merge cascade across fleet on single match (parallel ship)
When a single library entry's trigger matches across N projects, file ONE ticket and let the agent fan out to ship N PRs in parallel. Rejected per warp#1782 §pitfalls: a buggy fix that cascades across the fleet in parallel is the catastrophic failure mode the safety boundaries exist to prevent. Per-context dispatch is sequential by construction; if fire-2 succeeds and fire-3 fails, the operator has time to kill-switch before fire-4 ships.
Auto-merge using --squash strategy
Auto-merge with --squash so the auto-detected ticket's history compresses into one commit. Rejected: violates the existing fleet-wide rule that gyrum-complete-pr uses --merge (per CLAUDE.md's "NEVER use gh pr merge --squash"). Auto-merge is not a special case; it rides the same merge strategy.
Cross-references
- Operator framing 2026-05-07: "Things break and fail, how can we catch the not deploying or working issue structurally so you can raise a fix automatically, dogfooding ultrathink."
- Operator framing 2026-05-07 (the warp#1783 amendment): "Is this the move to the agent-executor, if a pipeline fails we could add that somewhere, maybe create a ticket and that goes through the queue and gets picked, so self improvements."
- ADR-167 — structured failure event schema. Provides the events the detector reads.
- ADR-168 — auto-diagnosis library shape. Provides the rule whose
proposed_fix.categoryandverification_specthis ADR's gates consume. - ADR-164 / warp#1735 — ticket category axis. The category values that drive dispatch eligibility.
- ADR-166 — ai-research executor isolation; the queue is the seam. The same architectural intuition: the queue absorbs what would have been a dispatcher.
- ADR-115 — principle-aware reviewers. Runs on every auto-merged PR; the second gate behind the auto-merge eligibility check.
- ADR-107 — gates as playbook primitives; provides the destructive-op pattern matcher this ADR composes with.
- ADR-110 — observe-and-file dominant pattern. The detector's filing-shape is the same shape: read events, decide, file ticket.
- warp#1782 — parent EPIC; Phase 0 sub-ticket S0.3 is this ADR. Phase 4 is the verification-only end-to-end test.
- warp#1783 — amendment that collapses Layer 4 into the existing queue.
- warp#1762 — ai-research executor isolation EPIC; the executor pool that picks up auto-filed tickets.
- warp#1763 — warp as fleet CI queue; the substrate that makes pipeline-failure-as-warp-ticket the natural shape of warp#1782 Phase 6.
Supersedes: none Superseded by: leave blank until a later ADR reverses this one