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
4 changes: 2 additions & 2 deletions 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.34.0",
"version": "0.34.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 — 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 @@ -33,7 +33,7 @@
"type": "string",
"multiple": true,
"title": "Babysit extra self identities",
"description": "Extra GitHub posting identities (e.g. a project bot account) added to your `gh api user` login — the self set babysit-prs treats as its own: self-comment suppression, same-login classification, readiness-gate classification rows, and the merge-gate self-exemption. Not a discovery filter — which authors' PRs the queue discovers is `--author`'s job, independent of this set. Absent: your gh login alone."
"description": "Extra GitHub posting identities (e.g. a project bot account) added to your `gh api user` login — the self set babysit-prs treats as its own: self-comment suppression, same-login classification, readiness-gate classification rows, the merge-gate self-exemption, and the resolve-thread bot-only test (a self-authored reply to a bot thread no longer counts as a disqualifying human participant). Not a discovery filter — which authors' PRs the queue discovers is `--author`'s job, independent of this set. Absent: your gh login alone."
},
"babysit_intended_write_identity": {
"type": "string",
Expand Down
53 changes: 53 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,59 @@
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.34.1]

### Fixed

- **A worker's own reply to a bot review thread could strand it outside every resolution scope
forever (#1729).** `babysit_resolve_thread.py`'s `project_thread` inspects EVERY fetched
comment for `botOnly`, not just the opener, so a bot-started thread carrying a later human reply
is correctly excluded from the default bot-only scope. But the worker's own documented reply to a
bot thread -- a classification reply, a `Fixed in <sha>` follow-up (`reference/orchestration.md`)
-- is a real API comment too, posted under the worker's own login, not the bot's. Before this
fix that reply was indistinguishable from a genuine third-party human joining the thread:
`botOnly` flipped `false` the moment it posted, which locked the thread out of the default
bot-only scope, and `--include-human` stays unset by design in worker/safe modes (it must never
touch a genuine human thread) so nothing lifted it back in -- a bot thread the worker correctly
replied to became permanently unresolvable by the normal flow, even though replying was exactly
the right action.
- `babysit_resolve_thread.py` gains a `--self-logins` flag (`@me` plus `babysit_self_logins`
extras, mirroring `babysit_merge.py`'s existing flag of the same name and the
`babysit_self_logins` userConfig key, which was never threaded through this script).
`project_thread`'s `botOnly` now treats a self-login-authored comment as a third admissible
authorship alongside bot and third-party human, admissible as a REPLY only: it does not
disqualify a bot-opened thread (unlike a genuine human reply), but neither can it open one.
`botOnly` requires the thread's OPENING comment to be bot-authored, which is what
`reference/review-discipline.md` D7.5 actually scopes resolution to ("resolve ONLY threads
whose OPENING comment is authored by a BOT reviewer... NEVER resolve your OWN threads") and
the same opening-author test `humanThreadsActed` has applied since #512. Neutralizing self
authorship against a weaker "some participant is a bot" test would have opened the converse
hole -- a SELF-OPENED thread would become `botOnly` the moment a bot replied to it, making the
caller's own thread resolvable with no `--include-human` and, once outdated, under
`--autonomous`. `botOnly` fails closed when the opening comment cannot be attributed at all
(no fetched comments, or an opener whose author the API withheld), as it already did on a
truncated comment page. `--self-logins` is resolved after the owner-scope refusal, mirroring
`babysit_merge.py`'s ordering, so an out-of-scope owner still refuses with no `gh` invocation
for `@me` resolution.
- Fixing `botOnly` alone was not sufficient: the mandated classification-reply table
(`reference/review-discipline.md`) restates the source finding's own severity marker (e.g. a
`CRITICAL`/`P1` column, or "VALID -- not a security concern") as part of the worker's own
reply, so a raw severity scan over that self-reply would re-trip `--autonomous`'s
`skipped-severity-marked` guard the moment `botOnly` stopped blocking it -- the stranding just
moved to a different verdict. `project_thread`'s severity scan now strips a self-authored
comment's classification-table rows (not its whole body) before scanning, mirroring
`babysit_classify.count_findings`'s identical rule for the finding-count gate; the underlying
`_strip_classification_rows` helper is promoted to public (`strip_classification_rows`) and
shared between the two modules rather than reimplemented. Non-table self content -- a
maintainer using a self-login to raise a genuine new finding -- still flags.
- `SKILL.md`'s thread-resolution bullet and flag-delivery table, `reference/safety.md`'s
documented resolve-thread command forms (both the read-only listing form and both
pinned-command-degradation forms), and `reference/orchestration.md`'s Worker Contract and
Worker Prompt Template now carry `--self-logins @me,<self-logins>` alongside
`--extra-bot-logins`, so every copyable resolve-thread command actually threads the caller's
own identity through. `plugin.json`'s `babysit_self_logins` description now names the
resolve-thread bot-only test among the surfaces the self set covers.

## [0.34.0]

### Changed
Expand Down
11 changes: 5 additions & 6 deletions plugins/source-control/skills/babysit-prs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,10 @@ home in [reference/safety.md](reference/safety.md). Both fail closed without `--
exactly as it governs a worker's turn — proving readiness is never a license to arm a watch.

- **Thread resolution** — `source-control-babysit-resolve-thread owner/repo#N --allowed-owners
<watched-owners> --extra-bot-logins <extra-bot-logins>` (lists by default; add `--resolve`). By
default it touches only bot-authored threads (structural `__typename == "Bot"` or the `[bot]`
login suffix — no hardcoded identity list) and never a human thread; `--extra-bot-logins` extends
that set with the configured non-structural bot accounts, and dropping it from any resolve-thread
form silently reclassifies their threads as human. In worker tier pass `--autonomous`, which
<watched-owners> --extra-bot-logins <extra-bot-logins> --self-logins @me,<self-logins>` (lists by
default; add `--resolve`). By default it touches only bot-authored threads (structural
`__typename == "Bot"` or the `[bot]` login suffix — no hardcoded identity list) and never a human
thread; `--extra-bot-logins` extends that set with the configured non-structural bot accounts (dropping it silently reclassifies their threads as human), and `--self-logins` rides on every form too — omitting it lets the worker's OWN bot-thread reply flip `botOnly` false and strand the thread outside every resolution scope (safety.md). In worker tier pass `--autonomous`, which
resolves only threads GitHub marks `isOutdated`, each pinned via `--expected-comment-count` and
`--expected-last-updated`. Those pins enforce comment-state only — they block a thread whose
comment count or latest comment-edit timestamp drifted after vetting. The worker must additionally
Expand Down Expand Up @@ -291,7 +290,7 @@ this block. Values reach scripts ONLY as explicit CLI flags (option environment
| Key | Value | Flag delivery | Unset behavior |
| --- | --- | --- | --- |
| `babysit_watched_owners` | `${user_config.babysit_watched_owners}` | `--owners` (snapshot), `--allowed-owners` (both wrappers, fail-closed) | infer the current repo's owner |
| `babysit_self_logins` | `${user_config.babysit_self_logins}` | `--extra-self` (readiness gate and snapshot); `--self-logins` (merge gate) | none — always added to your `gh api user --jq .login` login |
| `babysit_self_logins` | `${user_config.babysit_self_logins}` | `--extra-self` (readiness gate and snapshot); `--self-logins` (merge gate, resolve-thread) | none — always added to your `gh api user --jq .login` login |
| `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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ Each worker must:
- **auto-resolve only pre-push-outdated threads.** A worker may resolve a review thread only when
that thread was already `isOutdated` in the pre-push snapshot it was dispatched with, and only
through `bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread" owner/repo#42 --allowed-owners <watched-owners>
--extra-bot-logins <extra-bot-logins> --autonomous --resolve` pinned with `--thread-id`, `--expected-comment-count`, and
--extra-bot-logins <extra-bot-logins> --self-logins @me,<self-logins> --autonomous --resolve` pinned with `--thread-id`, `--expected-comment-count`, and
`--expected-last-updated` taken from that same snapshot (`safety.md`, thread-pin pair rule). A
thread that became outdated only because of the worker's own push has not thereby been addressed
— the push moving the diff under a finding does not answer the finding — so it is never
Expand Down Expand Up @@ -831,8 +831,8 @@ Never refresh branches, post review triggers, merge, enable auto-merge, force-pu
GitHub settings, or auto-fix human-authored feedback — classify, reply with evidence, and
surface human items instead. You may resolve a review thread only if it appears in the pre-push
outdated-thread list above, via bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread"
owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --autonomous
--resolve --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>, with the pins
owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --self-logins
@me,<self-logins> --autonomous --resolve --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>, with the pins
taken from that list; a
thread that becomes outdated only because of your own push is not addressed by that push — leave
it. Never arm a background monitor or poll loop waiting on CI — check once, report exactly what
Expand Down
21 changes: 17 additions & 4 deletions plugins/source-control/skills/babysit-prs/reference/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,27 @@ auto-mode safety classifier and blocks the call before the wrapper runs.
- Both wrappers **fail closed**: invoked without `--allowed-owners`, they exit `3` and refuse to
act. The read-only forms are `source-control-babysit-merge owner/repo#42 --allowed-owners
<watched-owners>` (merge-readiness gate) and `source-control-babysit-resolve-thread
owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins>`
(thread list).
owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins>
--self-logins @me,<self-logins>` (thread list).
- **`--extra-bot-logins <extra-bot-logins>` rides on every resolve-thread form**, listing and
mutating alike, whenever `babysit_extra_bot_logins` is configured. Bot classification is what
decides which threads the resolver may touch at all, and structural detection cannot see a
registered non-structural bot account (no `[bot]` suffix, API `__typename` of `User`); omitting
the flag silently reclassifies that account's threads as human and skips them in worker tier.
Omit the flag only when the key is unset.
- **`--self-logins @me,<self-logins>` rides on every resolve-thread form too**, listing and
mutating alike, always (`@me` resolves your own `gh` login; append `babysit_self_logins`
extras). The bot-only classifier (`project_thread`'s `botOnly`) requires a BOT OPENER **and**
inspects every other fetched participant — so the worker's OWN reply to a bot thread (a
classification reply, a `Fixed in <sha>` follow-up) is itself a comment the classifier sees.
Without `--self-logins` that reply is indistinguishable from a genuine third-party human joining the
thread: `botOnly` goes false, which locks the thread out of the default bot-only scope, and
`--include-human` stays unset by design in worker/safe modes — so nothing lifts it back in and a
bot thread the worker correctly handled is permanently unresolvable by the normal flow.
`--self-logins` marks the caller's own posting identity as neutral for that test instead —
neutral as a REPLY only: the OPENING comment must still be an ACTUAL bot's, so a thread the
worker itself opened stays out of scope even after a bot replies to it (`review-discipline.md`
D7.5 forbids resolving your own threads). Omit the flag only when `babysit_self_logins` is unset.
- The merge wrapper mutates only with `--merge --expected-head <post-push-head-sha> --method
<merge-method>`, and rejects `--allow-unpinned-head` outright — there is no unpinned merge. The
expected-head pin semantics live in `SKILL.md`; do not re-derive them here.
Expand Down Expand Up @@ -584,13 +597,13 @@ assessment. Pin each vetted thread individually (the wrapper accepts exactly one
per invocation; issue one pinned command per thread) with the thread-pin pair rule above:

```text
bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread" owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --autonomous --resolve --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>
bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread" owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --self-logins @me,<self-logins> --autonomous --resolve --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>
```

for the unattended-worker case, or

```text
bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread" owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --resolve --include-human --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>
bash "${CLAUDE_PLUGIN_ROOT}/bin/source-control-babysit-resolve-thread" owner/repo#42 --allowed-owners <watched-owners> --extra-bot-logins <extra-bot-logins> --self-logins @me,<self-logins> --resolve --include-human --thread-id <id> --expected-comment-count <n> --expected-last-updated <ts>
```

for the autopilot case. This degradation is a successful, material finding to report, not a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,12 +508,22 @@ def count_findings(
author = str(comment.get("author") or "")
body = str(comment.get("body") or "")
if is_self_login(author, self_logins):
body = _strip_classification_rows(body)
body = strip_classification_rows(body)
total += _severity_occurrences(body)
return total


def _strip_classification_rows(body: str) -> str:
def strip_classification_rows(body: str) -> str:
"""Drop classification-table rows (VALID/INCORRECT/UNCERTAIN) from a body.

Shared beyond `count_findings`: any severity scan over a SELF-authored
body faces the same false-positive -- the mandated disposition table
(`review-discipline.md`) restates the source finding's own severity marker
as one of its columns, so scanning it raw would re-trip a severity guard
on the worker's own echo of a finding it already classified. Stripping
only the table rows (not the whole body) preserves the "non-table self
content still counts" rule `count_findings` documents.
"""
return "\n".join(
line for line in body.splitlines() if not CLASSIFY_ROW_RE.search(line)
)
Expand Down
Loading