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
2 changes: 1 addition & 1 deletion plugins/source-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "source-control",
"version": "0.51.11",
"version": "0.51.12",
"description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only — with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.",
"author": {
"name": "Melodic Software",
Expand Down
27 changes: 27 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
All notable changes to the `source-control` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.51.12]

### Fixed

- **D6's verify-commit-pushed gate checks branch reachability, not repo-wide presence or the branch
tip** (`reference/review-discipline.md`, `skills/pull-request/SKILL.md`; #2244). The published
form — `commits?sha=<branch>&per_page=1` with `--jq '.[0].sha'` — asked "is my fix commit on the
remote?" but read only the branch tip, so any later push made it report the fix missing while it
was present: a false negative on a control gate, and a positional index on a list. A
repository-scoped `commits/<fix-sha>` lookup fixed the tip-read false negative but still answered
"does this object exist anywhere in the repo?" — satisfied by a force-pushed-off commit or an
identical commit on another branch. The gate now fetches the PR branch and runs
`git merge-base --is-ancestor <fix-sha> origin/<branch>` (exit 0 when the fix commit is on the
remote PR branch), matching the reachability primitive `babysit-prs` already uses in
`verify_fix_commit`.
Comment on lines +17 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"matching the reachability primitive babysit-prs already uses in verify_fix_commit" overstates the similarity. verify_fix_commit (skills/babysit-prs/scripts/babysit_resolve_thread.py:546) doesn't call git merge-base --is-ancestor — it resolves the PR's live headRefOid/headRepository via gh pr view --json and then hits the GitHub compare API, repos/{owner}/{repo}/compare/{sha}...{head_oid}. That's a deliberately different implementation: it works cross-fork (the function's own docstring calls this out — "differs from the base repository on a fork PR") and needs no local clone/fetch, where D6's git fetch origin <branch> && git merge-base --is-ancestor requires both.

Both check the same property (is the fix commit an ancestor of the branch tip), so "matching reachability guarantee" would be accurate — but "matching...primitive" reads as "same mechanism," which isn't the case, and a future reader who goes looking for a shared implementation to consolidate on won't find one. Given this PR's own stated impetus (2 rounds of review already caught overstated guarantees on this exact D6 gate — Codex's P1 and the prior Claude inline comment on "cannot be satisfied by the wrong record"), this is worth tightening rather than shipping a third instance of the same overstatement class in the CHANGELOG.

Fix this →

- **Every remaining `--paginate` list read carries `per_page=100`**, conforming to rule 1 as
`readiness.md` publishes it (#2246): `skills/pull-request/reference/merge.md` (three
comment-source re-checks), `skills/pull-request/SKILL.md` (C1–C3),
`skills/pull-request/reference/monitor.md` (poll-loop comment fetch),
`scripts/fetch-all-pr-comments.sh` (the shared surface pager), and
`skills/babysit-loop/reference/telemetry-upsert.md` (sentinel LOOKUP). Not truncation defects —
`--paginate` alone fetches every page — but the default 30-per-page form costs 3.3x the
requests and diverges from the rule the same skill states as absolute.
`skills/babysit-prs/scripts/babysit_gh.py` and `scripts/request_review.py` were reported in the
#2246 sweep but were already conformant: their `per_page=100` sits in the endpoint URL on the
line adjacent to the `--paginate` flag the line-based sweep matched.

## [0.51.11]

