Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .claude/rules/pr-verdict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# PR verdict protocol

**Applies to every repo, every project, every surface.** The maintainer reviewing a PR is not necessarily an engineer, and a diff plus a wall of review-bot output does not answer the only question that matters: is this safe to merge? This protocol makes a PR answerable in under 30 seconds.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Use Assistants for AI actors.

Replace generic assistant, bot, and review-bot terms with Assistant terminology. Keep product names such as CodeRabbit unchanged.

  • .claude/rules/pr-verdict.md#L3-L3: replace review-bot output.
  • .claude/rules/pr-verdict.md#L72-L91: rename the section and generic AI references.
  • .github/workflows/pr-verdict.yml#L8-L9: replace assistant.
  • .github/workflows/pr-verdict.yml#L169-L175: replace generic review-bot terms.

As per coding guidelines, β€œUse Agents for human support staff and Assistants for AI actors; never conflate the terms in schema, code, documentation, or prose.”

πŸ“ Affects 2 files
  • .claude/rules/pr-verdict.md#L3-L3 (this comment)
  • .claude/rules/pr-verdict.md#L72-L91
  • .github/workflows/pr-verdict.yml#L8-L9
  • .github/workflows/pr-verdict.yml#L169-L175
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/rules/pr-verdict.md at line 3, Replace generic AI actor terminology
with β€œAssistant” consistently: in .claude/rules/pr-verdict.md lines 3 and 72-91,
rename the relevant section and references; in .github/workflows/pr-verdict.yml
lines 8-9 and 169-175, update the generic assistant/review-bot terms. Preserve
product names such as CodeRabbit, and use β€œAgents” only for human support staff
across these sites.

Source: Coding guidelines


Earned 2026-07-20, after an audit found **nine normative decisions** merged across PRs #87–#100 of the Helpthread repo that the maintainer never made β€” including two amendments to the project's constitution β€” each authored, self-reviewed, and merged with zero human review comments, several later cited back as "per the maintainer's decision."

## The one rule that matters most

**Never attribute a decision to the maintainer without quoting them.**

Not "per your decision," not "as you chose earlier," not "the maintainer decided," not "(maintainer, 2026-07-19)" β€” unless you can paste their actual words. If you cannot quote it, it is **yours**, and it must be labelled `INFERRED` wherever it is written: PR body, spec text, commit message, ticket, charter amendment.

This single rule would have prevented every finding in that audit.

## The banned move

Writing "N decisions shape this" β€” or "the key question is X" β€” and then answering it yourself without a user turn in between.

If you pose a decision, **stop and ask**. Do not declare it "locked," "recorded," or "decided" until the maintainer has answered in their own words. A tool result is not an answer. Silence is not consent.

## Required PR body structure

Every PR body opens with this block, before anything else:

```markdown
## 🟒 SAFE TO MERGE
Gates green. No new decisions. CodeRabbit: 3 findings, 0 real.
```

Three verdicts, nothing else:

| Verdict | Means |
|---|---|
| 🟒 **SAFE TO MERGE** | No new decisions. Gates green. Review findings adjudicated and resolved. |
| 🟑 **NEEDS YOUR DECISION** | Encodes N decisions the maintainer has not made. Listed below. Do not merge until answered. |
| πŸ”΄ **DO NOT MERGE** | Unresolved defect, failing gate, or a one-way door not yet accepted. |

When the verdict is 🟑 or πŸ”΄, a **Decision provenance** table follows immediately:

```markdown
## Decision provenance

| Decision β€” in plain words | Source |
|---|---|
| Knowledge base becomes a paid module | You, 2026-07-19 16:46: "i want the KB to be a module" |
| Resonant IQ runs module code on its own servers | ⚠️ INFERRED β€” no authorization found |
```

**Plain words, not jargon.** "Resonant IQ runs module code on its own servers" β€” not "managed hosting becomes the mainline install path." If the maintainer would need to ask what a row means, the row is written wrong. The noun that would make them say "wait, what?" must appear in the row.

## One-way doors get their own line

Anything expensive or impossible to reverse β€” a licensing term, a public promise, a published API, a schema migration, deleting data, anything in a constitution or `legal/` β€” gets flagged explicitly:

```markdown
**One-way door:** narrows the own-your-data promise in CHARTER Β§2.
```

