From 1f4ec3f851caeb1c68e6e6b7198cafd13e7ee02a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 14:39:36 +0000 Subject: [PATCH 1/2] fix(source-control): exclude self-logins from automation human-stop gate Fixes #902 Add human_stop.external_required for freshness refresh and review triggers while human_stop.required still blocks merge for solo maintainer stops. Co-authored-by: Kyle Sexton --- .../source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 10 ++++++ .../babysit-prs/scripts/babysit_delta.py | 8 ++++- .../babysit-prs/scripts/babysit_feedback.py | 31 ++++++++++++++++++- .../babysit-prs/scripts/refresh_pr_branch.py | 6 ++-- .../babysit-prs/scripts/request_review.py | 6 ++-- .../scripts/tests/test_babysit_delta.py | 1 + 7 files changed, 55 insertions(+), 9 deletions(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 7f85f8d33..9cc00eace 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.53.11", + "version": "0.53.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 \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", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 187baabaf..8d853d961 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,7 @@ 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. +<<<<<<< HEAD ## [0.53.11] ### Fixed @@ -10,6 +11,15 @@ All notable changes to the `source-control` plugin are documented here. Format f - **Finding-extractor reads anchor to the PR worktree cwd (#2454).** Review-discipline dispatch text now substitutes `` and quotes it in `git -C` examples so paths with spaces stay valid. +======= +## [0.53.10] + +### Fixed + +- **Branch refresh and review triggers no longer halt on self-authored human stops (#902).** + `human_stop.external_required` excludes configured self-logins from automation gates while + `human_stop.required` still blocks merge when the maintainer posts under their own login. +>>>>>>> a9b3d4b5 (fix(source-control): exclude self-logins from automation human-stop gate) ## [0.53.9] diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py index b7ac6aa08..12f46bf23 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py @@ -464,12 +464,19 @@ def classify_pr( dispositions = dict(prev.get("feedback_dispositions") or {}) feedback = collect_feedback(pr, inline_comments, dispositions, config.feedback) review_decision = str(pr.get("reviewDecision") or "") + self_logins = normalize_self_logins(config.self_logins) human_changes_requested = any( item.get("kind") == "review" and item.get("state") == "CHANGES_REQUESTED" for item in feedback["human_blocking"] ) + external_human_blocking = [ + item + for item in feedback["human_blocking"] + if not is_self_login(item.get("author"), self_logins) + ] human_stop = { "required": bool(feedback["human_blocking"]), + "external_required": bool(external_human_blocking), "human_changes_requested": human_changes_requested, "human_blocking_count": len(feedback["human_blocking"]), } @@ -581,7 +588,6 @@ def classify_pr( # gate. Filtering it there instead would silently strip the solo maintainer's # ability to human-stop their own PR. Here it only stops re-dispatching a # worker onto the engine's own prior output. - self_logins = normalize_self_logins(config.self_logins) new_blocking_feedback = [ item for item in feedback["blocking"] if item["id"] not in prev_blocking_ids ] diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py index f95bd15c6..0337bb447 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py @@ -55,6 +55,7 @@ "body_text", "collect_feedback", "fetch_current_human_stop", + "human_stop_blocks_automation", "has_blocking_severity", "has_blocking_text", "human_stop_state", @@ -258,14 +259,27 @@ def human_stop_state( pr: dict[str, Any], inline_comments: list[dict[str, Any]] | None, config: FeedbackConfig = DEFAULT_FEEDBACK_CONFIG, + *, + self_logins: frozenset[str] | None = None, ) -> dict[str, Any]: feedback = collect_feedback(pr, inline_comments, config=config) human_changes_requested = any( item.get("kind") == "review" and item.get("state") == "CHANGES_REQUESTED" for item in feedback["human_blocking"] ) + normalized_self = ( + normalize_self_logins(self_logins) if self_logins is not None else frozenset() + ) + external_blocking = [ + item + for item in feedback["human_blocking"] + if not is_self_login(item.get("author"), normalized_self) + ] return { "required": bool(feedback["human_blocking"]), + # Self-authored classification replies must not block freshness refresh or + # review triggers (#902); merge/triage still consult `required`. + "external_required": bool(external_blocking), "human_changes_requested": human_changes_requested, "human_blocking_count": len(feedback["human_blocking"]), } @@ -276,9 +290,24 @@ def fetch_current_human_stop( number: int, pr: dict[str, Any], config: FeedbackConfig = DEFAULT_FEEDBACK_CONFIG, + *, + self_logins: frozenset[str] | None = None, ) -> dict[str, Any]: hydrated = dict(pr) hydrated["comments"] = fetch_issue_comments(repo, number) rest_hydrate_reviews(hydrated, repo, number) inline_comments = fetch_unresolved_review_comments(repo, number) - return human_stop_state(hydrated, inline_comments, config) + return human_stop_state( + hydrated, inline_comments, config, self_logins=self_logins + ) + + +def human_stop_blocks_automation(human_stop: dict[str, Any]) -> bool: + """Return whether automation (refresh, review trigger) must halt. + + Prefer `external_required` when present (#902): self-authored lane replies + stay in `required` for merge/triage but must not block freshness refresh. + """ + if "external_required" in human_stop: + return bool(human_stop.get("external_required")) + return bool(human_stop.get("required")) diff --git a/plugins/source-control/skills/babysit-prs/scripts/refresh_pr_branch.py b/plugins/source-control/skills/babysit-prs/scripts/refresh_pr_branch.py index e6ebad592..7a5f62cd8 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/refresh_pr_branch.py +++ b/plugins/source-control/skills/babysit-prs/scripts/refresh_pr_branch.py @@ -12,7 +12,7 @@ import babysit_lease as leases from babysit_delta import compute_branch_freshness, head_repository_scope -from babysit_feedback import fetch_current_human_stop +from babysit_feedback import fetch_current_human_stop, human_stop_blocks_automation from babysit_gh import ( find_open_prs_for_head_ref, gh_json, @@ -64,7 +64,7 @@ def validate_current_candidate( raise RuntimeError("PR has a merge conflict; refusing branch refresh") if compute_branch_freshness(current)["state"] != "behind": raise RuntimeError("PR is no longer behind its base branch") - if fetch_current_human_stop(repo, number, current)["required"]: + if human_stop_blocks_automation(fetch_current_human_stop(repo, number, current)): raise RuntimeError("human review stop is active; refusing branch refresh") matching_prs = find_open_prs_for_head_ref(current) if matching_prs != [f"{repo}#{number}"]: @@ -125,7 +125,7 @@ def run_locked( "branch_write_allowed" ): raise RuntimeError("snapshot does not allow writes to this PR head repository") - if json_object(pr_state.get("human_stop")).get("required"): + if human_stop_blocks_automation(json_object(pr_state.get("human_stop"))): raise RuntimeError("snapshot has an active human review stop") uniqueness = json_object(pr_state.get("head_ref_uniqueness")) if not uniqueness.get("checked") or not uniqueness.get("unique"): diff --git a/plugins/source-control/skills/babysit-prs/scripts/request_review.py b/plugins/source-control/skills/babysit-prs/scripts/request_review.py index e6b5941a2..91766df22 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/request_review.py +++ b/plugins/source-control/skills/babysit-prs/scripts/request_review.py @@ -13,7 +13,7 @@ import babysit_lease as leases from babysit_delta import compute_branch_freshness, head_repository_scope -from babysit_feedback import fetch_current_human_stop +from babysit_feedback import fetch_current_human_stop, human_stop_blocks_automation from babysit_gh import ( flatten_paginated_items, gh_json, @@ -135,7 +135,7 @@ def validate_current_candidate( reaction_signals, expected_head_sha, review_trigger ) human_stop = fetch_current_human_stop(repo, number, current) - if human_stop["required"]: + if human_stop_blocks_automation(human_stop): raise RuntimeError( "human review stop is active; refusing to post a review trigger" ) @@ -324,7 +324,7 @@ def run_locked( raise RuntimeError( "snapshot base repository is outside configured owners or archived" ) - if json_object(pr_state.get("human_stop")).get("required"): + if human_stop_blocks_automation(json_object(pr_state.get("human_stop"))): raise RuntimeError("snapshot has an active human review stop") allowed_owners = allowed_owners_from_policy(mutation_policy) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py index dc1abebc4..f0e19ef5d 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py @@ -451,6 +451,7 @@ def test_self_login_item_is_still_classified_human_blocking(self) -> None: for item in result["feedback"]["human_blocking"]] self.assertEqual(authors, ["solo"]) self.assertTrue(result["human_stop"]["required"]) + self.assertFalse(result["human_stop"]["external_required"]) self.assertFalse(result["pr_clean_ready_for_direct_gate"]) def test_other_login_still_fires_under_same_config(self) -> None: From c65169a16dc92e7dbc99ab4a54d3f6929eb8fd4b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 17:26:53 +0000 Subject: [PATCH 2/2] chore: bump plugin versions for changelog parity (#2469) Co-authored-by: Kyle Sexton --- plugins/source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 9cc00eace..0420be57c 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.53.12", + "version": "0.53.13", "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", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 8d853d961..b3e5fd207 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to the `source-control` plugin are documented here. Format f [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. <<<<<<< HEAD + +## [0.53.13] + +### Fixed + +- Exclude self-logins from automation human-stop gate. + ## [0.53.11] ### Fixed