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.53.11",
"version": "0.53.14",
"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
20 changes: 20 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
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.14]

### Changed

- **Stale-branch recovery defaults to merge-forward, not rebase + force-push (#1436).** `monitor.md`'s
conflict and stale-branch paths prescribed "force-push with lease", which auto-mode permission
classifiers commonly deny — the observed cost was a fresh branch + fresh PR per rebase, with every
review thread re-opened. Merging the default branch *into* the PR branch pushes fast-forward with
no force-push, and under a squash-only default branch the merge commits collapse on merge, so
linear-history requirements stay satisfied. Rebase remains the exception for projects that require
a linear PR branch and where force-push is actually available.

### Added

- **`statusCheckRollup` running-check pitfall documented (#1436).** An unfinished check reports
`conclusion: ""` (empty string), not `null`, so complement-shaped failure filters
(`conclusion != null and != "SUCCESS"`) count still-running checks as failures. `monitor.md`'s
multi-PR scan now carries the correct value-positive jq selectors for "failed" and "still
running".

## [0.53.11]

### Fixed
Expand Down
18 changes: 16 additions & 2 deletions plugins/source-control/skills/pull-request/reference/monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ Other open PRs:
#103 fix/null-check — all checks green, awaiting review
```

**`statusCheckRollup` pitfall: a running check has `conclusion: ""` (empty string), not `null`.**
The obvious "did anything fail" filter — `select(.conclusion != null and .conclusion != "SUCCESS")`
— therefore matches every **in-progress** check and reports still-running CI as failures. Select on
the values you mean, never on the complement:

```bash
# failures only
--jq '[.statusCheckRollup[] | select(.conclusion=="FAILURE" or .conclusion=="TIMED_OUT" or .conclusion=="CANCELLED")]'
# still running
--jq '[.statusCheckRollup[] | select(.status!="COMPLETED")]'
Comment thread
kyle-sexton marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The two new jq selectors only classify CheckRun nodes correctly; statusCheckRollup also contains StatusContext nodes (legacy commit statuses), which carry state instead of status/conclusion.

  • "still running" (select(.status!="COMPLETED")): a StatusContext has no status field, so this is always true for it — a long-finished legacy status (state: "SUCCESS") is reported as still running forever.
  • "failures only": omits ACTION_REQUIRED, STALE, and STARTUP_FAILURE, which plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py:21-29 already classifies as failing states alongside FAILURE/TIMED_OUT/CANCELLED. Those conclusions read as neither failing nor running here and get silently dropped.

The repo already has the correct classification for this exact union type in babysit_checks.py's check_category() (plugins/source-control/skills/babysit-prs/scripts/babysit_checks.py:44-51) — worth mirroring its node-type/state handling here rather than introducing a second, narrower selector.

Fix this →

```

**Constraint: Monitor watches are branch-locked.** Monitor MUST run in the session that owns the branch (§3.5). Scanning is READ-ONLY — you cannot arm a Monitor watch for a PR on a different branch from this worktree. Report status and suggest: *"Switch to the worktree for `<branch>` to monitor PR #N."*

**When NO other open PRs found:** report `No other open PRs need attention.` and let the session idle.
Expand All @@ -190,7 +202,7 @@ Other open PRs:

After each push, run this loop until convergence (**every** check in a terminal state + all comments addressed):

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.**
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 (merge-forward first, per the stale-branch recovery rule in §3.2), resolve conflicts, push, 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 --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
Expand Down Expand Up @@ -276,7 +288,9 @@ For each security finding:
5. **Implement** (only after 1-4) — make the change, re-run the project's build/test/lint gate, commit, push
6. **Loop restarts** — new push triggers 3.1 again. Track iteration count

**Stale branch recovery** — if CI fails because the branch is out of date with the default branch (merge conflicts, "branch is not up to date" errors, or tests failing due to default-branch-only changes): integrate (merge or rebase per the project's convention and the branch's own history), resolve conflicts conservatively, force-push with lease, restart the monitor loop from 3.1. Distinct from code failures — no research gate for the integration itself, only for conflicts requiring intent judgment.
**Stale branch recovery** — if CI fails because the branch is out of date with the default branch (merge conflicts, "branch is not up to date" errors, or tests failing due to default-branch-only changes): integrate, resolve conflicts conservatively, push, restart the monitor loop from 3.1. Distinct from code failures — no research gate for the integration itself, only for conflicts requiring intent judgment.

**Merge-forward is the default integration; rebase is the exception.** `git merge <remote>/<default-branch>` *into* the PR branch resolves staleness and pushes **fast-forward** — no force-push, no history rewrite. A rebase rewrites the branch and demands `git push --force-with-lease`, which permission classifiers commonly deny in autonomous/auto-mode sessions; the observed failure shape is a lane concluding every rebase costs a fresh branch + fresh PR + closing the old one (re-opening every review thread from scratch), when the stale branch never needed a rebase at all. Under a squash-only default branch, the merge commits inside the PR branch collapse to one commit on merge and linear-history requirements stay satisfied — repeated merge-forwards as the default branch moves cost nothing. Rebase only when the project's convention requires a linear PR branch *and* force-push is actually available.
Comment thread
kyle-sexton marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This new default-integration guidance runs git merge <remote>/<default-branch> without first fetching, so it merges whatever the local remote-tracking ref happened to be at the last fetch — which can be stale by the time "stale branch recovery" (§3.2) or the mergeable pre-check (§3.1 step 1) actually fires, since CI/mergeable status can lag the last local fetch by minutes to hours. That reproduces the exact staleness this section is meant to resolve: merging an old tip either leaves the PR still CONFLICTING/behind, or falsely reports "already up to date" when it isn't.

create.md:84-93 already gets this right in the same skill — git fetch "$REMOTE" "$DEFAULT_BRANCH" immediately before computing the merge-base / merging. Worth adding the same fetch step here (or an explicit pointer to that snippet) before the git merge <remote>/<default-branch> instruction.

Fix this →


**Escalation guard** — after **3 fix iterations**, STOP. Present a history table. The root cause may be environmental.

Expand Down