From 994e810b6f8df482125bf1d2be94168b6ba8fe35 Mon Sep 17 00:00:00 2001 From: Bren Pike Date: Wed, 23 Sep 2026 08:22:22 -0600 Subject: [PATCH 1/5] feat(governance)!: gate the post-merge decision report behind an opt-in key BREAKING CHANGE: the post-merge decision report is now opt-in and off by default. Set HIVEMIND_ENABLE_DECISION_REPORT in the env block of .claude/settings.json or .claude/settings.local.json to restore it. While off, awaiting runs are marked done without any GitHub call. --- CLAUDE.md | 2 + ...config-gated-post-merge-decision-report.md | 94 +++++++++++++++++++ plugin/agents/overlord.md | 6 +- plugin/governance/decision-autonomy.md | 17 +++- 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 docs/adr/0030-config-gated-post-merge-decision-report.md diff --git a/CLAUDE.md b/CLAUDE.md index 507d3c15..67268019 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,8 @@ The local Codex review model is operator-overridable via `HIVEMIND_LOCAL_REVIEW_ The default post-PR watch (see Branching / PR workflow above) can be switched off standing-wide via `HIVEMIND_SKIP_PR_WATCH`, set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored, per-account). Unset/empty → the overlord watches normally per its default-watch rule (zero behavior change). Set (checked by presence) → the overlord never watches, short-circuiting the per-run `request.raw` read entirely. Because the committed settings file is in-repo, the key inherits into brood worktrees. Reference: ADR-0029. +The post-merge decision report is opt-in via `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored, per-account). Unset/empty → the report is off: the deferred-report scan renders nothing and makes no GitHub call, but still touches the zero-byte `.decision-report-done` marker for awaiting runs, so enabling it later only reports runs that finish after it is enabled. Set (checked by presence) → the overlord surfaces the post-merge decision report as before. The decision journal is written either way. Because the committed settings file is in-repo, the key inherits into brood worktrees. Reference: ADR-0030. + ## Brood execution The plugin supports parallel multi-overlord execution via spawn-brood and brood-status skills. Each brood session runs in its own git worktree as an independent Claude Code instance. diff --git a/docs/adr/0030-config-gated-post-merge-decision-report.md b/docs/adr/0030-config-gated-post-merge-decision-report.md new file mode 100644 index 00000000..d0602064 --- /dev/null +++ b/docs/adr/0030-config-gated-post-merge-decision-report.md @@ -0,0 +1,94 @@ +# The post-merge decision report is opt-in behind a presence-checked env key + +**Status:** accepted — 2026-09-23 + +## Context + +ADR-0026 (Decision 5, as widened by its 2026-09-18 amendment) makes the post-merge decision report UNCONDITIONAL: every run that journaled at least one `did-now`, `deferred`, or `recorded` decision produces a chat report on the next session start after its PR reaches `MERGED` or `CLOSED`. That rule was the structural mitigation for the autonomy shift — oversight moved from synchronous confirmation to asynchronous recounting, and the report was the thing being recounted. + +In practice the operator does not want the recounting delivered unsolicited. A session that opens to do new work is greeted instead by a narrative of decisions taken during a previous, already-merged run. The report is long by construction — situation, options, trade-offs, decision, why, per decision, in the consumer project's domain language — and it arrives at the moment the operator's attention is on something else. Delivered that way it reads as noise rather than oversight. + +The audit trail itself is not the problem and is not in question. The decision journal (`event.outputs.decisions[]`) is written regardless, persists in the ledger, and is readable on demand. What ADR-0026 assumed is that the journal needs a PUSH channel to be useful. That assumption is what this record revises: the journal is the audit trail, and the chat report is one presentation of it that an operator may or may not want automatically delivered. + +This record AMENDS ADR-0026's always-report rule. ADR-0026 is a historical record and is deliberately left unedited; where the two disagree on whether the report fires, this record governs. + +## Findings + +- **The report is the only unsolicited push in the resume path.** Everything else the Resume-On-Start scan does is either silent or answers a question the operator asked. The deferred-report scan is the one branch that produces unrequested chat output, and it produces the longest output of any branch. +- **The scan's only external dependency is the PR-state check.** Deriving the awaiting-report set is entirely local: read the run dirs, read the persisted PR event output, read the journal, check for the marker. The GitHub call exists solely to answer "has this PR merged or closed yet?" — which is only needed because a report is about to be rendered. +- **This repository already has an operator-override channel with two occupants.** `HIVEMIND_LOCAL_REVIEW_MODEL` (ADR-0022) and `HIVEMIND_SKIP_PR_WATCH` (ADR-0029, Decision 1) are both set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored, per-account). Because the committed settings file is in-repo, keys set there INHERIT into brood worktrees — the case where no human is present in the session at all. +- **`.decision-report-done` is a zero-byte presence marker, not a status record.** ADR-0026 Decision 5 makes its EXISTENCE the sole idempotency signal. Nothing reads its contents, and no ledger field, `run.status` value, or content file mirrors it. That gives the marker room to carry a slightly wider meaning without any schema consequence. + +## Decision + +### 1. The report becomes opt-in, off by default + +The post-merge decision report fires only when `HIVEMIND_ENABLE_DECISION_REPORT` is set. Unset or empty — the default for every consumer that does nothing — means no report. + +The justification is that the journal already provides the audit trail ADR-0026 was protecting. Turning the report off removes a delivery channel, not the record. An operator who wants the recounting sets one key; an operator who does not gets a quiet session start, and the decisions taken on their behalf remain fully reconstructable from the ledger. + +Tier-B autonomy is UNTOUCHED. The tier membership lists, the Autonomy 2x2, the promotion gate, the disposition vocabulary, and the journal entry shape are all exactly as ADR-0026 and its amendments leave them. Journaling is unconditional and does not consult the key. This record gates ONE thing: whether the chat report is rendered. + +### 2. The channel is a presence-checked env key on the existing operator-override path + +`HIVEMIND_ENABLE_DECISION_REPORT` is set in the `env` block of `.claude/settings.json` or `.claude/settings.local.json` — the same channel as `HIVEMIND_SKIP_PR_WATCH` and `HIVEMIND_LOCAL_REVIEW_MODEL`, and it inherits into brood worktrees for the same reason. + +It is checked by PRESENCE, with a simple non-empty test. No value parsing, no truthiness vocabulary, no enumerated accepted strings. This matches `HIVEMIND_SKIP_PR_WATCH` exactly, and it is the property that makes the check mechanical rather than interpretive: a key is present or it is not, and there is no third reading. + +### 3. The key is named `HIVEMIND_ENABLE_DECISION_REPORT`, not `HIVEMIND_DECISION_REPORT` + +A presence-only check on a bare-noun key is a trap. An operator who writes `HIVEMIND_DECISION_REPORT=off` — or `false`, or `0`, or `no` — is stating an intent to DISABLE, and a presence check reads that as ENABLE. The key would then do the opposite of what its own value says, and the failure is silent. + +Putting the verb in the name closes that gap: `HIVEMIND_ENABLE_DECISION_REPORT=` reads as "enable the decision report" for every value an operator could plausibly write, including the confused ones. The name and the check agree. This is the same construction `HIVEMIND_SKIP_PR_WATCH` uses — a verb in the key, presence as the signal. + +### 4. While off, the scan still runs locally and still leaves the marker + +With the key unset, the deferred-report scan renders NOTHING, never invokes `hivemind:decision-report`, and makes NO GitHub call of any kind. It nevertheless still derives the awaiting-report set from local ledger reads using the SAME predicate as the on path — a derivable PR event output, a journal holding at least one `did-now` / `deferred` / `recorded` entry, and no `.decision-report-done` marker — and unconditionally `touch`es the zero-byte marker for each awaiting run. No PR-state check is performed, because the marker is being written regardless of what that check would return. + +The reason is backlog containment. If the off path left no marker, every run completed while the report was off would stay awaiting-report indefinitely, and the first session after an operator enables the key would deliver a flood of reports for work merged weeks earlier — the exact unsolicited-noise failure this record exists to fix, concentrated into one session. Marking as we go means enabling the key produces reports for runs that finish AFTERWARD, which is what "turn the report on" should mean. + +The same mechanism discharges the existing backlog: the first session after this change lands, with the toggle off, marks every currently-awaiting run done and produces no output. The upgrade is quiet by construction. + +**The marker's meaning widens.** `.decision-report-done` now asserts "this run will produce no further decision report" — whether because the report was rendered or because it was suppressed while the feature was off. It remains zero-byte, remains the SOLE idempotency token, and carries no ledger field, no schema change, and no `run.status` value. Nothing reads its contents, so the widened meaning costs nothing structurally. + +**Fail-open behavior is unchanged.** The scan never blocks a session start. On the off path the only failure surface is the `touch` itself; a failed `touch` leaves the run awaiting-report, which is the pre-existing state and self-corrects on the next scan. + +### 5. Recorded residual: a still-open PR can be marked done while the report is off + +The off-path `touch` is unconditional, so a run whose PR is still OPEN gets marked done. If the operator enables the key before that PR merges, the run will never report — its marker already says otherwise. + +This is ACCEPTED, and recorded per the **Recorded Residual** clause of `plugin/governance/remediation-doctrine.md`. + +- **Root cause.** The off path deliberately makes no GitHub call, so it cannot distinguish an open PR from a merged one. Marking unconditionally is the only option consistent with that constraint. +- **Bounded impact.** The loss is one chat rendering for runs that straddle the moment the operator flips the key on. The run's decision journal is untouched and stays readable in the ledger — the audit trail survives in full, only the push delivery is missed, and only for the straddling window. +- **Obvious remediation considered and rejected.** Gate the off-path `touch` on the PR being terminal. That reintroduces a per-run GitHub call on the off path, which is precisely the cost the off path exists to avoid: an operator who has turned the feature OFF would still pay a network round trip per awaiting run at every session start, forever, to preserve a report they have declined to receive. Paying a standing cost on the disabled path to protect an edge of the enabled path is the wrong trade. + +### 6. The `hivemind:decision-report` skill needs no change + +The skill owns RENDERING mechanics — how a journal becomes narrative in the consumer project's domain language. It does not own FIRING POLICY, which is single-sourced in `plugin/governance/decision-autonomy.md` (`## Post-Merge Decision Report Trigger`). Gating happens at the caller, before invocation. A skill that is never invoked needs no knowledge of why. + +Keeping the gate out of the skill also preserves the skill's usefulness as a manual tool: an operator running it directly against a run gets the report, regardless of the key. + +## Considered Options + +| Option | Rejected because | +|---|---| +| An opt-OUT key (`HIVEMIND_SKIP_DECISION_REPORT`), report on by default | Leaves the default noisy, which is the entire complaint. An operator who has never heard of the key still gets unsolicited reports, and the many-consumer default is the case that matters | +| A bare-noun key `HIVEMIND_DECISION_REPORT` with a presence check | `=off` / `=false` / `=0` would ENABLE the report — the key would contradict its own value, silently. Either the name gets a verb or the check must parse values, and parsing values abandons the presence-check simplicity shared with `HIVEMIND_SKIP_PR_WATCH` | +| Parse the key's value (`true`/`false`/`1`/`0`) instead of checking presence | Invents a truthiness vocabulary this repo does not otherwise have, and every accepted-string list is a fixture waiting to be extended by the next value someone types. Presence is total over all inputs | +| While off, leave no marker | The awaiting-report set grows unbounded, and enabling the key floods one session with reports for long-merged work — the same unsolicited-noise failure, concentrated. It also leaves no way to land this change quietly, since the existing backlog would sit primed | +| While off, check PR state before marking | Reintroduces a per-run GitHub call on the path whose defining property is making none. A standing network cost on the disabled path, paid to protect a straddling-window edge of the enabled path | +| Delete the report feature outright | Loses real value for operators who DO want the asynchronous recounting, and discards the ADR-0026 mitigation entirely rather than making it elective. The objection was to unsolicited delivery, not to the report | +| Presence-checked `HIVEMIND_ENABLE_DECISION_REPORT`, off by default, unconditional off-path marker (CHOSEN) | — | + +## Consequences + +- **The default session start is quiet.** No consumer sees a decision report unless an operator sets the key. The first session after this change lands discharges the existing awaiting backlog silently. +- **The off path makes zero GitHub calls.** The deferred-report scan becomes purely local reads plus a `touch` per awaiting run. Session start gets cheaper for every consumer running the default. +- **The audit trail is unchanged.** Journaling is unconditional; `event.outputs.decisions[]` is written exactly as before. Oversight moves from pushed to pulled, not from present to absent. +- **Tier-A and Tier-B behavior are untouched.** No tier membership, gate, disposition, or journal field changes. ALL merge recommendations remain Tier-A and always surfaced. +- **ADR-0026's always-report rule is amended, not deleted.** The AWAITING-REPORT predicate survives verbatim and still governs which runs the scan considers; what changes is what happens to an awaiting run when the key is absent. ADR-0026 stays unedited as historical record. +- **The override channel now carries three keys.** `HIVEMIND_LOCAL_REVIEW_MODEL`, `HIVEMIND_SKIP_PR_WATCH`, and `HIVEMIND_ENABLE_DECISION_REPORT` share one `env`-block convention, one settings-file pair, and one brood-inheritance property. +- **Enabling the key mid-life reports forward only.** Runs marked while off stay marked. An operator enabling the report gets it for runs that complete afterward, and the straddling-window residual in Decision 5 applies to runs whose PRs were open at the moment of the flip. + +References: `plugin/governance/decision-autonomy.md` (`## Post-Merge Decision Report Trigger`), `plugin/governance/remediation-doctrine.md` (`### Recorded Residual`), `plugin/agents/overlord.md`, `plugin/skills/decision-report/`, `CLAUDE.md`; amends ADR-0026 Decision 5; follows the operator-override precedent of ADR-0022 and ADR-0029. diff --git a/plugin/agents/overlord.md b/plugin/agents/overlord.md index 0e834e14..f18513dd 100644 --- a/plugin/agents/overlord.md +++ b/plugin/agents/overlord.md @@ -85,9 +85,9 @@ On session start, scan `/.hivemind/runs/*/state.json` for `run.st **Version-skew gate:** on resume of a run whose `run.mode` is NOT already `intent_fallback`, if `ledger.run.workflow_version` != the on-disk definition `version`, do NOT auto-resume — present exactly TWO doors: (1) start fresh — BEFORE initializing the fresh run, invoke `hivemind:mark-intent-fallback` with `close_status: cancelled` against the OLD skewed run to flip its stale `run.status: running` → `cancelled`, so resume-on-start no longer rediscovers the abandoned run every session; (2) proceed intent-driven (the universal fallback below) — invoke `hivemind:mark-intent-fallback` (run_id + the current state string + a summary, NO `close_status`) to atomically set `run.mode: intent_fallback` and append a fallback event; transition gating is suspended and the run stays `running` as an append-only observability log while intent-driven work proceeds, finishing by judgment. `record-state-result.sh` / the engine HARD-REJECTS any id/version mismatch and exposes NO rebind, so on version skew the overlord does NOT attempt to continue the existing deterministic run against the new definition — `hivemind:mark-intent-fallback` is the separate sanctioned write-path door, not a rebind. -**Deferred post-merge decision-report trigger (overlord-inline).** Independently of the running-ledger reconciliation above, the Resume-On-Start scan ALSO derives any run AWAITING a decision report, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger). The overlord globs its OWN run dirs (`/.hivemind/runs/*/state.json`, anchored to `git rev-parse --show-toplevel` — the SAME ground-truth glob the running-ledger reconciliation above uses, NEVER an externally-supplied `run_id`). A discovered run is awaiting-report when ALL hold: a PR is DERIVABLE for the run from a recorded event output — read `` from `event.outputs.pr` of EITHER the recorded `open_pr` state-result event (for a standard-delivery run, persisted when recording the `open_pr` state per the execution-loop instruction above) OR the recorded `pr_branch_preflight`/`intake` state-result event (for a `pr-feedback-remediation` run, persisted when recording the `pr_branch_preflight` state per its Safety Rail above). A `pr-feedback-remediation` run watches/fixes an EXISTING PR and journals Tier-B decisions but never emits `open_pr`, so when no `open_pr` event exists derivation falls through to the `pr_branch_preflight`/`intake` event rather than treating the missing `open_pr` as non-derivable; this scan stays workflow-agnostic. Its events must also carry ≥1 `event.outputs.decisions[]` entry whose `disposition` is `did-now`, `deferred`, OR `recorded` (a run whose journal holds ONLY `surfaced` entries is NOT awaiting-report — it would never warrant a report and so would reprocess forever; this predicate matches the skill's fire condition exactly), AND its run dir does NOT yet contain the zero-byte `.decision-report-done` marker. For each awaiting-report run, run `gh pr view --json state -q .state`: on `OPEN` leave it untouched; on `MERGED` or `CLOSED` the overlord reads that run's OWN `decisions[]` from the ledger it owns and wrote (identical trust to the `state.json` already there — if that dir were a symlink the ledger writes would already be compromised, so reading the journal it just wrote opens no new surface), invokes `hivemind:decision-report` passing those `decisions[]` entries plus the resolved `pr_state` (and the run's changed-file set) — and passes NO report path, because the report is CHAT-ONLY. The skill RENDERS the narrative in the consumer's ubiquitous language and RETURNS it as chat text; the overlord SURFACES the returned narrative to the user, then `touch /.decision-report-done` to record completion. That marker is a ZERO-BYTE file (no content is ever written into it — `touch` writes no bytes), so it carries no splice/injection surface; the overlord writes NO `decision-report.md` content file and the skill writes nothing at all. The only disk write in the whole feature is this zero-byte `touch` of the overlord's OWN ground-truth run dir (the same dir it already wrote `state.json` into, enumerated from the ground-truth glob — NEVER an externally-supplied `run_id`), so there is no path oracle to guard. A CLOSED-not-merged PR still reports, with an abandoned header. The report fires only when ≥1 Tier-B AUTO decision (disposition `did-now`, `deferred`, or `recorded`) was journaled — a journal of only `surfaced` entries produces no report. The EXISTENCE of the zero-byte `/.decision-report-done` marker in the run dir is the SOLE idempotency marker: already-present means already-reported, so skip. This scan is workflow-agnostic and runs for ALL discovered runs regardless of which workflow produced them. +**Deferred post-merge decision-report trigger (overlord-inline).** Independently of the running-ledger reconciliation above, the Resume-On-Start scan ALSO derives any run AWAITING a decision report, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger). **This report is OPT-IN and OFF BY DEFAULT.** The standing operator preference `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored) — the same operator-override channel as `HIVEMIND_SKIP_PR_WATCH` / `HIVEMIND_LOCAL_REVIEW_MODEL` — is checked by PRESENCE with a simple non-empty test, and that check happens BEFORE any `gh pr view` and before ANY GitHub call whatsoever. SET (non-empty): the scan runs exactly as written below, unchanged. UNSET or EMPTY (the default): the overlord STILL derives the awaiting-report set below from LOCAL ledger reads under the SAME predicate, but makes NO GitHub call of any kind (no `gh pr view`), renders NOTHING, and never invokes `hivemind:decision-report`; for each awaiting-report run it `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state. The marker therefore means "this run will produce no further decision report" — reported OR suppressed-while-off — and stays the SOLE idempotency token, so enabling the key later reports only runs that finish after it is enabled. The canonical policy is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger) and is not restated here. Because the committed settings file lives in the repository, the preference inherits into brood worktrees. The decision journal (`event.outputs.decisions[]`) is written regardless of this key; only the chat report is gated. The overlord globs its OWN run dirs (`/.hivemind/runs/*/state.json`, anchored to `git rev-parse --show-toplevel` — the SAME ground-truth glob the running-ledger reconciliation above uses, NEVER an externally-supplied `run_id`). A discovered run is awaiting-report when ALL hold: a PR is DERIVABLE for the run from a recorded event output — read `` from `event.outputs.pr` of EITHER the recorded `open_pr` state-result event (for a standard-delivery run, persisted when recording the `open_pr` state per the execution-loop instruction above) OR the recorded `pr_branch_preflight`/`intake` state-result event (for a `pr-feedback-remediation` run, persisted when recording the `pr_branch_preflight` state per its Safety Rail above). A `pr-feedback-remediation` run watches/fixes an EXISTING PR and journals Tier-B decisions but never emits `open_pr`, so when no `open_pr` event exists derivation falls through to the `pr_branch_preflight`/`intake` event rather than treating the missing `open_pr` as non-derivable; this scan stays workflow-agnostic. Its events must also carry ≥1 `event.outputs.decisions[]` entry whose `disposition` is `did-now`, `deferred`, OR `recorded` (a run whose journal holds ONLY `surfaced` entries is NOT awaiting-report — it would never warrant a report and so would reprocess forever; this predicate matches the skill's fire condition exactly), AND its run dir does NOT yet contain the zero-byte `.decision-report-done` marker. For each awaiting-report run, run `gh pr view --json state -q .state`: on `OPEN` leave it untouched; on `MERGED` or `CLOSED` the overlord reads that run's OWN `decisions[]` from the ledger it owns and wrote (identical trust to the `state.json` already there — if that dir were a symlink the ledger writes would already be compromised, so reading the journal it just wrote opens no new surface), invokes `hivemind:decision-report` passing those `decisions[]` entries plus the resolved `pr_state` (and the run's changed-file set) — and passes NO report path, because the report is CHAT-ONLY. The skill RENDERS the narrative in the consumer's ubiquitous language and RETURNS it as chat text; the overlord SURFACES the returned narrative to the user, then `touch /.decision-report-done` to record completion. That marker is a ZERO-BYTE file (no content is ever written into it — `touch` writes no bytes), so it carries no splice/injection surface; the overlord writes NO `decision-report.md` content file and the skill writes nothing at all. The only disk write in the whole feature is this zero-byte `touch` of the overlord's OWN ground-truth run dir (the same dir it already wrote `state.json` into, enumerated from the ground-truth glob — NEVER an externally-supplied `run_id`), so there is no path oracle to guard. A CLOSED-not-merged PR still reports, with an abandoned header. The report fires only when ≥1 Tier-B AUTO decision (disposition `did-now`, `deferred`, or `recorded`) was journaled — a journal of only `surfaced` entries produces no report. The EXISTENCE of the zero-byte `/.decision-report-done` marker in the run dir is the SOLE idempotency marker: already-present means already-reported, so skip. This scan is workflow-agnostic and runs for ALL discovered runs regardless of which workflow produced them. -**This deferred-report scan is BEST-EFFORT and FAIL-OPEN — an explicit EXCEPTION to `## Tool-Error Recovery`.** The post-merge decision report is non-essential observability; it MUST NEVER wedge session startup. If the `gh pr view --json state -q .state` lookup (or ANY lookup) for a deferred-report run FAILS — auth expired, network/GitHub outage, PR/repo unavailable, or any other error — that failure is SWALLOWED, NOT escalated to the surfaced-stop condition `## Tool-Error Recovery` would otherwise apply to a post-retry read-only tool failure. Specifically: do NOT stop or block the session; LEAVE that run AWAITING (do NOT write the `.decision-report-done` marker, so it is retried on a future session); SKIP it for this session and CONTINUE normal startup. The failure may OPTIONALLY be surfaced as a non-blocking warning AFTER startup completes, but is never a session-blocking stop. Without this carve-out, one stale historical awaiting-report run whose PR lookup keeps failing could block EVERY future session at startup, before normal work begins — which this exception forbids. +**This deferred-report scan is BEST-EFFORT and FAIL-OPEN — an explicit EXCEPTION to `## Tool-Error Recovery`.** The post-merge decision report is non-essential observability; it MUST NEVER wedge session startup. If the `gh pr view --json state -q .state` lookup (or ANY lookup) for a deferred-report run FAILS — auth expired, network/GitHub outage, PR/repo unavailable, or any other error — that failure is SWALLOWED, NOT escalated to the surfaced-stop condition `## Tool-Error Recovery` would otherwise apply to a post-retry read-only tool failure. Specifically: do NOT stop or block the session; LEAVE that run AWAITING (do NOT write the `.decision-report-done` marker, so it is retried on a future session); SKIP it for this session and CONTINUE normal startup. The failure may OPTIONALLY be surfaced as a non-blocking warning AFTER startup completes, but is never a session-blocking stop. Without this carve-out, one stale historical awaiting-report run whose PR lookup keeps failing could block EVERY future session at startup, before normal work begins — which this exception forbids. With `HIVEMIND_ENABLE_DECISION_REPORT` unset or empty the same carve-out holds over a SMALLER failure surface: there is no lookup at all, so the only operation that can fail is the `touch` of the zero-byte marker — a failed `touch` is SWALLOWED identically, that run is LEFT AWAITING and retried on a future session, and startup continues; never a session-blocking stop. ## Intent-Driven Fallback (Universal) @@ -144,7 +144,7 @@ RUN-OWNERSHIP-01: a run ledger is owned and mutated only by the overlord instanc - `hivemind:create-working-branch` — create/confirm compliant working branch - `hivemind:molt` — commit completed phases, milestones, version bumps, review fixes - `hivemind:open-plan-pr` — open PR after validation and versioning gates pass -- `hivemind:decision-report` — renders the post-merge decision report in the consumer project's ubiquitous language and RETURNS it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and `touch`es a zero-byte done-marker) +- `hivemind:decision-report` — renders the post-merge decision report in the consumer project's ubiquitous language and RETURNS it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and `touch`es a zero-byte done-marker). OPT-IN, OFF BY DEFAULT: invoked only when `HIVEMIND_ENABLE_DECISION_REPORT` is set, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger) - `hivemind:github-review-loop` — main-session watch loop and the mandatory-by-default post-PR path; polls a PR for review activity and dispatches fix-mode remediation per actionable event; overlord-executed (hosts Monitor). `max_remediation_cycles` is a FLOOR declared and enforced by `${CLAUDE_PLUGIN_ROOT}/skills/github-review-loop/scripts/loop-state.sh`, which REJECTS any `max_remediation_cycles` below it: no caller may invoke this loop with a lower value. - `hivemind:adaptation-cycle` — invoked by local-reviewer internally, not by overlord - `hivemind:tdd` — invoked by coder internally when TDD is requested diff --git a/plugin/governance/decision-autonomy.md b/plugin/governance/decision-autonomy.md index a6abd835..fe80c8dd 100644 --- a/plugin/governance/decision-autonomy.md +++ b/plugin/governance/decision-autonomy.md @@ -100,7 +100,11 @@ The recurrence mechanics — the counting unit, the trip threshold, the Gate-A r ## Post-Merge Decision Report Trigger -After the PR for a run merges, the overlord ALWAYS surfaces a report of the decisions it made on the user's behalf, rendered in the CONSUMER project's ubiquitous language. The report is CHAT-ONLY: it is RENDERED to chat and surfaced to the user — NO report file is written to disk. This section defines the TRIGGER and policy only; the rendering mechanics live in the `hivemind:decision-report` skill and are not restated here. +After the PR for a run merges, the overlord surfaces a report of the decisions it made on the user's behalf, rendered in the CONSUMER project's ubiquitous language — but ONLY while the operator has enabled the report. The report is CHAT-ONLY: it is RENDERED to chat and surfaced to the user — NO report file is written to disk. This section defines the TRIGGER and policy only; the rendering mechanics live in the `hivemind:decision-report` skill and are not restated here. + +The report is CONFIG-GATED on `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of the project's `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored) — the same operator-override channel as `HIVEMIND_SKIP_PR_WATCH` and `HIVEMIND_LOCAL_REVIEW_MODEL`. Check it by PRESENCE with a simple non-empty test; that check is mechanical and reads no prose. Set and non-empty means every rule below applies unchanged. When `HIVEMIND_ENABLE_DECISION_REPORT` is unset or empty — the DEFAULT — the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind: no PR-state lookup, no `gh pr view`, no other GitHub request whatsoever. Because the committed settings file lives in the repository, the preference inherits into brood worktrees. + +The decision journal keeps being written regardless of this key: `event.outputs.decisions[]` is appended exactly as **Decision Journal** specifies, and Tier-B autonomy, the 2x2, and the promotion gate are UNAFFECTED. Only the chat report is gated. The report is deferred-on-merge — it is not produced at merge time but on a subsequent session start. The overlord's Resume-On-Start scan (per `${CLAUDE_PLUGIN_ROOT}/agents/overlord.md` `## Resume On Start`) derives an AWAITING-REPORT run when ALL of the following hold: @@ -108,11 +112,16 @@ The report is deferred-on-merge — it is not produced at merge time but on a su - the run's `event.outputs.decisions[]` carries at least one entry with a `disposition` of `did-now`, `deferred`, or `recorded` — a run whose journal holds only `surfaced` entries is NOT awaiting-report (it would never warrant a report and would otherwise reprocess every session) - the run dir does NOT yet contain the zero-byte `.decision-report-done` marker -For an awaiting-report run the overlord checks PR state and, on `MERGED` or `CLOSED`, invokes `hivemind:decision-report`, surfaces the returned narrative to the user, then `touch`es the zero-byte `.decision-report-done` marker in the run dir. +The awaiting-report predicate above is derived from LOCAL ledger reads alone and is evaluated identically whether the key is set or not. + +When the key is SET, for an awaiting-report run the overlord checks PR state and, on `MERGED` or `CLOSED`, invokes `hivemind:decision-report`, surfaces the returned narrative to the user, then `touch`es the zero-byte `.decision-report-done` marker in the run dir. + +When the key is UNSET or EMPTY, the overlord derives the same awaiting-report set from the same predicate and, for each run in it, `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state — the marker is written whether or not the run's PR has merged, because no PR state is read. A run with no derivable PR fails the predicate, is therefore NOT awaiting-report, and is NOT marked. Enabling the key later consequently reports only on runs that finish after it is enabled; runs already suppressed stay marked. -This scan is BEST-EFFORT and FAIL-OPEN: a PR-state lookup failure for a deferred-report run leaves the run AWAITING (no marker written) and is skipped without blocking session startup — never a session-blocking stop — and is retried on a future session (see `${CLAUDE_PLUGIN_ROOT}/agents/overlord.md` `## Resume On Start`). +This scan is BEST-EFFORT and FAIL-OPEN in both modes, and is never a session-blocking stop. With the key set, a PR-state lookup failure leaves the run AWAITING (no marker written) and is skipped without blocking session startup. With the key unset or empty, the `touch` is the ONLY failure surface: a failed `touch` is swallowed, the run stays AWAITING, and nothing else is attempted. Either way the run is retried on a future session (see `${CLAUDE_PLUGIN_ROOT}/agents/overlord.md` `## Resume On Start`). Firing condition and idempotency: -- The report fires ONLY when at least one Tier-B AUTO decision was journaled — an entry with a `disposition` of `did-now`, `deferred`, or `recorded`. A run whose journal holds only `surfaced` entries produces no report. +- The report fires ONLY when `HIVEMIND_ENABLE_DECISION_REPORT` is set and non-empty AND at least one Tier-B AUTO decision was journaled — an entry with a `disposition` of `did-now`, `deferred`, or `recorded`. A run whose journal holds only `surfaced` entries produces no report. - The EXISTENCE of the zero-byte `.decision-report-done` marker in the run dir is the SOLE idempotency marker — there is NO ledger marker and NO `decision-report.md` content file (the report is chat-only). The marker is created with `touch` and holds NO content, so it carries no splice/injection surface. The run-status enum is unchanged; no new `run.status` value is introduced. +- The marker means "this run will produce no further decision report" — it is written both when a report WAS rendered and when the report was SUPPRESSED because the key was unset or empty. Its shape and role do not widen with its meaning: still zero-byte, still the sole idempotency token, still no ledger marker and no schema change. From 905aae9a8fbf1c16ee8be713c851d9c8e2a4e4dd Mon Sep 17 00:00:00 2001 From: Bren Pike Date: Wed, 23 Sep 2026 08:28:14 -0600 Subject: [PATCH 2/5] feat(governance): document the opt-in decision report and pin its default --- CONTEXT.md | 4 +- README.md | 2 +- plugin/references/run-ledger-schema.md | 8 ++-- .../policy/safety-decision-report-toggle.json | 38 +++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 tests/policy/safety-decision-report-toggle.json diff --git a/CONTEXT.md b/CONTEXT.md index 20bb0d38..c338c340 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -211,7 +211,7 @@ The per-decision record (situation, options, trade-offs, recommendation strength _Avoid_: audit log, decision log **Decision Report**: -The post-merge narrative the overlord ALWAYS surfaces after a PR merges, recounting each autonomous decision in the consumer project's ubiquitous language; produced by `hivemind:decision-report`. +The OPT-IN post-merge narrative the overlord surfaces after a PR merges — only when the decision report is enabled via `HIVEMIND_ENABLE_DECISION_REPORT` — recounting each autonomous decision in the consumer project's ubiquitous language; produced by `hivemind:decision-report`. _Avoid_: summary, changelog **Disposition**: @@ -388,7 +388,7 @@ _Avoid_: degraded mode, manual mode, safe mode - An **Unsafe Git State** blocks all modifying agent operations until resolved - An **Overlord** records a **Decision Journal** entry for every Tier-B decision; the **Promotion Gate** decides whether the decision is auto-taken or surfaced to the **Overmind** -- A **Decision Report** is produced once per merged PR that contains ≥1 auto **Disposition** (`did-now`, `deferred`, or `recorded`), rendered in the consumer's ubiquitous language +- A **Decision Report** is produced once per merged PR that contains ≥1 auto **Disposition** (`did-now`, `deferred`, or `recorded`) when the decision report is enabled via `HIVEMIND_ENABLE_DECISION_REPORT`, rendered in the consumer's ubiquitous language - **Remediation** Tier-B choices (planner-escalation, root-cluster routing) are auto-taken + journaled unless the **Promotion Gate** trips - A **Brood** contains one or more **Strains**, each running in a separate git worktree diff --git a/README.md b/README.md index edaafcb8..1b9d11b0 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ All skills are invoked using the namespaced form: | `hivemind:record-state-result` | Record the outcome of the current workflow state into the run ledger and advance `state.current` to the legal next state, validated against the workflow definition | | `hivemind:next-wave` | Read-only engine for the overlord's intra-run parallel implement loop — reads the run ledger and computes the ready WAVE (the maximal, file-disjoint set of dependency-satisfied plan steps) to dispatch as concurrent agent delegations this iteration | | `hivemind:mark-intent-fallback` | Sanctioned engine write-path for the version-skew intent-fallback resume door and start-fresh stale-run closeout — sets `run.mode:intent_fallback`, appends a fallback event, and optionally closes a stale run via `run.status` | -| `hivemind:decision-report` | After a run's PR merges or closes, render a chronological narrative of the auto-decisions the run made on the user's behalf — in the consumer project's domain language — and return it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and touches a zero-byte done-marker) | +| `hivemind:decision-report` | Opt-in, off by default — enable it by setting `HIVEMIND_ENABLE_DECISION_REPORT` in the `env` block of `.claude/settings.json` or `.claude/settings.local.json`. When enabled, after a run's PR merges or closes, render a chronological narrative of the auto-decisions the run made on the user's behalf — in the consumer project's domain language — and return it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and touches a zero-byte done-marker) | | `hivemind:zoom-out` | Zoom out for broader context — maps relevant modules and callers using the project's domain glossary vocabulary | | `hivemind:improving-architecture` | Read-only architecture analysis — emits a ranked refactoring blueprint of deepening opportunities (shallow → deep modules) for testability and AI-navigability | | `hivemind:refactor-to-depth` | Execution skill that performs a behavior-preserving deepening refactor via a refactor-under-green loop — the in-implementation counterpart to the read-only `improving-architecture` blueprint | diff --git a/plugin/references/run-ledger-schema.md b/plugin/references/run-ledger-schema.md index 34275973..f32d6a90 100644 --- a/plugin/references/run-ledger-schema.md +++ b/plugin/references/run-ledger-schema.md @@ -135,9 +135,9 @@ Append-only blocker log. **Convention (proactive-recurrence-origin marker):** on a `root-cluster-suspected` transition, `event.outputs` MAY carry the named origin-marker key (`recurrence_origin`). Its presence distinguishes a proactively-derived zoom-out from a reviewer-returned one. The key's name, values, and absence semantics are defined SOLELY in `${CLAUDE_PLUGIN_ROOT}/governance/remediation-doctrine.md (### Proactive Zoom-Out Ledger Marker)` — that subsection is the single source; this note does not restate them. -**Convention (open_pr PR identity):** on an `open_pr` transition, `event.outputs` MAY carry the opened PR's identity — `pr` (the PR URL) and `head_ref_oid` (the PR head SHA) — recorded verbatim like `recurrence_origin`. The overlord forwards these from the routing YAML `hivemind:open-plan-pr` returns (`url` → `pr`, `head_ref_oid`). The deferred post-merge decision report derives the run's PR from `event.outputs.pr`; the trigger and policy single source is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Post-Merge Decision Report Trigger)`. This is additive and free-form — NO schema change and NO new required field. +**Convention (open_pr PR identity):** on an `open_pr` transition, `event.outputs` MAY carry the opened PR's identity — `pr` (the PR URL) and `head_ref_oid` (the PR head SHA) — recorded verbatim like `recurrence_origin`. The overlord forwards these from the routing YAML `hivemind:open-plan-pr` returns (`url` → `pr`, `head_ref_oid`). The deferred post-merge decision report derives the run's PR from `event.outputs.pr` — that report is CONFIG-GATED (opt-in via `HIVEMIND_ENABLE_DECISION_REPORT`, OFF by default), so recording these keys does NOT imply a report will fire; the trigger and policy single source is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Post-Merge Decision Report Trigger)`. This is additive and free-form — NO schema change and NO new required field. -**Convention (pr-feedback-remediation PR identity):** a `pr-feedback-remediation` run has no `open_pr` state, so on a `pr_branch_preflight` (and/or `intake`) transition `event.outputs` MAY carry the resolved PR's `pr` (URL) and `head_ref_oid` (head SHA), recorded verbatim like the `open_pr` keys above. The overlord records these from the PR it resolves and checks out at `pr_branch_preflight`. The deferred post-merge decision report derives the run's PR from `event.outputs.pr` of EITHER event, single-sourced to `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Post-Merge Decision Report Trigger)`. This is additive and free-form — NO schema change, NO new required field, and a key/path DISTINCT from `decisions[]`, `recurrence_origin`, and `plan.steps`. +**Convention (pr-feedback-remediation PR identity):** a `pr-feedback-remediation` run has no `open_pr` state, so on a `pr_branch_preflight` (and/or `intake`) transition `event.outputs` MAY carry the resolved PR's `pr` (URL) and `head_ref_oid` (head SHA), recorded verbatim like the `open_pr` keys above. The overlord records these from the PR it resolves and checks out at `pr_branch_preflight`. The deferred post-merge decision report derives the run's PR from `event.outputs.pr` of EITHER event and is CONFIG-GATED the same way (opt-in via `HIVEMIND_ENABLE_DECISION_REPORT`, OFF by default), single-sourced to `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Post-Merge Decision Report Trigger)`. This is additive and free-form — NO schema change, NO new required field, and a key/path DISTINCT from `decisions[]`, `recurrence_origin`, and `plan.steps`. **Convention (decision-journal array):** `event.outputs` MAY carry an OPTIONAL, free-form `decisions[]` array, recorded verbatim like `recurrence_origin`. Each entry carries the fields `ts`, `state`, `situation`, `options`, `tradeoffs`, `rec_strength`, `gate`, `disposition`, `decision`, `rationale`, and `reversible`. The semantics single source — the autonomy posture, the 2x2, the promotion gate, the disposition vocabulary, and these fields — is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Decision Journal)`; this note does not restate the 2x2 or promotion-gate mechanics. `decisions[]`, `recurrence_origin`, the `pr` / `head_ref_oid` open_pr keys, and the `plan.steps` plan-steps writers are DISTINCT keys/paths on or around `event.outputs` and do not collide. @@ -150,8 +150,8 @@ Done-ness of a plan step is DERIVED from the union of `events[].outputs.complete **Non-change clarifications (so a future reader does not "fix" a non-bug):** - NO `schema_version` bump is implied by the decision-journal convention — `decisions[]` is a free-form `event.outputs` key, not a required ledger field. -- NO new `run.status` value is introduced — the enum stays `running | complete | blocked | cancelled`. The post-merge report's "awaiting" condition is DERIVED at Resume-On-Start (a PR exists, `event.outputs.decisions[]` carries ≥1 `did-now`/`deferred`/`recorded` entry, and the zero-byte `.decision-report-done` marker is absent), NOT stored. -- NO `artifacts.decision_report` ledger marker is used — the post-merge report is CHAT-ONLY (rendered and surfaced to the user, never written to disk). Its idempotency token is the EXISTENCE of a zero-byte `.decision-report-done` marker `touch`ed in the run dir — NOT a `decision-report.md` content file (which no longer exists), and not a ledger field. +- NO new `run.status` value is introduced — the enum stays `running | complete | blocked | cancelled`. The post-merge report's "awaiting" condition is DERIVED at Resume-On-Start (a PR exists, `event.outputs.decisions[]` carries ≥1 `did-now`/`deferred`/`recorded` entry, and the zero-byte `.decision-report-done` marker is absent), NOT stored. That derived predicate is IDENTICAL whether or not the report is enabled — when `HIVEMIND_ENABLE_DECISION_REPORT` is unset or empty the same awaiting set is derived from the same local ledger reads, with no PR-state check and no report rendered, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md (## Post-Merge Decision Report Trigger)`. +- NO `artifacts.decision_report` ledger marker is used — the post-merge report is CHAT-ONLY (rendered and surfaced to the user, never written to disk). Its idempotency token is the EXISTENCE of a zero-byte `.decision-report-done` marker `touch`ed in the run dir — NOT a `decision-report.md` content file (which no longer exists), and not a ledger field. The marker means "this run will produce no further decision report" — it is written on BOTH paths: already reported, OR suppressed while the report was off. Marker shape is unchanged by that wider meaning: still zero-byte, still the sole idempotency token, still no ledger marker and no schema change. ## Blocker shape diff --git a/tests/policy/safety-decision-report-toggle.json b/tests/policy/safety-decision-report-toggle.json new file mode 100644 index 00000000..7512c750 --- /dev/null +++ b/tests/policy/safety-decision-report-toggle.json @@ -0,0 +1,38 @@ +{ + "rule": "Post-merge decision report is opt-in and OFF by default", + "description": "P3 coupling. The post-merge decision report is gated on the operator key `HIVEMIND_ENABLE_DECISION_REPORT`, checked by PRESENCE, and the DEFAULT (unset or empty) is OFF. The source pin sits on ONE span in `plugin/governance/decision-autonomy.md` carrying BOTH halves of that conditional: the discriminating condition (the key is unset or empty — the DEFAULT) and the whole obligation it triggers (the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind). Either half alone is rewritable while the other stays green — deleting the `unset or empty — the DEFAULT` keying flips the gate to opt-out, and deleting the no-GitHub-call clause re-admits a PR-state lookup on the off path — so both live inside the pinned span. The off path's second obligation, the one that keeps the feature idempotent while disabled, is pinned as a separate entry in the same canonical file: the overlord derives the same awaiting-report set from the same predicate and `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state. That entry is separate because the unconditional-marker rule and the renders-nothing rule fail independently: the scan could keep rendering nothing while silently reading PR state to decide whether to mark, which would re-introduce exactly the GitHub call the gate exists to remove. Four claim-carrying spans in the agent consumer `plugin/agents/overlord.md` are pinned as separate entries, because the default rests on each: (a) the key's NAME and its operator-config CHANNEL (the `env` block of `.claude/settings.json` committed or `.claude/settings.local.json` gitignored), so renaming the key or relocating its home is loud; (b) the PRESENCE test together with its ORDERING obligation (the check happens BEFORE any `gh pr view` and before ANY GitHub call whatsoever) in one span, because a presence test that runs AFTER the lookup satisfies the letter of the gate while performing the call it forbids, and because each fragment alone is ambiguous with the sibling `HIVEMIND_SKIP_PR_WATCH` presence rule in the same file; (c) the off-path marker obligation as the agent states it (`touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state), so the agent and the canonical file cannot drift apart on the one write the off path still performs; (d) the POINTER, in POINTER shape per rule 3 of the pin-authoring contract — the agent names the canonical `decision-autonomy.md` (Post-Merge Decision Report Trigger) section and says it is not restated here, so severing the agent site from the canonical rule is loud while honest rewording of the surrounding scan prose is not. The section name is pinned only inside that full pointer sentence, never bare, because the bare parenthetical recurs elsewhere in the same file. A fifth agent entry pins the Skills-list annotation that marks `hivemind:decision-report` OPT-IN, OFF BY DEFAULT and invoked only when the key is set, so the skill roster cannot present the report as unconditional. The repo-guidance consumer `CLAUDE.md` is pinned at the key's operator-config home, asserting that the opt-in framing survives there for contributors configuring the key. What this fixture honestly guarantees is exactly that these word sequences exist in these three named files; it claims nothing about meaning and nothing about runtime behavior. Residuals, stated plainly: (1) a presence pin proves the off-path rules are still WRITTEN DOWN, never that the overlord actually skips the GitHub call, renders nothing, or touches the marker at runtime — the ordering clause in particular is text, not an executed sequence; (2) a meaning-preserving rewrite-around is invisible — surrounding prose that re-enables the report by default, or that re-introduces a PR-state read on the off path, while leaving the pinned bytes intact stays green; (3) the pin sees only the three files it names, so a fourth surface describing the key differently is outside its reach; (4) a NEW consumer — another agent, skill, or workflow that invokes the decision report and never carries the gate at all — is an ABSENCE defect that no fixture in this directory can detect per rule 7 of the pin-authoring contract; catching it needs a structural check in `tools/policy_check.sh`, not a presence pin.", + "source": { + "file": "plugin/governance/decision-autonomy.md", + "pattern": "When `HIVEMIND_ENABLE_DECISION_REPORT` is unset or empty — the DEFAULT — the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind" + }, + "consumers": [ + { + "file": "plugin/governance/decision-autonomy.md", + "pattern": "When the key is UNSET or EMPTY, the overlord derives the same awaiting-report set from the same predicate and, for each run in it, `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state" + }, + { + "file": "plugin/agents/overlord.md", + "pattern": "The standing operator preference `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored)" + }, + { + "file": "plugin/agents/overlord.md", + "pattern": "is checked by PRESENCE with a simple non-empty test, and that check happens BEFORE any `gh pr view` and before ANY GitHub call whatsoever" + }, + { + "file": "plugin/agents/overlord.md", + "pattern": "for each awaiting-report run it `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state" + }, + { + "file": "plugin/agents/overlord.md", + "pattern": "The canonical policy is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger) and is not restated here." + }, + { + "file": "plugin/agents/overlord.md", + "pattern": "OPT-IN, OFF BY DEFAULT: invoked only when `HIVEMIND_ENABLE_DECISION_REPORT` is set" + }, + { + "file": "CLAUDE.md", + "pattern": "The post-merge decision report is opt-in via `HIVEMIND_ENABLE_DECISION_REPORT`" + } + ] +} From debb35b36dc13e2e30d47464cd4d22befc412f1f Mon Sep 17 00:00:00 2001 From: Bren Pike Date: Wed, 23 Sep 2026 08:30:49 -0600 Subject: [PATCH 3/5] chore(release): bump version to 4.0.0 --- CHANGELOG.md | 13 +++++++++++++ plugin/.claude-plugin/plugin.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1951fdca..53a18edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +## [4.0.0] - 2026-09-23 + +### Added + +- ADR-0030 (`docs/adr/0030-config-gated-post-merge-decision-report.md`): records the decision to config-gate the post-merge decision report. +- `tests/policy/safety-decision-report-toggle.json`: policy fixture pinning the opt-in gate and its default-off state. + +### Changed + +- **BREAKING:** The post-merge decision report is now opt-in and OFF by default. This gates the overlord's Resume-On-Start deferred-report scan and its `hivemind:decision-report` invocation behind a new standing key, `HIVEMIND_ENABLE_DECISION_REPORT`. To restore the report, set `HIVEMIND_ENABLE_DECISION_REPORT` (any non-empty value, checked by presence) in the `env` block of `.claude/settings.json` or `.claude/settings.local.json`. +- While the key is off, the scan makes no GitHub call and instead touches the zero-byte `.decision-report-done` marker for each awaiting run, so the first session after upgrading marks all existing awaiting runs done. Enabling the key afterward only reports runs that finish after enabling — a run whose PR is still open while the key is off will not report, even after the key is later turned on. +- The decision journal (`event.outputs.decisions[]`) is still written regardless of this key; Tier-B autonomy is unchanged. + ## [3.1.0] - 2026-09-23 ### Changed diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 8fd993d4..a89c1f9d 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "hivemind", - "version": "3.1.0", + "version": "4.0.0", "description": "Claude Code plugin providing a structured multi-agent framework with overlord, cerebrate, drone, changeling, local-reviewer, and github-reviewer agents plus workflow skills for git branching, commits, PRs, and code review remediation.", "author": { "name": "brenpike" From caa0a813bc131e82adbf5039fa216887f0456375 Mon Sep 17 00:00:00 2001 From: Bren Pike Date: Wed, 23 Sep 2026 08:56:24 -0600 Subject: [PATCH 4/5] fix(tests): pin consumers by pointer, claims by canonical file Per tests/policy/README.md rule 3, safety-decision-report-toggle.json pinned copied implementation prose at its consumers, so truthful rewording of the overlord's environment, ordering, or marker wording failed --strict while the canonical contract stayed intact. Move every behavioral claim onto plugin/governance/decision-autonomy.md (key identity + channel + presence test; unconditional off-path marker touch) and reduce plugin/agents/overlord.md to its pointer-only assertion. Drop the CLAUDE.md pin, which carries no canonical pointer and could only be copied claim prose. Rewrite the description to state the new division of labor and its residuals. Bite-proven both directions per rule 4: mutating each pinned canonical clause and the pointer sentence turns SAFETY red; reverting is green; rewording the now-unpinned agent prose stays green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018P3FsvsiiYroMetYGtMDQs --- .../policy/safety-decision-report-toggle.json | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/tests/policy/safety-decision-report-toggle.json b/tests/policy/safety-decision-report-toggle.json index 7512c750..75485cd8 100644 --- a/tests/policy/safety-decision-report-toggle.json +++ b/tests/policy/safety-decision-report-toggle.json @@ -1,6 +1,6 @@ { "rule": "Post-merge decision report is opt-in and OFF by default", - "description": "P3 coupling. The post-merge decision report is gated on the operator key `HIVEMIND_ENABLE_DECISION_REPORT`, checked by PRESENCE, and the DEFAULT (unset or empty) is OFF. The source pin sits on ONE span in `plugin/governance/decision-autonomy.md` carrying BOTH halves of that conditional: the discriminating condition (the key is unset or empty — the DEFAULT) and the whole obligation it triggers (the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind). Either half alone is rewritable while the other stays green — deleting the `unset or empty — the DEFAULT` keying flips the gate to opt-out, and deleting the no-GitHub-call clause re-admits a PR-state lookup on the off path — so both live inside the pinned span. The off path's second obligation, the one that keeps the feature idempotent while disabled, is pinned as a separate entry in the same canonical file: the overlord derives the same awaiting-report set from the same predicate and `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state. That entry is separate because the unconditional-marker rule and the renders-nothing rule fail independently: the scan could keep rendering nothing while silently reading PR state to decide whether to mark, which would re-introduce exactly the GitHub call the gate exists to remove. Four claim-carrying spans in the agent consumer `plugin/agents/overlord.md` are pinned as separate entries, because the default rests on each: (a) the key's NAME and its operator-config CHANNEL (the `env` block of `.claude/settings.json` committed or `.claude/settings.local.json` gitignored), so renaming the key or relocating its home is loud; (b) the PRESENCE test together with its ORDERING obligation (the check happens BEFORE any `gh pr view` and before ANY GitHub call whatsoever) in one span, because a presence test that runs AFTER the lookup satisfies the letter of the gate while performing the call it forbids, and because each fragment alone is ambiguous with the sibling `HIVEMIND_SKIP_PR_WATCH` presence rule in the same file; (c) the off-path marker obligation as the agent states it (`touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state), so the agent and the canonical file cannot drift apart on the one write the off path still performs; (d) the POINTER, in POINTER shape per rule 3 of the pin-authoring contract — the agent names the canonical `decision-autonomy.md` (Post-Merge Decision Report Trigger) section and says it is not restated here, so severing the agent site from the canonical rule is loud while honest rewording of the surrounding scan prose is not. The section name is pinned only inside that full pointer sentence, never bare, because the bare parenthetical recurs elsewhere in the same file. A fifth agent entry pins the Skills-list annotation that marks `hivemind:decision-report` OPT-IN, OFF BY DEFAULT and invoked only when the key is set, so the skill roster cannot present the report as unconditional. The repo-guidance consumer `CLAUDE.md` is pinned at the key's operator-config home, asserting that the opt-in framing survives there for contributors configuring the key. What this fixture honestly guarantees is exactly that these word sequences exist in these three named files; it claims nothing about meaning and nothing about runtime behavior. Residuals, stated plainly: (1) a presence pin proves the off-path rules are still WRITTEN DOWN, never that the overlord actually skips the GitHub call, renders nothing, or touches the marker at runtime — the ordering clause in particular is text, not an executed sequence; (2) a meaning-preserving rewrite-around is invisible — surrounding prose that re-enables the report by default, or that re-introduces a PR-state read on the off path, while leaving the pinned bytes intact stays green; (3) the pin sees only the three files it names, so a fourth surface describing the key differently is outside its reach; (4) a NEW consumer — another agent, skill, or workflow that invokes the decision report and never carries the gate at all — is an ABSENCE defect that no fixture in this directory can detect per rule 7 of the pin-authoring contract; catching it needs a structural check in `tools/policy_check.sh`, not a presence pin.", + "description": "P3 coupling. The post-merge decision report is gated on the operator key `HIVEMIND_ENABLE_DECISION_REPORT`, checked by PRESENCE, and the DEFAULT (unset or empty) is OFF. Pins are divided per rule 3 of the pin-authoring contract: the CANONICAL file `plugin/governance/decision-autonomy.md` carries every behavioral claim, and the agent CONSUMER carries a POINTER-only assertion. The source pin sits on ONE span carrying BOTH halves of the gate's conditional: the discriminating condition (the key is unset or empty — the DEFAULT) and the whole obligation it triggers (the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind). Either half alone is rewritable while the other stays green — deleting the `unset or empty — the DEFAULT` keying flips the gate to opt-out, and deleting the no-GitHub-call clause re-admits a PR-state lookup on the off path — so both live inside the pinned span. Two further canonical claims are pinned as separate entries against the same canonical file, because each fails independently of the source span. The first is the key's identity: its NAME, its operator-config CHANNEL (the `env` block of `.claude/settings.json` committed or `.claude/settings.local.json` gitignored), and the PRESENCE test itself, pinned as one span — renaming the key, relocating its home, or swapping the presence test for a value comparison each changes what the gate IS, and the presence fragment alone is ambiguous with the sibling `HIVEMIND_SKIP_PR_WATCH` presence rule in this repo. The second is the off path's idempotency obligation: the overlord derives the same awaiting-report set from the same predicate and `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state. That entry is separate because the unconditional-marker rule and the renders-nothing rule fail independently — the scan could keep rendering nothing while silently reading PR state to decide whether to mark, which would re-introduce exactly the GitHub call the gate exists to remove. The agent consumer `plugin/agents/overlord.md` is pinned in POINTER shape ONLY: the agent names the canonical `decision-autonomy.md` (Post-Merge Decision Report Trigger) section and says it is not restated there, so severing the agent site from the canonical rule is loud while honest rewording of the agent's surrounding scan prose — its environment, ordering, and marker phrasing — is NOT punished. The section name is pinned only inside that full pointer sentence, never bare, because the bare parenthetical recurs elsewhere in the same file. What this fixture honestly guarantees is exactly that these word sequences exist in these two named files; it claims nothing about meaning and nothing about runtime behavior. Residuals, stated plainly: (1) a presence pin proves the off-path rules are still WRITTEN DOWN, never that the overlord actually skips the GitHub call, renders nothing, or touches the marker at runtime; (2) a meaning-preserving rewrite-around is invisible — surrounding prose that re-enables the report by default, or that re-introduces a PR-state read on the off path, while leaving the pinned bytes intact stays green; (3) the agent consumer is pinned only at its pointer, so the agent could restate the gate incorrectly beside an intact pointer and stay green — that is the deliberate price of rule 3, which trades byte-level consumer coverage for a suite that does not break on truthful rewording; (4) the contributor-facing `CLAUDE.md` paragraph describing the key is deliberately UNPINNED, because that file carries no pointer to the canonical section and a pin there could only be copied claim prose, which rule 3 forbids; (5) the pin sees only the two files it names, so a third surface describing the key differently is outside its reach; (6) a NEW consumer — another agent, skill, or workflow that invokes the decision report and never carries the gate at all — is an ABSENCE defect that no fixture in this directory can detect per rule 7 of the pin-authoring contract; catching it needs a structural check in `tools/policy_check.sh`, not a presence pin.", "source": { "file": "plugin/governance/decision-autonomy.md", "pattern": "When `HIVEMIND_ENABLE_DECISION_REPORT` is unset or empty — the DEFAULT — the deferred-report scan renders no report, NEVER invokes `hivemind:decision-report`, and makes NO GitHub call of any kind" @@ -8,31 +8,15 @@ "consumers": [ { "file": "plugin/governance/decision-autonomy.md", - "pattern": "When the key is UNSET or EMPTY, the overlord derives the same awaiting-report set from the same predicate and, for each run in it, `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state" - }, - { - "file": "plugin/agents/overlord.md", - "pattern": "The standing operator preference `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored)" + "pattern": "The report is CONFIG-GATED on `HIVEMIND_ENABLE_DECISION_REPORT`, set in the `env` block of the project's `.claude/settings.json` (committed) or `.claude/settings.local.json` (gitignored) — the same operator-override channel as `HIVEMIND_SKIP_PR_WATCH` and `HIVEMIND_LOCAL_REVIEW_MODEL`. Check it by PRESENCE with a simple non-empty test" }, { - "file": "plugin/agents/overlord.md", - "pattern": "is checked by PRESENCE with a simple non-empty test, and that check happens BEFORE any `gh pr view` and before ANY GitHub call whatsoever" - }, - { - "file": "plugin/agents/overlord.md", - "pattern": "for each awaiting-report run it `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state" + "file": "plugin/governance/decision-autonomy.md", + "pattern": "When the key is UNSET or EMPTY, the overlord derives the same awaiting-report set from the same predicate and, for each run in it, `touch`es the zero-byte `.decision-report-done` marker WITHOUT checking PR state" }, { "file": "plugin/agents/overlord.md", "pattern": "The canonical policy is `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger) and is not restated here." - }, - { - "file": "plugin/agents/overlord.md", - "pattern": "OPT-IN, OFF BY DEFAULT: invoked only when `HIVEMIND_ENABLE_DECISION_REPORT` is set" - }, - { - "file": "CLAUDE.md", - "pattern": "The post-merge decision report is opt-in via `HIVEMIND_ENABLE_DECISION_REPORT`" } ] } From 316daa1c14c69a11e1590e0955c5be1a2bfa8b2c Mon Sep 17 00:00:00 2001 From: Bren Pike Date: Wed, 23 Sep 2026 09:11:59 -0600 Subject: [PATCH 5/5] fix(docs): scope the decision-report key to automatic invocation Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018P3FsvsiiYroMetYGtMDQs --- README.md | 2 +- plugin/agents/overlord.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b9d11b0..6cbc3040 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ All skills are invoked using the namespaced form: | `hivemind:record-state-result` | Record the outcome of the current workflow state into the run ledger and advance `state.current` to the legal next state, validated against the workflow definition | | `hivemind:next-wave` | Read-only engine for the overlord's intra-run parallel implement loop — reads the run ledger and computes the ready WAVE (the maximal, file-disjoint set of dependency-satisfied plan steps) to dispatch as concurrent agent delegations this iteration | | `hivemind:mark-intent-fallback` | Sanctioned engine write-path for the version-skew intent-fallback resume door and start-fresh stale-run closeout — sets `run.mode:intent_fallback`, appends a fallback event, and optionally closes a stale run via `run.status` | -| `hivemind:decision-report` | Opt-in, off by default — enable it by setting `HIVEMIND_ENABLE_DECISION_REPORT` in the `env` block of `.claude/settings.json` or `.claude/settings.local.json`. When enabled, after a run's PR merges or closes, render a chronological narrative of the auto-decisions the run made on the user's behalf — in the consumer project's domain language — and return it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and touches a zero-byte done-marker) | +| `hivemind:decision-report` | The AUTOMATIC post-merge invocation is opt-in and off by default — enable it by setting `HIVEMIND_ENABLE_DECISION_REPORT` in the `env` block of `.claude/settings.json` or `.claude/settings.local.json`; the key gates only that automatic path, and invoking the skill explicitly still works regardless of the key. When enabled, after a run's PR merges or closes, render a chronological narrative of the auto-decisions the run made on the user's behalf — in the consumer project's domain language — and return it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and touches a zero-byte done-marker) | | `hivemind:zoom-out` | Zoom out for broader context — maps relevant modules and callers using the project's domain glossary vocabulary | | `hivemind:improving-architecture` | Read-only architecture analysis — emits a ranked refactoring blueprint of deepening opportunities (shallow → deep modules) for testability and AI-navigability | | `hivemind:refactor-to-depth` | Execution skill that performs a behavior-preserving deepening refactor via a refactor-under-green loop — the in-implementation counterpart to the read-only `improving-architecture` blueprint | diff --git a/plugin/agents/overlord.md b/plugin/agents/overlord.md index f18513dd..ab8b987c 100644 --- a/plugin/agents/overlord.md +++ b/plugin/agents/overlord.md @@ -144,7 +144,7 @@ RUN-OWNERSHIP-01: a run ledger is owned and mutated only by the overlord instanc - `hivemind:create-working-branch` — create/confirm compliant working branch - `hivemind:molt` — commit completed phases, milestones, version bumps, review fixes - `hivemind:open-plan-pr` — open PR after validation and versioning gates pass -- `hivemind:decision-report` — renders the post-merge decision report in the consumer project's ubiquitous language and RETURNS it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and `touch`es a zero-byte done-marker). OPT-IN, OFF BY DEFAULT: invoked only when `HIVEMIND_ENABLE_DECISION_REPORT` is set, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger) +- `hivemind:decision-report` — renders the post-merge decision report in the consumer project's ubiquitous language and RETURNS it as chat text (render-to-chat; writes nothing to disk — the overlord surfaces the narrative and `touch`es a zero-byte done-marker). The AUTOMATIC deferred post-merge invocation is OPT-IN and OFF BY DEFAULT: the Resume-On-Start scan invokes this skill only when `HIVEMIND_ENABLE_DECISION_REPORT` is set, per `${CLAUDE_PLUGIN_ROOT}/governance/decision-autonomy.md` (Post-Merge Decision Report Trigger). The key gates ONLY that automatic path — EXPLICIT/manual invocation of the skill stays available regardless of the key - `hivemind:github-review-loop` — main-session watch loop and the mandatory-by-default post-PR path; polls a PR for review activity and dispatches fix-mode remediation per actionable event; overlord-executed (hosts Monitor). `max_remediation_cycles` is a FLOOR declared and enforced by `${CLAUDE_PLUGIN_ROOT}/skills/github-review-loop/scripts/loop-state.sh`, which REJECTS any `max_remediation_cycles` below it: no caller may invoke this loop with a lower value. - `hivemind:adaptation-cycle` — invoked by local-reviewer internally, not by overlord - `hivemind:tdd` — invoked by coder internally when TDD is requested