Skip to content
11 changes: 11 additions & 0 deletions .changes/source-control/feat-10763-worker-resolve-c306d1f7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
bump: minor
---

### Added

- `/source-control:babysit-prs` worker tier: the thread resolver takes `--own-pr-lane-logins`, filled from `babysit_lane_logins`. On a PR opened by your own login, `--autonomous` refuses the whole PR. With the flag, it resolves an outdated, bot-opened thread that a configured lane login other than the opener has answered with address-feedback's fixed disposition (`VALID, fixed`) as its latest reply; a left-open disposition is no answer. It still never resolves a CRITICAL or security thread, a thread a person opened, or a thread with a comment from your own login. Other threads report `skipped-own-pr-not-lane-answered`. With `babysit_lane_logins` unset, the lane login is the repository's `merge.agent-identity`, read at the PR's base commit and the default branch head, never the PR head; the setting wins when set. The resolve runs under your existing login, so no permission is added.

### Fixed

- The resolver's own-PR refusal never fired. The resolver read the PR author's login from the wrong level of the `gh pr view --json author` payload and always got an empty login, so `--autonomous` treated every PR as someone else's. On your own PRs it could resolve an outdated bot thread you had replied to yourself. It now reads the login correctly, and it refuses when the author cannot be read. With no lane login from either source, the worker now resolves nothing on your own PRs.
2 changes: 1 addition & 1 deletion plugins/source-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"babysit_lane_logins": {
"type": "string",
"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 on) refuses fail-closed.",
"description": "Logins recognized as pipeline lanes: the autopilot merge tier's lane-authored criterion, and the worker tier's resolve of outdated bot threads a lane already answered on your own PRs. Absent: the tier (when on) refuses fail-closed, and the worker resolves nothing on your own PRs.",
"multiple": true
},
"babysit_approver_bot_logins": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/source-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ reads it from.
| `babysit_merge_method` | string | `"auto"` | `CLAUDE_PLUGIN_OPTION_BABYSIT_MERGE_METHOD` | Merge method for gate-proven merges. auto (default) uses the repository's declared method, then squash; squash, merge, or rebase forces that method when the repository declares none. |
| `babysit_stacked_prs` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_BABYSIT_STACKED_PRS` | Lets the merge gate merge a native stacked PR layer, which lands every open layer below it. Each of those layers must pass the same gate. Off by default: a stack layer is held for a human. |
| `babysit_autopilot_merge_tier` | boolean | `false` | `CLAUDE_PLUGIN_OPTION_BABYSIT_AUTOPILOT_MERGE_TIER` | Turns on the autopilot merge tier: a distinct bot account submits an approving review, then the gate merges only when every criterion holds. Off by default; PRs go to the human merge-ready list. Requires babysit_lane_logins, babysit_approver_bot_logins, and babysit_merge_block_labels. |
| `babysit_lane_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_LANE_LOGINS` | Author logins recognized as pipeline lanes for the autopilot merge tier's lane-authored criterion. Absent: the tier (when on) refuses fail-closed. |
| `babysit_lane_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_LANE_LOGINS` | Logins recognized as pipeline lanes: the autopilot merge tier's lane-authored criterion, and the worker tier's resolve of outdated bot threads a lane already answered on your own PRs. Absent: the tier (when on) refuses fail-closed, and the worker resolves nothing on your own PRs. |
| `babysit_approver_bot_logins` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_APPROVER_BOT_LOGINS` | Bot logins whose approving review satisfies the autopilot merge tier's author-is-not-approver criterion. Absent: the tier (when on) refuses fail-closed. |
| `babysit_merge_block_labels` | string (multiple) | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_MERGE_BLOCK_LABELS` | Labels that veto an autopilot-merge-tier merge, for example do-not-merge. Absent and undeclared in the target repository: the tier (when on) refuses fail-closed. |
| `babysit_review_trigger_phrase` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_REVIEW_TRIGGER_PHRASE` | Comment phrase that requests an AI re-review (posted and recognized). Read only from this option: a target repository cannot supply it. Absent: the review-trigger module stays dormant. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,15 @@ release changes how `gh pr merge` treats a queue base, or the GraphQL schema cha
## Rerun a rate-limited review

Rerun the whole workflow run with `gh run rerun <run-id>`, never `gh run rerun --failed`.

## Resolve identity on a person-opened PR

The spoke's own-PR exception resolves a lane-answered thread under the PR author's login, and the
lanes App does not resolve threads on a PR a person opened.