### Changed
Expand Down
8 changes: 6 additions & 2 deletions plugins/source-control/reference/review-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,12 @@ D1–D7 cycles. Exploration and validation must run on the PR's head branch.
since these endpoints return 30 per page oldest-first and your reply is the newest item
- [ ] D6 — Fix if VALID (fix now) → edit, `git add <specific-files>` (never `-A` or `.`),
commit, push
- [ ] **verify commit pushed:** `gh api "repos/{owner}/{repo}/commits?sha=<branch>&per_page=1"
--jq '.[0].sha'` — confirm the fix commit SHA on the remote
- [ ] **verify commit pushed:** `git fetch origin <branch> && git merge-base --is-ancestor <fix-sha> origin/<branch>` —
exit 0 means the fix commit is on the remote PR branch; non-zero means it is not. A reachability
check after fetch, never the branch-tip read (`commits?sha=<branch>&per_page=1` + `.[0]`), which
any later push turns into a false "missing", and never a repository-scoped `commits/<fix-sha>`
lookup alone, which answers "does this object exist anywhere in the repo?" and can pass when the
commit was force-pushed off the PR branch
- [ ] D7 — Post a follow-up reply citing the fix commit SHA
- [ ] **verify follow-up reply posted — same surface routing as D5:** inline thread →
`pulls/<pr>/comments` filtered by `in_reply_to_id`; issue-level → `gh api --paginate
Expand Down
2 changes: 1 addition & 1 deletion plugins/source-control/scripts/fetch-all-pr-comments.sh
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ SURFACE_JSON=""
fetch_surface() {
local endpoint="$1" projection="$2" raw
SURFACE_JSON=""
raw=$(gh api --paginate "repos/$OWNER/$REPO/$endpoint" 2>/dev/null) || {
raw=$(gh api --paginate "repos/$OWNER/$REPO/$endpoint?per_page=100" 2>/dev/null) || {
printf 'fetch-all-pr-comments: gh api %s failed\n' "$endpoint" >&2
exit 2
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ esac
}
MARKER="source-control:babysit-loop@$INSTANCE"
SENT="<!-- claude-ops:lane-telemetry marker=$MARKER -->" # $BODY_FILE MUST open with this line
LOOKUP() { gh api --paginate "repos/$REPO/issues/$ISSUE/comments" \
LOOKUP() { gh api --paginate "repos/$REPO/issues/$ISSUE/comments?per_page=100" \
--jq ".[] | select(.body | startswith(\"$SENT\")) | .id"; }
SENTINEL_OK() { # $1 = text; true iff line 1 is exactly $SENT and >=16 payload bytes follow
[ "$(printf '%s' "$1" | head -c ${#SENT})" = "$SENT" ] &&
Expand Down
8 changes: 4 additions & 4 deletions plugins/source-control/skills/pull-request/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ When a channel event, Monitor notification, or poll iteration fires, complete AL
- [ ] **A — Terminal state:** `gh pr view <N> --json state -q .state` — MERGED/CLOSED → self-terminate
- [ ] **B — CI checks:** `gh pr checks <N>` — classify EVERY non-pending check (pass/fail/skipped). Read logs for ANY failure per §3.1 fetch chain
- [ ] **C — Fetch ALL comments from ALL sources:** read every update on the PR regardless of author or format. Three API surfaces + reviews:
- [ ] C1 — Review-thread comments: `gh api repos/<owner>/<repo>/pulls/<N>/comments --paginate`
- [ ] C2 — Issue-level comments: `gh api repos/<owner>/<repo>/issues/<N>/comments --paginate` (includes AI-review summaries, user replies, bot task-completion posts)
- [ ] C3 — PR reviews: `gh api repos/<owner>/<repo>/pulls/<N>/reviews --paginate` (review bodies contain findings — APPROVED/CHANGES_REQUESTED/COMMENTED reviews all may carry actionable content)
- [ ] C1 — Review-thread comments: `gh api --paginate "repos/<owner>/<repo>/pulls/<N>/comments?per_page=100"`
- [ ] C2 — Issue-level comments: `gh api --paginate "repos/<owner>/<repo>/issues/<N>/comments?per_page=100"` (includes AI-review summaries, user replies, bot task-completion posts)
- [ ] C3 — PR reviews: `gh api --paginate "repos/<owner>/<repo>/pulls/<N>/reviews?per_page=100"` (review bodies contain findings — APPROVED/CHANGES_REQUESTED/COMMENTED reviews all may carry actionable content)
- [ ] C4 — Read every comment body in full. Summaries and review posts from ANY AI agent (claude[bot], codex, cursor, copilot) contain findings that require classification — these are NOT informational. **Extract individual findings** per [`${CLAUDE_PLUGIN_ROOT}/reference/review-discipline.md`](../../reference/review-discipline.md) §2 — one comment with N findings = N work items, each needing individual D1-D7. **For ≥3 findings, MANDATORY subagent dispatch** per the same §2 — preserves main session context, structurally enforces per-finding ledger shape
- [ ] **D — For EACH unaddressed **finding** (not comment — one comment may contain multiple findings):**
- [ ] D1 — Read full finding context (parent comment body + surrounding findings). For multi-finding comments dispatched to a subagent ([review-discipline](../../reference/review-discipline.md) §2), this work is in the subagent; the main session receives the ledger
Expand All @@ -178,7 +178,7 @@ When a channel event, Monitor notification, or poll iteration fires, complete AL
- [ ] D5 — Reply with a per-finding classification table + evidence (before fixing). **Route by comment type — REQUIRED, not interchangeable:** inline review comments MUST reply THREADED via `gh api repos/<owner>/<repo>/pulls/<N>/comments/<id>/replies`; issue-level / review-level → `gh pr comment <N>`. Answering an inline finding with a detached `pr comment` is a routing error, not a style choice. Use the project's bot-identity wrapper for these writes when it has one; plain `gh` otherwise
- [ ] **Verify reply exists — on the surface it was posted to:** inline threaded replies land on the review-comment surface — `gh api --paginate "repos/<owner>/<repo>/pulls/<N>/comments?per_page=100" --jq '.[] | select(.in_reply_to_id == <original-id>)'`; issue-level replies — `gh api --paginate "repos/<owner>/<repo>/issues/<N>/comments?per_page=100" --jq '.[].body'`. Querying only issues/comments false-fails a correctly posted inline reply; so does dropping `--paginate`, since these endpoints return 30 per page oldest-first and your reply is the newest item
- [ ] D6 — Fix if VALID (fix now) — edit, `git add <files>`, commit, push
- [ ] **Verify commit pushed:** `gh api "repos/<owner>/<repo>/commits?sha=<branch>&per_page=1" --jq '.[0].sha'` — confirm the fix commit SHA appears on the remote
- [ ] **Verify commit pushed:** `git fetch origin <branch> && git merge-base --is-ancestor <fix-sha> origin/<branch>` — exit 0 means the fix commit is on the remote PR branch; non-zero means it is not. A reachability check after fetch, never the branch-tip read (`commits?sha=<branch>&per_page=1` + `.[0]`), which any later push turns into a false "missing", and never a repository-scoped `commits/<fix-sha>` lookup alone, which can pass when the commit was force-pushed off the PR branch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve the PR branch's actual remote before fetching

When the checkout uses the skill's supported non-origin or triangular-fork configuration, this mandatory gate fetches the wrong remote—or fails because origin does not exist—even though reference/create.md pushes through push-branch.sh and resolve-remote.sh --push. A successful push to a vendor/fork remote can therefore be followed by a false D6 failure, preventing D7 and thread resolution; an origin base repository with a same-named branch can also verify the wrong ref. Fresh evidence in this revision is the newly hardcoded origin; resolve the PR head/push remote or compare the fix SHA against the PR's head repository and OID instead.

Useful? React with 👍 / 👎.

- [ ] D7 — Post a follow-up reply citing the fix commit SHA
- [ ] **Verify follow-up reply posted — same surface routing as D5:** inline thread → `pulls/<N>/comments` filtered by `in_reply_to_id`; issue-level → `gh api --paginate "repos/<owner>/<repo>/issues/<N>/comments?per_page=100" --jq '.[] | select((.body | contains("<sha>")) and .user.login == "<posting-identity>") | .body'` — confirm the follow-up with SHA appears on GitHub. Constrain on BOTH the SHA and the posting identity, and never on `.[-1]`. `.[-1]` is wrong because these endpoints return oldest-first, so on an unpaginated list it is the 30th-oldest comment. SHA alone is wrong because this is a control gate you act on: anyone else quoting the fix SHA — a reviewer, another bot — satisfies it, and the check reports your reply as posted when the write failed. `<posting-identity>` is the login you posted as (the bot-identity wrapper's account when the project has one, your own otherwise)
- [ ] D7.5 — Resolve review thread — **author- and classification-conditional, inline only**. Eligible only when EVERY finding in the thread carries a recorded disposition: `VALID (fix now)` with the fix pushed and cited, `VALID (defer)` grounded per D4.6 with the item id cited, or `INCORRECT` with counter-evidence posted — one dispositioned finding never makes a multi-finding thread eligible, and a single `UNCERTAIN` escalates and holds the thread open. Resolve threads opened by a BOT reviewer that you addressed. NEVER resolve HUMAN-authored threads (the human resolves their own). NEVER resolve your OWN (your posting identity — bot or personal). **A `VALID (defer)` never clears the gate for a merge this same session performs** — `full` mode monitors and merges in one session, so route the deferral to an independent adjudicating context, or leave the thread unresolved and do not merge. Fail closed. Detect bot via the API surface in use — REST `user.type==Bot`; GraphQL `author.__typename==Bot` (resolution runs via GraphQL). Verify `isResolved == true` via GraphQL <!-- contract-restatement: D7.5-thread-eligibility --> <!-- contract-restatement: D7.5-merge-authorization -->
Expand Down
6 changes: 3 additions & 3 deletions plugins/source-control/skills/pull-request/reference/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Resolve `<pr_number>` via `gh pr view --json number -q '.number'`. Pass it expli
gh pr checks <pr_number> --json name,state,bucket

# 2. Re-check for new comments since monitoring completed (all 3 sources, paginated)
gh api --paginate repos/{owner}/{repo}/pulls/<pr_number>/reviews | jq -r '.[].user.login'
gh api --paginate repos/{owner}/{repo}/pulls/<pr_number>/comments | jq -r '.[].user.login'
gh api --paginate repos/{owner}/{repo}/issues/<pr_number>/comments | jq -r '.[].user.login'
gh api --paginate "repos/{owner}/{repo}/pulls/<pr_number>/reviews?per_page=100" | jq -r '.[].user.login'
gh api --paginate "repos/{owner}/{repo}/pulls/<pr_number>/comments?per_page=100" | jq -r '.[].user.login'
gh api --paginate "repos/{owner}/{repo}/issues/<pr_number>/comments?per_page=100" | jq -r '.[].user.login'
```

