feat(source-control): evidence-gated independent-resolver mode for thread resolution - #1782
Conversation
…read resolution --autonomous admits only threads GitHub marks isOutdated. That is the right guard for the merging worker -- a self-resolved thread would otherwise satisfy the merge gate's zero-unresolved-threads predicate -- but isOutdated means the referenced code MOVED. On a prose or documentation PR a finding is normally addressed by rewriting elsewhere in the file, so the anchor never moves, the finding is genuinely addressed, and the guard refuses. An autonomous prose lane was left with no sanctioned route to zero unresolved threads. --independent-resolver is a parallel third mode, never a relaxation. It replaces isOutdated with caller independence -- a fresh context that is neither the merging worker nor the author of the fix, so the actor resolving is not the actor whose permission slip it is -- plus disposition evidence the script validates against the world rather than trusting. Independence is a property of the dispatch that no script can verify, which is why the evidence half is machine-checked here. --disposition carries exactly its own evidence flag: fixed + --fix-commit (must be reachable from the PR's current head, resolved through the head repository so a fork PR compares correctly), deferred + --tracker-item (must exist and still be open), incorrect + --counter-evidence (must already appear in a reply on the thread, the opener excluded so the bot's own finding cannot prove itself wrong). Fail-closed throughout. Missing, unparseable, mismatched, or surplus evidence is a usage error before any lookup; evidence the world rejects refuses with its own per-thread action, with refused-evidence-unverifiable kept distinct so an API outage is never reported as a false claim. Evidence is validated in list mode too, so a dry run proves the evidence instead of predicting the resolve. Every other guard is retained: bot-only authorship, both TOCTOU pins, and the security/P1 bright line, because an independent resolver is still an unattended path. --autonomous, --include-human, and --allow-unpinned-thread are each refused alongside it, and bulk is refused in every mode here since evidence is a claim about one finding. The bin wrapper needed no change; its contract row already pins that it filters nothing. Refs #1632 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C
The typos gate's dictionary rewrites "unparseable"; every other occurrence in this repository already uses the corrected form. Refs #1632 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C
|
Claude finished @kyle-sexton's task in 4m 19s —— View job PR Review:
|
|
Claude finished @kyle-sexton's task in 6m 16s —— View job Security Review — PR #1782
No CRITICAL issues found. Two findings below. IMPORTANT · PLAUSIBLE —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae3a16894c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The rolling link-check report listed **8 errors**. Only **3 were dead
links** — the other 5 are live
URLs the checker cannot reach, and their fix lands upstream (see below).
Every replacement here was
verified against the live target, not inferred from the URL shape.
## The three dead links
**`docs/adr/0002-…md`** — GitHub retired the
`repositories/configuring-branches-and-merges…/managing-protected-branches/`
path for the required-status-checks troubleshooting page. It now lives
under
`pull-requests/how-tos/merge-and-close-pull-requests/`. Verified 200
with **no redirect**, H1 reads
"Troubleshooting required status checks", and it still covers the
check-never-reports case the ADR
cites it for ("Associated checks stay in a 'Pending' state and block
merging").
An independent fresh-context lookup caught that my first replacement
here was itself a 301 — the
`collaborating-with-pull-requests/…` path redirects to the `how-tos/`
one. Both resolve today, but a
redirect is a second thing that can be retired, and lychee already hints
to prefer resolved URLs, so
the second commit swaps in the canonical target. Verified both
directions: the old path returns 301
with that Location, the new one returns 200 with none.
**`plugins/dometrain/README.md`** — Dometrain moved its plans page from
`/pro/` to `/dometrain-pro/`.
Verified 200, `<title>Dometrain Plans - Dometrain</title>`. The link
text stays "Dometrain Pro"
because the slug and the product name both still are.
**`plugins/source-control/skills/babysit-prs/reference/freshness.md`** —
the most interesting of the
three. `graphql/reference/enums` did not 404; it became a **navigation
index** and no longer carries
any enum definitions at all, which is why the failure was `Cannot find
fragment` rather than a dead
page. GitHub split the GraphQL reference by domain, so
`MergeStateStatus` now lives on the `pulls`
page. The replacement was verified structurally, not just by status
code: `id="enum-mergestatestatus"`
is present in the **served HTML** (so lychee's fragment check resolves
it, rather than the anchor
being JS-injected), and the page carries both descriptions this doc
quotes verbatim — "The head ref
is out of date" and "The merge is blocked".
## The other five are not content defects, and are fixed upstream
`lychee.toml` is a **`managed` component** for this repo per
`standards/distribution/sync-manifest.yml`,
so editing it here would be silently overwritten by the next sync. The
config half of this report is
therefore **melodic-software/standards#303**:
- **`www.gnu.org/software/coreutils/…` (429)** — verified 200. A 429 is
the server rate-limiting the
checker and lands on whichever host the shared runner IP is throttled
against that run, so the fix
is `accept`-ing 429 rather than excluding a healthy host that would just
be replaced by a
different one next run.
- **`dl.acm.org` and `queue.acm.org` (403)** — 403 even with a full
browser User-Agent; no header
tuning reaches them.
- **`docs.genius.com` (403)** — 200 with a browser User-Agent; the
documented bot-block case.
- **`www.ntia.gov` (SSL not trusted)** — the chain verifies locally
(`openssl s_client` →
`Verify return code: 0 (ok)`, curl 200 under strict verification). A
trust store failing an ECC
chain, not an untrustworthy host.
This PR merging alone will not clear the report; #303 has to land and
sync. Flagging that plainly
rather than letting a half-clear look like a regression.
## Verification
Run with the real `lychee.toml` plus the proposed upstream config, over
all seven files the report
named:
```text
🔍 118 Total 🔗 116 Unique ✅ 114 OK 🚫 0 Errors 👻 4 Excluded
```
All 8 reported errors resolved. Also run against this repo's gates:
- `scripts/check-changed-skills.sh origin/main` — 1 skill checked, 0
failed
- `scripts/check-contract-slice-prune.sh --check-diff origin/main` —
pass (no `docs/topics/` path is
touched; that file's two ACM URLs are handled by exclusion, not by
editing it)
- `scripts/check-changelog-parity.sh --check-bump origin/main` — pass.
**No plugin version bump**: a
corrected external URL in a reference doc changes no behavior contract,
no gate requires one, and
bumping `source-control` would collide with the in-flight bump on #1782
— the collision class
tracked as #1746.
- `markdownlint-cli2`, `typos` — clean
## Related
- Fixes #640
- melodic-software/standards#303 — the upstream half; owns `lychee.toml`
for this repo
- #1746 — the concurrent version-bump collision class, the reason this
change deliberately bumps
nothing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…the thread Four review findings on the independent-resolver mode, all fail-closed. A multi-finding thread is now refused (`skipped-multi-finding-thread`). One `--disposition` is a claim about ONE finding while `resolveReviewThread` clears the whole thread, dropping every comment it carries out of the readiness denominator — so evidence for finding A suppressed an unaddressed finding B and let the merge gate pass over it. The count comes from the shared severity vocabulary (`babysit_classify.severity_occurrences`, now public so the resolver and the readiness counters cannot drift) over the thread's own comments, with a self classification reply's table rows stripped, and an unknown count refuses. Scoped to this mode: `--autonomous` rests on the thread-level `isOutdated`, so it carries no per-finding claim to under-cover. Only a confirmed HTTP 404 now earns an evidence-specific refusal. Every other `gh` failure — 403, 429, 5xx, timeout, no HTTP response — reports `refused-evidence-unverifiable`, so an outage no longer sends a caller off to replace evidence that may be perfectly valid. `--counter-evidence` excludes every reply under the thread OPENER's login, not just the opening comment: the mandated classification reply restates the finding's own text, so a finding bot replying on its own thread supplied the words asserted as the rebuttal. A different bot's reply and the caller's own `--self-logins` reply stay admissible. The compare URL's path segments are format-validated before interpolation, matching `babysit_gh.fetch_blocked_base_compare` for the identical call shape. Two of the three arrive in an API response body, so "the API said so" was their only provenance. Also: a stale `--thread-id` pin is reported in list mode, so a dry run predicts what `--resolve` would do; and `verify_disposition`'s fallthrough refuses rather than reaching whichever validator happened to be last. Six classifier predicates added to the guard contract; 37 tests added, covering the reachability and tracker-item gaps the review named.
|
Claude finished @kyle-sexton's task in 3m 0s —— View job PR Review Update —
|
|
Claude finished @kyle-sexton's task in 7m 13s —— View job Security Review — PR #1782
Scope note: The four findings the prior review runs raised (tracker-item error conflation, No CRITICAL issues found. SUGGESTION · CONFIRMED —
|
|
Per-finding classification for the security review. Both findings were valid and both are fixed in
Finding 1. Every segment is format-validated before interpolation now, using the same regexes you pointed at: I also validated Your framing is the accurate one and worth restating so the record is not overstated: the call boundary was already safe (list args, no shell), and an unexpected response always refused, so this narrows the reachable surface rather than fixing an exploitable resolve. The concrete argument — that the guard existed once already in Tests: Finding 2. Fixed by the first of your two options, filtering on the thread opener's login rather than the configured self-logins. Taking the narrower reading would have broken the mode outright: the worker's own reply under a So the rule is now: the opening comment is excluded, and so is every later reply under the opener's login (casefolded). A different bot's reply stays admissible, and the caller's own reply stays admissible. An opener whose login the API withheld admits nothing at all, since independence cannot be established against an unknown author — such a thread is already not Tests: On your other observations. The four you confirmed as correct — the Verification: 572 tests pass (baseline before this commit was 535). |
|
Per-finding classification for the code review. Seven work items, all valid, all fixed in
Findings 1 and 2 — the error-code conflation. Fixed together, since they are one defect at two call sites. Only a confirmed 404 now earns an evidence-specific refusal at either site; a 403, 429, 5xx, timeout, or a failure that never reached an HTTP response at all reports Finding 3 — the stale-pin dry-run gap. Fixed rather than noted. You offered a note in SKILL.md or a This does change Findings 4 through 6 — the test gaps. All three are covered, plus the fourth gap you named in the same list (list-mode dry run with a stale pin, which is finding 3's regression test):
Finding 7 — the implicit dispatch. On the stale-pin inconsistency you traced to Verification: 572 tests pass (baseline before this commit was 535, so 37 added). |
…e lookup `TRACKER_ITEM_RE` admits an owner/repo SHAPE, not a valid one: its character class allows a leading dot and a bare `..`, so `validowner/..#1` built `repos/validowner/../issues/1` — a path that was never a GitHub endpoint. The resulting 404 reported `refused-tracker-item-not-found`, naming a missing item for a lookup that never addressed one. `verify_tracker_item` now applies the same format validation `verify_fix_commit` applies to the analogous compare URL, on the resolved repo so there is one rule rather than two. Still fail-closed either way; what changes is that the refusal names the malformed reference. Also records why `gh_http_status` parses another tool's message text: a format or locale change there yields None, which every caller already treats as unverifiable, so the coupling costs accuracy and never correctness.
|
Claude finished @kyle-sexton's task in 2m 30s —— View job PR Review Update —
|
| Test | Input | Expected |
|---|---|---|
test_tracker_item_owner_carrying_path_syntax_is_unverifiable |
validowner/..#1 |
refused-evidence-unverifiable |
test_tracker_item_leading_dot_owner_is_unverifiable |
.org/repo#1 |
refused-evidence-unverifiable |
test_a_well_formed_explicit_repo_is_still_looked_up |
other-owner/other.repo#1 |
resolves → (True, "") |
The refusal tests use side_effect=[], proving the guard fires before gh_capture is reached (any call to the mock would raise). The happy-path test confirms that a well-formed explicit owner/repo still reaches the API and resolves correctly.
One minor note: GITHUB_REPOSITORY_RE also admits trailing dots and repeated dots (e.g., re..po), both valid on GitHub. The name not in {".", ".."} guard covers only the two filesystem-special paths. This is the correct scope — any other dot-containing name is a legitimate repo name. Consistent with verify_fix_commit's treatment.
Assessment
All findings from all three prior review passes are now addressed:
| Finding | Source | Status |
|---|---|---|
verify_tracker_item conflates 403/5xx with 404 |
Claude review | Fixed in 4556fe4 |
verify_fix_commit has same conflation |
Codex P2 | Fixed in 4556fe4 |
verify_fix_commit uses unvalidated API-returned fields in URL |
Security review | Fixed in 4556fe4 |
Opener bot's classification reply can satisfy incorrect counter-evidence |
Security review | Fixed in 4556fe4 |
| Multi-finding thread bypass | Codex P1 | Fixed in 4556fe4 |
| Stale pin not checked in list mode | Claude review | Fixed in 4556fe4 |
verify_disposition implicit else branch |
Claude review | Fixed in 4556fe4 |
verify_tracker_item skips format-validation of caller-supplied owner/repo |
Security review (suggestion) | Fixed in 634cd89 |
No new issues found. The implementation is correct, the fix matches the pattern verify_fix_commit already uses, and the tests close the named gap without gaps of their own. The PR is clean.
|
Claude finished @kyle-sexton's task in 5m 16s —— View job Security Review — PR #1782 (commit
|
|
Classification for the two review runs on
Finding 1. Fixed, and your reading of the consequence is exactly right — it was a misleading refusal reason rather than a bypass, since the malformed path 404s and the resolve refuses either way. What made it worth fixing rather than deferring is that it is the same inconsistency argument that justified the One deviation from your suggested fix, and it is deliberate. You scoped the validation to the explicit The refusal is Tests: 3 cases — a Finding 2. Comment added on On the rest of your assessment. Recording the points you confirmed so they are not re-litigated next round: list-args-only subprocess calls, the Verification on |
…ker-ref shape The comment's illustrative value carried an `owner/repo#N` shape, which the comment-hygiene scanner reads as a back-reference to a tracker item. The constraint it documents is unchanged; the example now names the offending repository component in prose instead.
|
Claude finished @kyle-sexton's task in 1m 34s —— View job PR Review Update —
|
|
Claude finished @kyle-sexton's task in 5m 8s —— View job Security Review — PR #1782 (commit
|
|
Closing classification for the four review runs on
The code review on One observation worth answering rather than leaving silent. The Scope note on one detail in the Consolidated state of this PR's review round. Every finding raised across all passes is fixed and pushed, with no deferrals and nothing left open:
Verification on |
|
Claude finished @kyle-sexton's task in 4m 42s —— View job Security Review — PR #1782 (commit
|
|
Claude finished @kyle-sexton's task in 1m 25s —— View job PR Review Update —
|
|
Classification for the two review runs on the sync merge The rows below account for every severity-marker occurrence those two bodies carry. Each is prose — a stated absence, or the reviewer quoting the code's own vocabulary constants while explaining how two guards compose. None is a work item.
Two observations in the security pass worth answering rather than leaving silent, since both concern the multi-finding guard's intended scope: The reviewer notes the two severity vocabularies differ — The reviewer also notes State of this PR. Every finding raised across all review passes is fixed and pushed; none deferred, none outstanding. Head |
…ts behavior The test built its thread through the `_bot_thread` fixture helper, which takes `replyBodies` as a literal — so a hardcoded empty list asserted nothing about `project_thread`'s opener-login exclusion and passed identically with that fix reverted. The three sibling cases in the class carried the real coverage. It now projects through `project_thread` from a record whose OPENER posts the later reply carrying the exact counter-evidence text. Verified by reverting the exclusion in a scratch copy: the class goes from 6 passing to 4 failing, including this case.
|
Claude finished @kyle-sexton's task in 1m 12s —— View job PR Review Update —
|
|
Claude finished @kyle-sexton's task in 3m 56s —— View job Security Review — PR #1782 (commit
|
|
Classification for the two review runs on
The code review on What
|
Resolves the source-control version collision: this branch bumped 0.41.0 -> 0.41.1 for the shared hook-utils sync while main landed 0.42.0 (#1782). - plugins/source-control/.claude-plugin/plugin.json: version 0.42.1, taking main's 0.42.0 as the new floor and reapplying this branch's patch bump. - plugins/source-control/CHANGELOG.md: this branch's entry re-headed ## [0.42.1] above main's ## [0.42.0], keeping the file newest-first with no repeated version. Entry prose is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5KaGv34aWCL1epZMSiupC
…urrent bot thread (#1826) Fixes #1641 ## Summary A `babysit-prs worker` that correctly disproves a bot finding — classifies it `INCORRECT`, posts counter-evidence — ships no fix by definition, so the thread stays **current** and satisfies neither `babysit_resolve_thread.py::classify`'s `isOutdated` requirement under `--autonomous` nor the Worker Contract's tighter pre-push-outdated rule. A grounded `VALID (defer)` and a prose fix that rewrote elsewhere in the file land in exactly the same place. Under a base whose ruleset requires thread resolution, the PR then sits unmergeable on a finding that was fully and correctly addressed. **Scope note, because the issue predates the mechanism.** #1641 was filed before #1782 landed `--independent-resolver`. That PR supplied the *mechanism* — an evidence-gated third mode that replaces `isOutdated` with caller independence plus validated disposition evidence. Nothing supplied the **route**: the only dispatch that could invoke it was `babysit-loop`'s pre-escalation resolver, reachable only on the explicit `autopilot` + `--merge c3-this-run` widening. So on every ordinary worker-tier run the D7.5 routing rule #1633 wrote down terminated in a fail-closed report, and the capability gap the issue names stayed open. This is what makes that routing rule executable rather than merely descriptive. This takes the issue's **option 1** (orchestrator-side retirement), which the orchestrator lane's comment on the issue endorsed. ## Fix **The route.** The worker now **reports** an addressed-but-unresolvable current bot thread — thread id, disposition, and where the evidence lives (the reply carrying the counter-evidence, the tracker item id, or the commit SHA) — instead of leaving it silently. Reporting nothing strands the thread, because the orchestrator cannot re-derive from a snapshot which current threads were addressed this round. The orchestrator then routes it, **under the PR's worker lease and before Cleanup releases it**, to a fresh subagent that authored neither the fix nor the counter-evidence. **The guard is untouched.** `classify`'s `isOutdated` requirement under `--autonomous` is not weakened — no script changed at all. The property it was a proxy for is what the dispatch preserves: *the context that authored the evidence is not the context that acts on it.* The orchestrator does **not** resolve the thread itself; it holds the merge decision, so adjudicating its own unblock would be the same self-certification one hop up. **Independence is made load-bearing in prose, because the wrapper cannot check it.** `verify_counter_evidence` requires only that the text appear in a reply by someone other than the thread's **opener** — a worker's own reply under a `--self-logins` identity is admissible input. So the dispatched resolver re-derives the evidence at the live head rather than passing the orchestrator's brief through; otherwise the dispatch is a laundering hop, not an adjudication. **Pins are read fresh, never forwarded.** The worker's dispatch-snapshot `commentCount` / `lastCommentUpdatedAt` are pre-reply, and the worker's own mandated D5 classification reply moves both — forwarding them produces `refused-stale-pin` deterministically. The documented flow lists the thread first (list mode validates the evidence too), takes the pins from that output, then resolves. `--self-logins` is documented as non-optional on this route: omit it and the worker's reply flips `botOnly` false and the thread returns `skipped-human-thread`. **One contract, two callers.** Almost the whole of `babysit-loop/reference/pre-escalation-dispatch.md` was babysit-prs mechanics — the D7.5 per-finding ledger, the worker lease, the worktree lifecycle, the guarded wrappers. Writing a second copy into `orchestration.md` would have forked it, so the reusable contract moved to the skill that owns the wrapper: new `babysit-prs/reference/independent-resolution.md`. `pre-escalation-dispatch.md` keeps only its widening-specific bounds (frontier tier, the four blocker classes it never touches, the post-dispatch re-partition) and points there. **The fail-closed fallback survives verbatim** for every bound the dispatch cannot cross — a security/P1 thread (`skipped-severity-marked`), a multi-finding thread (`skipped-multi-finding-thread`), a human thread, evidence the world rejects, or no subagent tools to dispatch to: *leave the thread unresolved, do not merge, and report the PR with the addressed-but-unresolvable thread named.* This adds a path; it does not replace the fallback. `safety.md`'s Security/P1 "only one dispatch path" bullet is unchanged in substance and now says so explicitly — the orchestrator-side dispatch is not a second route to that exception, because the wrapper's severity bright line refuses those threads on it. **Stale claims corrected.** `review-discipline.md`'s D7.5 authorization rule and `babysit-prs/reference/loop.md`'s Never-Do entry both asserted the dispatch was "reachable only on the explicit `autopilot` + `--merge c3-this-run` widening". True when written; now it names the two invocations that reach one. ## Verification | Criterion (issue #1641) | Status | |---|---| | A disproved current finding reaches a terminal state — retired by an authorized context, or a clearly-reported escalation | Done — orchestrator dispatch on the ordinary worker-tier path; the fail-closed report survives verbatim for every bound the dispatch cannot cross | | `classify`'s `isOutdated` requirement under `--autonomous` is not weakened | Done — **no script logic changed**; `git diff` touches only `tests/guard_contract.py`'s doc-source table | | The context that authored the counter-evidence is not the one that unblocks its own merge | Done — worker reports and never resolves; orchestrator dispatches and never resolves; the resolver re-derives evidence at the live head rather than accepting the brief's | | `guard-contract.md` gains a row for any new refusal or allowance | Done — no new refusal or allowance exists (the mode shipped in #1782), so the row added is the new file's `independent-resolution.dispatch-commands` doc-command source; `test_every_doc_naming_a_wrapper_is_covered` requires it, and `guard-contract.md` is regenerated, never hand-edited | Commands run in the worktree: - `bash plugins/source-control/skills/babysit-prs/scripts/engine.test.sh` — **575 tests, OK**; ruff clean; guarded-wrapper behavior suite green. This includes `test_every_documented_wrapper_command`, which now parser-validates both copyable commands in the new reference file, and `test_every_doc_naming_a_wrapper_is_covered`, which fails if a new .md spells a wrapper command with no `DOC_COMMAND_SOURCES` row. - `python tests/guard_contract.py --emit` — `guard-contract.md` regenerated from the module (hand edits fail CI). - `npx markdownlint-cli2 "plugins/source-control/**/*.md"` — 49 files, 0 errors. - `scripts/check-changelog-parity.sh --check` / `--check-bump origin/main` / `--check-order` — pass. - `scripts/check-skill-portability.sh origin/main`, `check-skill-leaf-names.sh`, `check-silent-skips.sh` — pass. `babysit-prs/SKILL.md` is 490 lines, under the 500 cap #1626 tracks — the new contract went into a reference file, not SKILL.md. Rebased onto `origin/main` after `0.42.1` landed mid-work, and later merged `origin/main` again after `0.48.0` shipped; `plugins/source-control` bumped to `0.49.0` (feature: a new route, plus a new reference file). ## Related - Refs #1782 — shipped `--independent-resolver`, the mechanism this PR supplies the route for - Refs #1633 — wrote down the D7.5 routing rule that had no reachable dispatch on the worker path - Refs #1614 — the adjudication whose `isOutdated` guard must not be weakened, and is not - Refs #571 — the still-open machine-enforced displacement fix, untouched here 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

The gap
--autonomousresolves only threads GitHub marksisOutdated— the one deterministic "addressed"signal it has. That guard is right about the actor it constrains: a self-resolved thread would
otherwise satisfy the merge gate's zero-unresolved-threads predicate, the actor signing its own
permission slip.
But
isOutdatedmeans the referenced code moved. On a prose or documentation PR a finding isnormally fixed by rewriting elsewhere in the file, so the anchor never moves, the finding is
genuinely addressed, and the guard refuses. Across the two runs #1632 measured, 7 of 20 resolved
threads were still not
isOutdated— and that undercounts, since a worker's own push flips the flagwithout touching a comment. The practical consequence was that an autonomous prose lane had no
legitimate route to zero unresolved threads: resolve outside the guard, stay blocked forever, or
escalate every current thread to a human. All 20 of those resolves took the first route, via raw
gh api graphql. This closes that.The design
--independent-resolveris a third mode, parallel to--autonomousand never a relaxation ofit. Widening
--autonomouswould delete the anti-self-certification property for exactly theactor it was written to constrain, so the two properties that replace
isOutdatedare differentones:
nor the author of the fix. The actor resolving is not the actor whose permission slip it is. This
is a property of the dispatch and no script can verify it, which is precisely why the other
half is machine-checked.
--dispositionnames the claim and carries exactly its ownevidence flag, and the script validates it against the world, never against the assertion:
fixed+--fix-commit <sha>— must be reachable from the PR's current head commit, notmerely exist. Resolved through the head repository, so a fork PR compares correctly. A SHA on
some other branch is not evidence that this PR carries the fix.
deferred+--tracker-item <owner/repo#N|#N|N>— must exist and still be open. A closedfollow-up is not a deferral; it is the finding disappearing.
incorrect+--counter-evidence <text>— must already appear in a reply on the thread.The opening comment is excluded, so the bot's own finding text can never satisfy the claim that
the finding is wrong, and the rebuttal is visible where the finding is rather than only on the
command line of the process resolving it.
Fail-closed throughout. Missing, unparsable, mismatched, or surplus evidence is a usage error
at exit
2before any lookup. Evidence the world rejects refuses the resolve with its ownper-thread
action:refused-fix-commit-not-on-head,refused-tracker-item-not-found,refused-tracker-item-not-open,refused-counter-evidence-not-found, andrefused-evidence-unverifiable— the last kept deliberately distinct, because reporting an APIoutage as a false claim would send a caller to fix the wrong thing. Refusing leaves the thread
unresolved, which is the recoverable direction; a suppressed finding is not.
Two judgment calls worth flagging for review:
validation would report
would-resolvefor evidence the world rejects — the one answer this modeexists to prevent. Cost is one or two
ghcalls on a single pinned thread.--autonomous, whichrefuses bulk only under
--resolve. Evidence is a claim about ONE finding, so a bulk call wouldapply one thread's evidence to every thread.
Everything else
--autonomousguards is retained deliberately: bot-only authorship, a single pinned--thread-idwith both TOCTOU pins, and the security/P1 bright line — this is still an unattendedpath, so "never a security or P1 thread" stays unconditional and no evidence buys past it.
--autonomous,--include-human, and--allow-unpinned-threadare each refused alongside the mode.Acceptance criteria
ExistingModesUnchangedasserts--autonomousstill refuses a non-outdated thread;resolve.autonomous-bulk-refusedstill passestest_human_thread_is_still_refusedplus predicateclassify.independent-keeps-the-human-lineghmode/disposition/refusedEvidenceonly; five distinctrefused-*actionsscripts/engine.test.shgreen"$@"passthrough, andresolve.wrapper-filters-nothingalready pins that it filters nothingSKILL.md"Guarded mutations" andreference/safety.mdOn the
SKILL.mdline budget (#1626)1632's body warned that
babysit-prs/SKILL.mdsat at 499/500 and that documenting this mode wouldneed that wall addressed first. It no longer sits there: on current
mainthe file is 469 lines,so the hub edit fits without touching #1626. With this change it is 484/500. That is under the
hard cap and CI passes, but it does consume half the remaining headroom, so #1626's underlying
pressure is real and unaddressed — flagging it rather than letting it be discovered at the next
edit.
Verification
bash scripts/engine.test.sh— 535 tests OK,ruff checkclean, guarded-wrapper behaviour suiteall PASS (this is the suite feat(source-control): independent-resolver mode for babysit_resolve_thread.py (evidence-gated, non-outdated bot threads) #1632 names as the gate)
python tests/guard_contract.py --emit—reference/guard-contract.mdregenerated from the fivenew refusal rows and three new predicates;
GeneratedDocIsCurrentpasses against itpython -m unittest test_babysit_resolve_thread— 67 tests (was 41; 26 added)pr-body-linkage-gate.test.sh(135passed),
pr-linkage-mcp-gate.test.sh,babysit-wrapper-help.test.sh,babysit-readiness-gate.test.sh— all PASSorigin/main:check-changelog-parity.sh --check,--check-bump,--check-order(0.41.0 → 0.42.0 with its## [0.42.0]entry);check-changed-skills.sh(1 skill, 0 failed);
check-skill-portability.sh;check-orphaned-fixtures.sh;check-contract-slice-prune.sh --check-diff;check-cross-plugin-source-drift.sh;check-silent-skips.sh;check-shell-portability.sh;validate-plugins.shmarkdownlint-cli2 "plugins/source-control/**/*.md"— 48 files, 0 errorsRelated
blocked notes were written, which is what unblocked this
SKILL.mdline cap; no longer a blocker at 469 lines onmain, but see the budgetnote above
--autonomous's pins;related surface, deliberately not conflated
🤖 Generated with Claude Code
https://claude.ai/code/session_01RhS3T7ShwJgKTrvk2Mvd3C