Tier-3 OpenRouter shadow-mode escalation¶
TL;DR. Tier-3 picks up the failing-on-main cases that Tier-1 (contract-drift-autofix.yml) and Tier-2 (bernstein-ci-fix.yml) both produced nothing on. It runs a free-tier OpenRouter model under the bernstein run --cli qwen adapter, captures a unified-diff plus a lineage / decision-log / envelope row, and exits without pushing. Promotion stays governed by a second env var that is off by default until the shadow-week metrics review lands.
| Layer | Status | Source |
|---|---|---|
| Feature flag | Off by default | vars.BERNSTEIN_CI_SELF_DRIVE |
| Promotion gate | Off by default | vars.BERNSTEIN_CI_SELF_DRIVE_PROMOTE_FROM_SHADOW |
| Provider call | OpenRouter free-tier | scripts/run_tier3_shadow.py |
| Capture | .sdd/autoheal/tier3-shadow/<run_id>.diff | core.autofix.tier3 |
| Cordon | Auto-heal cordon + tests/contract/contracts/*.yaml | core.autoheal.cordon |
| Lineage | Lineage-v2 child body, identical shape to real heal commits | core.autoheal.lineage_writer |
| Decision log | tier3_shadow / cordon_violation / recurrence_escalation | core.observability.decision_log |
| Cost envelope | quota_envelope="ci-autoheal", daily hard cap 0 USD by default | core.cost.cost_rollup_by_envelope |
Environment variables¶
| Variable | Default | Effect |
|---|---|---|
BERNSTEIN_CI_SELF_DRIVE | unset | Tier-3 fires only when this equals tier3. Any other value is a no-op. |
BERNSTEIN_CI_SELF_DRIVE_PROMOTE_FROM_SHADOW | unset | When equal to 1, a captured patch is signalled for push. Stays off until the shadow-week metrics review. |
BERNSTEIN_OPENROUTER_BASE_URL | unset | OpenRouter base URL override. The shipped wheel never bakes a default hostname; this var (or OPENAI_BASE_URL) must be set in the workflow env. |
OPENAI_BASE_URL | unset | Fallback when BERNSTEIN_OPENROUTER_BASE_URL is unset so the qwen / codex CLIs work without rewiring. |
OPENROUTER_API_KEY_FREE | unset | OpenRouter free-tier API key. Read from env in the workflow only - never placed on argv. |
BERNSTEIN_CI_AUTOHEAL_HARD_CAP_USD | 0.0 | Daily hard cap on the ci-autoheal quota envelope. Stays at zero by default because the fallback list is all :free models; an operator opts into a paid fallback by raising this. |
Fallback model order¶
Tier-3 walks the list in the order below until one model accepts the call. All entries are OpenRouter free-tier ids so the per-call dollar accounting stays at zero by default. The list is documented as operator-tunable; the defaults live in src/bernstein/core/autofix/tier3.py.
qwen/qwen3-coder-480b:free(primary)deepseek/deepseek-r1:freemeta-llama/llama-4-maverick:freemistralai/devstral-small-2:freeqwen/qwen3-235b-a22b-instruct:free
Cordon¶
A captured patch is accepted only when every touched path lands in one of:
- The standard auto-heal cordon (
core.autoheal.cordon): root-level config / docs files (typos.toml,AGENTS.md,CLAUDE.md,.goosehints,CONVENTIONS.md) and.cursor/rules/*.mdc. - The Tier-3 extra glob
tests/contract/contracts/*.yaml, added so contract-drift fixtures can be regenerated in shadow mode.
Anything outside that union is a hard refusal. The runner emits a cordon_violation decision-log row that names the offending paths, drops the patch, and exits without writing a diff. The cordon-zone question itself stays governance, not engineering - widening it requires an operator-approved RFC.
The cordon walks every file-pair header in the unified diff, not only the new-side +++ b/<path> line:
- Deletions. A pure deletion has
+++ /dev/null; the old-side--- a/<path>is the file the patch would delete and must pass the cordon. Without this the patch could delete an arbitrary out-of-cordon file silently. - Renames. Both the old-side
--- a/<old>and new-side+++ b/<new>paths must pass the cordon. A rename that moves a cordoned file outside the cordon (or pulls an out-of-cordon file in) is refused. The decision-log entry records both sides undertouched_pathsand the offending side(s) underrejected_paths.
How to read shadow captures¶
Every Tier-3 capture writes four artefacts under .sdd/:
| Path | Format | Notes |
|---|---|---|
autoheal/tier3-shadow/<run_id>.diff | unified diff | The patch the model proposed. Apply with git apply for a local replay. |
autoheal/ci-autoheal-envelope.jsonl | JSONL | One row per capture under quota_envelope="ci-autoheal". Picked up by cost_rollup_by_envelope. |
autoheal/recurrence.jsonl | JSONL | One row per capture keyed by (failure_class, failing_test_nodeid). Used by the recurrence detector. |
runtime/decisions.jsonl | JSONL | The structured decision-log entry for the capture / refusal / escalation. Discoverable via bernstein decisions tail. |
The workflow artefact tier3-shadow-<short_sha> ships all four paths together. Retention is 14 days; the operator review process is expected to land before that window expires.
Recurrence escalation (Tier-4 hand-off)¶
When the same (failure_class, failing_test_nodeid) pair has been captured more than the configured threshold (DEFAULT_RECURRENCE_THRESHOLD, currently 2) inside the rolling window (DEFAULT_RECURRENCE_WINDOW_SECONDS, 24h), Tier-3 stops and emits a recurrence_escalation decision-log entry instead of running the provider. The workflow surface is expected to read the kind and route to the operator via the existing Tier-4 contract (Telegram + GH issue). The recurrence ledger itself is append-only so longer-term recurrence stats survive the window boundary.
Promotion path¶
The captured patch is not pushed by Tier-3 itself. Promotion is controlled by a second env var, BERNSTEIN_CI_SELF_DRIVE_PROMOTE_FROM_SHADOW. Until the shadow-week metrics review concludes:
- The variable stays unset on the canonical repo.
- Captured patches are reviewed out-of-band by an operator looking at
.sdd/autoheal/tier3-shadow/and the matching decision-log row. - Any decision to flip the gate is documented as a separate operator RFC; the engineering surface is already in place.
When promotion is enabled, the Tier-3 outcome surfaces a promoted_push kind instead of shadow_captured. The actual push (branch creation, PR open) still flows through the standard auto-heal branch convention (auto-heal/<short_sha>) so the existing required-status-checks gate on branch protection makes the final call.
Adversary role pre-merge gate¶
Even once promotion is enabled, a Tier-3 promoted patch must still flow through the adversary role pre-merge gate (templates/roles/adversary/) before merge. The shadow-mode capture does not bypass the gate - it predates the push entirely, so the gate fires on the proposed PR like any other change. If the adversary integration needs explicit wiring (e.g. a workflow that posts the captured diff to the adversary check before opening the PR), that remains a follow-up tracked under Refs #1711.
Cost envelope wiring¶
Every Tier-3 capture appends one row to .sdd/autoheal/ci-autoheal-envelope.jsonl. The row carries the canonical quota_envelope="ci-autoheal" tag, so the existing cost_rollup_by_envelope job picks it up without configuration. The daily hard cap defaults to 0.0 USD because every model in the fallback list is :free; an operator who wires a paid fallback opts in by setting BERNSTEIN_CI_AUTOHEAL_HARD_CAP_USD. The hard cap then flows through the existing #1330 circuit breaker via the same envelope name.
Disabling Tier-3 in an emergency¶
Three independent kill switches exist; any one is sufficient.
| Action | Effect |
|---|---|
Clear vars.BERNSTEIN_CI_SELF_DRIVE | The workflow job's if: gate stops it from firing. No code changes. |
BERNSTEIN_AUTOHEAL_DISABLE_LLM=1 | The shared auto-heal cost-guard refuses every LLM call regardless of tier. |
Touch .sdd/autoheal-disabled on the runner | The existing kill-switch primitive halts every tier including Tier-3. |