Two-way doors need no flag. Match the noise to the cost of undo.

## Risk tiering β€” so this stays usable

| PR touches | Treatment |
|---|---|
| A charter, `README`, `legal/`, `LICENSE`, licensing, pricing, or any public promise | Full block + provenance table + the maintainer reads the changed text itself |
| Specs, ADRs, architecture docs | Full block + provenance table |
| Code, tests, config | Verdict line only |
| Typos, formatting, dependency bumps | Verdict line only |

Most PRs are one line. Reserve the ceremony for what can hurt.

## Review bots are yours, never the maintainer's

The maintainer never reads CodeRabbit, Codex, or any bot output raw. You adjudicate and report one line:

> CodeRabbit: 7 findings β€” 5 real and fixed, 2 wrong (it misread the token scope).

If a finding is real, fix it or explain why not. "The bot said something" is not a report; a verdict on each finding is.

**A bot's silence is not approval.** Check that a review actually ran on the current head β€” a passing check can mean skipped, rate-limited, or reviewing a commit you have since replaced.

**Bots review after a PR opens, so nothing is green at open time.** The sequence is fixed:

1. Open the PR at 🟑 or πŸ”΄. Never 🟒 β€” no bot has run yet.
2. Wait for gates and bots. Verify a review actually landed on the current head.
3. Adjudicate every finding: real or not, fixed or why not.
4. If you pushed fixes, **request re-review explicitly** β€” incremental review is off in these repos, so a fix-up push is otherwise never looked at.
5. Only then update the verdict to 🟒 and add the one-line bot summary.
6. Tell the maintainer it is ready. **A PR they have not been told about is not ready**, whatever its checks say.

The gate enforces step 5 mechanically: a 🟒 verdict with no review-bot adjudication line fails.

## Before you write the verdict

1. Diff the PR against the base. List every change that adds or alters a **rule, default, invariant, commitment, licensing term, price, or public promise**.
2. For each, find the maintainer's authorizing words in the conversation. Quote them with a timestamp.
3. Anything without a quote is `INFERRED`. Say so plainly. Do not soften it.
4. If there is at least one INFERRED item, the verdict is 🟑. Never 🟒.

## What "approval" is and is not

- "merge it," "looks good," "go ahead," "please merge everything else" β†’ **approval to merge**, not evidence they decided the contents.
- "I agree with everything above" β†’ covers **the message it replies to**, nothing else. If that message was about refunds and the PR also encodes a hosting model, the hosting model is INFERRED.
- Approving one PR never authorizes the next.

Blanket agreement is the most common way an inferred decision acquires a false pedigree. Treat it as narrowly as it was given.

## Mechanical gate

Instructions drift β€” that is exactly how the audit's findings happened. So the highest-risk tier gets a CI check, not a promise: any PR touching the constitution, `README`, `legal/`, `LICENSE`, or anything under `specs/modules/` fails unless its body carries a verdict marker and a provenance section. See `.github/workflows/pr-verdict.yml` in repos where it is installed.
222 changes: 222 additions & 0 deletions .github/workflows/pr-verdict.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
name: PR verdict

# High-blast-radius changes β€” the constitution, legal text, licensing and pricing β€”
# must carry a verdict and a decision-provenance table in the PR body.
#
# Why this is a CI check and not a documented convention: on 2026-07-20 an audit of
# PRs #87-#100 found nine normative decisions merged that the maintainer never made,
# including two amendments to CHARTER.md. Every one of them was made by an assistant
# that had the convention available and did not follow it. Instructions drift; a
# required check does not.
#
# The rule this enforces lives at .claude/rules/pr-verdict.md.

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read
pull-requests: read

jobs:
verdict:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Does this PR touch the high-risk tier?
id: tier
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# via env, not inline β€” refs may contain $, backtick, or ;
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
git fetch --no-tags --depth=1 origin "$BASE_SHA" 2>/dev/null || true

# THREE-dot: what this PR changed, not what main changed since it forked.
# Two-dot compares tips, so a branch merely behind main reports every file
# main touched β€” measured at 74 files against this PR's actual 3. That fires
# the gate on PRs that touched nothing protected, which trains contributors to
# paste ceremonial provenance tables: the exact habit this gate exists to break.
#
# --no-renames: with rename detection on, `git mv CHARTER.md docs/old.md`
# reports ONLY the new path, so moving a protected file out of its directory
# silences the gate. Deleting the constitution by relocating it is precisely
# the change this must catch.
CHANGED=$(git diff --no-renames --name-only "$BASE_SHA...$HEAD_SHA" \
|| git diff --no-renames --name-only "origin/$BASE_REF...HEAD")
echo "Changed files:"; echo "$CHANGED"

# The tier: constitution, legal text, and the specs that carry licensing,
# pricing, and the free/paid line.
# Files that carry a promise, a licensing boundary, or money.
HIGH_RISK=$(echo "$CHANGED" | grep -E \
-e '^CHARTER\.md$' \
-e '^README\.md$' \
-e '^legal/' \
-e '^LICENSE' \
-e '^specs/modules/' \
-e '^\.github/workflows/pr-verdict\.yml$' \
-e '^\.claude/rules/pr-verdict\.md$' \
|| true)
Comment on lines +58 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Protect the documented catalog and marketplace paths.

The PR’s declared high-risk set names root-level catalog.md and marketplace-v1.md, but this only matches specs/modules/.... Changes to the declared files receive high_risk=false and bypass the gate.

Proposed fix
-            -e '^specs/modules/(catalog|marketplace-v1)\.md$' \
+            -e '^catalog\.md$' \
+            -e '^marketplace-v1\.md$' \
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
HIGH_RISK=$(echo "$CHANGED" | grep -E \
-e '^CHARTER\.md$' \
-e '^legal/' \
-e '^LICENSE' \
-e '^specs/modules/(catalog|marketplace-v1)\.md$' \
|| true)
HIGH_RISK=$(echo "$CHANGED" | grep -E \
-e '^CHARTER\.md$' \
-e '^legal/' \
-e '^LICENSE' \
-e '^catalog\.md$' \
-e '^marketplace-v1\.md$' \
|| true)
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-verdict.yml around lines 43 - 48, Update the HIGH_RISK
path matching in the workflow to include the declared root-level catalog.md and
marketplace-v1.md files in addition to the existing specs/modules paths.
Preserve the current matching for CHARTER.md, legal/, LICENSE, and the
documented module paths.


if [ -n "$HIGH_RISK" ]; then
echo "high_risk=true" >> "$GITHUB_OUTPUT"
{ echo 'files<<EOF'; echo "$HIGH_RISK"; echo 'EOF'; } >> "$GITHUB_OUTPUT"
else
echo "high_risk=false" >> "$GITHUB_OUTPUT"
fi

- name: Require a verdict and decision provenance
if: steps.tier.outputs.high_risk == 'true'
env:
BODY: ${{ github.event.pull_request.body }}
FILES: ${{ steps.tier.outputs.files }}
run: |
set -euo pipefail
# Pin the locale: several patterns below contain non-ASCII characters
# (the verdict emoji, curly quotes). Under a byte-oriented locale grep
# matches those byte-by-byte, so a bracket or case-fold over them
# silently stops working β€” the check would pass everything and look green.
export LC_ALL=C.UTF-8
fail() { echo "::error::$1"; FAILED=1; }
FAILED=0

echo "This PR touches the high-risk tier:"
echo "$FILES"
echo

# Normalize the body ONCE, before any check reads it, stripping the two
# things that look like content to a regex and like nothing to a human:
# - HTML comments: `<!-- 🟒 SAFE TO MERGE -->` as the first line satisfied
# the verdict and bot-line checks while rendering invisible. The same flaw
# also failed legitimate PRs opening with a template comment.
# - Fenced blocks: the protocol doc's own ```markdown example of a
# "## Decision provenance" table satisfied the provenance requirement, so
# quoting the rule counted as following it.
BODY=$(printf '%s\n' "$BODY" \
| perl -0777 -pe 's/<!--.*?-->//gs' \
| awk '/^[[:space:]]*(```|~~~)/ { f = !f; next } !f')

# 1. A verdict marker, one of exactly three, ALONE on the FIRST non-empty
# line. Matching anywhere would let a PR bury the verdict under arbitrary
# prose β€” the whole point is that it is the first thing read.
#
# The whole line must BE the verdict, not merely contain it: leading `#`s
# and the emoji are allowed (`## 🟒 SAFE TO MERGE`), but trailing words are
# not, so `Status: SAFE TO MERGE pending review` is rejected. `[^[:alnum:]]*`
# covers the emoji without this file having to carry a Unicode class.
#
# VERDICT is captured here and reused by check 5, so a body that merely
# *mentions* another verdict cannot change how it is validated.
FIRST_LINE=$(grep -m1 -vE '^[[:space:]]*$' <<<"$BODY" || true)
verdict_is() {
grep -qE "^#{0,6}[[:space:]]*[^[:alnum:]]*[[:space:]]*$1[[:space:]]*$" <<<"$FIRST_LINE"
}
VERDICT=""
if verdict_is 'SAFE TO MERGE'; then VERDICT=safe
elif verdict_is 'NEEDS YOUR DECISION'; then VERDICT=needs
elif verdict_is 'DO NOT MERGE'; then VERDICT=blocked
fi
if [ -z "$VERDICT" ]; then
fail "The PR body's first line must be exactly the verdict, on its own: 🟒 SAFE TO MERGE / 🟑 NEEDS YOUR DECISION / πŸ”΄ DO NOT MERGE. Found: ${FIRST_LINE:0:80}"
fi

# 2. A decision-provenance section.
# Anchored: "## Decision provenance deliberately omitted" is a heading that
# says the opposite of what it would otherwise satisfy.
if ! grep -qiE '^#+[[:space:]]*Decision provenance[[:space:]]*$' <<<"$BODY"; then
fail "No '## Decision provenance' section. Changes to the constitution, legal text, or licensing must list every decision and where it came from."
Comment on lines +130 to +134

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Require provenance immediately after a yellow or red verdict.

This check accepts ## Decision provenance anywhere in the PR body. A PR can place arbitrary decision text before the table and still pass, although the protocol requires the table to follow a 🟑 or πŸ”΄ verdict immediately.

After parsing the opening verdict, require Decision provenance to be the next section heading for 🟑 and πŸ”΄ verdicts.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-verdict.yml around lines 118 - 120, Update the PR-body
validation around the opening verdict and the existing Decision provenance check
so that, for 🟑 or πŸ”΄ verdicts, the next section heading must be Decision
provenance immediately after the verdict/table content. Reject bodies with
intervening headings or text, while preserving the current provenance
requirement and behavior for other verdicts.

fi

# 3. Every provenance row is either quoted to the maintainer or marked INFERRED.
# A row citing them without quote marks is the exact failure this gate exists for.
# sed form is BRE-portable (BSD and GNU) β€” `\+` is a GNU extension.
if grep -qiE '^#{1,}[[:space:]]*Decision provenance[[:space:]]*$' <<<"$BODY"; then
# Stop at the next heading β€” reading to end-of-body swallows any later
# table (a test-results table, for instance) and parses it as provenance rows.
# Terminate on an ATX heading (# Foo) OR a setext underline (--- / ===),
# which the earlier awk missed β€” a "Test results" setext heading let the
# table under it be parsed as provenance rows.
ROWS=$(awk '/^#+[[:space:]]*[Dd]ecision provenance[[:space:]]*$/{f=1;next} \
f&&/^#+[[:space:]]/{exit} \
f&&/^(-{3,}|={3,})[[:space:]]*$/{exit} f' <<<"$BODY" \
| grep -E '^\|' | grep -vE '^\|[[:space:]]*[-:[:space:]]+\|' | tail -n +2 || true)
if [ -z "$ROWS" ]; then
fail "The 'Decision provenance' section has no rows. If this PR encodes no decisions, say so in a row ('None β€” no new decisions')."
else
while IFS= read -r row; do
[ -z "$row" ] && continue
# Only an explicit INFERRED label, or an explicit no-decision row, excuses a
# missing quote. 'no authorization' alone does NOT β€” the protocol requires
# the INFERRED label itself so the disclosure is unmissable.
#
# An INFERRED row also forces the verdict to yellow. The protocol is
# explicit that one inferred item means 🟑 and never 🟒, and without this
# a PR could disclose an unauthorized decision and still call itself
# safe to merge β€” disclosure without the consequence.
if grep -qiE 'INFERRED' <<<"$row"; then
if [ "$VERDICT" = safe ]; then
fail "Verdict is SAFE TO MERGE but a provenance row is marked INFERRED. One inferred decision means the verdict is NEEDS YOUR DECISION: ${row}"
fi
continue
fi
if grep -qiE 'none β€”|no new decision' <<<"$row"; then continue; fi
# A sourced row must contain an actual quotation.
if ! grep -qE '["β€œβ€]' <<<"$row"; then
fail "Provenance row has no quote and is not marked INFERRED: ${row}"
fi
done <<<"$ROWS"
fi
fi

