Skip to content

Commit b76de25

Browse files
repo-ops: review-reply-postcondition, an opt-in PostToolUse hook (#84)
* repo-ops: review-reply-postcondition, an opt-in PostToolUse hook A review reply that claims a fix must name a commit reachable from HEAD (or its upstream). Gated on review.replyNamesCommit (default false). Adds lib/gh-exec-words.sh beside the existing scanner without touching it. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition — runtime bodies, long bodies, subdir config Review round 1 on #84: - a body whose SHA is a shell expansion ($VAR, ${VAR}, $(cmd), backtick) is no longer judged, matching the documented limit (address-review posts "Fixed in $NEW_HEAD") - gh_exec_words no longer truncates the segment at 2048 chars, so a SHA late in a long body is still seen - the opt-in key is read from the checkout's top level when the cwd is a subdirectory - SHA candidates are split on non-alphanumerics, so adjacent tokens are each checked - the diff context bases on the fork point from origin/HEAD, upstream, or origin/main|master, and notes when it falls back to HEAD~1 Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition — runtime check honors shell quoting Review round 2 on #84: '$NEW_HEAD' in single quotes, an escaped \$, a quoted- delimiter heredoc, or a --body-file holding "$VAR" all post literal text, so they are judged rather than skipped. word_is_runtime walks the raw word with single/double-quote and backslash state, and treats a $(cat <<TAG …) heredoc as runtime only when TAG is unquoted and its body expands. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition — heredoc bodies walked, not grepped Review round 3 on #84: in an unquoted-delimiter heredoc a backslash escapes only the next character, so `\\$X` still expands; and `$@`/`$*`/`$#` expand too. heredoc_expands walks the body with that rule instead of a regex. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition — name merge-guard in the hook-tests CI step after rebase Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition reads only the reply's own body, in the checkout it posts from - --body-file - reads the segment's own heredoc or here-string, not the whole command, so a later git show <sha> no longer satisfies the check; stdin from a pipe is runtime. - gh api -F/--field body=@file reads the file, as gh does; @- reads stdin; an unreadable file is not judged. - cd/pushd earlier in the command moves the directory whose opt-in and commits are checked; a runtime cd target makes later replies unjudged. - The feedback says a reply that never posted (short-circuited &&) needs no correction. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition tracks pushd/popd and whole-line heredoc terminators - The opted-out fast path no longer skips a command that only uses pushd/popd. - pushd/popd keep a stack, so popd returns to the directory the reply is really posted from. - A stdin heredoc ends at a line that is exactly the tag (leading tabs stripped for <<-); a body line that merely starts with the tag stays body; no terminator means not judged. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE * repo-ops: review-reply-postcondition — pushd by verb, unterminated heredocs judged - The directory stack pushes only when the resolved verb is pushd, not when the segment merely contains the substring (cd work/pushd). - An unterminated stdin heredoc still sends its lines, so its body is judged, not skipped. Claude-Session: https://claude.ai/code/session_01T7j4GUt15DJp7G9UK4tMNE
1 parent 31beb28 commit b76de25

16 files changed

Lines changed: 942 additions & 10 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
- name: The PR-template helper checks headings and resolves prTemplateSource correctly
9595
run: ./scripts/test-pr-template-check.sh
9696

97-
- name: The repo-ops pr-template-guard and skip-label-race-guard hooks behave, and fail closed
97+
- name: The repo-ops pr-template-guard, skip-label-race-guard, merge-guard and review-reply-postcondition hooks behave, and fail closed
9898
run: ./scripts/test-repo-ops-hooks.sh
9999

100100
- name: The repo-ops wait-for-review and wait-for-checks tools report each outcome, and never act

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ skill generates that contract for any repo.
1515
| Plugin | Skills | Install when |
1616
| --- | --- | --- |
1717
| **[maintainerd-core](plugins/core/README.md)** | `bootstrap`, `doctor`, `new-repo` | Always — `bootstrap` generates the config every other plugin needs; `doctor` validates it; `new-repo` brings a repo to a fleet's standard. |
18-
| **[repo-ops](plugins/repo-ops/README.md)** | `create-pr`, `address-review`, `release`, `daily-changelog`, `daily-update` (plus three `PreToolUse` Bash hooks: `pr-template-guard`, `skip-label-race-guard`, `merge-guard`) | You want the baseline PR + changelog dev flow. |
18+
| **[repo-ops](plugins/repo-ops/README.md)** | `create-pr`, `address-review`, `release`, `daily-changelog`, `daily-update` (plus three `PreToolUse` Bash hooks, `pr-template-guard`, `skip-label-race-guard` and `merge-guard`, and an opt-in `PostToolUse` hook, `review-reply-postcondition`) | You want the baseline PR + changelog dev flow. |
1919
| **[audits](plugins/audits/README.md)** | `audit-architecture`, `audit-tests`, `audit-security`, `audit-deps`, `audit-design-docs`, `audit-product-docs` | You want scheduled tech-debt / test / security / dependency / doc sweeps. |
2020
| **[research](plugins/research/README.md)** | `research-radar` | You want proactive research surfaced — a periodic arXiv scan for papers relevant to this repo. |
2121
| **[journal](plugins/journal/README.md)** | `worklog` | You want a day's shipped work captured into your Obsidian vault (user-scoped — spans all your repos). |
@@ -142,7 +142,7 @@ maintainerd/
142142
scripts/test-renumber-migration.sh
143143
plugins/
144144
core/ .claude-plugin/plugin.json plugin.json skills/{bootstrap,doctor,new-repo}/ references/{config-schema,model-tiers,profile-schema,gh-rest-fallbacks}.md scripts/{coverage-adapt,coverage-check,profile-resolve,settings-diff}.sh
145-
repo-ops/ .claude-plugin/plugin.json plugin.json skills/{create-pr,address-review,release,daily-changelog,daily-update}/ hooks/{hooks.json,scripts/{pr-template-guard,skip-label-race-guard,merge-guard}.sh} scripts/{wait-for-review,wait-for-checks,renumber-migration}.sh
145+
repo-ops/ .claude-plugin/plugin.json plugin.json skills/{create-pr,address-review,release,daily-changelog,daily-update}/ hooks/{hooks.json,scripts/{pr-template-guard,skip-label-race-guard,merge-guard,review-reply-postcondition}.sh} scripts/{wait-for-review,wait-for-checks,renumber-migration}.sh
146146
audits/ .claude-plugin/plugin.json plugin.json skills/{audit-architecture,audit-tests,audit-security,audit-deps,audit-design-docs,audit-product-docs}/ references/pattern-promotion.md
147147
research/ .claude-plugin/plugin.json plugin.json skills/{research-radar}/
148148
journal/ .claude-plugin/plugin.json plugin.json skills/{worklog}/

‎plugins/audits/references/config-schema.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ Read with whatever is convenient — the `Read` tool, or `jq` for a single value
234234
// hook, which warns when `gh pr create` carries this label
235235
// without `--draft` — the bot schedules its review on the open
236236
// event, before a label applied in the same command lands.
237+
"replyNamesCommit": false // opt-in. true = the repo-ops `review-reply-postcondition` hook
238+
// requires a review reply that claims a fix to name a commit
239+
// reachable from HEAD. false/absent = the hook says nothing.
237240
},
238241

