Skip to content

feat(safety): outcome-tied circuit breaker and post-merge revert proposals #2388

Description

@matedev01

Problem (current state, with file refs)

Every safety mechanism vanguarstew ships today is static and pre-merge — it inspects who authored a PR, what paths it touches, or how it scores before any effect lands, and then relies on the fact that the runtime never acts at all. Nothing observes what happens after a change is merged, and nothing trips on a bad outcome.

  • Pre-merge static gates (author/path/shape): scripts/benchmark_pr_policy.py auto-closes out-of-surface PRs; .github/workflows/pr-target-check.yml auto-closes any PR to main whose author is not matedev01; plus the pr-gaming-policy, pr-integrity, pr-limit, pr-reopen-policy, pr-source-check workflows. All key off identity/path/shape, not results.
  • Pre-merge value gate (still not an outcome): scripts/score_pr_delta.py bands a benchmark delta with a Pareto floor; it is explicitly a reporter, always exits 0, and only looks at two run_eval artifacts produced before merge.
  • "Safety = do nothing": the runtime is read-only and advisory by construction (vanguarstew_runtime/github.py:1-6; service.py:56-64); dry_run=true is the default; can_auto_execute() returns False "ever" (openvang/factory.py:434-438).
  • The one place "rollback" is named is unbuilt and pre-effect: ROADMAP.md §4 lists "containment/rollback rules," but its acceptance is only that "a malformed, stale, duplicate, or unapproved request cannot reach the external effect" (ROADMAP.md:61-69) — validating a request before it acts, not catching a regression after it acted.

There is no post-merge regression detector, no revert path, and no outcome-conditioned kill-switch anywhere in the repo (grep-confirmed: rollback occurs only as SQLite ROLLBACK; revert only as a commit-kind classifier). Tellingly, agent/decider.py:51 already asks the model to weigh "stability, blast radius, rollback cost" while the system has no rollback capability at all. The existing kill-switches (dry_run, allow_external_inference, the per-repo allow-list) are all input-conditioned — set once by the operator up front — never tripped by observed harm.

Why it blocks autonomous external maintenance

Once autonomy is earned on a low-risk surface (docs, tests, mechanical refactors) of an external product repo, the pre-merge gates stop being sufficient: a merge can be individually "safe" under every gate and still regress the live system. On an external repo there is no benchmark delta to gate on (that apparatus only exists for changes to our own agent/**), and the pre-merge judge signal that would substitute currently saturates and cannot discriminate (#2379). So the only trustworthy discrimination signal for external PRs is what actually happens after the merge — which the system does not measure. An autonomous maintainer with no post-merge feedback loop and no outcome-tripped stop can degrade a repo monotonically and never notice.

Proposed direction (concrete, staged; reuses existing components, no new external APIs)

Stage A — Post-merge outcome recorder (read-only, no writes). For each low-risk PR the bot recommended-and-that-merged, persist {repo, pr_number, base_sha, head_sha, merge_commit, decision, band} to the durable queue (state.py), owner-only 0600. Watch a declared outcome signal for that merge commit — main-branch check-runs / commit status / workflow-run conclusion — by extending the read-only client (github.py) with the missing read endpoints only, keeping it mutation-free by construction. Emit an owner-only outcome record per merge. This stage changes nothing the bot does; it only starts measuring.

Stage B — Outcome-tied circuit breaker (the kill-switch). Compute a rolling merge-error rate from Stage-A records (fraction of the last N autonomous merges whose declared outcome went red within a window). When it crosses an operator-set threshold, trip a hard stop: force dry_run on, stop claiming new jobs, require an explicit operator re-arm to resume. Wire the trip check as a precondition inside run_once() / serve_forever() (service.py:199-217) and persist the tripped state durably so a restart cannot silently clear it. This is categorically different from the existing switches: those are configured up front; this is tripped by observed harm, and it is safe in both directions because tripping only ever reduces capability.

Stage C — Revert as a non-executable, receipt-bound proposal. When a regression is confirmed for a specific merged PR, generate the revert as a non-executable ActionIntent — a GITHUB_WRITE owner action (factory.py:90-99, 137-167) for which can_auto_execute() still returns False — bound by digest to {merge_commit, regression evidence} so it is exactly-approvable. Route execution through the owner-action execution gateway (external signing, exact approval binding, idempotency, audit; ROADMAP §4). Emit a per-decision receipt for the revert via benchmark/attestation.py build_evidence so the corrective action is independently auditable.

Honesty on what is automated: detection (A), the outcome-tripped hard stop (B), and a bounded, receipt-bound revert proposal (C). The revert write itself stays human-gated.

Acceptance criteria

  • Post-merge outcome record persisted (owner-only, durable) for every autonomously-merged low-risk PR, capturing {repo, pr_number, base_sha, head_sha, merge_commit, decision, band, outcome_signal}.
  • The read-only client gains only read endpoints; a test asserts github.py still exposes zero mutation methods.
  • A rolling merge-error rate is computed over an operator-configurable window/threshold.
  • Crossing the threshold trips a hard stop that (i) forces dry_run, (ii) halts new job claims, and (iii) survives process restart until explicit operator re-arm.
  • A test proves a tripped breaker cannot be cleared by restart alone and blocks all downstream effects while tripped.
  • A confirmed regression produces a revert ActionIntent bound by digest to {merge_commit, evidence}; can_auto_execute() returns False; a test proves it cannot reach an external effect without the gateway.
  • Each revert proposal carries a report_data-bound evidence bundle reproducible offline by the owner.
  • Circuit breaker and revert proposal are inert on privileged paths: a test asserts a regression on an incentive/weights/payment path yields no automatic revert intent and no auto-action.
  • Docs distinguish this outcome-tied envelope from the existing static pre-merge guardrails.

Dependencies

Out of scope / stays human-gated

  • Incentive / consensus / emission / weights / payment / wallet code paths stay PERMANENTLY human-gated. The circuit breaker and revert-proposal apply only to the low-risk surface (docs, tests, mechanical refactors). A regression touching a privileged path never receives an automatic revert intent and never any auto-action — it escalates to a human. Autonomy is never earned here.
  • Auto-execution of a revert (the actual GitHub write) stays gated behind the owner-action execution gateway with external signing and human approval. This issue automates detection, the hard stop, and a bounded proposal only.
  • No change to benchmark scoring, the Pareto floor, or the existing static pre-merge guardrails — this is additive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomy-roadmapRoadmap toward autonomous external-repo maintenanceenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions