Skip to content

feat: require a session claim before commit/push on a PR branch (#4155) - #4156

Merged
dem-extra1 merged 10 commits into
mainfrom
feat/claim-required-hook-4155
Oct 1, 2026
Merged

dem-extra1 merged 10 commits into
mainfrom
feat/claim-required-hook-4155

Conversation

@dem-extra1

@dem-extra1 dem-extra1 commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #4155

What

Local half of #4155: a PreToolUse hook on Bash that makes a session claim a PR branch before it commits or pushes to it.
The incident was two sessions on #4132, #4134 and #4138 with no claim comment from either (PR #4134 lost 471 lines of memories/preferences.md).

hooks/no-pr-work-without-claim.py binds to PreToolUse (Bash) and fires on git commit and git push only (argv-split via scripts/lib/shellcmd.py, so quoted or heredoc mentions are silent; --dry-run, branch deletes, tag and ref-set pushes are skipped).
When the target branch has an open PR in a Morrison-Lab repo it reads the PR's comments:

PR state Result
no claim comment deny, with the claim text to post
only claims naming a different session, PR active in the last 2 h deny (stand down)
only claims naming a different session, PR idle over 2 h warn (treated as lapsed)
only claims naming no session (every emitter but claim-pr today, #4160) warn
a claim naming this session (session id or worktree path), none newer from another session allow
another session's claim newer than mine deny (takeover)

A claim is a comment with the agent marker plus hold off / paws off wording; a session is named by a Session worktree: or Session id: line.
Before a push it also reads repos/<o>/<r>/activity?ref=refs/heads/<branch> and warns (never denies) about pushes since the claim whose commit is not in local history.
Every network or gh failure, an unresolvable cd, a commit inside bash -c, and a spent 20 s budget are visible fail-opens (stderr plus additionalContext).
Escape hatch: ALLOW_UNCLAIMED_PR_WORK=1 (env prefix, leading export, or process env).

Also: a Session worktree: line in the skills/claim-pr PR template, a Do/Don't pair in shared/workflow/claim-pr.md citing #4155 and PR #4134, README rows, and the hooks/hooks.json entry with its generated mirror.

Activation

Merging this PR activates the hook on the plugin path (the hooks.json entry is the activation); nothing was registered locally with install-hooks.py --fix.
On the non-plugin path, post-merge step 3.75 owns the registration.

Not done here

Verification

  • hooks/test-no-pr-work-without-claim.py: 104 cases against real temporary git repos with a fake gh (a fixture, so these show the hook's decisions given canned responses, not forge behaviour; the activity and pulls response shapes were checked once against the live API).
  • Mutation check: single-condition mutants of each decision branch (claim recognition, session matching, takeover, directory resolution, push-target parsing, dry-run grammar, pagination, fail-open paths), each killed by at least one case.
  • Eight rounds of foreground adversarial review (adversarial-reviewer, sonnet) on the committed diff; findings addressed in the follow-up commits.
    The eighth round read 82ee03a0 and returned CLEAN with four non-blocking notes: a session that claims in one checkout and then commits from another reads its own claim as a peer's, /mnt/c/ and 8.3 path spellings do not match, the fragment's one-line summary omits the hold-off wording, and any claim containing the words "session id" counts as a peer's.
  • The push of 82ee03a0 used ALLOW_UNREVIEWED_PUSH=1: the pre-push guard looks for the verdict in the Agent call's own result, and this harness returns a foreground reviewer's report as a message instead, so the guard could not see a verdict that exists.
  • Heads-up for Add Session worktree line to every claim-comment emitter (follow-up to #4155) #4160: once every emitter carries the worktree line, a subagent committing in its own worktree on a parent-claimed branch is denied as a peer (documented in the hook docstring).

🤖 Generated with Claude Code

dem-extra1 and others added 3 commits September 30, 2026 17:13
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
 #4155

Add hooks/no-pr-work-without-claim.py (PreToolUse, Bash). On a branch with an
open Morrison-Lab PR it denies `git commit` / `git push` unless a claim comment
(agent marker plus hold-off wording) names this session by session id or
worktree path, and denies when another session's claim is newer. Before a push
it warns on pushes since the claim that local history lacks, read from the
forge activity endpoint. Fails open with a visible warning on any gh/network
failure. Escape hatch: ALLOW_UNCLAIMED_PR_WORK=1.

Adds 37 cases in hooks/test-no-pr-work-without-claim.py (8 of 8 mutants
killed), registers the hook in hooks/hooks.json and the generated plugin
mirror, documents it in README.md, and adds a Do/Don't pair to
shared/workflow/claim-pr.md citing #4155 and PR #4134.

Local half only: hooks are inert in remote/web sessions (#2004).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Resolve the directory a command runs in from earlier `cd`s and `git -C`;
  an unresolvable move (`cd -`, `$VAR`, `--git-dir`) is a visible fail-open.
- Match a worktree path or session id only as a whole token, so a nested
  worktree or a longer id is not read as this session's claim.
- Decode `gh api --paginate` pages one at a time instead of rewriting `][`,
  which corrupted comment bodies containing `] [`; read the activity
  endpoint as a single page.
- Give the activity read its own try so a 403/404 keeps the claim outcomes.
- Dry-run grammar: push `-n`, last occurrence wins for `--no-dry-run`.
- A claim needs `working on this` plus hold-off wording and must not be an
  unclaim; a standing peer claim gets stand-down advice instead of "post a
  claim".
- Document the known limits (claim age, release comments, fork PRs, cost).
- 56 cases now; 20 of 20 mutants killed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dem-extra1 and others added 7 commits September 30, 2026 19:28
- Accept every claim wording skills/ emits: marker plus `hold off` or
  `paws off`, minus an unclaim and a negated "no need to hold off". Drop the
  `working on this` requirement that rejected the ardi, handoff and
  review-only wordings.
- Add the `Session worktree:` line to the skills/claim-pr PR template, the
  one the hook matches on; the other emitters are tracked in #4160.
- Worktree match is bounded on both sides: a sentence-final period is fine,
  an extension of the path (either end) is not.
- Judge a push by its refspec's destination branch, skip tag pushes.
- URL-encode the branch in the pulls and activity queries; accept a remote
  URL with a trailing slash.
- Cap the activity SHA checks and put every subprocess under one 20s budget
  (PR_CLAIM_TOTAL_BUDGET) below the 30s hook timeout.
- Tests log each requested forge path and assert the head= and ref= queries;
  69 cases, every mutant added this round killed by one targeted case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A claim that names no session now WARNS instead of denying, so the claim
  emitters that predate the session line (#4160) keep working; only no claim,
  or claims naming a different session, deny. Deny text no longer implies a
  lapse/release check the hook does not make.
- Read a Git Bash `/c/x` path as `C:/x` on both sides of the worktree match;
  the claim-pr template asks for `git rev-parse --show-toplevel`.
- Scan commands nested in a shell's `-c`: unlocatable, so a visible
  fail-open rather than a silent bypass.
- Skip option values (`-m`, `-am msg`, ...) when reading `--dry-run`; stop
  treating `--signed` / `--recurse-submodules` as value-taking.
- Skip pushes the lookup cannot judge: colon-refspec deletes, `--tags` /
  `--all` / `--mirror`, and non-origin remotes.
- Document the shared-checkout, unfetched-commit and shallow-clone limits.
- 80 cases; real-commit ancestry cases replace the made-up SHA; each mutant
  added this round is killed by a targeted case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A shell-built push destination (`$BRANCH`, `$(...)`) falls back to the
  current branch instead of being looked up as a literal name, which found no
  PR and passed silently.
- The push activity warning now also runs when the only claim names no
  session (it uses that claim's time as the baseline).
- Apply claim-pr's 2-hour rule to a PEER claim through the PR's updated_at: a
  peer claim on a PR idle that long warns instead of denying.
- Recognize "releasing my claim" style releases as non-claims.
- A broken shellcmd import is a visible fail-open on a git command, not a
  silent no-op; accept `github.com-<alias>` SSH remotes.
- skills/claim-pr/SKILL.md no longer says the hook denies a session-less
  claim (it warns).
- 88 cases; each mutant added this round killed by a targeted case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Use the canonical release terms (unclaim, released, PR is free, now
mergeable), accept a worktree path with a trailing slash, state the
subagent-worktree and busy-branch limits accurately, and reword the prose
that said the hook denies a session-less claim. 94 cases; four new mutants
killed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Strip the Session worktree / Session id lines before reading a claim for
release terms (a worktree named after an issue slug like fix-unclaim-wording
made a correct claim fail), accept the back-off wording claim-pr.md lists,
and treat a missing cwd as a quiet pass. 102 cases; wrapped git invocations
(/usr/bin/git, timeout, command) are now pinned by tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A bare 'released' is no longer a release term (a claim saying a fix 'can be
released' was dropped and its session denied); only 'paws off released',
'claim released', 'released my claim', 'PR is free', 'now mergeable',
'unclaim' and 'releasing my claim' are. Name the reads a cloud session should
run in claim-pr.md. 104 cases; three new mutants killed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.

2026-10-01 04:52 UTC

@dem-extra1

Copy link
Copy Markdown
Collaborator Author

Claude Code CLI (local session, gia mwc daytb) is working on this. Please hold off on pushing to its branch until I'm done.

Posted by Claude Code (AI agent) --- not written by a human.

@dem-extra1
dem-extra1 marked this pull request as ready for review October 1, 2026 04:39
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished review — View run


Fully-clean instrument reading

Per the Stop hook's requirement, I ran the authoritative instrument rather than resting the verdict on gh pr checks:

python3 scripts/check-pr-fully-clean.py 4156 -R Morrison-Lab/ai-config

Output (exit 1, NOT fully clean, two - finding bullets):

✓ No review threads found.
❌ PR is NOT fully clean:
  - Check run 'review / claude-review' is still in status 'in_progress'
  - No automated review comments or reviews found on PR #4156

Both reasons are this review's own in-flight sibling run: the review / claude-review check is the workflow executing this very review, and the "no automated review found" condition is true only because this review has not been posted yet — posting it is what will resolve both. This is exactly the self-referential case the task's own Verdict semantics section calls out (the permanent-loop pattern tracked as ai-config#2475): the instrument cannot read clean while the review that gates it is still running, and a status-conditioned non-clean reading here is not a content finding about the diff. Per that explicit instruction, I do not defer the verdict to this reading. Independently, gh pr checks showed no check that finished red (validate and review were pending/in-progress, everything else — build, CodeQL, lint-markdown, lint-qmd, new-line-breaks — passed), and my own direct inspection of the diff (104/104 tests passing, both hooks.json registrations valid and consistent, structural hook validators clean, referenced issues real) found no content defect.

Verdict

Ready for merge — judged on the diff's content; the only non-clean signal from the fully-clean instrument is this review's own in-flight completion, not a defect in the change.

Structured Review Data (JSON)
{
  "schema_version": "1.1",
  "reviewer": "claude",
  "commit_sha": "82ee03a05c4a3da3c04bc6d66734ab224b010b8d",
  "verdict": "CLEAN",
  "findings": [],
  "detailed_assessment": "Traced hooks/no-pr-work-without-claim.py's claim-matching, argv-parsing, and fail-open paths against its 104-case test suite, which passes in full with no uncovered failure mode found.",
  "holistic_assessment": "Registration is consistent across both hooks.json files, referenced issues exist, the claim-pr skill template matches what the hook parses, and no CI check has failed."
}

Stopping Point: Clean stopping point reached --- session done; review posted with no blocking findings; no follow-up items pending.

Reviewed commit: 82ee03a

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

💰 Cost: $1.7263 (review) — run

@dem-extra1
dem-extra1 merged commit 083cf17 into main Oct 1, 2026
28 checks passed
@dem-extra1
dem-extra1 deleted the feat/claim-required-hook-4155 branch October 1, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce one agent session per PR branch: require a claim before commit/push

1 participant