**If any readiness gate fails on re-verification:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ After each push, run this loop until convergence (**every** check in a terminal

1. **Mergeable pre-check (MANDATORY before polling)** — `gh pr view <N> --json mergeable,mergeStateStatus` FIRST. If `mergeable == "CONFLICTING"`, GitHub will NOT trigger workflows — integrate the default branch, resolve conflicts, force-push with lease, and restart the loop. Only proceed to CI polling when `mergeable == "MERGEABLE"`. **Never blame the platform for missing CI runs before checking this.**
2. **Poll CI** — `gh pr checks <N>` every 30s (the standard monitor cadence), max 15 minutes per cycle. **Wait for ALL checks to reach a terminal state** (pass/fail/skipped) before suggesting merge — no exceptions, regardless of PR type. Never merge while any check is still pending or in_progress
3. **Check for new comments** — on each poll, also fetch new review comments (`gh api repos/<owner>/<repo>/pulls/<N>/comments --paginate`)
3. **Check for new comments** — on each poll, also fetch new review comments (`gh api --paginate "repos/<owner>/<repo>/pulls/<N>/comments?per_page=100"`)
4. **Process comments immediately** — if a bot comments while CI is still running, start evaluating/researching that comment now. Don't wait for CI
5. **On CI failure** — route to 3.2 (research-driven fix)
6. **On new comment** — route to 3.3 (evaluate + respond)
Expand Down
Loading