Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d775a4d
feat(source-control): babysit autopilot merge tier, gate-enforced and…
kyle-sexton Jul 20, 2026
a926807
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
da239d8
fix(source-control): keep babysit SKILL.md under the 500-line skill-q…
kyle-sexton Jul 20, 2026
fddd27c
fix(source-control): harden babysit autopilot-merge-tier human-veto a…
kyle-sexton Jul 20, 2026
9912da4
fix(source-control): restrict babysit tier approval to the configured…
kyle-sexton Jul 20, 2026
a12e770
feat(source-control): add decision-default veto criterion to the baby…
kyle-sexton Jul 20, 2026
d8b561d
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
d3c9bbf
fix(source-control): require an explicit ratification signal to clear…
kyle-sexton Jul 20, 2026
ec9fc5b
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
a9bb74d
fix(source-control): read decision-default veto comments from the ref…
kyle-sexton Jul 20, 2026
871a9e8
fix(source-control): honor later maintainer revocations of a ratified…
kyle-sexton Jul 20, 2026
d9b19d6
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
c404522
fix(source-control): classify configured approver/lane logins as bots…
kyle-sexton Jul 20, 2026
2cc68c4
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
ab9d9e5
test(source-control): assert not-ready on case-insensitive do-not-mer…
kyle-sexton Jul 20, 2026
88ee348
Merge remote-tracking branch 'origin/main' into feat/476-autopilot-me…
kyle-sexton Jul 20, 2026
b3bedc9
fix(source-control): reject approver-bot approvals that report blocki…
kyle-sexton Jul 20, 2026
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
26 changes: 25 additions & 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.14.0",
"version": "0.15.0",
"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), /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 Expand Up @@ -51,6 +51,30 @@
"title": "Babysit merge method",
"description": "Merge method for gate-proven merges: merge, squash, or rebase. Absent: repo convention, then squash."
},
"babysit_autopilot_merge_tier": {
"type": "boolean",
"title": "Babysit autopilot merge tier",
"description": "Enable the #476 autopilot merge tier: a distinct bot account submits a genuine approving review, then the gate merges only when every criterion holds (issue-linked, lane-authored, no do-not-merge label, distinct-bot approval on the live head, no human blocking comment). Ships DISABLED; a deliberate operator opt-in. Requires babysit_lane_logins, babysit_approver_bot_logins, and babysit_merge_block_labels to be set. Absent/false: the tier does not exist and PRs go to the human merge-ready list.",
"default": false
},
"babysit_lane_logins": {
"type": "string",
"multiple": true,
"title": "Babysit pipeline lane logins",
"description": "Author logins recognized as pipeline lanes for the autopilot merge tier's lane-authored criterion. Absent: the tier (when enabled) refuses fail-closed."
},
"babysit_approver_bot_logins": {
"type": "string",
"multiple": true,
"title": "Babysit approver bot logins",
"description": "Bot logins whose approving review satisfies the autopilot merge tier's author != approver criterion. Absent: the tier (when enabled) refuses fail-closed."
},
"babysit_merge_block_labels": {
"type": "string",
"multiple": true,
"title": "Babysit merge block labels",
"description": "Labels that veto an autopilot-merge-tier merge, e.g. do-not-merge. Absent: the tier (when enabled) refuses fail-closed."
},
"babysit_review_trigger_phrase": {
"type": "string",
"title": "Babysit review trigger phrase",
Expand Down
26 changes: 26 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
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.15.0]

### Added