# 4. Attribution without a quote, anywhere in the body.
# "your decision" is deliberately NOT in this list β€” it matches the
# "NEEDS YOUR DECISION" verdict marker itself.
#
# Backticked code spans are stripped before matching: a doc that
# *discusses* these patterns (`maintainer decision` as a literal example,
# in a checklist or an adjudication table) is not attributing anything.
# Without this, any text explaining the rule trips the rule.
#
# The patterns are role-based, never a person's name: this file is public,
# and the maintainer's name does not belong in it.
BODY_NO_CODE=$(sed 's/`[^`]*`//g' <<<"$BODY")
while IFS= read -r line; do
grep -qE '["β€œβ€]' <<<"$line" && continue
grep -qiE 'INFERRED' <<<"$line" && continue
fail "Attributes a decision to the maintainer without quoting them: ${line}"
done < <(grep -iE "(maintainer('|’)s (decision|call|choice)|per the maintainer('|’)s|maintainer decided|per your decision|you decided|you chose|as you decided|decided by the maintainer|CONFIRMED by the maintainer|SIGNED OFF by the maintainer|\(maintainer,? [0-9]{4}-|maintainer decision, [0-9]{4}-)" <<<"$BODY_NO_CODE" || true)

# 5. A green verdict must show the review bots were adjudicated.
# Bots review AFTER a PR opens, so nothing can honestly be green at open
# time. This blocks the failure mode where a PR is marked green before
# CodeRabbit has run, or against a commit that has since been replaced.
#
# Keyed to the VERDICT parsed in check 1, never to the phrase appearing
# anywhere: a 🟑 body that merely names the three verdicts, or quotes this
# protocol, was otherwise forced to carry an adjudication line it had no
# business carrying yet. Read against BODY_NO_CODE for the same reason
# check 4 does β€” a backticked example is documentation, not an adjudication.
#
# Matched by SHAPE β€” "<reviewer>: <n> findings ..." β€” rather than against a
# list of vendors. The protocol allows a substitute reviewer when the usual
# one is unavailable, so hardcoding names would fail an honest substitution;
# it also keeps third-party product names out of a public repo.
if [ "$VERDICT" = safe ]; then
if ! grep -qiE ':[^:]*[^[:alnum:]]*\b([0-9]+|no|none|zero)[[:space:]]+findings?\b' <<<"$BODY_NO_CODE"; then
fail "Verdict is SAFE TO MERGE but no review adjudication line is present. Add one line naming the reviewer and giving a COUNT β€” e.g. '<reviewer>: 7 findings, 5 real and fixed, 2 wrong (misread the token scope)' or '<reviewer>: no findings'. The bare word 'findings' does not adjudicate anything. If no review has run on the current head yet, the verdict cannot be green."
fi
fi

if [ "$FAILED" = "1" ]; then
echo
echo "See .claude/rules/pr-verdict.md for the protocol."
exit 1
fi
echo "Verdict block present and every decision is sourced or marked INFERRED."
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Helpthread is open-source support infrastructure (AGPL-3.0 core; a Resonant IQ, Inc. product). **CHARTER.md is the constitution** β€” read it before substantive work.

## PR verdict protocol

Every PR opens with a verdict the maintainer can act on in under 30 seconds, and **no decision is ever attributed to the maintainer without a direct quote**. Mirrored from the canonical rule so it loads in every session that reads this repo. Enforced mechanically for the constitution, `legal/`, and licensing specs by `.github/workflows/pr-verdict.yml`.

@.claude/rules/pr-verdict.md

## References & provenance

- Helpthread is an independent implementation. No code copied or derived from copyleft-licensed projects, ever.
Expand Down
Loading