- **Pointer**: when deciding which identity can resolve a thread on a person-opened PR, fetch
[Resolving conversations](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request#resolving-conversations)
live; the probe is [pr-unblock run 40737011221](https://github.com/melodic-software/claude-code-plugins/actions/runs/40737011221)
(`publish-feedback / run`, a lanes App resolve on a person-opened PR that failed). **As of**:
2026-10-28. **Recheck trigger**: a lanes App resolve on a person-opened PR succeeds, or that
section changes who may resolve a conversation.
4 changes: 2 additions & 2 deletions plugins/source-control/skills/babysit-prs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ An invocation is `[mode] [scope]`; mode and scope are orthogonal. Combine them f
| Mode | What it does |
| --- | --- |
| *(none)*, the configured `default_tier` (`safe` unless the consumer changed it) | Safe tier: discover in-scope PRs (your own, under the current repo's owner by default), check readiness, fix clear branch-owned issues and push, report blockers and the next cadence. Never resolves threads or merges. |
| `worker` | Everything safe does, and additionally auto-resolves *pre-push-outdated* bot threads and merges PRs the merge gate proves 100% ready. Requires Python. For dedicated 24/7 loops over your own PRs. |
| `worker` | Everything safe does, and additionally auto-resolves *pre-push-outdated* bot threads and merges PRs the merge gate proves 100% ready. On a PR you opened, it resolves only outdated bot threads a configured lane login already answered, never a CRITICAL or security thread and never one a person wrote or joined. Requires Python. For dedicated 24/7 loops over your own PRs. |
| `autopilot` | Maximum autonomy for a solo owner: every open PR under the watched owners regardless of author; fix everything it can; resolve every thread it has addressed (bot, AI-review, and human); merge everything the gate proves ready. Requires Python. A deliberate power-user opt-in, never a default. |
| `help` (or `?`) | Print the common flows and the effective configuration below; take no other action. |

Expand Down Expand Up @@ -291,7 +291,7 @@ this block. Values reach scripts ONLY as explicit CLI flags (option environment
| `babysit_merge_method` | `${user_config.babysit_merge_method}` | deprecated fallback `--method` (merge wrapper) | `auto`: repo convention, then squash |
| `babysit_stacked_prs` | `${user_config.babysit_stacked_prs}` | `--stacked-prs` (merge gate, every form) when `true`; omit it otherwise | `false` (a stack layer is held for a human) |
| `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_lane_logins` | `${user_config.babysit_lane_logins}` | `--lane-logins` (merge wrapper, autopilot merge tier); `--own-pr-lane-logins` (resolve-thread, worker `--autonomous` forms) | tier refuses fail-closed when enabled; on your own PRs the resolver uses the repository's `merge.agent-identity` from its base and default branch, else resolves nothing |
| `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}` | deprecated fallback `--block-labels` (merge wrapper, autopilot merge tier); omit it when unset | tier refuses fail-closed when enabled and the target repository declares none |
| `babysit_review_trigger_phrase` | `${user_config.babysit_review_trigger_phrase}` | `--trigger-phrase` (snapshot, request_review); omit it when unset | review-trigger module dormant |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Each row is executed. `Refused by` says which layer rejected the invocation. On
| `resolve.independent-requires-disposition` | `skills/babysit-prs/scripts/babysit_resolve_thread.py` | `owner/repo#1 --allowed-owners owner --independent-resolver` | 2 | python-cli | -- | `--disposition` | `babysit_resolve_thread.py::main` | --independent-resolver without --disposition is a usage error at exit 2: the mode resolves on validated evidence, and an unnamed disposition has no evidence to validate. |
| `resolve.independent-requires-matching-evidence` | `skills/babysit-prs/scripts/babysit_resolve_thread.py` | `owner/repo#1 --allowed-owners owner --independent-resolver --disposition deferred --thread-id PRRT_abc` | 2 | python-cli | -- | `--tracker-item` | `babysit_resolve_thread.py::main` | Each disposition is admissible only with ITS OWN evidence flag: --disposition deferred without --tracker-item refuses at exit 2 rather than resolving on an unproven claim. |
| `resolve.evidence-flag-requires-the-mode` | `skills/babysit-prs/scripts/babysit_resolve_thread.py` | `owner/repo#1 --allowed-owners owner --fix-commit abc1234` | 2 | python-cli | -- | `--independent-resolver` | `babysit_resolve_thread.py::main` | An evidence flag outside --independent-resolver is refused at exit 2, not accepted and ignored: silently dropping it would report an unchecked claim to the caller as a checked one. |
| `resolve.own-pr-lane-logins-requires-autonomous` | `skills/babysit-prs/scripts/babysit_resolve_thread.py` | `owner/repo#1 --allowed-owners owner --own-pr-lane-logins lane` | 2 | python-cli | -- | `--autonomous` | `babysit_resolve_thread.py::main` | --own-pr-lane-logins is the worker tier's own-PR exception and nothing else: outside --autonomous it is refused at exit 2, so it can never widen the explicit or independent-resolver modes. |
| `resolve.wrapper-reaches-failclosed-cli` | `scripts/source-control-babysit-resolve-thread` | `owner/repo#1` | 3 | python-cli | -- | -- | `babysit_resolve_thread.py::main` | The resolve wrapper is a pure passthrough: no --allowed-owners refuses at exit 3 from the CLI, unchanged by the wrapper. |
| `resolve.wrapper-filters-nothing` | `scripts/source-control-babysit-resolve-thread` | `owner/repo#1 --allowed-owners owner --autonomous --resolve --thread-id PRRT_abc --allow-unpinned-thread` | 2 | python-cli | -- | `allow-unpinned-thread` | `babysit_resolve_thread.py::main` | ASYMMETRY: unlike the merge wrapper, the resolve wrapper filters no argument. Its --allow-unpinned-thread refusal comes from Python -- proven by the JSON envelope on stdout -- so an operator may not assume a bash-level refusal exists on this entry point just because one exists on the other. |
| `ledger.apply-requires-lease-token` | `skills/babysit-prs/scripts/manage_feedback_ledger.py` | `dispose --pr owner/repo#1 --expected-head-sha 0123456789abcdef0123456789abcdef01234567 --feedback-id F1 --reason contract-row --state-dir {state_dir} --apply` | 2 | python-cli | -- | `lease token` | `manage_feedback_ledger.py::main` | A ledger write is lease-gated: --apply without --lease-token refuses at exit 2 rather than writing an unowned entry. |
Expand Down Expand Up @@ -104,6 +105,9 @@ These decide per fetched thread, so no argument shape can express them and no ar
| `classify.independent-refuses-a-multi-finding-thread` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=False, findingCount=2 | autonomous=False, only_outdated=False, include_human=False, independent=True | `skipped-multi-finding-thread` | `babysit_resolve_thread.py::classify` | A permission rule reading '--independent-resolver resolves any addressed bot thread' is FALSE for a thread carrying more than one source finding: one --disposition is a claim about ONE finding while resolution clears the whole thread, so evidence for finding A would drop an unaddressed finding B out of the readiness denominator and let the merge gate pass over it. |
| `classify.independent-refuses-an-unknown-finding-count` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=False, findingCount=None | autonomous=False, only_outdated=False, include_human=False, independent=True | `skipped-multi-finding-thread` | `babysit_resolve_thread.py::classify` | The multi-finding guard fails CLOSED: the projection reports an unknown count (None) when a truncated comment page could be hiding another finding, and an unknown count refuses exactly as a known multi-finding count does. |
| `classify.multi-finding-guard-is-independent-only` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=False, findingCount=4 | autonomous=True, only_outdated=False, include_human=False | `eligible` | `babysit_resolve_thread.py::classify` | The multi-finding guard is scoped to --independent-resolver. --autonomous rests on isOutdated, which GitHub computes for the thread as a whole rather than per finding, so there is no per-finding claim there to under-cover: a multi-finding outdated bot thread stays eligible. |
| `classify.own-pr-requires-a-lane-answer` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=False | autonomous=True, only_outdated=False, include_human=False, own_pr=True | `skipped-own-pr-not-lane-answered` | `babysit_resolve_thread.py::classify` | On a PR whose author is a self login, an outdated clean bot thread is still refused unless a lane login answered it: the resolver is the PR's author, so the 'handled' claim must come from another actor. An absent signal fails closed. |
| `classify.own-pr-keeps-the-severity-line` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=True, laneAnswered=True | autonomous=True, only_outdated=False, include_human=False, own_pr=True | `skipped-severity-marked` | `babysit_resolve_thread.py::classify` | A lane answer never buys past the severity line: a CRITICAL or security thread on an own PR stays with a person. |
| `classify.own-pr-lane-answered-is-eligible` | isResolved=False, botOnly=True, isOutdated=True, humanDeferred=False, severityFlagged=False, laneAnswered=True | autonomous=True, only_outdated=False, include_human=False, own_pr=True | `eligible` | `babysit_resolve_thread.py::classify` | The own-PR exception admits exactly an outdated, bot-opened, unflagged thread a lane already answered. |
| `classify.already-resolved-precedes-every-guard` | isResolved=True, botOnly=False, isOutdated=False, humanDeferred=False | autonomous=True, only_outdated=False, include_human=False | `skipped-already-resolved` | `babysit_resolve_thread.py::classify` | An already-resolved thread is skipped before any other guard is consulted, so a re-run never re-resolves and never reports a human thread it declined. |

## Effects: what reaches disk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,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 "<plugin-root>/scripts/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` pinned with `--thread-id`, `--expected-comment-count`, and
--extra-bot-logins <extra-bot-logins> --self-logins @me,<self-logins> --own-pr-lane-logins <lane-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,
so it is never auto-resolved on that basis. The push moving the diff under a finding does not
Expand Down Expand Up @@ -977,7 +977,7 @@ GitHub settings, or auto-fix human-authored feedback. Classify, reply with evide
surface human items instead. You may resolve a review thread only if it appears in the pre-push
outdated-thread list above, via bash "<plugin-root>/scripts/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>, with the pins
@me,<self-logins> --own-pr-lane-logins <lane-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. If you address a finding on a CURRENT bot thread, whether with counter-evidence for an INCORRECT, a grounded
Expand Down
Loading
Loading