Skip to content
Closed
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.53.2",
"version": "0.53.3",
"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 \u2014 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 \u2014 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 \u2014 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 \u2014 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
6 changes: 6 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
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.53.3]

### Fixed

- **`fetch_pull_request_commits` fails closed when the pull-commits walk is shorter than the PR's `commits` count (#2387).** GitHub caps that endpoint at 250 commits regardless of pagination.

## [0.53.2]

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,11 +481,23 @@ def fetch_pull_request_commits(repo: str, number: int) -> list[dict[str, Any]]:
whose verification block is missing is reported unverified with reason
`unreadable` rather than skipped: the caller enforcing a signature rule may
only ever over-report.

GitHub's pull-commits endpoint returns at most 250 commits regardless of
pagination; when the PR's `commits` count exceeds the walked rows, this
raises rather than returning a silently truncated list (#2387).
"""
pr_commits = gh_json(["api", f"repos/{repo}/pulls/{number}", "-q", ".commits"])
expected = int(pr_commits) if pr_commits is not None else 0
rows = fetch_paginated_api(
f"repos/{repo}/pulls/{number}/commits?per_page=100",
f"{repo}#{number} commits",
)
walked = len(rows)
if expected > walked:
raise RuntimeError(
"commit list walked "
f"{walked} of {expected} (pull-commits endpoint caps at 250)"
)
out: list[dict[str, Any]] = []
for row in rows:
commit = row.get("commit")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,13 @@ def test_verification_is_projected_per_commit(self) -> None:
"commit": {"verification": {"verified": False, "reason": "no_user"}},
},
]
with mock.patch.object(gh, "gh_json", return_value=rows) as gh_json:

def gh_side_effect(args: list[str]) -> Any:
if "-q" in args and args[1].endswith("/pulls/5"):
return 2
return [rows]

with mock.patch.object(gh, "gh_json", side_effect=gh_side_effect):
out = gh.fetch_pull_request_commits("owner/repo", 5)
self.assertEqual(
out,
Expand All @@ -297,19 +303,32 @@ def test_verification_is_projected_per_commit(self) -> None:
{"sha": "b" * 40, "verified": False, "reason": "no_user"},
],
)
[call] = gh_json.call_args_list
endpoint = call.args[0][1]
self.assertIn("repos/owner/repo/pulls/5/commits", endpoint)
self.assertIn("per_page=100", endpoint)
self.assertIn("--paginate", call.args[0])

def test_truncated_commit_list_raises(self) -> None:
rows = [{"sha": "a" * 40, "commit": {"verification": {"verified": True}}}]

def gh_side_effect(args: list[str]) -> Any:
if "-q" in args and args[1].endswith("/pulls/99"):
return 251
return [rows]

with mock.patch.object(gh, "gh_json", side_effect=gh_side_effect):
with self.assertRaisesRegex(RuntimeError, "walked 1 of 251"):
gh.fetch_pull_request_commits("owner/repo", 99)

def test_missing_verification_reads_unverified_unreadable(self) -> None:
rows = [
{"sha": "c" * 40, "commit": {}},
{"sha": "d" * 40},
{"sha": "e" * 40, "commit": {"verification": {"verified": True}}},
]
with mock.patch.object(gh, "gh_json", return_value=rows):

def gh_side_effect(args: list[str]) -> Any:
if "-q" in args and args[1].endswith("/pulls/5"):
return 3
return [rows]

with mock.patch.object(gh, "gh_json", side_effect=gh_side_effect):
out = gh.fetch_pull_request_commits("owner/repo", 5)
self.assertEqual(
out,
Expand Down
Loading