- **`babysit-prs` autopilot merge tier (#476), shipped DISABLED behind an explicit operator
flag.** At day-scale throughput, human approve-and-merge is the pipeline bottleneck. The new
tier lets the fleet satisfy the branch ruleset instead of bypassing it: a second bot account
(author ≠ approver) runs a genuine review pass through the review plugin and submits an
approving review only when clean, after which the pinned merge gate merges **only when every
criterion holds** — required checks green including the review workflow (`mergeStateStatus`
CLEAN, ruleset untouched), issue-linked, authored by a configured pipeline lane, no human
`CHANGES_REQUESTED` / blocking comment / unresolved thread, no configured do-not-merge label,
no unratified `Decision defaulted` marker on the linked issue (the triage lane's maintainer
veto window, which a maintainer ratifies by comment before the default rides into a merge),
and a distinct-bot approval on the live head (head SHA unchanged since review). Any criterion
failing falls back to today's behavior: the PR is reported on the human merge-ready list. The
gate flag `--autopilot-merge-tier` is **fail-closed** — it refuses unless `--lane-logins`,
`--approver-bot-logins`, and `--block-labels` are all supplied — and every criterion predicate
is reused from the shared `babysit_classify` module rather than re-implemented. The tier exists
only while `babysit_autopilot_merge_tier` is enabled (new boolean userConfig, default off);
enabling it and any later gate-off flip is a separate, announced operator step. New userConfig:
`babysit_autopilot_merge_tier`, `babysit_lane_logins`, `babysit_approver_bot_logins`,
`babysit_merge_block_labels`. Absent the flag the merge gate is byte-for-byte its prior self, so
worker/autopilot's existing gate-proven merges are unchanged. `safety.md`'s "Never do
automatically: merge" contract is updated deliberately to codify the tier and its criteria.

## [0.14.0]

### Added
Expand Down
9 changes: 9 additions & 0 deletions plugins/source-control/skills/babysit-prs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ it does **not** widen the owner allowlist, and it does not gain force-push, `--a
settings powers — those still escalate. Run it looped:
`/loop 15m /source-control:babysit-prs autopilot`.

## Autopilot merge tier (#476)

A config-gated escalation of autopilot's merge authority, **shipped DISABLED** and active only while the operator sets `babysit_autopilot_merge_tier` (enabling it, and the later gate-off flip, are separate announced steps; without it every merge decision is exactly today's). When enabled, per candidate PR autopilot runs a **genuine review pass** under a **second bot account** (author ≠ approver) that submits an approving review **only when clean**, then runs the pinned merge gate with the tier flags (`--autopilot-merge-tier --lane-logins <lane-logins> --approver-bot-logins <approver-bot-logins> --block-labels <merge-block-labels>`) added to `--merge --expected-head <post-push-head-sha>`.
Comment thread
kyle-sexton marked this conversation as resolved.
That gate merges **only when every criterion holds** — the criteria and the safety-contract rationale are codified in [reference/safety.md](reference/safety.md). It is **fail-closed** (the umbrella flag refuses unless all three parameter sets are supplied; predicates reused from the shared `babysit_classify` module), and any criterion failing falls back to the human merge-ready list — the tier never routes around the gate.

## Guarded mutations: deterministic gates, agent judgment

The two mutation gates are invoked ONLY by their bare wrapper names (they resolve their own
Expand Down Expand Up @@ -288,6 +293,10 @@ tier authority.
| `babysit_intended_write_identity` | `${user_config.babysit_intended_write_identity}` | `--intended-write-identity` (snapshot) | attribution-drift check dormant |
| `babysit_default_tier` | `${user_config.babysit_default_tier}` | prose only — tier of explicit bare invocations | `safe` |
| `babysit_merge_method` | `${user_config.babysit_merge_method}` | `--method` (merge wrapper) | repo convention, then squash |
| `babysit_autopilot_merge_tier` | `${user_config.babysit_autopilot_merge_tier}` | prose only — gates whether the tier's `--autopilot-merge-tier` merge flags are wired at all | `false` (tier disabled; PRs go to the human merge-ready list) |
| `babysit_lane_logins` | `${user_config.babysit_lane_logins}` | `--lane-logins` (merge wrapper, autopilot merge tier) | tier refuses fail-closed when enabled |
| `babysit_approver_bot_logins` | `${user_config.babysit_approver_bot_logins}` | `--approver-bot-logins` (merge wrapper, autopilot merge tier) | tier refuses fail-closed when enabled |
| `babysit_merge_block_labels` | `${user_config.babysit_merge_block_labels}` | `--block-labels` (merge wrapper, autopilot merge tier) | tier refuses fail-closed when enabled |
| `babysit_review_trigger_phrase` | `${user_config.babysit_review_trigger_phrase}` | `--trigger-phrase` (snapshot, request_review) | review-trigger module dormant |
| `babysit_review_bot_logins` | `${user_config.babysit_review_bot_logins}` | `--review-bot-logins` (snapshot, request_review) | review-trigger module dormant |
| `babysit_review_gate_context` | `${user_config.babysit_review_gate_context}` | `--review-gate-context` (snapshot) | gate treated as absent |
Expand Down
45 changes: 44 additions & 1 deletion plugins/source-control/skills/babysit-prs/reference/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ auto-mode safety classifier and blocks the call before the wrapper runs.
to merge on an unprotected repository — zero required reviews AND zero required status contexts
— when the PR author is not one of `<self-logins>`, absent `--allow-unprotected`. Both
overrides are human decisions, never passed autonomously.
- The merge wrapper's `--autopilot-merge-tier` flag layers the #476 tier criteria (issue-linked,
lane-authored, no blocking label, a distinct-bot approval on the live head, no human blocking
comment) onto the base gate. It is **fail-closed**: the umbrella flag refuses (exit `3`) unless
`--lane-logins`, `--approver-bot-logins`, and `--block-labels` are all non-empty, and supplying
any of those three without the umbrella is a usage error (exit `2`). Absent the flag the gate is
exactly its prior self, so worker/autopilot's existing gate-proven merges are unchanged. This
tier is only ever wired when `babysit_autopilot_merge_tier` is enabled.
- The resolve wrapper's mutating forms are `--autonomous --resolve` (worker tier, constrained by
the pre-push-outdated rule in `orchestration.md`) and `--resolve --include-human` (autopilot's
addressed-thread widening).
Expand Down Expand Up @@ -221,7 +228,43 @@ as done and re-running the gate.
## Never Do Automatically

- Merge in default (safe) mode, or merge through any path other than the pinned merge wrapper's
gate.
gate. Worker and autopilot merge only a PR that gate proves 100% ready.
- Generate an approving review to satisfy a required-review ruleset, or merge on a review the
fleet produced itself — **except** under the autopilot merge tier (#476), a deliberate,
config-gated opt-in that ships **DISABLED**. It engages only when the operator sets
`babysit_autopilot_merge_tier`; enabling that flag, and any later gate-off flip, is a
separate, loudly-announced operator step, never a default and never a side effect of another
change. When the tier is enabled, a second bot account (author ≠ approver) runs a **genuine**
review pass and submits an approving review **only when it is clean**, and the pinned merge
wrapper's `--autopilot-merge-tier` gate then merges **only when every criterion holds**, each
enforced deterministically:
- required checks green, including the review workflow, with the base ruleset satisfied
(`mergeStateStatus` CLEAN — the ruleset itself is never bypassed);
- the PR is issue-linked (carries a closing-issue reference);
- the PR is authored by a configured pipeline lane;
- no human `CHANGES_REQUESTED`, no human blocking comment, no unresolved review thread;
- no configured do-not-merge label is present;
- the PR's linked issue carries no unratified `Decision defaulted` marker — the triage lane
records a defaulted (maintainer-vetoable) decision only as a `Decision defaulted: X — veto
before merge` issue comment, invisible to the gate, so the default rides into an autopilot
merge only once a maintainer has **ratified** it: a human `OWNER`/`MEMBER` comment posted
after the marker carrying an explicit ratification signal — a closed, whole-word token set
(`ratify`/`ratified`, `approve`/`approved`, `confirm`/`confirmed`), and not a
withheld-approval negation (`not approved`, `cannot approve`). All maintainer comments
after the marker are scanned and the **latest decisive signal wins**: a ratification token
ratifies, while a revocation reusing the veto vocabulary (`not approved`, `do not merge`)
re-holds, so a maintainer who ratifies and then revokes holds the PR. Matching is strict and
fail-closed: an unrelated maintainer comment, a signal appearing before the marker, a
ratify/revoke tie at the same timestamp, an unratified marker, or an issue whose comments
cannot be read all hold the PR;
- the approving review is by a **distinct bot identity** (author ≠ approver) and was
submitted against the **live head** (head SHA unchanged since review), pinned as always by
`--expected-head`.

Any criterion failing falls back to today's behavior — the PR is reported on the human
merge-ready list. The tier never routes around the gate and never rubber-stamps: the bot
review is a real review pass, and the ruleset stays meaningful. Absent the enable flag this
tier does not exist and the first bullet governs unchanged.
- Enable auto-merge.
- Force-push.
- Rebase or force-update a PR branch as freshness maintenance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,18 +212,28 @@ def is_bot(
def actor_kind(
item: dict[str, Any], config: FeedbackConfig = DEFAULT_FEEDBACK_CONFIG
) -> str:
"""Classify actors from authoritative type metadata, then exact fallbacks."""
"""Classify actors from authoritative type metadata, then exact fallbacks.

An explicitly configured `extra_bot_logins` identity outranks a `User`
typename: a bot account whose metadata misreports it as a user is the one
case that field exists for, so the operator's declaration wins over the
structural signal, matching `is_bot`, which already ignores any non-`Bot`
typename. An unconfigured caller (the empty default) never reaches this
override and classifies from structure alone.
"""
author = item.get("author")
login = author_login(item).casefold()
if is_json_object(author):
typename = str(author.get("__typename") or "")
if typename == "Bot" or author.get("is_bot") is True:
return "bot"
if config.extra_bot_logins and is_bot(login, None, config.extra_bot_logins):
return "bot"
if (
typename in {"Mannequin", "Organization", "User"}
or author.get("is_bot") is False
):
return "human"
login = author_login(item).casefold()
return "bot" if is_bot(login, None, config.extra_bot_logins) else "human"


Expand Down
Loading
Loading