From 5196367a9b97915dbccc8d6eba86d09a770f958b Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:58:10 -0400 Subject: [PATCH 1/2] fix(source-control): accept the folded security-review check in the merge gate ci-workflows#653 folds the security lane's status job into its review job and names that job `security-review`, so the lane's check becomes `security-review / security-review` (the context github-iac's security-review-gate ruleset names) and `claude-security-review-status` stops reporting. The babysit merge gate's `--auto` held on that missing check forever. AI_REVIEW_CHECKS now maps each lane to the job segments its check may carry, and every matching check must succeed, so a caller on an older pin still holds on a red `claude-security-review-status` beside its green `security-review` review job. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 11 +++++++ .../skills/babysit-prs/reference/safety.md | 9 +++--- .../babysit-prs/scripts/babysit_merge.py | 24 ++++++++++----- .../scripts/tests/test_babysit_merge.py | 30 +++++++++++++++++++ 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index eee31347d8..9065d9b9ab 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.76.0", + "version": "0.76.1", "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, which interviews the repo and writes 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", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index d12d356d3d..7b63579209 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,17 @@ 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.76.1] - 2026-10-02 + +### Fixed + +- **The babysit merge gate accepts the folded security lane's check.** The ci-workflows security + lane is becoming one job named `security-review`, so its check is + `security-review / security-review` and `claude-security-review-status` stops reporting. + `--auto` now takes either job name as the security lane's check, and every check that matches + must succeed, so a caller on an older pin still holds on a red `claude-security-review-status` + beside its green `security-review` job. + ## [0.76.0] - 2026-10-02 ### Changed diff --git a/plugins/source-control/skills/babysit-prs/reference/safety.md b/plugins/source-control/skills/babysit-prs/reference/safety.md index c871619626..8f5869626a 100644 --- a/plugins/source-control/skills/babysit-prs/reference/safety.md +++ b/plugins/source-control/skills/babysit-prs/reference/safety.md @@ -662,10 +662,11 @@ partition is the only class check, so the PR is already C2 (mechanical) or C3 (s `--auto`, a PR that is ready except for running checks gets `gh pr merge --auto --squash --match-head-commit ` instead of a hold, and only when: -- both AI review checks, `review / claude-review-status` and - `security-review / claude-security-review-status`, report success on the live head, which is - the pinned head (a missing, skipped, failed, or running check holds, and so does a head that - moved off the pin); +- both AI review checks, `review / claude-review-status` and the security lane's + `security-review / security-review`, report success on the live head, which is the pinned head + (a missing, skipped, failed, or running check holds, and so does a head that moved off the + pin). A caller pinned to a ci-workflows release that still runs a separate status job reports + `security-review / claude-security-review-status` too, and it must succeed as well; - no review thread is unresolved, and every other gate blocker is clear. Any other running check does not hold the arm: GitHub waits out a running required check diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py index 8658dc6ba0..4a4864b7e1 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py @@ -140,11 +140,21 @@ # pre-receive hooks (GHES) -- GitHub returns one OR the other, so both are ready. READY_MERGE_STATES = {"CLEAN", "HAS_HOOKS"} -# The two AI review status checks `--auto` waits for. `ci-status` is the only -# required check and does not wait on these separate workflows, so auto-merge -# armed before both pass on the live head could merge ahead of their review. -# Matched on the job segment of the check name (`review / claude-review-status`). -AI_REVIEW_CHECKS = ("claude-review-status", "claude-security-review-status") +# The two AI review lanes `--auto` waits for. `ci-status` is the only required +# check and does not wait on these separate workflows, so auto-merge armed +# before both pass on the live head could merge ahead of their review. Each +# lane maps to the job segments its check may carry (`review / +# claude-review-status`). The security lane is one job named +# `security-review`; a pin that predates that fold reports +# `claude-security-review-status` beside a review job of the same +# `security-review` name. Every matching check must succeed. +AI_REVIEW_CHECKS = { + "claude-review-status": ("claude-review-status",), + "claude-security-review-status": ( + "claude-security-review-status", + "security-review", + ), +} @dataclass(frozen=True) @@ -1225,12 +1235,12 @@ def evaluate( # check (which the check buckets count as success) passes. ai_review_holds = [ f"AI review check {lane!r} has not succeeded on the live head" - for lane in AI_REVIEW_CHECKS + for lane, names in AI_REVIEW_CHECKS.items() if not ( matches := [ c for c in checks["checks"] - if c["name"].rsplit("/", 1)[-1].strip() == lane + if c["name"].rsplit("/", 1)[-1].strip() in names ] ) or any(c["effective_state"] != "SUCCESS" for c in matches) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py index b131fccdfe..1d7124d6f0 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py @@ -1273,6 +1273,36 @@ def test_running_ai_lane_holds(self) -> None: ) self.assertFalse(result["autoMerge"]["ready"]) + def test_folded_security_lane_check_satisfies_the_security_lane(self) -> None: + result = self._evaluate( + [ + _check("ci-status", None), + _check("review / claude-review-status", "SUCCESS"), + _check("security-review / security-review", "SUCCESS"), + ], + mergeStateStatus="BLOCKED", + ) + self.assertTrue(result["autoMerge"]["ready"], result["autoMerge"]) + + def test_old_security_status_job_still_holds_beside_a_green_review_job( + self, + ) -> None: + result = self._evaluate( + [ + _check("ci-status", None), + _check("review / claude-review-status", "SUCCESS"), + _check("security-review / security-review", "SUCCESS"), + _check("security-review / claude-security-review-status", "FAILURE"), + ], + mergeStateStatus="BLOCKED", + ) + self.assertFalse(result["autoMerge"]["ready"], result["autoMerge"]) + self.assertIn( + "AI review check 'claude-security-review-status' has not " + "succeeded on the live head", + result["autoMerge"]["blockers"], + ) + def test_missing_or_skipped_ai_lane_holds(self) -> None: result = self._evaluate( [_check("ci-status", None), _check("claude-review-status", "SKIPPED")], From 052a7f0e9a2b2c211cf3ef97c6beebec250306c5 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:11:46 -0400 Subject: [PATCH 2/2] fix(source-control): match the folded security check by its whole name The babysit merge gate counted any check whose job segment was `security-review` as the security lane, so another workflow's job of that name could satisfy it. It now requires `security-review / security-review`. safety.md points at the ci-workflows reusables for the lane check names with an as-of date and recheck trigger, and the 0.77.1 entry is dated after 0.77.0. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/source-control/CHANGELOG.md | 9 +++++---- .../skills/babysit-prs/reference/safety.md | 17 +++++++++++++---- .../babysit-prs/scripts/babysit_merge.py | 19 +++++++++++++------ .../scripts/tests/test_babysit_merge.py | 19 +++++++++++++++++++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index ee9a09f842..bf75cf4668 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,16 +3,17 @@ 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.77.1] - 2026-10-02 +## [0.77.1] - 2026-10-03 ### Fixed - **The babysit merge gate accepts the folded security lane's check.** The ci-workflows security lane is becoming one job named `security-review`, so its check is `security-review / security-review` and `claude-security-review-status` stops reporting. - `--auto` now takes either job name as the security lane's check, and every check that matches - must succeed, so a caller on an older pin still holds on a red `claude-security-review-status` - beside its green `security-review` job. + `--auto` now takes `security-review / security-review` (whole name only) or the old status job as + the security lane's check, so another workflow's `security-review` job cannot satisfy it. Every + check that matches must succeed, so a caller on an older pin still holds on a red + `claude-security-review-status` beside its green `security-review` job. ## [0.77.0] - 2026-10-03 diff --git a/plugins/source-control/skills/babysit-prs/reference/safety.md b/plugins/source-control/skills/babysit-prs/reference/safety.md index aae8db61e9..e63b6eeae8 100644 --- a/plugins/source-control/skills/babysit-prs/reference/safety.md +++ b/plugins/source-control/skills/babysit-prs/reference/safety.md @@ -762,12 +762,21 @@ partition is the only class check, so the PR is already C2 (mechanical) or C3 (s `gh pr merge --auto --squash --match-head-commit ` instead of a hold, and only when: - both AI review checks, `review / claude-review-status` and the security lane's - `security-review / security-review`, report success on the live head, which is the pinned head - (a missing, skipped, failed, or running check holds, and so does a head that moved off the - pin). A caller pinned to a ci-workflows release that still runs a separate status job reports - `security-review / claude-security-review-status` too, and it must succeed as well; + `security-review / security-review` (matched by its whole name, never by the job segment alone), + report success on the live head, which is the pinned head (a missing, skipped, failed, or + running check holds, and so does a head that moved off the pin). Any + `claude-security-review-status` check the rollup also carries must succeed as well; - no review thread is unresolved, and every other gate blocker is clear. +The gate's check names follow the lane jobs the ci-workflows reusables define. + +- **Pointer**: when a lane check name in a rollup does not match the gate's, fetch the job keys + in [claude-review.yml](https://github.com/melodic-software/ci-workflows/blob/main/.github/workflows/claude-review.yml) + and [claude-security-review.yml](https://github.com/melodic-software/ci-workflows/blob/main/.github/workflows/claude-security-review.yml) + live. +- **As of**: 2026-10-03 +- **Recheck trigger**: a ci-workflows release that renames or adds a job in either reusable. + Any other running check does not hold the arm: GitHub waits out a running required check (`ci-status`) itself, and a non-required check never holds a merge. diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py index d169a1a30d..63c5700191 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py @@ -163,18 +163,25 @@ # check and does not wait on these separate workflows, so auto-merge armed # before both pass on the live head could merge ahead of their review. Each # lane maps to the job segments its check may carry (`review / -# claude-review-status`). The security lane is one job named -# `security-review`; a pin that predates that fold reports -# `claude-security-review-status` beside a review job of the same -# `security-review` name. Every matching check must succeed. +# claude-review-status`). A name holding ` / ` must match the whole check +# name: the security lane is one job named `security-review`, a name generic +# enough that another workflow's job could carry it, so it counts only as +# `security-review / security-review`. A pin that predates that fold reports +# `claude-security-review-status` beside it. Every matching check must succeed. AI_REVIEW_CHECKS = { "claude-review-status": ("claude-review-status",), "claude-security-review-status": ( "claude-security-review-status", - "security-review", + "security-review / security-review", ), } + +def is_ai_review_check(check_name: str, names: tuple[str, ...]) -> bool: + full = " / ".join(part.strip() for part in check_name.split("/")) + segment = full.rsplit(" / ", 1)[-1] + return any(full == n if " / " in n else segment == n for n in names) + # The async merge API (`PUT .../pulls/{n}/merge-async`) answers with a request # UUID and runs the merge in the background; the gate polls it to a terminal # status for at most this long. A request still pending past the bound is left @@ -1453,7 +1460,7 @@ def evaluate_layer(layer_number: int, layer_head: str) -> dict[str, Any]: matches := [ c for c in checks["checks"] - if c["name"].rsplit("/", 1)[-1].strip() in names + if is_ai_review_check(c["name"], names) ] ) or any(c["effective_state"] != "SUCCESS" for c in matches) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py index e5eabfefc6..2c62155184 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_merge.py @@ -1284,6 +1284,25 @@ def test_folded_security_lane_check_satisfies_the_security_lane(self) -> None: ) self.assertTrue(result["autoMerge"]["ready"], result["autoMerge"]) + def test_another_workflows_security_review_job_does_not_satisfy_the_lane( + self, + ) -> None: + result = self._evaluate( + [ + _check("ci-status", None), + _check("review / claude-review-status", "SUCCESS"), + _check("scanner / security-review", "SUCCESS"), + _check("security-review", "SUCCESS"), + ], + mergeStateStatus="BLOCKED", + ) + self.assertFalse(result["autoMerge"]["ready"], result["autoMerge"]) + self.assertIn( + "AI review check 'claude-security-review-status' has not " + "succeeded on the live head", + result["autoMerge"]["blockers"], + ) + def test_old_security_status_job_still_holds_beside_a_green_review_job( self, ) -> None: