Skip to content

Commit 928a1f7

Browse files
committed
docs(agent): PR body comment-only frustration chain + Layer 0 rules/hooks
1 parent 0b95fc8 commit 928a1f7

9 files changed

Lines changed: 511 additions & 94 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PR body frustration chain — comment-only doctrine (2026-08-01)
2+
3+
> **Status:** active Layer 0 prohibition on Cursor agents
4+
> **Open PR:** [#319](https://github.com/diazMelgarejo/Perpetua-Tools/pull/319) (`cursor/guard-audit-hardening-f559`)
5+
> **Canonical ledger:** `orama-system/bin/orama-system/references/pr-body-anti-clobber-incident-ledger.md`
6+
7+
## User intent (spirit of the plan)
8+
9+
Stop agents from **automatically rewriting PR summaries**. Progress belongs in **comments**,
10+
not in the description field. The description is a historical record; agents kept treating
11+
turn-end "update the PR" as permission to replace the whole body with the latest paragraph.
12+
13+
If a human **explicitly** authorizes a body edit, append-only rules still apply — never
14+
delta-only clobber.
15+
16+
## Frustration chain (this wave)
17+
18+
| Step | What happened | Why it hurt |
19+
| ---- | ------------- | ----------- |
20+
| 1 | PT #314 merged guard manifest; CodeRabbit nitpicks left open | Drift between "merged" and "done" |
21+
| 2 | PR #315 falsely claimed to supersede #314 | Would have removed tests — duplicate PR trap |
22+
| 3 | Blind orama→PT `sync-attribution-guard-scripts.sh` risked clobbering PT hardening | Anti-clobber divergence guard born |
23+
| 4 | PT #319 opened as **one PR** for guard wave | Correct consolidation |
24+
| 5 | Agent `ManagePullRequest update_pr` with **delta-only** body on #319 | Erased original Summary + template sections |
25+
| 6 | User asked if PR bodies were clobbered across 3 repos | PT #319 yes; orama #255 / AC #26 no |
26+
| 7 | Integrative restore + Layer 7 hooks (append-only) | Still not enough — agents bypass by habit |
27+
| 8 | **Layer 0** — comment only, hooks block **all** body writes | Top prohibition; override → Layers 1–6 |
28+
29+
## Layer 0 — what agents do now
30+
31+
| Do | Never (automatic) |
32+
| -- | ----------------- |
33+
| `ManagePullRequest post_comment` | `update_pr` with `body=` |
34+
| `gh pr comment` | `gh pr edit`, `append-pr-body.sh` |
35+
36+
## Human override
37+
38+
```bash
39+
export CURSOR_PR_BODY_HUMAN_OVERRIDE_ACK=1
40+
```
41+
42+
Then: READ → BACKUP → MERGE → WRITE (`append-pr-body.sh` or `gh pr edit --body-file`).
43+
44+
## Enforcement stack
45+
46+
1. `.cursor/rules/pr-body-comment-only.mdc` (alwaysApply, top)
47+
2. `.cursor/rules/append-only-pr-body.mdc` (override only)
48+
3. Cursor hooks: `beforeSubmitPrompt`, `preToolUse`, `beforeMCPExecution`, `beforeShellExecution`
49+
4. `scripts/cursor/hooks/pr-body-guard-core.py` (single decision core)
50+
5. Hookify: `.claude/hookify.pr-body-comment-only.local.md`
51+
6. `remind-pr-body-append-only.sh` at push time
52+
7. CI: `verify-pr-body-not-clobbered.sh`
53+
54+
## Related memory on this branch
55+
56+
- `GUARD_SYNC_EPIC_SAGA_COMPLETION_2026-08-01.md`
57+
- `GUARD_SYNC_DIVERGENCE_GUARD_2026-08-01.md`
58+
- `PR_BODY_ANTI_CLOBBER_ENFORCEMENT_PLAN.md`
59+
60+
## Merge order (unchanged)
61+
62+
PT #319 → orama #255 → AlphaClaw #26
Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,32 @@
11
---
2-
description: Append-only PR body updates — never clobber existing PR descriptions (READ → backup → merge → write)
2+
description: Append-only PR body updates when human override authorizes a body edit (Layers 1–6)
33
alwaysApply: true
44
---
55

6-
# Append-only PR body workflow
6+
# Append-only PR body workflow (Layers 1–6 — after human override only)
77

8-
`ManagePullRequest update_pr`, `gh pr edit`, and GitHub's API **replace the entire PR body** when `body` is set. Passing only the latest follow-up paragraph deletes the original Summary and any CodeRabbit release notes.
8+
**Layer 0 applies first:** `.cursor/rules/pr-body-comment-only.mdc` — Cursor agents
9+
**comment only** by default. This file governs body writes **only** when the human
10+
set `CURSOR_PR_BODY_HUMAN_OVERRIDE_ACK=1` in the current session.
911

10-
**NEVER** use `ManagePullRequest update_pr` with delta-only `body=`. Use `append-pr-body.sh` instead.
12+
`ManagePullRequest update_pr`, `gh pr edit`, and GitHub's API **replace the entire PR body**
13+
when `body` is set. Passing only the latest follow-up paragraph deletes the original Summary.
1114

1215
**Canonical skill:** `bin/orama-system/skills/cursor-pr-body/SKILL.md`
1316
**Incident ledger:** `bin/orama-system/references/pr-body-anti-clobber-incident-ledger.md`
14-
**Canonical script:** `scripts/cursor/append-pr-body.sh`
15-
**Curriculum:** `bin/orama-system/cidf/references/integrative-editing-examples.md` §1
17+
**Canonical script:** `scripts/cursor/append-pr-body.sh`
1618

17-
## Non-negotiable workflow
19+
## Non-negotiable workflow (human-authorized body edit only)
1820

1921
| Step | Action |
20-
|------|--------|
22+
| ---- | ------ |
2123
| 1. **READ** | `gh pr view <N> --repo <owner/repo> --json body --jq .body` |
2224
| 2. **BACKUP** | Save to `.git/pr-body-backups/<repo-slug>-pr<N>-<UTC-timestamp>.md` |
23-
| 3. **MERGE** | Keep original `## Summary` and scope at top; append `## Follow-up: …` blocks chronologically; preserve CodeRabbit auto-generated sections and Cursor metadata below unchanged |
24-
| 4. **WRITE** | Full merged body only — never delta-only |
25+
| 3. **MERGE** | Keep original `## Summary`; append `## Follow-up:` chronologically |
26+
| 4. **WRITE** | `append-pr-body.sh` or `gh pr edit --body-file` with **full** merged body |
2527

26-
## Preferred write path
28+
## Forbidden (even with human override)
2729

28-
```bash
29-
bash scripts/cursor/append-pr-body.sh <owner/repo> <pr-number> \
30-
--title "Follow-up: <short title>" \
31-
--file follow-up.md
32-
```
33-
34-
Or: edit the backup copy integratively, then `gh pr edit <N> --repo <owner/repo> --body-file merged-body.md`.
35-
36-
## Tool-specific constraints
37-
38-
| Tool | Rule |
39-
|------|------|
40-
| `append-pr-body.sh` | Inserts before `<!-- CURSOR_AGENT_PR_BODY_END -->` or CodeRabbit marker; aborts if body changed since read |
41-
| `ManagePullRequest update_pr` | **Avoid.** Agent-managed PRs only; if unavoidable, pass **full** integrative merged body — never delta-only. Prefer `append-pr-body.sh` |
42-
| `gh pr edit` | Requires token with `updatePullRequest` scope; same full-body rule applies |
43-
44-
## Forbidden
45-
46-
- `update_pr` / `gh pr edit` with `body=` containing only the latest paragraph or CI delta
47-
- Rewriting or deleting the original Summary to match a side quest (aguara, harmonization, etc.)
30+
- Delta-only `body=` containing only the latest paragraph or CI delta
4831
- Skipping backup because "it's a small follow-up"
49-
- Hand-editing CodeRabbit `<!-- release notes by coderabbit.ai -->` blocks unless explicitly fixing a factual error
50-
51-
## When to append
52-
53-
- Stack harmonization / rebase notes after resolving conflicts
54-
- CodeRabbit or human review fixes applied in a follow-up commit
55-
- CI failure root cause + verification command output
56-
- Post-merge integration status across a PR chain (#244 → #245 → …)
57-
58-
## Insertion order
59-
60-
New `## Follow-up:` blocks go **below** the original agent summary and **above** CodeRabbit auto-generated release notes (when present). Chronological order — never reorder prior follow-ups.
32+
- Reordering or deleting prior follow-ups
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
description: >-
3+
Layer 0 — Cursor agents never auto-change PR descriptions. Comment only
4+
(post_comment / gh pr comment). Body writes require explicit human override.
5+
alwaysApply: true
6+
---
7+
8+
# Layer 0 — PR body comment-only (Cursor agents)
9+
10+
**This rule is the top prohibition.** Hooks, hookify, skills, and CI layers below
11+
cannot be bypassed by habit, turn-end checklists, or "small follow-up" reasoning.
12+
13+
## Positive instructions (what to do)
14+
15+
| Situation | Required action |
16+
| --------- | ---------------- |
17+
| Report progress on an open PR | `ManagePullRequest post_comment` or `gh pr comment` |
18+
| CI failed / fix landed | Post a **comment** with job URL, root cause, verification command |
19+
| Stack harmonization / rebase | Post a **comment** with base SHA and commits preserved |
20+
| User asks "update the PR" | **Comment only** unless they explicitly authorize a body edit |
21+
| End of turn with code pushed | Push commits; **comment** on PR if status changed — do **not** edit description |
22+
23+
## Negative instructions (never do automatically)
24+
25+
- `ManagePullRequest update_pr` with `body=` on an **existing** PR
26+
- `gh pr edit` with `--body` or `--body-file`
27+
- `bash scripts/cursor/append-pr-body.sh` …
28+
- `gh api` PATCH/PUT that changes pull request `body` or description
29+
- Replacing, shortening, or "refreshing" the Summary to match the latest side quest
30+
- Turn-end "update PR before summary" that mutates the description field
31+
32+
## Human override (rare)
33+
34+
Only when the **human explicitly** authorizes a body edit in the current turn:
35+
36+
```bash
37+
export CURSOR_PR_BODY_HUMAN_OVERRIDE_ACK=1
38+
```
39+
40+
Then **Layers 1–6 still apply** (append-only, READ→BACKUP→MERGE→WRITE). Delta-only
41+
writes remain forbidden. See `.cursor/rules/append-only-pr-body.mdc`.
42+
43+
## Enforcement
44+
45+
- Cursor hooks: `preToolUse` (ManagePullRequest), `beforeMCPExecution`, `beforeShellExecution`, `beforeSubmitPrompt`
46+
- Skill: `bin/orama-system/skills/cursor-pr-body/SKILL.md`
47+
- Incident ledger: `bin/orama-system/references/pr-body-anti-clobber-incident-ledger.md`
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# preToolUse / beforeMCPExecution — Layer 0: never auto-mutate PR bodies (comment only).
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
# shellcheck source=pr-body-backup-lib.sh
7+
source "$SCRIPT_DIR/pr-body-backup-lib.sh"
8+
9+
input="$(cat)"
10+
decision="ALLOW"
11+
deny_msg=""
12+
13+
while IFS= read -r line; do
14+
case "$line" in
15+
BACKUP\|*)
16+
repo="${line#BACKUP|}"
17+
pr="${repo##*|}"
18+
repo="${repo%|*}"
19+
pr_body_backup_if_needed "$repo" "$pr" "mcp-preflight"
20+
;;
21+
DENY\|*)
22+
decision="DENY"
23+
deny_msg="${line#DENY|}"
24+
;;
25+
DENY)
26+
decision="DENY"
27+
;;
28+
esac
29+
done < <(python3 "$SCRIPT_DIR/pr-body-guard-core.py" manage_pr <<<"$input")
30+
31+
if [[ "$decision" == "DENY" ]]; then
32+
deny_msg="${deny_msg:-Layer 0: comment only — never auto-change PR body.}"
33+
python3 - "$deny_msg" <<'PY'
34+
import json, sys
35+
msg = sys.argv[1]
36+
print(json.dumps({
37+
"permission": "deny",
38+
"agent_message": msg,
39+
"user_message": "Cursor agent blocked from changing PR description. Use post_comment only.",
40+
}))
41+
PY
42+
exit 0
43+
fi
44+
45+
printf '%s\n' '{"permission":"allow"}'
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# beforeShellExecution — Layer 0: block gh pr edit / append-pr-body; allow gh pr comment.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
# shellcheck source=pr-body-backup-lib.sh
7+
source "$SCRIPT_DIR/pr-body-backup-lib.sh"
8+
9+
input="$(cat)"
10+
decision="ALLOW"
11+
deny_msg=""
12+
13+
while IFS= read -r line; do
14+
case "$line" in
15+
BACKUP\|*)
16+
repo="${line#BACKUP|}"
17+
pr="${repo##*|}"
18+
repo="${repo%|*}"
19+
pr_body_backup_if_needed "$repo" "$pr" "shell-preflight"
20+
;;
21+
DENY\|*)
22+
decision="DENY"
23+
deny_msg="${line#DENY|}"
24+
;;
25+
DENY)
26+
decision="DENY"
27+
;;
28+
esac
29+
done < <(python3 "$SCRIPT_DIR/pr-body-guard-core.py" shell <<<"$input")
30+
31+
if [[ "$decision" == "DENY" ]]; then
32+
deny_msg="${deny_msg:-Layer 0: comment only — never auto-change PR body.}"
33+
python3 - "$deny_msg" <<'PY'
34+
import json, sys
35+
msg = sys.argv[1]
36+
print(json.dumps({
37+
"permission": "deny",
38+
"agent_message": msg,
39+
"user_message": "Cursor agent blocked from changing PR description. Use gh pr comment.",
40+
}))
41+
PY
42+
exit 0
43+
fi
44+
45+
printf '%s\n' '{"permission":"allow"}'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
# beforeSubmitPrompt — inject Layer 0 reminder so agents cannot "forget" between turns.
3+
set -euo pipefail
4+
5+
input="$(cat)"
6+
python3 - "$input" <<'PY'
7+
import json, sys
8+
9+
raw = sys.argv[1]
10+
try:
11+
data = json.loads(raw) if raw.strip() else {}
12+
except json.JSONDecodeError:
13+
data = {}
14+
15+
prompt = str(data.get("prompt") or data.get("user_message") or "")
16+
lower = prompt.lower()
17+
triggers = (
18+
"update_pr", "pr body", "pull request", "managepullrequest",
19+
"append-pr-body", "gh pr edit", "pr description", "pr summary",
20+
)
21+
if not any(t in lower for t in triggers):
22+
print(json.dumps({"continue": True}))
23+
sys.exit(0)
24+
25+
print(json.dumps({
26+
"continue": True,
27+
"additional_context": (
28+
"LAYER-0 PR BODY RULE (Cursor agents): NEVER automatically change an existing "
29+
"PR description. Use post_comment / gh pr comment ONLY. "
30+
"ManagePullRequest update_pr with body=, gh pr edit, and append-pr-body.sh are "
31+
"BLOCKED by hooks unless the human set CURSOR_PR_BODY_HUMAN_OVERRIDE_ACK=1 — "
32+
"then append-only rules still apply (no delta-only writes)."
33+
),
34+
}))
35+
PY
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# pr-body-backup-lib.sh — shared READ→BACKUP helpers for Cursor PR-body hooks.
3+
# Source from hook scripts; do not execute directly.
4+
set -euo pipefail
5+
6+
pr_body_backup_dir() {
7+
local git_common_dir
8+
if ! git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null)"; then
9+
printf '%s\n' "${TMPDIR:-/tmp}"
10+
return 0
11+
fi
12+
if [[ "$git_common_dir" != /* ]]; then
13+
git_common_dir="$(git rev-parse --show-toplevel 2>/dev/null)/$git_common_dir"
14+
fi
15+
printf '%s/pr-body-backups\n' "$(cd "$git_common_dir" && pwd)"
16+
}
17+
18+
pr_body_backup_if_needed() {
19+
local repo_slug="$1"
20+
local pr_number="$2"
21+
local reason="${3:-preflight-read}"
22+
23+
if ! command -v gh >/dev/null 2>&1; then
24+
return 0
25+
fi
26+
if [[ -z "$repo_slug" || -z "$pr_number" ]]; then
27+
return 0
28+
fi
29+
30+
local backup_dir body ts safe_slug path
31+
backup_dir="$(pr_body_backup_dir)"
32+
mkdir -p "$backup_dir"
33+
body="$(gh pr view "$pr_number" --repo "$repo_slug" --json body --jq .body 2>/dev/null || true)"
34+
[[ -n "$body" ]] || return 0
35+
36+
ts="$(date -u +%Y%m%dT%H%M%SZ)"
37+
safe_slug="${repo_slug//\//-}"
38+
path="${backup_dir}/${safe_slug}-pr${pr_number}-${ts}-${reason}.md"
39+
printf '%s\n' "$body" >"$path"
40+
printf 'PR-BODY-BACKUP: %s (%s)\n' "$path" "$reason" >&2
41+
}

0 commit comments

Comments
 (0)