Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
95dea20
start: replace paws-off claim phrasing and add bot disclosure (closes…
d-morrison Aug 24, 2026
c3bc43a
feat(claim): professional claim wording; every agent comment disclose…
d-morrison Aug 24, 2026
c41f0d3
fix(claim,disclosure): address adversarial review findings
d-morrison Aug 24, 2026
cd3b626
fix(disclosure): address second-round adversarial review findings
d-morrison Aug 24, 2026
d8216c3
Merge origin/main into claude/paws-off-phrasing-6c5f20
d-morrison Aug 24, 2026
fffc9aa
fix(disclosure): address third-round adversarial review findings
d-morrison Aug 24, 2026
a9ff133
fix(disclosure): address fourth-round adversarial review findings
d-morrison Aug 24, 2026
de50078
fix(disclosure): address fifth-round adversarial review findings
d-morrison Aug 24, 2026
781d286
fix(disclosure): address sixth-round adversarial review findings
d-morrison Aug 24, 2026
0fff5eb
fix(disclosure): address pre-push review; restore a clause deleted as…
d-morrison Aug 24, 2026
28b2a44
fix(claim): add the third retired wording, `back off`, to every matcher
d-morrison Aug 24, 2026
9565d9f
fix(prose): satisfy the new-line-breaks CI check, which I had verifie…
d-morrison Aug 24, 2026
5aa36bb
fix(claim-pr): strike `iterate` from the emitter list; widen the deri…
d-morrison Aug 24, 2026
594fdce
fix(claim-pr): key the derivation on the wordings, not the --body flag
d-morrison Aug 24, 2026
b744d6a
Merge origin/main: take its reflow of the three files, reapply this b…
d-morrison Aug 24, 2026
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
42 changes: 33 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,35 @@ See [`shared/workflow/check-before-pushing.md`](shared/workflow/check-before-pus
When printing a status recap or summary, include a timestamp in the user's local time zone (Pacific Time, `America/Los_Angeles` --- get it from `TZ=America/Los_Angeles date "+%Y-%m-%d %H:%M %Z"`).
Each reading expires immediately: run the command fresh for every recap rather than extrapolating elapsed time from a prior reading.

## Every comment you post to a forge says an agent posted it

See [`disclose-agent-authorship`](shared/workflow/disclose-agent-authorship.md).

An agent driving `gh`/`glab` under the account holder's credentials posts as **that person**: their login, their avatar, a `MEMBER` association, and `type: User`.
Nothing in the API distinguishes such a comment from one they typed, so a reader deciding how much weight to give a claim, a status note, or a review has no way to tell which they are reading.
The forge cannot say it; the body must.

End every comment an agent posts with this line, on its own, after a blank line:

```
_Posted by Claude Code (AI agent) --- not written by a human._
```

Substitute your own agent's name where you are not Claude Code, and keep the rest of the line verbatim so one query finds every disclosed comment.
Check the substituted **name** against `REVIEW_BODY_MARKERS` as well as a replacement marker: `code review` is one of its entries, so an agent named for code review would reintroduce through its own name the false-clean the emoji ban exists to prevent.

The marker deliberately contains **no robot emoji**: [`scripts/check-pr-fully-clean.py`](scripts/check-pr-fully-clean.py) matches that emoji as a review-body marker, so a disclosed claim comment would be admitted into the fully-clean verdict scan as a finding-free review.
Check any replacement marker against that script's `REVIEW_BODY_MARKERS` and `REVIEW_AGENT_MARKERS` before adopting it.

Scope: comment bodies, on every surface --- claims, releases, status notes, review replies, self-reviews, issue comments filed on the user's behalf.
Not commit messages, not titles, not issue bodies, not PR bodies, each of which has its own attribution convention.
Two exemptions.
A comment another machine parses as a command (`@dependabot rebase`), where the test is the audience rather than the length.
And a comment posted under a genuine bot token, where the forge already reports `type: Bot` and the marker adds nothing.

- **Do:** append the marker to every agent-posted comment, including ones whose prose already identifies the session.
- **Don't:** use the robot emoji in the marker, and don't read "the account holder knows an agent is running" as making the disclosure unnecessary --- the reader is whoever finds the thread later.

## File formatting & links

- Use GitHub-style markdown for all responses and documentation.
Expand All @@ -179,15 +208,10 @@ Each reading expires immediately: run the command fresh for every recap rather t

## Deliver completed implementation work

When asked to implement, edit, or write up a change on a feature branch, do
not stop at an uncommitted worktree.
Complete the delivery cycle: create the applicable tracking issue when
issue-first workflow applies, commit the scoped changes, run local
adversarial self-review to a clean verdict, push the branch, open or update
its Pull Request, request AI review after the final push, and drive CI and
review findings to a clean result.
This does not grant merge authority; the strict merge policy below still
applies.
When asked to implement, edit, or write up a change on a feature branch, do not stop at an uncommitted worktree.
Complete the delivery cycle: create the applicable tracking issue when issue-first workflow applies, commit the scoped changes, run local adversarial self-review to a clean verdict, push the branch, open or update its Pull Request, request AI review after the final push, and drive CI and review findings to a clean result.
This does not grant merge authority.
The strict merge policy below still applies.

## Every self-review is an adversarial review by a separate subagent

Expand Down
28 changes: 22 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,10 +567,13 @@ A `CHANGES_REQUESTED` state is blocking regardless of whether an automated re-re
When the user gives feedback, corrections, or guidance in the CLI or chat while working a PR, paraphrase it and post it as a PR comment:

```
gh pr comment <N> --body "..."
gh pr comment <N> --body "<paraphrase>

_Posted by Claude Code (AI agent) --- not written by a human._"
```

One to three sentences is enough.
The trailing marker is required, per the section above: this comment paraphrases the user in the user's own voice under the user's own login, which is the shape most easily read as their own writing.
Don't quote verbatim — paraphrase so it reads naturally in the PR thread.
Skip trivial acknowledgments or conversational exchanges with nothing to act on.

Expand Down Expand Up @@ -607,15 +610,28 @@ Re-derive it from a live query rather than trusting the earlier verdict.

The `claim-pr` skill operationalizes this (the exact claim wording, when it applies, and the closing/unclaim comment).

## Every comment you post to a forge says an agent posted it

[shared/workflow/disclose-agent-authorship.md](shared/workflow/disclose-agent-authorship.md)

A comment posted through `gh`/`glab` under the account holder's credentials carries **their** login and reads as `type: User`, so nothing in the API distinguishes it from a comment they typed --- `memories/github.md` records auditors mistaking exactly that.
The forge cannot say it, so the body must: end every agent-posted comment with

```
_Posted by Claude Code (AI agent) --- not written by a human._
```

The marker deliberately avoids the robot emoji, which `scripts/check-pr-fully-clean.py` matches as a `REVIEW_BODY_MARKERS` entry --- a disclosed claim comment would otherwise scan as a finding-free **review**.
The fragment carries the rest: the two exemptions, the comment-bodies-only scope, and the queries that verify a marker or a bot identity.

- **Do:** append the marker to every claim, release, status, reply, and self-review comment, including ones whose prose already names the session.
- **Don't:** use the robot emoji in it, or put it in a commit message, a title, an issue body, or a PR body.

## Read a repo's canonical contributor doc before starting work, not just before pushing

[shared/workflow/read-canonical-doc-before-starting.md](shared/workflow/read-canonical-doc-before-starting.md)

When a short `CLAUDE.md` names a fuller document as the actual authority
--- `.github/copilot-instructions.md`, `CONTRIBUTING.md`, a linked style
guide --- read that document before the first edit, and front-load its
pre-PR requirements into the first commit rather than discovering them via
a red CI check.
When a short `CLAUDE.md` names a fuller document as the actual authority --- `.github/copilot-instructions.md`, `CONTRIBUTING.md`, a linked style guide --- read that document before the first edit, and front-load its pre-PR requirements into the first commit rather than discovering them via a red CI check.

## Open a PR immediately after claiming an issue

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ the rule is consulted when it is *read* and broken when a message is
| `warn-status-read-after-pipe.py` | `PreToolUse` (Bash) | warns, never blocks, when an expandable `$?` sits in the segment immediately after a pipeline carrying no `pipefail`, so the status read belongs to the trailing formatter rather than to the command. Excludes single-quoted spans, heredoc bodies and `#` comments so the corpus's own documentation of the bug cannot trip it, which also means a `bash -c '...'` one-liner gets no warning |
| `no-push-without-self-review.py` | `PreToolUse` (Bash) | blocks `git push` unless a separate `adversarial-reviewer` subagent returned a clean verdict as its own call result AND that report's `Reviewed-Commit:` fingerprint matches the commits the push would ship (refspec resolved), or the push itself is prefixed with `ALLOW_UNREVIEWED_PUSH=1`; a verdict quoted anywhere else --- in another file, or in this guard's own denial --- does not count |
| `flag-uncited-rebuttal.py` | `PreToolUse` (Bash) | warns, never blocks, when a PR/issue comment about to be posted disputes a finding whose most recently fetched citation named an external URL that no earlier `WebFetch`/`WebSearch` in the transcript touched -- ai-config#2070's wrong rebuttal, retracted two rounds later once the URL was finally fetched |
| `require-agent-disclosure.py` | `PreToolUse` (Bash, mcp__github__.*) | warns, never blocks, on a `gh`/`glab` command or MCP call that posts a forge comment without the agent-disclosure marker -- such a comment carries the account holder's own login and reads as `type: User`, indistinguishable from one they typed. Three verdicts, not one: the marker is missing, the body is somewhere the check cannot read (`--body-file`, `--editor`, `$BODY`) so it says so rather than accusing, or the body discloses with the robot emoji, which `check-pr-fully-clean.py` matches as a review-body marker |

For agent-independent monitoring across all projects and sessions, install the
user service after the hook files are installed:
Expand Down
28 changes: 23 additions & 5 deletions commands/release-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ allowed-tools:
- mcp__github__pull_request_read
---

Counterpart to `/claim-pr`. Post a single, recognisable "paws off released" comment so other agents and sessions know the PR is free for the next person.
Counterpart to `/claim-pr`.
Post a single, recognisable "claim released" comment so other agents and sessions know the PR is free for the next person.

## Arguments

Expand All @@ -31,23 +32,40 @@ If only one positional arg is given, treat it as `pr_number`.

Call `mcp__github__pull_request_read(method = "get_comments", owner = <owner>, repo = <repo>, pullNumber = <pr_number>)`. Walk the last ~10 comments and confirm:

- the most recent claim/release exchange is an unmatched claim — a "paws off until I'm done" claim comment that hasn't yet been followed by a release. Treat **either** release phrasing as a release marker: this command's `… done — paws off released.` **or** the existing `claim-pr` skill's `Done with my local session — unclaiming.`,
- the most recent claim/release exchange is an unmatched claim that hasn't yet been followed by a release.
**Match the two-word invariant `hold off` (case-insensitively), never a full sentence** --- the PR claim reads `please hold off on pushing to this branch until I'm done` while the issue claim reads `please hold off until I'm done`, so neither sentence is a substring of the other and a matcher keyed on either one misses the other.
**Also match both retired invariants, `paws off` and `back off`** (the second was `ardi`'s, and is easy to miss because seven other skills agreed on the first): claims posted before 2026-08-24 are still live on open PRs, since a claim expires on activity rather than on age, and a matcher narrowed to the new wording returns nothing on them --- indistinguishable from no claim at all.
So the claim test is `test("hold off|paws off|back off"; "i")`.
Treat **any** of these as a release marker --- four the corpus posts today plus one retired form still sitting on open PRs, and enumerating only this command's own is what makes it post a stray release over somebody else's completed handover:
- this command's `… done --- claim released.`
- its pre-2026-08-24 form `… done --- paws off released.`
- `claim-pr`'s `Done with my local session --- unclaiming.`
- `ardi`'s on-clean unclaim, `Done --- PR is free.`
- `post-merge`'s conflict unclaim, `Conflict resolved --- branch is now mergeable. …`

Derive that list rather than trusting this one, since a skill may add a sixth: `grep -rn "unclaim\|released\|PR is free\|now mergeable" skills/ commands/`.
- and that claim's `lane` matches the lane we're releasing.

If the most recent signal is already a release, or the claim was by a different lane, stop and tell the user — don't post a stray release that misrepresents who was holding the PR.

3. Compose the comment body, exactly in this shape so other agents recognise it:

```
<lane> done — paws off released.
<lane> done --- claim released.

_Posted by Claude Code (AI agent) --- not written by a human._
```

If `summary` is provided, append it in parentheses on the same line:
If `summary` is provided, append it in parentheses on the first line:

```
<lane> done — paws off released. (<summary>)
<lane> done --- claim released. (<summary>)

_Posted by Claude Code (AI agent) --- not written by a human._
```

The trailing marker is required on every agent-posted comment, and is deliberately emoji-free --- see [`disclose-agent-authorship`](../shared/workflow/disclose-agent-authorship.md).

4. Post the comment:

`mcp__github__add_issue_comment(owner = <owner>, repo = <repo>, issue_number = <pr_number>, body = <body>)`.
Expand Down
55 changes: 55 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,54 @@
"timeout": 15,
"script": "flag-uncited-rebuttal.py",
"why": "shared/workflow/address-every-comment.md's 'read the cited source' rule, from Morrison-Lab/ai-config#2070 (2026-08-24): a rebuttal disputing a finding that cited an external URL was posted using only local grep evidence, never fetching the URL the reviewer named -- and the rebuttal was wrong. Warns, never blocks, when a PR/issue comment about to be posted disputes a finding whose most recently fetched citation named an external URL that no earlier WebFetch/WebSearch in the transcript touched."
},
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/require-agent-disclosure.py\"",
"timeout": 10,
"script": "require-agent-disclosure.py",
"why": "cai 2026-08-24 'all comments online posted by bots should say so', citing UCD-SERG/ucd-serg.github.io#108's agent-authored claim comment posted under d-morrison as type: User, author_association MEMBER -- indistinguishable from a human's. memories/github.md already recorded auditors making that mistake. shared/workflow/disclose-agent-authorship.md is the rule; this is the instrument, since the rule is read at read time and the omission happens at composition time. WARNS, never blocks: a missing marker is cheap to repair with a follow-up comment, while a blocked `gh pr comment` interrupts the one action that makes a claim visible to other sessions -- trading a labelling defect for a collision. Anchored at a command position over text whose heredoc BODIES have been stripped -- the opener tail is kept, so piping a heredoc into `--body-file -` still fires while a doc-writing heredoc quoting `gh pr comment` does not. This corpus quotes that command constantly, and matching such a quote is the near-miss require-gh-repo-flag.py shipped with. Fails open.",
"_note_marker_avoids_the_robot_emoji": [
"The disclosure marker is prose, NOT the robot emoji, and the hook",
"warns separately when a body discloses with the emoji instead.",
"scripts/check-pr-fully-clean.py matches that emoji as a",
"REVIEW_BODY_MARKERS entry, so a disclosed claim comment would be",
"admitted into the fully-clean verdict scan as a review -- and a claim",
"carries no findings, so it would scan as a CLEAN one. That is the",
"false-clean failure shared/workflow/fully-clean.md already records for",
"a human-authored self-review, arriving through the very mechanism",
"added to make authorship legible."
],
"_note_cannot_see_every_body": [
"--body-file, --editor, `-F <file>` and an interpolated $BODY all put the",
"text where a literal scan cannot reach. The hook reports those as an",
"UNREADABLE body rather than as a missing marker, so its warning never",
"asserts more than it observed. That partial visibility is the second",
"reason it cannot deny: a block built on a check that sees a fraction of",
"its population would refuse compliant commands.",
"",
"skills/ard/SKILL.md's per-round summary is exactly that shape -- posted",
"via --body-file on every round of every PR -- so it states the marker",
"requirement in its own text rather than relying on this guard.",
"",
"REGISTERED TWICE, under Bash and under mcp__github__.*, because",
"hooks.json matches by TOOL NAME: the MCP tool list inside the script is",
"unreachable from a Bash-only entry however complete it is. A remote/web",
"session has no `gh` at all, so a Bash-only registration would be silent",
"in exactly the population the MCP branch was written for."
],
"_note_segmentation_is_quote_aware": [
"Segments are split on shell separators OUTSIDE quotes. A plain",
"re.split(r'[;&|\\n]+', ...) looked adequate and was not: the marker sits",
"on its own line at the END of a body, so splitting on every newline cuts",
"the marker off the very command it discloses, and every correctly",
"disclosed multi-line comment -- the normal shape -- warned. Caught by the",
"test suite before this hook was ever registered.",
"",
"Judging PER SEGMENT rather than per call is what stops one disclosed body",
"vouching for an undisclosed sibling in a batched round, which",
"shared/workflow/efficient-pr-babysitting.md actively encourages."
]
}
]
},
Expand Down Expand Up @@ -433,6 +481,13 @@
"timeout": 10,
"script": "warn-pr-create-without-dupe-check.py",
"why": "Dual-registered with the Bash entry, following no-unauthorized-merge.py's precedent. tool-mappings.md maps CREATE_PR to mcp__github__create_pull_request, and CLAUDE.md mandates that substitute in remote sessions -- so registering only under Bash let a remote session discharge the guard via MCP reads while never being able to trip it via MCP creation."
},
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/require-agent-disclosure.py\"",
"timeout": 10,
"script": "require-agent-disclosure.py",
"why": "The MCP half of the Bash entry above. A remote/web session has no `gh` on PATH, so mcp__github__add_issue_comment and its siblings are the ONLY way it posts a comment -- and hooks.json matches by tool name, so the Bash entry never sees them. Same script, same warn-only behaviour; it reads tool_input.body directly rather than parsing a command line, and stays silent when no string body is present (some pull_request_review_write methods submit without one)."
}
]
}
Expand Down
Loading
Loading