239242
// ── release (the `release` skill) — null/omitted = repo ships continuously, no versioned release ──
@@ -458,6 +461,16 @@ Pointers to the markdown rule files. See [Guidelines files](#guidelines-files).
458461
for on a `gh pr create` — many review bots schedule their run on the `opened` webhook, before a
459462
label applied in the same command lands, so the label alone doesn't reliably suppress the review.
460463
`null`/absent = this repo has no such label; the hook says nothing.
464+
- `review.replyNamesCommit` *(optional; default `false`)* — opt in to the `repo-ops`
465+
`review-reply-postcondition` hook (see that plugin's README). When `true`, a `gh pr comment` or
466+
inline-thread reply (`gh api …/pulls/N/comments/ID/replies`, or the GraphQL
467+
`addPullRequestReviewThreadReply`) whose body claims a fix — "fixed", "addressed", "resolved",
468+
"done in", … — must name a 7–40 hex-digit commit SHA that exists in the local checkout and is
469+
reachable from `HEAD` or its upstream; otherwise the hook hands the agent a correction (with the
470+
diff of any repo file the reply names) to rewrite the reply from the diff. A claim an agent makes
471+
about its own change is a postcondition, not a sentence. Default `false` because the hook can
472+
only judge from a git checkout and from reply text it can read; a repo opts in once its reviewers
473+
and agents post replies from the PR's own worktree.
461474

462475
Either breaker takes `null` to disable **that** breaker; the other keeps working. Neither takes
463476
`0` — a cap of zero would halt before the first round, which is not a policy anyone wants and is

‎plugins/auto-dev/references/config-schema.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ Read with whatever is convenient — the `Read` tool, or `jq` for a single value
234234
// hook, which warns when `gh pr create` carries this label
235235
// without `--draft` — the bot schedules its review on the open
236236
// event, before a label applied in the same command lands.
237+
"replyNamesCommit": false // opt-in. true = the repo-ops `review-reply-postcondition` hook
238+
// requires a review reply that claims a fix to name a commit
239+
// reachable from HEAD. false/absent = the hook says nothing.
237240
},
238241

239242
// ── release (the `release` skill) — null/omitted = repo ships continuously, no versioned release ──
@@ -458,6 +461,16 @@ Pointers to the markdown rule files. See [Guidelines files](#guidelines-files).
458461
for on a `gh pr create` — many review bots schedule their run on the `opened` webhook, before a
459462
label applied in the same command lands, so the label alone doesn't reliably suppress the review.
460463
`null`/absent = this repo has no such label; the hook says nothing.
464+
- `review.replyNamesCommit` *(optional; default `false`)* — opt in to the `repo-ops`
465+
`review-reply-postcondition` hook (see that plugin's README). When `true`, a `gh pr comment` or
466+
inline-thread reply (`gh api …/pulls/N/comments/ID/replies`, or the GraphQL
467+
`addPullRequestReviewThreadReply`) whose body claims a fix — "fixed", "addressed", "resolved",
468+
"done in", … — must name a 7–40 hex-digit commit SHA that exists in the local checkout and is
469+
reachable from `HEAD` or its upstream; otherwise the hook hands the agent a correction (with the
470+
diff of any repo file the reply names) to rewrite the reply from the diff. A claim an agent makes
471+
about its own change is a postcondition, not a sentence. Default `false` because the hook can
472+
only judge from a git checkout and from reply text it can read; a repo opts in once its reviewers
473+
and agents post replies from the PR's own worktree.
461474

462475
Either breaker takes `null` to disable **that** breaker; the other keeps working. Neither takes
463476
`0` — a cap of zero would halt before the first round, which is not a policy anyone wants and is

‎plugins/core/references/config-schema.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ Read with whatever is convenient — the `Read` tool, or `jq` for a single value
229229
// hook, which warns when `gh pr create` carries this label
230230
// without `--draft` — the bot schedules its review on the open
231231
// event, before a label applied in the same command lands.
232+
"replyNamesCommit": false // opt-in. true = the repo-ops `review-reply-postcondition` hook
233+
// requires a review reply that claims a fix to name a commit
234+
// reachable from HEAD. false/absent = the hook says nothing.
232235
},
233236

234237
// ── release (the `release` skill) — null/omitted = repo ships continuously, no versioned release ──
@@ -453,6 +456,16 @@ Pointers to the markdown rule files. See [Guidelines files](#guidelines-files).
453456
for on a `gh pr create` — many review bots schedule their run on the `opened` webhook, before a
454457
label applied in the same command lands, so the label alone doesn't reliably suppress the review.
455458
`null`/absent = this repo has no such label; the hook says nothing.
459+
- `review.replyNamesCommit` *(optional; default `false`)* — opt in to the `repo-ops`
460+
`review-reply-postcondition` hook (see that plugin's README). When `true`, a `gh pr comment` or
461+
inline-thread reply (`gh api …/pulls/N/comments/ID/replies`, or the GraphQL
462+
`addPullRequestReviewThreadReply`) whose body claims a fix — "fixed", "addressed", "resolved",
463+
"done in", … — must name a 7–40 hex-digit commit SHA that exists in the local checkout and is
464+
reachable from `HEAD` or its upstream; otherwise the hook hands the agent a correction (with the
465+
diff of any repo file the reply names) to rewrite the reply from the diff. A claim an agent makes
466+
about its own change is a postcondition, not a sentence. Default `false` because the hook can
467+
only judge from a git checkout and from reply text it can read; a repo opts in once its reviewers
468+
and agents post replies from the PR's own worktree.
456469

457470
Either breaker takes `null` to disable **that** breaker; the other keeps working. Neither takes
458471
`0` — a cap of zero would halt before the first round, which is not a policy anyone wants and is

‎plugins/deps-flow/references/config-schema.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ Read with whatever is convenient — the `Read` tool, or `jq` for a single value
234234
// hook, which warns when `gh pr create` carries this label
235235
// without `--draft` — the bot schedules its review on the open
236236
// event, before a label applied in the same command lands.
237+
"replyNamesCommit": false // opt-in. true = the repo-ops `review-reply-postcondition` hook
238+
// requires a review reply that claims a fix to name a commit
239+
// reachable from HEAD. false/absent = the hook says nothing.
237240
},
238241

239242
// ── release (the `release` skill) — null/omitted = repo ships continuously, no versioned release ──
@@ -458,6 +461,16 @@ Pointers to the markdown rule files. See [Guidelines files](#guidelines-files).
458461
for on a `gh pr create` — many review bots schedule their run on the `opened` webhook, before a
459462
label applied in the same command lands, so the label alone doesn't reliably suppress the review.
460463
`null`/absent = this repo has no such label; the hook says nothing.
464+
- `review.replyNamesCommit` *(optional; default `false`)* — opt in to the `repo-ops`
465+
`review-reply-postcondition` hook (see that plugin's README). When `true`, a `gh pr comment` or
466+
inline-thread reply (`gh api …/pulls/N/comments/ID/replies`, or the GraphQL
467+
`addPullRequestReviewThreadReply`) whose body claims a fix — "fixed", "addressed", "resolved",
468+
"done in", … — must name a 7–40 hex-digit commit SHA that exists in the local checkout and is
469+
reachable from `HEAD` or its upstream; otherwise the hook hands the agent a correction (with the
470+
diff of any repo file the reply names) to rewrite the reply from the diff. A claim an agent makes
471+
about its own change is a postcondition, not a sentence. Default `false` because the hook can
472+
only judge from a git checkout and from reply text it can read; a repo opts in once its reviewers
473+
and agents post replies from the PR's own worktree.
461474

462475
Either breaker takes `null` to disable **that** breaker; the other keeps working. Neither takes
463476
`0` — a cap of zero would halt before the first round, which is not a policy anyone wants and is

‎plugins/journal/references/config-schema.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ Read with whatever is convenient — the `Read` tool, or `jq` for a single value
234234
// hook, which warns when `gh pr create` carries this label
235235
// without `--draft` — the bot schedules its review on the open
236236
// event, before a label applied in the same command lands.
237+
"replyNamesCommit": false // opt-in. true = the repo-ops `review-reply-postcondition` hook
238+
// requires a review reply that claims a fix to name a commit
239+
// reachable from HEAD. false/absent = the hook says nothing.
237240
},
238241

239242
// ── release (the `release` skill) — null/omitted = repo ships continuously, no versioned release ──
@@ -458,6 +461,16 @@ Pointers to the markdown rule files. See [Guidelines files](#guidelines-files).
458461
for on a `gh pr create` — many review bots schedule their run on the `opened` webhook, before a
459462
label applied in the same command lands, so the label alone doesn't reliably suppress the review.
460463
`null`/absent = this repo has no such label; the hook says nothing.
464+
- `review.replyNamesCommit` *(optional; default `false`)* — opt in to the `repo-ops`
465+
`review-reply-postcondition` hook (see that plugin's README). When `true`, a `gh pr comment` or
466+
inline-thread reply (`gh api …/pulls/N/comments/ID/replies`, or the GraphQL
467+
`addPullRequestReviewThreadReply`) whose body claims a fix — "fixed", "addressed", "resolved",
468+
"done in", … — must name a 7–40 hex-digit commit SHA that exists in the local checkout and is
469+
reachable from `HEAD` or its upstream; otherwise the hook hands the agent a correction (with the
470+
diff of any repo file the reply names) to rewrite the reply from the diff. A claim an agent makes
471+
about its own change is a postcondition, not a sentence. Default `false` because the hook can
472+
only judge from a git checkout and from reply text it can read; a repo opts in once its reviewers
473+
and agents post replies from the PR's own worktree.
461474

462475
Either breaker takes `null` to disable **that** breaker; the other keeps working. Neither takes
463476
`0` — a cap of zero would halt before the first round, which is not a policy anyone wants and is

‎plugins/repo-ops/.claude-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "repo-ops",
33
"version": "0.6.12",
4-
"description": "Baseline maintainer dev flow as config-driven skills: create-pr (opens PRs only after running the repo's format/lint/build/test pre-flight), address-review (drive the iterative response loop on bot + human PR feedback to approval — fix, push, reply to every inline thread and a PR-level summary), release (cut a versioned release: gather changes since the last tag, update notes, run the gate, bump, tag, publish), daily-changelog (one markdown file per day from merged PRs), and daily-update (runs the repo's per-day housekeeping skills and bundles their output into one PR). Also ships two PreToolUse Bash hooks: pr-template-guard (denies a gh pr create/edit whose body is missing a heading the repo's own PR template requires) and skip-label-race-guard (warns when gh pr create applies the repo's review-skip label without --draft). Reads .claude/maintainerd.json.",
4+
"description": "Baseline maintainer dev flow as config-driven skills: create-pr (opens PRs only after running the repo's format/lint/build/test pre-flight), address-review (drive the iterative response loop on bot + human PR feedback to approval — fix, push, reply to every inline thread and a PR-level summary), release (cut a versioned release: gather changes since the last tag, update notes, run the gate, bump, tag, publish), daily-changelog (one markdown file per day from merged PRs), and daily-update (runs the repo's per-day housekeeping skills and bundles their output into one PR). Also ships three PreToolUse Bash hooks: pr-template-guard (denies a gh pr create/edit whose body is missing a heading the repo's own PR template requires), skip-label-race-guard (warns when gh pr create applies the repo's review-skip label without --draft), merge-guard (warns, never denies, when a command merges a pull request), plus an opt-in PostToolUse hook, review-reply-postcondition (a review reply that claims a fix must name a commit reachable from HEAD). Reads .claude/maintainerd.json.",
55
"author": {
66
"name": "Allen Hutchison",
77
"url": "https://github.com/allenhutchison"

0 commit comments

Comments
 (0)