fix(source-control): scope babysit classification credit to its surface (#642)#666
Conversation
…ce (#642) ## Summary The readiness gate blocks while source findings outnumber their per-finding classification rows. `count_classified` counted self-authored classification pipe-rows in ANY comment, including PR-level review-summary comments that are never thread-resolved. Because a review thread's findings are discounted when it resolves (the #465 lifetime-vs-open guard) but a PR-level comment can never be, a stale classification posted outside a thread kept counting after its finding was discounted — inflating `classified` past a fresh, still-unclassified open-thread finding and emitting a fail-open `READINESS_OK`. This is the single live fail-open on main and the ratified backstop for the #476 gate-off flip. The naive fix (only count classifications inside review threads) is unsafe: it contradicts the D5 routing rule in `review-discipline.md`, which *mandates* that issue/review-level (PR-level) findings be answered with a detached PR-level classification comment. Under thread-only counting every such classification would count zero and the gate would permanently block any PR whose findings come from review summaries. ## Fix - **Per-surface classification credit (`babysit_classify.py`).** `count_effective_classified` buckets comments by surface (review-thread vs PR-level) and caps credit within each bucket via `min(classified, findings)`, then sums: a non-thread classification can only offset a non-thread finding, an open-thread classification only an open-thread finding. A stale PR-level row can no longer spill over to cover an open-thread finding. `count_classified` and `count_findings` stay pure raw counters; the bucketing composes them. On thread-state-free input the thread bucket is empty and this collapses to `min(classified, findings)`, so existing behavior and the bash convergence property are preserved. - **Surface stamping (`babysit_findings.py`).** `_comment()` records `in_review_thread` (true only when a comment is fetched from a review thread); the new `is_thread_comment` discriminator reads it. Issue-level and review-summary comments are stamped PR-level. The entrypoint emits the effective count. - **Bash degrade cap (`babysit-readiness-gate.sh`).** The thread-blind safe-tier degrade gains the thread-state-free analogue `classified = min(classified, findings)`, so an over-count of rows can't mask a finding and the degrade stays convergent with the Python `min` on thread-state-free input. Per-surface bucketing is inherently thread-aware and remains Python-only, exactly like the #465 discount. ## Residual (documented, not closable here) An orphaned PR-level classification covering a fresh *PR-level* finding is irreducible: GitHub's flat issue comments carry no finding↔classification linkage, so a stale PR-level row is data-identical to a live one. It is reachable only via a D5 routing violation (an inline finding answered with a detached PR comment) or a reviewer editing/deleting a finding; a D5-compliant, no-edit run never hits it. The per-bucket fix closes the entire linkable (thread) side. Strict per-surface enforcement means a single consolidated PR-level classification table will not cover inline-thread findings — which is exactly what D5 already forbids ("NEVER a detached pr comment"), so this enforces the already-ratified routing rather than introducing new policy. ## Verification - `python -m unittest discover -s tests` (266 tests) — OK; new `EffectiveClassifiedTests`, `SurfaceStampingTests`, `Main642FailOpenTests`. - `babysit-readiness-gate.test.sh` — 56 pass, incl. the #642 scenario (fresh open-thread finding + stale PR-level row → BLOCKED, Python-gated like #465) and the over-classified-cap convergence case. - `engine.test.sh` — pass (unittest + `ruff check`). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 0s —— View job PR Review — fix(source-control): scope babysit classification credit to its surface (#642)
SummaryThis is a correct, well-scoped fix for a real fail-open. The surface-bucketing approach in Findings1. Minor — def _capped_credit(comments, self_logins):
return min(
count_classified(comments, self_logins),
count_findings(comments, self_logins),
)Each counter iterates 2. Observation — convergence test uses
F=$(mkjson conv-overclassified '[
{author:"claude[bot]", body:"CRITICAL a"},
{author:"me[bot]", body:"| 1 | a | VALID | x |\n| 2 | spurious | INCORRECT | y |"}
]')
converge "over-classified-cap" "$F"In bash double-quotes, If If the intent is to verify the cap with classified=2 > findings=1, it's worth confirming F=$(mkjson conv-overclassified '[
{author:"claude[bot]", body:"CRITICAL a"},
{author:"me[bot]", body:"| 1 | a | VALID | x |\n| 2 | spurious | INCORRECT | y |"}
]')…or splitting the body across the fixture like existing multi-line fixtures. The unit test 3. Observation —
4. Confirmed correct — When 5. Confirmed correct —
6. Confirmed correct — bash degrade cap ((classified > findings)) && classified="$findings"Prevents an over-count of classification rows from masking an unclassified finding in the bash degrade, and keeps the degrade convergent with Python's SecurityNo new security concerns. No new shell injection surfaces, no unvalidated external input at new boundaries, no filesystem writes or egress. Regex patterns are well-bounded. VerdictApprove. The fix is architecturally correct, the per-surface bucketing is the right abstraction, and the tests cover the key failure scenario. Finding 2 above is worth verifying before merge to confirm the bash convergence test actually exercises the classified>findings cap scenario. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08fe4d7969
ℹ️ 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".
…cap (#642) The DETECTION header still described `classified` as an uncapped row count and `BLOCK when classified < findings` with no mention of the cap added in the fix below it — the exact header/behavior drift #534's single-source-of-truth intent guards against. State the min-cap (and the Python per-surface refinement) at the `classified`/BLOCK bullet, and widen the `usage()` sed range so the three extra header lines don't push exit codes 1/3/4 out of `--help`. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 4m 4s —— View job PR Review — fix(source-control): scope babysit classification credit to its surface (#642)
SummaryThe surface-bucketing fix in Two findings from this review pass worth addressing before merge: Finding 1 — Prior review's
|
…omments type (#642) Review (Codex P2) surfaced a fail-open still reachable on the `--comments-json` reuse path: `fetch-all-pr-comments.sh` tags inline review comments `type: "inline"` but carries no `in_review_thread` stamp, so `is_thread_comment` bucketed an inline finding as PR-level alongside a detached PR-level classification row, which then cross-credited it — the exact #642 fail-open, on the reuse path. `is_thread_comment` now falls back to the schema's `type == "inline"` tag when the explicit stamp is absent, making the reuse path surface-aware. The explicit `in_review_thread` stamp still wins when present, so a live PR-level comment is never re-inferred as a thread; legacy `{author, body}` fixtures (no stamp, no type) stay PR-level, so the existing convergence cases are untouched. The tag-driven bucketing is a Python enrichment the thread-blind bash degrade cannot see, so the new regression is Python-gated like the #465 and live-path #642 cases. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 56s —— View job PR Review — fix(source-control): scope babysit classification credit to its surface (#642)
Reviewing new commit
|
…it surface credit (#642) Follow-up to the reuse-path fix: harden the absent/unknown-surface case fail closed. `is_thread_comment` (bool) forced every comment into thread-or-not, so a comment bearing neither an `in_review_thread` stamp nor a `fetch-all-pr-comments.sh` `type` tag defaulted to the PR-level bucket, where a PR-level classification row could offset it. A single 2-bucket default cannot be fail-closed in both directions — with per-bucket `min`, whichever bucket holds "unknown", that bucket's rows can offset that bucket's findings. Replace the bool discriminator with `comment_surface` → thread / pr_level / unknown, and credit three buckets. Unknown provenance is isolated: its rows cannot offset a known-surface finding and its findings cannot be offset by a known-surface row. This preserves the "no signal = PR-level lifetime" model `thread_is_open` documents and needs no fixture edits — uniform unsignalled input (the legacy `{author, body}` and bash-degrade shapes) collapses to one bucket, identical to today, so existing tests and the convergence property hold. The guarded surfaces are defensive: the live path stamps every comment and fetch-all-pr-comments.sh always emits `type`. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 25s —— View job PR Review — fix(source-control): scope babysit classification credit to its surface (#642)
Reviewing new commit
|
…edit (#642) Bump the source-control plugin to 0.13.3 and add the Keep-a-Changelog entry for the #642 readiness-gate fix. The entry documents the fail-open closure and explicitly calls out the behavior flip: a PR whose inline-thread findings are answered only by detached PR-level classification replies now reports READINESS_BLOCKED where it previously passed, mechanically enforcing review-discipline.md §D5's already-ratified reply routing. Fail-closed direction. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 14s —— View job PR Review — fix(source-control): scope babysit classification credit to its surface (#642)
Reviewing new commit
|
| Claim in CHANGELOG | Matches implementation |
|---|---|
| Fail-open mechanism (lifetime discount discounting a finding, PR-level classification still counting) | ✓ root cause in babysit_classify.py |
| Three-way surface bucket (review-thread, PR-level, unsignalled) | ✓ THREAD_SURFACE/PR_LEVEL_SURFACE/UNKNOWN_SURFACE from 1120f22 |
| Cap within each bucket | ✓ _capped_credit per bucket in count_effective_classified |
Bash degrade min(classified, findings) |
✓ babysit-readiness-gate.sh:251 |
| Per-surface refinement is Python-only; convergent with degrade on unsignalled input | ✓ UNKNOWN bucket collapses to single-bucket min, identical to bash cap |
| Behavior flip (D5-violating detached reply → BLOCKED) | ✓ documented correctly; fail-closed direction |
No inaccuracies found.
Carry-forward: stale test comment still unfixed
test_babysit_classify.py:200–201
# Convergence invariant: with no thread markers every comment is PR-level,
# so effective classified is min(classified, findings) -- what the bashThis was flagged as Finding 1 in the previous review pass (commit 1120f22). After 1120f22, bare {author, body} comments (no type, no in_review_thread) land in UNKNOWN_SURFACE, not PR_LEVEL_SURFACE. The convergence invariant still holds — one bucket collapses to min — but the comment says "PR-level" when the correct description is "UNKNOWN_SURFACE (the bare author/body fixture shape)". The assertion at line 212 is unaffected.
Still not a bug. Still trivial. Fix this →
Previously confirmed findings (all still hold, nothing regressed)
- Functional correctness (all prior passes): surface-bucketing, priority chain in
comment_surface,_capped_credit, bash degrade cap — all correct. \nin bash convergence test (pass 1 finding 2, resolved in pass 2): jq expands\n— the cap is exercised.- Reuse-path type-tag inference (pass 2 finding 2, fixed in
7d575dd):type == "inline"→ thread bucket; explicitin_review_threadstamp wins. - Unsignalled isolation (pass 3 finding 1, fixed in
1120f22): UNKNOWN_SURFACE bucket is fail-closed for absent-signal comments. - D5 test exercises UNKNOWN not PR_LEVEL (pass 3 finding 2): documented follow-up gap; bucket logic is identical for both surfaces — no bug.
Security
No new concerns. The release commit touches only plugin.json (version scalar) and CHANGELOG.md (prose documentation).
Verdict
Approve. The release commit is correct: the version bump is justified, the CHANGELOG entry is accurate, and it covers all four functional commits completely. The one carry-forward (stale "PR-level" comment text in test_thread_state_free_input_collapses_to_min) is the same trivial documentation gap flagged in the prior pass — not a blocker. The full PR is ready to merge.
main advanced source-control to 0.13.3 (#651, #666) while this branch bumped from an older 0.13.1 base; resolve the version and CHANGELOG conflict by re-homing this PR's entry as [0.13.4] on top of main's [0.13.3], keeping all main entries intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FM1RfM3jHkgenpdbMv4o64
… filter (#654) (#668) ## Summary The `work-items` GitHub adapter's "Open linked PRs" mechanic (shipped in #463 / PR #643) decided whether an issue already had an open closing PR by running a closing-keyword `jq` regex over the **raw** PR body. GitHub treats fenced code blocks and HTML comments as inert for auto-close linkage, but the raw-body regex did not: a PR body carrying a fenced `Closes #<N>` example matched `true` and dropped the still-open, still-pickable issue `#<N>` from the `/work-items:work` frontier. #643 shipped a partial `gsub` fence-stripper alongside the regex, but it recognized only exactly-three backtick/tilde fences and silently missed four-or-more-backtick and indented fences — exactly the fragile hand-roll #654 flagged as not-small. ## Fix Replace the entire raw-body heuristic (the `gh pr list --search "<N> in:body"` prefilter, the closing-keyword `jq` `test`, and the partial `gsub` fence-stripper) with GitHub's own computed close-linkage, queried directly against the target issue number via the GraphQL `Issue.closedByPullRequestsReferences` connection, keeping only `OPEN`-state PR nodes. This is the root-cause fix #654 pointed toward: the signal is now GitHub's parsed issue linkage — the same set GitHub renders in the issue sidebar and acts on at merge time — rather than a text match this mechanic has to keep fence-, word-, and number-boundary-aware itself. Consequences folded into the mechanic's rationale in the README: - **Fenced code blocks and HTML comments are inert for free** — no fence-tracking heuristic to maintain, and the fence-blindness #654 filed is closed. - **No word-boundary / number-boundary guards** — `#463` cannot collide with `#4630` / `#1463` because the reference is GitHub's parsed linkage, not a regex over raw text. - **Base-branch correctness (behavior change, called out explicitly):** GitHub forms the close-link only for a PR that targets the default branch. A `Closes #<N>` on a non-default-base PR is ignored by GitHub and now no longer excludes its issue — where the retired raw-body regex counted it regardless of base. This is *more* correct per GitHub's real merge-time auto-close semantics. - **Opt-out (`Refs #<num>`) stays intrinsic** — a reference-without-closing never enters the closing linkage, so it correctly does not exclude its issue. Scope is held to the `work-items` adapter. The sibling fence-blind matcher noted in #654 — the pre-create gate `KEYWORD_REGEX` in `plugins/source-control/skills/pull-request/reference/create.md` — is **not** touched here: it is a different plugin, and it gates a PR body *before the PR exists*, so it cannot use this issue-side GraphQL linkage. Whether it should adopt a different fence-aware approach is left as a forward-reference follow-up, not folded into this adapter patch. ## Verification Empirical research proving `Issue.closedByPullRequestsReferences` is queryable via `gh api graphql` and returns GitHub's genuine auto-close linkage (open-state filtered) — not body text matches. **Schema exists and is queryable** (live introspection, `gh api graphql`): ``` closedByPullRequestsReferences args: includeClosedPrs Boolean = false "Include closed PRs in results" first/last/after/before, orderByState, userLinkedOnly ``` **Positive cases — real prose closing keywords link, filtered to open PRs** (the exact mechanic this PR ships, run live against this repo): ``` issue 642 -> true (open issue; open PR #666 carries prose `Closes #642`) issue 654 -> false (this issue; no open closing PR) issue 487 -> false (closed issue; its closing PR #629 is MERGED, so filtered out by state=="OPEN") issue 644 -> false (baseline before this PR) issue 667 -> false (baseline before this PR) ``` **Negative isolation — fenced and HTML-comment closing keywords are excluded.** This PR body is itself the live experiment. It carries a real `Closes #654` in prose (positive control: an open issue closed via prose, in *this* draft PR, at *this* moment), plus two deliberately inert fixtures — a closing keyword for open issue **#644 only inside a fenced code block**, and one for open issue **#667 only inside an HTML comment**: <!-- inert HTML-comment fixture (must NOT link #667): Fixes #667 --> ```text inert fenced fixture (must NOT link #644): Closes #644 ``` Because #644 and #667 are open issues (baselines `false` above) and this same draft PR *does* link #654 from prose, the only difference for #644/#667 is that their keyword sits in a fence / comment. Live query results against this PR are recorded below. Live results (this PR is #668, base branch `main`): ``` # PR-side — what GitHub links THIS PR as closing: pullRequest(668).closingIssuesReferences -> [654] # ONLY 654. #644 (fenced) and #667 (comment) excluded. # Issue-side — the exact mechanic this PR ships: issue(654).closedByPullRequestsReferences -> [{668, OPEN}] # real prose `Closes #654` links (positive control) issue(644).closedByPullRequestsReferences -> [] # fenced `Closes #644` inert — #668 absent issue(667).closedByPullRequestsReferences -> [] # HTML-comment `Fixes #667` inert — #668 absent ``` Conclusion: within one PR body at one instant, GitHub linked the prose closing keyword (#654) and excluded the identical keyword when it sat inside a fenced code block (#644) or an HTML comment (#667). Since #644/#667 are open issues that this same draft PR would otherwise be capable of linking, the fence / comment is the sole cause of exclusion. Both the fenced-example bug #654 reported and HTML-comment inertness are handled by `closedByPullRequestsReferences` with no heuristic of our own. Closes #654 ## Related - #654 — this issue (fence-blindness in the open-linked-PR closing-keyword regex). - #463 / PR #643 — shipped the "Open linked PRs" mechanic (and its partial `gsub` fence-stripper) this PR replaces. 🤖 Generated with a Claude Code implementation subagent (issue #654) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… disabled by default (#665) ## Summary Implements the #476 autopilot merge tier for `babysit-prs`: at day-scale throughput, human approve-and-merge is the pipeline bottleneck. The tier lets the fleet **satisfy** the branch ruleset instead of bypassing it — a second bot account (author ≠ approver) runs a genuine review pass and submits an approving review **only when clean**, after which the pinned merge gate merges **only when every criterion holds**. The ruleset itself is never touched; the bot review is what makes this a real gate rather than a rubber stamp. **HELD FOR OPERATOR: do-not-merge until the operator personally reviews the safety-contract change.** This PR moves through the normal pipeline as a proposal only. The babysit safety contract (`reference/safety.md`) changes *if and only if* the operator merges this PR. It carries the `do-not-merge` label at creation; `do-not-merge.yml` will hold the required check red by design — that is the intended hold, not a CI failure to fix. ## Criteria (verbatim from the 2026-07-19 15:49 maintainer decision) Enforced deterministically in `babysit_merge.py` behind the fail-closed `--autopilot-merge-tier` umbrella flag. Base-gate criteria already existed; the tier layers the rest. | Decision criterion | Where enforced | | --- | --- | | required checks green incl. review workflow | base gate (`mergeStateStatus` CLEAN + required-context reconciliation) | | issue-linked | tier — `closingIssuesReferences` non-empty | | authored by a pipeline lane | tier — `--lane-logins` membership | | no human CHANGES_REQUESTED | base gate (`reviewDecision`) | | no human blocking comment | tier — shared `has_blocking_text` / `has_blocking_severity` over human comments | | no unresolved thread | base gate (unresolved review threads) | | no do-not-merge label | tier — `--block-labels` | | no unratified decision-default marker on the linked issue | tier — scans `closingIssuesReferences` comments for a `Decision defaulted` marker; ratified only by a human `OWNER`/`MEMBER` comment after it | | head SHA unchanged since review | tier — distinct-bot approval pinned to the live head; `--expected-head` as always | | author ≠ approver via bot identity | tier — distinct-bot approving review (`--approver-bot-logins`, shared `is_bot`) | **No deviations from the decision comment.** Every criterion predicate is reused from the shared `babysit_classify` module (#634), not re-implemented. 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. ## Flip preconditions (tier stays disabled until these land) Both are enforcement-in-the-gate placements recorded on #476; the pre-tier world (a human reads the comments at GATE-ON) stays safe meanwhile. - **Decision-default veto** — added in this PR as a merge criterion (table above). Reactions are deliberately not consulted for ratification: the reactions API carries no `authorAssociation`, so a reaction cannot be attributed to a maintainer, and attributing it via the operator's self-logins would let pipeline automation clear its own veto (the #450 attribution-drift hazard). Ratification is therefore a maintainer comment, or a manual merge. Note: #476's own thread quotes the marker phrase, so once the tier is active #665 would self-hold under this criterion — harmless, since it is `do-not-merge`-held and merged by the operator by hand regardless. - **#642** — the `count_classified` fail-open (stale pipe-rows in non-thread comments) has **landed** (source-control 0.13.3 via #666) and is merged into this branch, so this flip-precondition is now cleared. It was never worked around locally — the tier consumes the shared classifier directly. The remaining holds are the operator's safety-contract review and the disabled-by-default flip. ## Disabled by default The tier exists only while the new `babysit_autopilot_merge_tier` userConfig (boolean, default **off**) is enabled; the skill wires the `--autopilot-merge-tier` flags only then. Enabling the flag, and any later gate-off flip, is a separate, announced operator step. Absent the flag the merge gate is byte-for-byte its prior self, so `worker`/`autopilot`'s existing gate-proven merges are unchanged. The umbrella flag is fail-closed: it refuses (exit 3) unless `--lane-logins`, `--approver-bot-logins`, and `--block-labels` are all supplied. ## safety.md rationale `reference/safety.md`'s "Never do automatically → merge" contract is updated deliberately to codify the tier and its criteria as an explicit, config-gated carve-out: the fleet may generate its own approving review and merge **only** under the enumerated criteria, fail-closed, with a genuine bot review and an untouched ruleset. This is the safety-posture change the operator signs off by merging. ## Test plan - `python -m unittest discover -s tests` — 287 tests pass, including new `test_babysit_merge.py` (each criterion with a passing **and** a fall-back fixture, the decision-default veto's pass / unratified / fetch-error cases, plus the tier-absent no-network invariant) and guard / skill-contract additions for the fail-closed CLI and tier prose. - `engine.test.sh` — unittest suite + ruff clean + guarded-wrapper checks (including the new wrapper-level tier fail-closed) all pass. - `babysit-readiness-gate.test.sh` — unchanged, green (Python-free degrade + convergence intact). - markdownlint, typos, and plugin-manifest JSON-schema validation all clean locally. ## Related Closes #476 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
The babysit readiness gate blocks while source findings outnumber their
per-finding classification rows. The shared classifier counted a self-authored
classification pipe-row in ANY comment, including PR-level review-summary
comments that are never thread-resolved. A review thread's findings are
discounted when it resolves (the #465 lifetime-vs-open guard), but a PR-level
comment can never be — so a stale classification posted outside a thread kept
counting after its finding was discounted, inflating the classified count past a
fresh, still-unclassified open-thread finding and emitting a fail-open
READINESS_OK.This was the single live fail-open on
mainand the ratified backstop for the#476 gate-off flip: per the backstop clause it must be fixed before the flip.
The dispatched fix shape ("non-thread PR-level comments never contribute to
classified") turned out unsafe on verification:review-discipline.md§D5mandates that issue/review-level (PR-level) findings be answered with a
detached PR-level classification comment. Thread-only counting would count every
such classification as zero and permanently block any PR whose findings come
from review summaries — and break the Python↔bash convergence. Corrected in
agreement with the tower.
Fix
babysit_classify.py).count_effective_classifiedbuckets comments by surface and caps credit withineach bucket via
min(classified, findings), then sums — a classification canonly offset a finding on its own surface, so a stale PR-level row can no longer
spill over to cover an open-thread finding.
count_classifiedandcount_findingsstay pure raw counters; the bucketing composes them. Onunsignalled input every comment lands in one bucket and this collapses to
min(classified, findings), preserving existing behavior and the bashconvergence property.
comment_surface). Three surfaces — review-thread,PR-level, and an isolated bucket for comments bearing no surface signal —
resolved from two signals in order: the explicit
in_review_threadstamp(authoritative when present), then the
fetch-all-pr-comments.shtypetag onthe
--comments-jsonreuse path (inline→ thread;general/review→PR-level). A comment with neither signal is isolated so its rows cannot offset —
and its findings cannot be offset by — a known surface (fail-closed for unknown
provenance), preserving the "no signal = PR-level lifetime" model
thread_is_opendocuments.
babysit_findings.py)._comment()recordsin_review_thread(true only when fetched from a review thread); issue-level andreview-summary comments are stamped PR-level. The entrypoint emits the effective
count.
babysit-readiness-gate.sh). The thread-blind safe-tierdegrade gains the thread-state-free analogue
classified = min(classified, findings)so a row over-count can't mask a finding and the degrade staysconvergent with the Python
minon unsignalled input. Per-surface bucketing isinherently surface-aware and remains Python-only, exactly like the source-control:babysit-prs: babysit-readiness-gate.sh over-counts lifetime P-badges as 'findings', false READINESS_BLOCKED reason=under-decomposed on fully-classified PRs #465 discount.
Behavior flip (documented, fail-closed)
A PR whose inline-thread findings are answered only by detached PR-level
classification replies now reports
READINESS_BLOCKEDwhere it previouslypassed — a PR-level row no longer offsets an inline-thread finding. This
mechanically enforces §D5's already-ratified reply routing (inline findings MUST
reply threaded, "NEVER a detached
pr comment"). Runs already following §D5 areunaffected; only runs relying on the previously-tolerated detached-reply shape
change verdict.
Residual (documented, not closable here)
An orphaned PR-level classification covering a fresh PR-level finding is
irreducible: GitHub's flat issue comments carry no finding↔classification
linkage, so a stale PR-level row is data-identical to a live one. It is reachable
only via a §D5 routing violation (an inline finding answered with a detached PR
comment) or a reviewer editing/deleting a finding; a §D5-compliant, no-edit run
never hits it. The per-surface fix closes the entire linkable (thread) side, on
both the live and
--comments-jsonreuse paths.Verification
python -m unittest discover -s tests— 269 tests OK, incl. newEffectiveClassifiedTests(per-surface credit, reuse-pathtypeinference,explicit-stamp precedence, isolated-unknown),
SurfaceStampingTests, andMain642FailOpenTests.babysit-readiness-gate.test.sh— 60 pass, incl. the live-path babysit classifier: count_classified counts stale pipe-row classifications from non-thread review-summary comments #642 scenario,the reuse-path inline-
typescenario, the unsignalled-provenance isolationscenario (all Python-gated like source-control:babysit-prs: babysit-readiness-gate.sh over-counts lifetime P-badges as 'findings', false READINESS_BLOCKED reason=under-decomposed on fully-classified PRs #465), and all five convergence cases including
the over-classified cap.
engine.test.sh— pass (unittest +ruff check); shellcheck + shfmt clean;markdownlint-cli2+validate-plugins.shclean.Versioning
plugins/source-control/.claude-plugin/plugin.json→ 0.13.3; CHANGELOGentry under
[0.13.3]documenting the fail-open closure and the §D5 PASS→BLOCKbehavior flip.
origin/mainmerged in (0.13.2 base from fix(source-control): drop Refs #N from pull-request create opt-out gate (#630) #651).Related
Closes #642
🤖 Generated with Claude Code
https://claude.ai/code/session_01FM1RfM3jHkgenpdbMv4o64