diff --git a/AUTONOMY.md b/AUTONOMY.md index 408cf84..e486b83 100644 --- a/AUTONOMY.md +++ b/AUTONOMY.md @@ -31,7 +31,7 @@ Where the dev workflow stops for a human today: | Plan approval | write plan to the issue, proceed | write plan to the issue, proceed | present + wait | | Ship PR sign-off | proceed through the autonomous-ship gate; end at PR-open | park (`awaiting-input`), question to the issue, continue elsewhere | present + wait | | Heart YELLOW | park, unless the reason set was human-acknowledged at launch (see the autonomous-ship gate) | same as `safe` | present + wait | -| Heart RED | stop, report | stop, report | stop, report | +| Heart RED | stop, report | stop, report | stop, report — a human may separately invoke the corrective-PR exception (below), which is not an autonomy level | | Merge / close | human, always | human, always | human, always | | Version ask | n/a — release stays `human-required` (sole exception: the scheduled-nightly standing grant, dated below) | n/a | ask | | Cleanup | proceed + log | proceed + log | confirm | @@ -175,6 +175,74 @@ applicability rule so "n/a" is a stated fact, never an assumption: A failed leg downgrades the run to a human checkpoint: state written to the issue, nothing force-shipped, never modify code to make a leg pass. +## Corrective-PR exception for Heart RED (human-authorized) + +Heart RED forbids commit, push and PR-open at every autonomy level (the levels +table). But Heart cannot clear a RED until the fixing source reaches `main`, +fresh wheels are built, and release-integration validation passes — so a source +fix that directly repairs the exact defect named by the RED reason cannot be +shipped, and recovery is impossible without violating policy. This section is +the **one** authorized way through that deadlock, and it is a **human act**, not +an autonomy level: it never fires under `--auto` (the hard invariant "Heart +YELLOW/RED is never acknowledged autonomously" stands verbatim — an unattended +`--auto` run on RED still stops and reports). A human invokes it, live, per +incident. + +- **Trigger** — Heart is RED, and a source fix directly repairs a defect named + by a RED reason. +- **Authorization** — explicit, contemporaneous human authorization that + (a) quotes the exact RED reason string and (b) approves the specific + corrective issue. It is recorded as a human comment on that issue; a stored, + reused or "standing" authorization does not count — it must be for this RED, + now. **The agent provides the quote.** When the circumstance arises, the agent + surfaces the exact RED reason string(s) **verbatim from Heart's current + verdict** (`pyauto-heart readiness`) together with the specific corrective + request it is asking the human to approve — so the human authorizes what the + agent put in front of them, never a string reconstructed from memory. It is the + human's judgement, on the agent-surfaced reason, that authorizes. +- **Permitted, and nothing else** — commit, push, and opening **one** + pending-release feature PR whose issue, plan and diff all map to the named + reason. +- **Forbidden** — automatic merge, issue close, release, release rehearsal, and + any unrelated scope. Merge stays a separate human act; **every release stays + blocked while Heart is RED**. +- **Recorded in four sinks** — the authorization, the exact RED reason, the + causal mapping (reason → issue → plan → diff), the tests, and the validation + plan are written to: the **GitHub issue**; the **PR body**; the corrective + task's **`PyAutoMind/active.md`** entry (a `- corrective-red:` block naming + the reason and pointing at the authorization comment); and a + **`autonomy_log.md`** row whose outcome is tagged `corrective` (it is not an + `--auto` run, but the calibration log still records that the exception was + used). +- **Multiple RED reasons** — the authorization names exactly one. The PR body + states which reason the diff clears and that any sibling RED reasons remain, + so Heart stays RED and release stays blocked until every reason is cleared by + its own corrective PR. A corrective PR never claims to clear a reason it does + not address. + +**Failure behaviour — park without shipping** (set the `active.md` entry to +`status: blocked` or `awaiting-input`, write why on the issue, open no PR): + +- **Mixed-scope diff** — the diff touches anything beyond the named reason's + fix. The narrow permission covers only the causal fix; bundle nothing with it. +- **Stale or changed RED reason** — re-read Heart's verdict at ship time; if the + named reason string has changed, split, or cleared, the authorization no + longer matches the world. Park and re-authorize against the current verdict. +- **Missing evidence** — no causal mapping, no tests, or no validation plan. +- **Review finds the patch is not causal** — the review faculty (or a human + reviewer) judges the diff does not actually repair the named reason. + +**Recovery sequence** — the exception opens a PR; it does not resume release +work. That resumes only along this path: + +1. A **human merges** the corrective PR (a separate human act — the exception + never merges). +2. **Fresh post-merge wheels are built** and **release-integration validation** + is re-run on `main`. +3. Heart emits a **new verdict** over that fresh evidence. +4. Release work resumes only on that new verdict, and release stays + `human-required` throughout (the release cap is untouched by this section). + ## Calibration log `PyAutoMind/autonomy_log.md` — append-only. Every `--auto` run appends a row @@ -184,9 +252,11 @@ at PR-open (or on parking): | date | task | effective level | gates (tests/smoke/review/heart) | outcome | ``` -Outcome ∈ `merged-unchanged` / `amended` / `rejected` / `parked`. This is the -evidence base for raising or lowering caps — autonomy grows by demonstrated -calibration, not by optimism. +Outcome ∈ `merged-unchanged` / `amended` / `rejected` / `parked` / `corrective` +(the last records a use of the human-authorized corrective-PR exception above — +not an `--auto` run, but logged so the exception's use is auditable alongside the +autonomy rows). This is the evidence base for raising or lowering caps — autonomy +grows by demonstrated calibration, not by optimism. ### Calibration review — 2026-07-09 @@ -223,6 +293,11 @@ tier), never by weakening leg 4. - **Heart YELLOW/RED is never acknowledged autonomously.** A launch-time human acknowledgement of a named reason set is a human acknowledgement — it binds to that exact set, for that launch, and never extends to new reasons. +- **The corrective-PR exception for Heart RED is a contemporaneous human act** + (the section above), never reachable under `--auto`. It permits only commit, + push and opening one pending-release PR that repairs the named RED reason — + never merge, close, release or unrelated scope; every release stays blocked + while Heart is RED. - **Never rewrite history** (`AGENTS.md` rules apply verbatim to autonomous runs). - The `Autonomy:` header is a model's own estimate. The caps, the explicit @@ -234,7 +309,8 @@ tier), never by weakening leg 4. - `start_dev` — `--auto` usage, effective-level computation, plan-to-issue for `safe`, launch-acknowledgement recording (its "--auto mode" section). - `ship_library` / `ship_workspace` — the four-leg gate at step 4, stop at - PR-open, validation checklist, calibration append. + PR-open, validation checklist, calibration append; the RED-handling step + points here for the human-authorized corrective-PR exception. - `run_queue` — the generic queue loop: launching it is the batch's `--auto` activation; per-entry effective-level dispatch, `PARKED` checkpointing, RED stops the run. diff --git a/skills/WORKFLOW.md b/skills/WORKFLOW.md index bae88b5..159bcf6 100644 --- a/skills/WORKFLOW.md +++ b/skills/WORKFLOW.md @@ -153,7 +153,13 @@ pyauto-heart readiness --json # authoritative GREEN / YELLOW / RED verdict - **YELLOW** → surface the warnings; proceed only with explicit user acknowledgement (a human checkpoint at **every** autonomy level — [`../AUTONOMY.md`](../AUTONOMY.md)). -- **RED** → stop; report what failed. Do not ship. +- **RED** → stop; report what failed. Do not ship — **unless** a human + authorizes the narrow corrective-PR exception naming the exact RED reason + ([`../AUTONOMY.md`](../AUTONOMY.md) "Corrective-PR exception for Heart RED"): + commit/push/PR-open of one reason-scoped fix only, never merge or release. + When that exception is in play, surface the exact RED reason string(s) + verbatim from `pyauto-heart readiness` so the human authorizes the quote the + agent provided rather than hunting for the wording. Tests/smoke runs that feed the verdict are Heart's domain — invoke them through the vitals faculty rather than re-deriving pass/fail criteria in the skill. diff --git a/skills/ship_library/ship_library.md b/skills/ship_library/ship_library.md index a737bff..9777e5a 100644 --- a/skills/ship_library/ship_library.md +++ b/skills/ship_library/ship_library.md @@ -53,7 +53,13 @@ pyauto-heart readiness --json # authoritative GREEN / YELLOW / RED The library test suites are part of Heart's verdict — they run as the gate, not as an ad-hoc step the skill re-judges. **GREEN** → proceed to step 4. **YELLOW** → surface warnings, proceed only on explicit user acknowledgement. -**RED** → stop and report; do not ship. If `pyauto-brain`/`pyauto-heart` are +**RED** → stop and report; do not ship — **unless** a human authorizes the +narrow corrective-PR exception naming the exact RED reason (`AUTONOMY.md` +"Corrective-PR exception for Heart RED"), which permits commit/push/PR-open of +one reason-scoped fix only, never merge or release. When that exception is in +play, **surface the exact RED reason string(s) verbatim from `pyauto-heart +readiness`** and the specific corrective request, so the human authorizes the +quote the agent provided rather than hunting for the wording. If `pyauto-brain`/`pyauto-heart` are unavailable, run the per-repo `pytest / -x` inside the worktree as the gate and treat any failure as RED (WORKFLOW.md). Under `--auto`, this step is the four-leg **autonomous-ship gate** — tests (+ downstream dependents on diff --git a/skills/ship_workspace/ship_workspace.md b/skills/ship_workspace/ship_workspace.md index 9a56454..13bbe29 100644 --- a/skills/ship_workspace/ship_workspace.md +++ b/skills/ship_workspace/ship_workspace.md @@ -50,7 +50,14 @@ pyauto-heart readiness --json # GREEN / YELLOW / RED ``` Workspace **smoke tests** are part of Heart's verdict. **GREEN** → execute. -**YELLOW** → proceed only on explicit acknowledgement. **RED** → stop. If the +**YELLOW** → proceed only on explicit acknowledgement. **RED** → stop — +**unless** a human authorizes the narrow corrective-PR exception naming the +exact RED reason (`AUTONOMY.md` "Corrective-PR exception for Heart RED"), which +permits commit/push/PR-open of one reason-scoped fix only, never merge or +release. When that exception is in play, **surface the exact RED reason +string(s) verbatim from `pyauto-heart readiness`** and the specific corrective +request, so the human authorizes the quote the agent provided rather than +hunting for the wording. If the organism CLIs are unavailable, run `/smoke_test` (with `activate.sh` sourced) as the gate and treat any failure as RED. Under `--auto`, this step is the four-leg **autonomous-ship gate** (`AUTONOMY.md` "The autonomous-ship gate"); diff --git a/tests/test_corrective_red_exception.py b/tests/test_corrective_red_exception.py new file mode 100644 index 0000000..c7510a4 --- /dev/null +++ b/tests/test_corrective_red_exception.py @@ -0,0 +1,135 @@ +"""Doc-contract tests for the human-authorized corrective-PR exception to +Heart RED (PyAutoBrain#112). + +The exception is doctrine, not code, so its guardrails are pinned against the +canonical text of AUTONOMY.md — a weakening edit (dropping a forbidden action, +a record sink, or the human-only clause) fails loudly here rather than silently +widening the one authorized path through a RED gate. The seam tests also pin +the "link, don't duplicate policy" contract: the ship skills point at +AUTONOMY.md and do not carry a second copy of the permitted/forbidden lists. +""" + +from pathlib import Path + +BRAIN_HOME = Path(__file__).resolve().parents[1] +AUTONOMY = (BRAIN_HOME / "AUTONOMY.md").read_text() +SHIP_LIBRARY = (BRAIN_HOME / "skills" / "ship_library" / "ship_library.md").read_text() +SHIP_WORKSPACE = (BRAIN_HOME / "skills" / "ship_workspace" / "ship_workspace.md").read_text() +WORKFLOW = (BRAIN_HOME / "skills" / "WORKFLOW.md").read_text() + +SECTION_HEADER = "## Corrective-PR exception for Heart RED (human-authorized)" + + +def _norm(text): + """Lowercase, strip markdown emphasis, and collapse whitespace so assertions + ignore ``**``/``` ` ``` and line wrapping.""" + return " ".join(text.replace("*", "").replace("`", "").lower().split()) + + +def _section(): + """The corrective-PR exception section body (header to the next ``## ``).""" + start = AUTONOMY.index(SECTION_HEADER) + rest = AUTONOMY[start + len(SECTION_HEADER):] + end = rest.find("\n## ") + return _norm(rest if end == -1 else rest[:end]) + + +def test_section_exists(): + assert SECTION_HEADER in AUTONOMY + + +def test_permitted_set_is_commit_push_pr_open_only(): + sec = _section() + for verb in ("commit", "push", "opening one"): + assert verb in sec, verb + # the permission is explicitly bounded + assert "nothing else" in sec + + +def test_forbidden_set_named(): + sec = _section() + for forbidden in ("merge", "issue close", "release", "release rehearsal", + "unrelated scope"): + assert forbidden in sec, forbidden + assert "every release stays blocked while heart is red" in sec + + +def test_four_record_sinks_named(): + sec = _section() + for sink in ("github issue", "pr body", "pyautomind/active.md", + "autonomy_log.md", "- corrective-red:"): + assert _norm(sink) in sec, sink + + +def test_park_without_shipping_cases_named(): + sec = _section() + assert "park without shipping" in sec + for case in ("mixed-scope diff", "stale or changed red reason", + "missing evidence", "not causal"): + assert case in sec, case + + +def test_multiple_reasons_names_exactly_one(): + sec = _section() + assert "multiple red reasons" in sec + assert "names exactly one" in sec + + +def test_recovery_sequence_is_merge_then_fresh_validation_then_new_verdict(): + sec = _section() + assert "recovery sequence" in sec + assert "human merges" in sec + assert "post-merge wheels" in sec + assert "release-integration validation" in sec + assert "new verdict" in sec + assert "human-required" in sec # release cap untouched + + +def test_agent_provides_the_quote(): + # The human quotes the exact RED reason, so the agent must surface it + # verbatim from Heart's verdict — the human never reconstructs the wording. + sec = _section() + assert "the agent provides the quote" in sec + assert "verbatim from heart's current verdict" in sec + # and the ship gates instruct the agent to surface it at the RED step + for doc in (SHIP_LIBRARY, SHIP_WORKSPACE, WORKFLOW): + assert "verbatim from" in _norm(doc) + + +def test_exception_is_human_only_never_under_auto(): + sec = _section() + assert "human act" in sec + assert "never fires under --auto" in sec + + +def test_never_acknowledged_autonomously_invariant_survives_verbatim(): + # The exception must not weaken this hard invariant; it stays word-for-word. + assert ( + "**Heart YELLOW/RED is never acknowledged autonomously.**" in AUTONOMY + ) + + +def test_hard_invariant_bullet_scopes_the_exception(): + inv = _norm(AUTONOMY) + assert "corrective-pr exception for heart red is a contemporaneous human act" in inv + + +def test_ship_skills_link_the_exception(): + # Each ship gate points at the canonical section (link, not restatement). + marker = "Corrective-PR exception for Heart RED" + assert marker in SHIP_LIBRARY + assert marker in SHIP_WORKSPACE + assert marker in WORKFLOW + + +def test_ship_skills_do_not_duplicate_policy(): + # The exception's permitted/forbidden lists, record sinks and failure cases + # live only in AUTONOMY.md; the ship skills reference the section by name and + # must not carry a second copy that could drift. (Tokens checked here are + # distinctive to the exception — not incidental words like "autonomy_log.md", + # which ship_library legitimately uses for the calibration append.) + for doc in (SHIP_LIBRARY, SHIP_WORKSPACE, WORKFLOW): + norm = _norm(doc) + assert "- corrective-red:" not in norm # the active.md block form + assert "park without shipping" not in norm # the failure-case list + assert "recovery sequence" not in norm # the resume-release steps