refactor(source-control): one shared babysit authorship/finding/approval classifier + golden fixtures (#534) - #634
Conversation
…pproval classifier (#534) Authorship (self/bot/human), finding (severity + lifetime-vs-open), and approval-verdict detection were hand-rolled independently across the babysit snapshot, merge gate, resolve-thread reporter, and readiness gate, and the surfaces disagreed on identical input -- the six-issue misclassification class. Extract one shared babysit_classify module (leaf: depends only on babysit_util) and migrate every surface to it: babysit_delta/feedback/merge import the self-login membership test and authorship/finding/approval primitives directly, babysit_resolve_thread shares the same is_bot test, and babysit-readiness-gate.sh shells out to a Python finding counter (babysit_findings.py) instead of re-implementing the severity vocabulary in bash grep. The bash counting is kept only as the Python-free safe-tier degrade; a convergence test pins the two. Fixes folded in as golden fixtures by construction: - #465 finding lifetime-vs-open: the counter discounts markers in resolved or outdated threads (mechanical resolved/outdated discount only; dedup-by-concern out of scope). babysit_merge remains the merge-ready boolean owner. - #512 humanThreadsActed: count only threads whose opening author is human. Members #499/#497/#473 are already fixed on main -> regression fixtures; #455 is a different (retry) classifier, referenced only. Every surface stays a pure predicate with no writes (#440 report-only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC
|
Claude finished @kyle-sexton's task in 3m 55s —— View job PR Review: refactor(source-control): one shared babysit classifier
SummaryThe extraction is well-executed. Finding 1 —
|
babysit_classify.py and babysit_findings.py carry shebangs; match the executable git index mode of the sibling babysit-prs scripts (hygiene shebang-exec gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC
|
Claude finished @kyle-sexton's task in 5m 19s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
OverallSolid extraction. The dependency graph is correctly acyclic ( One medium-severity correctness gap, one minor inconsistency, and a few notes below. Medium:
|
| Round | Event | count_findings |
count_classified |
Gate |
|---|---|---|---|---|
| 1 | 3 findings posted, all classified, all threads resolved | 0 | 3 | OK |
| 2 | Bot posts 1 NEW finding in a fresh (open) thread; no classification yet | 1 | 3 | False OK — 3 >= 1 |
This is different from the "dedup-by-concern across re-review rounds" that the PR explicitly defers. That deferral concerns the same concern restated within still-open threads; this is an unclassified new finding absorbing historical classifications from unrelated resolved threads.
The fix would be to compute count_classified only over self comments in currently-open threads — mirroring count_findings's thread_is_open filter. Alternatively, document this as a known limitation if the operational cost of false-OKs is judged tolerable.
Low: is_bot in project_thread and humanThreadsActed silently drops extra_bot_logins
File: babysit_resolve_thread.py L117–122 and L476–478
The is_bot call in project_thread (for computing botOnly) and the humanThreadsActed counter both call is_bot(login, typename) with no extra_bot_logins argument. If an operator registers a non-structural bot in babysit_extra_bot_logins, that account's threads will be treated as human-opened threads by this CLI:
botOnly=Falsefor a thread where all commenters are fromextra_bot_loginsonly (no[bot]suffix, noBottypename) → thread classified asskipped-human-threadinstead of eligiblehumanThreadsActedcounts that account as a human if it opens a thread that gets acted on
In practice GitHub App bots always carry the [bot] suffix or Bot typename, so this is only triggered by accounts that require the extra_bot_logins escape hatch. Worth at minimum a code comment noting the omission; worth fixing if the operator UX of babysit_extra_bot_logins is meant to propagate everywhere.
Note: count_classified counts thread-state-free issue-comments / review summaries too
From babysit_findings.py, fetch_live_comments builds the corpus with:
- issue comments →
isResolved: False, isOutdated: False(not thread-scoped) - review summaries → same
These are never in review threads, so thread_is_open always returns True for them — they are always counted in count_findings. Similarly, a self-authored classification reply posted as a top-level issue comment will always be counted in count_classified. This is correct and consistent; just noting the architecture for future readers.
Regex correctness spot-check: NEGATED_SEVERITY_MARKER_RE
The scoped inline-flag design ((?i:...) on the negator and trailing noun, bare (?:CRITICAL|IMPORTANT) in the middle) is correct and matches the documented intent. "No CRITICAL or IMPORTANT findings" → redacted; "No critical findings" → not redacted (because critical is lowercase and the middle alternation is case-sensitive). ✓
Shell degrade convergence proof is correctly scoped
The 47 pre-existing bash gate fixtures running through the Python counter is an accurate convergence proof for thread-state-free input (bash cannot see thread state; the fixtures don't carry isResolved/isOutdated). The #465 case is correctly guarded by a Python 3.11+ version probe so it's skipped on the bash-only safe-tier path. ✓
babysit_resolve_thread.py fix for #512 is correct
humanThreadsActed now gates on not is_bot(r["author"], r["authorType"]) — the opening author's type — rather than not botOnly (which was False for any thread with a human reply, regardless of who opened it). The three new golden fixtures in test_babysit_resolve_thread.py (bot-opened/human-reply, human-opened, mixed) are sufficient regression coverage. ✓
babysit_findings.py error handling
On Python failure, stderr is suppressed by the gate's 2>/dev/null, stdout is empty, the findings=..., classified=... regex doesn't match, and the bash degrade counts stand. This fail-open-to-bash fallback is intentional and correctly implemented. ✓
Security
- No shell injection vectors:
COMMENTS_JSONis quoted everywhere;self_csv_joinedis constructed viaIFS=, printf '%s' "${SELF_LOGINS[*]}"(array expansion, not eval). - The Python helper path sourcing (
PY_SCRIPTSderived fromSCRIPT_DIRvia${BASH_SOURCE[0]}) is robust against CWD manipulation. babysit_classify.pycarries no writes and has no network access — pure predicate as specified.- No secrets or PII introduced.
The ported badge-vocabulary comment reused the literal `![PN Badge]` alt-text token, which the typos hygiene gate flags (PN -> ON). Reword to describe the alt text without the token; also tidy a garbled test fixture string. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC
|
Claude finished @kyle-sexton's task in 1m 38s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
Latest commit (
|
…eads Two review findings on the shared classifier: - count_classified applied no thread_is_open guard, unlike count_findings, so a classification row from an already-resolved thread kept inflating the denominator -- a fresh finding in a newly-open thread could pass the gate's classified >= findings check with zero real classification. Apply the same #465 lifetime-vs-open discount symmetrically. - actor_kind used .lower() instead of .casefold() before is_bot, unlike every other login normalization in this module. Adds a regression test proving a resolved-thread classification no longer masks a fresh open-thread unclassified finding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC
|
Claude finished @kyle-sexton's task in 1m 17s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
Latest commit (
|
| Finding | Severity | Status |
|---|---|---|
count_classified not thread-state-aware → false READINESS_OK |
Medium | Fixed in 3406846 |
actor_kind uses .lower() vs .casefold() |
Low | Fixed in 3406846 |
is_bot in project_thread/humanThreadsActed drops extra_bot_logins |
Low (informational) | Accepted — structural bots always carry [bot] suffix or Bot typename; this only matters for the extra_bot_logins escape hatch, which is a documented operator action |
All three member issue fixes (#465, #512, #499 regression) remain correctly implemented and covered by golden fixtures. The acyclic dependency graph, backward-compatible re-exports, bash degrade contract, and shell injection safety from the prior structural review are all unchanged. ✓
This PR is ready to merge.
… discount stale classifications Add a real dual-path convergence test: BABYSIT_READINESS_BASH_ONLY=1 forces the Python-free degrade so the gate test asserts the shared Python counter and the bash grep counting agree on thread-state-free fixtures (the operator escape also makes the degrade path exercisable). The severity vocabulary can no longer drift between the two silently. Make count_classified apply the same resolved/outdated discount as count_findings (#465 symmetry): a classification row in an already-resolved thread is a lifetime artifact, so without the discount a stale row would offset a fresh unclassified finding and false-pass the gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC
|
Claude finished @kyle-sexton's task in 2m 17s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
Both prior findings resolvedMedium —
Low —
New commit
|
| Fixture | Vocabulary shape |
|---|---|
severity-words |
CRITICAL/IMPORTANT/SUGGESTION |
codex-badges |
/badge/P1-, /badge/P2- shields.io URLs |
plain-p-markers |
[P1], [P2] bracketed labels |
self-row-exclusion |
| CRITICAL: null deref | VALID | table row excluded from count |
All fixtures are thread-state-free (no isResolved/isOutdated fields), which is the correct scope for pinning bash-vs-Python: bash cannot see thread state, so convergence can only be asserted on input both paths handle identically. The header comment in the test makes this explicit. ✓
mkjson is defined at line 27 of the test script and is correctly reused. ✓
Remaining note (low, from prior pass — still open)
is_bot in babysit_resolve_thread.py omits extra_bot_logins at two call sites (L117–120 for botOnly, L477 for humanThreadsActed). If an operator registers a non-structural bot via babysit_extra_bot_logins, that account's threads are miscategorized here. This is pre-existing relative to the PR's scope (the PR changes these lines only to use the shared is_bot — the extra_bot_logins omission was latent before). Worth a follow-up or a code comment acknowledging the omission, but not a blocker for this PR.
Overall
Both priority findings are fixed with correct implementations and regression fixtures. The convergence test is a genuine improvement: it transforms the "the bash and Python counters shouldn't drift" property from a prose claim into an executable assertion. The PR is structurally sound and ready for merge.
Review classification (work-2 lane, post-green pass)
Findings 1-2 fixed and verified by an independent re-review pass (commit Also did a manual diff read (rule 10 due diligence, given this touches merge-readiness gating): the PR's one deviation from the ratified brief — retaining a bash-only Python-free degrade path via |
|
Claude finished @kyle-sexton's task in 2m 39s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
SummaryThis is the final review pass on HEAD Prior findings — all resolvedMedium (fixed The Low (fixed
Low/informational (accepted, #637 filed) — Pre-existing gap surfaced by the migration, not introduced by it. Structural bots always carry the Convergence test (
|
| Area | Check |
|---|---|
| Acyclicity | babysit_classify.py imports only babysit_util. All consumer surfaces import from babysit_classify (or babysit_feedback which re-exports). ✓ |
| Backward compat | babysit_feedback.__all__ lists all 17 moved names; all have a corresponding from babysit_classify import .... Existing consumers unchanged. ✓ |
| #465 lifetime discount | thread_is_open returns True when neither isResolved nor isOutdated is truthy; comments without either field (issue-level, bash-fixture shape) correctly count. ✓ |
| #512 fix | humanThreadsActed at L477 gates on not is_bot(r["author"], r["authorType"]) — the opening author's type — not botOnly. Three golden fixtures cover bot-opened/human-reply, human-opened, and mixed. ✓ |
| #499 regression | test_approve_with_nits_downgrades_and_has_no_severity_finding covers the exact scenario from the issue. ✓ |
| Python degrade contract | On Python failure, stderr suppressed by 2>/dev/null, stdout empty, findings=/classified= regex doesn't match, bash counts stand. ✓ |
fetch_live_comments resolved thread handling |
include_resolved=True is intentional — resolved threads fetched so they can be discounted, not silently dropped. ✓ |
| Shell injection | SELF_JSON built via printf ... | jq -R . | jq -s .; self CSV joined with array expansion, not eval. ✓ |
| All-bodies finding count | Bash path uses all_bodies = non_self_bodies + self_source_bodies (self classification rows stripped). Python count_findings applies the same strip for self-authored comments. Symmetric. ✓ |
| Pure predicate | babysit_classify.py has no writes and no network access. babysit-readiness-gate.sh detection-only. ✓ |
Overall
Both priority findings are correctly fixed with regression coverage, the convergence test transforms a prose claim into an executable assertion, and the three member issues (#465, #512, #499) each have a golden fixture locking in the fix. The dependency graph is acyclic, the re-export surface is intact, and the bash-degrade contract is preserved.
This PR is ready to merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 206dad5764
ℹ️ 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".
… review thread fetch_review_threads caps each thread's comment connection and flags an oversized thread comments_truncated instead of raising, so one giant thread cannot fail the whole snapshot for consumers that do not need every comment. fetch_live_comments IS such an exact-count consumer but ignored the flag: past the 100-comment cap a severity marker or classification row was silently dropped, under-counting findings so the readiness gate could report READINESS_OK while a later open finding sat unclassified. Honor fetch_review_threads' documented contract and fail closed: raise on any truncated thread. main maps this to exit 2, which emits no findings= line, so the gate stays on its complete REST/bash degrade count (fetch-all-pr-comments.sh surface 3 paginates every inline review comment PR-wide, with no per-thread cap). Falling back over-counts at worst (safe direction), never false-passes. Adds test_babysit_findings.py pinning the gate-observable contract: a truncated thread makes main exit 2 with no findings= line on stdout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FM1RfM3jHkgenpdbMv4o64
|
Claude finished @kyle-sexton's task in 1m 50s —— View job PR Review: refactor(source-control): one shared babysit authorship/finding/approval classifier
Latest commit
|
| Test | What it pins |
|---|---|
test_truncated_thread_raises |
fetch_live_comments raises RuntimeError on a truncated thread |
test_untruncated_thread_collects_comments |
Normal operation unchanged |
test_main_exits_2_with_no_count_line_on_truncation |
Gate-observable contract: exit 2, no findings= on stdout |
The third test is the load-bearing one — it proves what the gate actually observes (empty stdout, no regex match → bash degrade stands). Patching on the bf namespace (imported-by-name) is the correct mock target for this pattern. ✓
Docstring update (L97–104): explains why this counter is an exact-count consumer (unlike snapshot consumers that tolerate partial thread data), and why the fail-closed contract is correct here. This is the right kind of comment. ✓
All findings resolved
| Finding | Severity | Status |
|---|---|---|
count_classified not thread-state-aware → false READINESS_OK |
Medium | Fixed in 3406846 |
actor_kind uses .lower() vs .casefold() |
Low | Fixed in 3406846 |
is_bot in babysit_resolve_thread.py omits extra_bot_logins at two call sites |
Low (informational) | Accepted — structural bots always carry [bot] suffix or Bot typename; filed as #637 |
| [Codex P2] Truncated review-thread page silently dropped → false READINESS_OK | Medium | Fixed in bb55a5d |
Full structural review — unchanged and confirmed correct
Everything from prior passes holds: acyclic dependency graph, correct #465 lifetime discount, correct #512 bot-thread fix, #499 regression fixture, backward-compatible re-exports, bash degrade contract, shell injection safety. No regressions from the two changes in bb55a5d (new if guard in a loop, new test file).
This PR is ready to merge.
…) (#643) ## Summary `/work-items:work` selection had no rule excluding an issue that already has an open linked PR from the pickable frontier. An issue keeps `status: ready` for its entire window with an open PR (from open through merge/auto-close), so a picker had to manually cross-check `gh pr list` to avoid re-picking work already in flight and starting a duplicate branch. This ships the ratified in-repo open-linked-PR selection filter (operator decision, 2026-07-19): an issue with an open linked PR is not pickable. ## Fix - **Selection-time frontier filter (`skills/work/SKILL.md`, Step 1).** After `list-frontier` derives the frontier, tiers 2–3 candidates are additionally filtered to drop any number that has an open PR targeting it for closure — so an in-flight item leaves the pickable set instead of being re-picked. The **closing-keyword linkage** is authoritative (the same `Closes #N` / native-closing-keyword signal `pr-issue-linkage` enforces); an intentional `Refs #N` opt-out does not exclude its issue. The filter **fails open** when the bound provider exposes no PR host (offline `local-markdown` is never a coordination surface and touches no network tool). - **New GitHub adapter mechanic (`tools/work-item-tracker/adapters/github/README.md`, "Open linked PRs").** The provider mechanics stay in the adapter per this repo's seam/adapter separation — the skill core inlines no `gh`. It uses `gh pr list --state open --search "<N> in:body"` (truncation-safe per item, no page-size race) and a closing-keyword `jq` test with an exact-number boundary so `#463` does not match `#4630` / `#1463`. - **Retires the interim heuristic.** The prior "Already-in-flight (interim, retire on `#463`)" bullet in the execute-step staleness pre-check is removed — the frontier itself now excludes in-flight items, which is exactly the retirement that bullet anticipated. Scope-narrowed to the closing-keyword signal (the interim branch-pattern-only match is dropped): a standard-flow PR always carries the gate-enforced `Closes #N`, so keyword-matching is faithful to the ratified "references it as a closing target" scope. - **No seam-contract change.** `CONTRACT.md` and `list-frontier` are untouched; the addition is a README-documented adapter mechanic, not a new seam verb. The durable seam-level in-review state is explicitly NOT built here (see Related). ## Verification The adapter mechanic was exercised against live repo data (read-only). `<N>` substituted per row: ``` issue #435 open-closing-PR: true (open, unassigned, status: ready; open PR #638 closes it) issue #487 open-closing-PR: true (open, unassigned, status: ready; open PR #629 closes it) issue #534 open-closing-PR: true (open, unassigned, status: ready; open PR #634 closes it) issue #463 open-closing-PR: false (no open PR closes it → stays pickable) issue #4630 open-closing-PR: false (exact-number boundary: does not false-match on #463's PR) ``` Each of #435 / #487 / #534 is a genuine frontier-eligible candidate (open, unassigned, `status: ready`) that under the old logic stayed pickable despite an open PR already in flight; the filter returns `true` for exactly those and excludes them, while #463 (no open closing PR) returns `false` and remains pickable. `list-frontier` itself was not run end-to-end here because the `work-items` plugin repo binds no tracker (`.work-item-tracker.json` is a consuming-repo artifact) — the per-number filter, which is the added logic, is what is demonstrated above. Closes #463 ## Related - #463 — this issue (open-linked-PR selection filter). - #416 — planning routes through the tracker seam; argues the durable in-review state belongs at the seam, not a GitHub-only label. This PR does NOT implement that durable state. - #498 — seam read-verb coverage; where a durable in-review / container-scoped read would live. This PR does NOT implement that durable state. Note: PR #641 concurrently bumps `work-items` to `0.14.0` (minor, for #478) and also edits `plugin.json` + `CHANGELOG.md`. This PR is a patch bump to `0.13.1` from the current `origin/main` base (`0.13.0`) — a different version slot, not a race — but whichever merges second will need a mechanical rebase on those two files (the same-plugin concurrency #464 treats as an awareness note, not a block). 🤖 Generated with a Claude Code implementation subagent (issue #463) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ce (#642) (#666) ## 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 `main` and 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` §D5 *mandates* 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 - **Per-surface classification credit (`babysit_classify.py`).** `count_effective_classified` buckets comments by surface and caps credit within each bucket via `min(classified, findings)`, then sums — a classification can only 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_classified` and `count_findings` stay pure raw counters; the bucketing composes them. On unsignalled input every comment lands in one bucket and this collapses to `min(classified, findings)`, preserving existing behavior and the bash convergence property. - **Surface discriminator (`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_thread` stamp (authoritative when present), then the `fetch-all-pr-comments.sh` `type` tag on the `--comments-json` reuse 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_open` documents. - **Surface stamping (`babysit_findings.py`).** `_comment()` records `in_review_thread` (true only when fetched from a review thread); 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 a row over-count can't mask a finding and the degrade stays convergent with the Python `min` on unsignalled input. Per-surface bucketing is inherently surface-aware and remains Python-only, exactly like the #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_BLOCKED` where it previously passed — 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 are unaffected; 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-json` reuse paths. ## Verification - `python -m unittest discover -s tests` — 269 tests OK, incl. new `EffectiveClassifiedTests` (per-surface credit, reuse-path `type` inference, explicit-stamp precedence, isolated-unknown), `SurfaceStampingTests`, and `Main642FailOpenTests`. - `babysit-readiness-gate.test.sh` — 60 pass, incl. the live-path #642 scenario, the reuse-path inline-`type` scenario, the unsignalled-provenance isolation scenario (all Python-gated like #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.sh` clean. ## Versioning - `plugins/source-control/.claude-plugin/plugin.json` → **0.13.3**; CHANGELOG entry under `[0.13.3]` documenting the fail-open closure and the §D5 PASS→BLOCK behavior flip. `origin/main` merged in (0.13.2 base from #651). ## Related Closes #642 - #534 / #634 — shared babysit classifier this hardens (found during the #634 digest). - #465 — the lifetime-vs-open discount this mirrors for classifications. - #476 — gate-off flip this fail-open blocks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FM1RfM3jHkgenpdbMv4o64 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…s, zero orphans (#708) ## Summary Fix-direction (a) from the issue, per the #634 graded-fixture idiom: every fixture under `plugins/autonomy/skills/setup/evals/fixtures/security-binding/` is now graded. - **One table-driven runner** (`check-security-binding.fixtures.test.mjs`) + **one co-located expectations manifest**: each entry pins the checker invocation (`--probe-evidence-root` at the fixtures dir; per-fixture `--egress-hosts`/`--evidence` where needed) and the expected outcome — exit code plus defect-naming stderr substrings. 109 fixtures: 14 pass-expected (12 valid bindings + 2 evidence-input pairings), 95 reject-expected. **Zero quarantined — no name-vs-behavior mismatches surfaced.** - **Self-policing both directions**: a new top-level fixture without a manifest (or quarantine) entry fails; a manifest ref whose file vanished fails; the 67 `probe-transcripts/` suite inputs are enumerated and reconciled against disk both ways. - **Baseline drained**: all 178 security-binding lines leave `scripts/orphaned-fixtures-baseline.txt`; the orphaned-fixture gate (#681) passes with the set consumed, exactly as its stale-guard demands. - Thin `.test.sh` wrapper joins `plugins/**` CI test discovery. Suite: **394/394 checks pass.** `validate-plugin-contracts.mjs`, orphaned-fixtures `--check`, changelog-parity `--check`/`--check-bump` all green. Autonomy plugin bumped 0.7.3 with CHANGELOG entry. Note for reviewers: the convention-level decision about the golden-fixture idiom repo-wide stays with #664 (needs-human); this PR instantiates the already-precedented #634 shape for the one suite #662 names, which the issue's own fix-direction (a) authorizes. ## Related - #681 (the orphaned-fixture gate whose baseline this drains) - #664 (repo-wide golden-fixture convention decision — untouched) Closes #662 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 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>
…1337) Closes #637 *This was generated by AI during work-loop execution.* ## Summary `babysit_resolve_thread.py` called the shared `is_bot` classifier at two sites without passing the caller's `extra_bot_logins` config — `project_thread`'s `botOnly` computation (L117-120) and the `humanThreadsActed` reporting counter (L477) — unlike every other classifier call site (e.g. `actor_kind` in `babysit_classify.py`). An operator who registered a non-structural bot account via `babysit_extra_bot_logins` (no `[bot]` login suffix, API `__typename` reports `User`) had that account's threads miscategorized at both sites. Pre-existing relative to #534/#634 (that PR migrated these call sites to the shared `is_bot` but did not introduce the omission). This PR adds an `--extra-bot-logins` CLI flag to `babysit_resolve_thread.py` (same comma-separated shape as the snapshot wrapper's flag), threads it through `project_thread` and `fetch_threads` via a closure, and passes it to both `is_bot` call sites. `SKILL.md`'s `babysit_extra_bot_logins` flag-delivery table now lists `resolve-thread` alongside `snapshot`. Patch-bumps `source-control` to 0.26.3 with a matching CHANGELOG entry. ## Test plan - Added `ProjectThreadExtraBotLogins` (site 1) and `HumanThreadsActedExtraBotLogins` (site 2) to `tests/test_babysit_resolve_thread.py` — each asserts a configured non-structural login is correctly classified as bot, and that an unconfigured one still falls back to structural detection alone (regression coverage for both directions). - `python -m pytest tests/ -q` in `plugins/source-control/skills/babysit-prs/scripts` — 352 passed, 58 subtests passed (full existing suite, no regressions). - `python -c "import ast; ast.parse(...)"` — syntax check on the modified script. ## Related N/A 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Authorship (self/bot/human), finding (severity + lifetime-vs-open state), and approval-verdict detection were hand-rolled independently across the babysit snapshot (
babysit_delta/babysit_feedback), the merge gate (babysit_merge), the resolve-thread reporter (babysit_resolve_thread), and the readiness gate (babysit-readiness-gate.sh). The surfaces disagreed on identical input — the six-issue misclassification class this umbrella closes (the smoking gun: #499, where the gate returnedfindings=0while the snapshot called the same review "1 blocking bot finding").This extracts one shared classifier and migrates every surface to it, so the surfaces cannot diverge, and folds each member issue in as a golden fixture — regression-proof by construction. Implements the operator-ACCEPTED Option A from the decision brief verbatim.
Fix
babysit_classify.py(depends only onbabysit_util) owns the three concern areas: authorship (is_bot,actor_kind, self-login normalization/membership, dependency-author test), finding (blocking/severity heuristics + the readiness gate's finding/classification counting), and approval-verdict (approval/skip downgrades). The primitives moved out ofbabysit_feedback, which re-exports them so existing consumers are unchanged.babysit_deltaandbabysit_mergereplace hand-rolledself_loginscasefold-compare (delta ×3 + merge) with the sharednormalize_self_logins/is_self_login;babysit_feedbackorchestrates the shared primitives;babysit_resolve_threadshares the sameis_bot.babysit_mergeremains the merge-ready boolean owner — it just stops carrying its own private authorship copy.babysit-readiness-gate.shshells out tobabysit_findings.pyfor finding-counting (mirroring the existingsource-control-babysit-mergebash→Python wrapper) instead of re-implementing the severity vocabulary in bash grep. The bash counting is retained only as the Python-free safe-tier degrade (reference/loop.mdis that path and it runs this gate; hard-requiring Python would regress that documented tier). A convergence test pins the two counts together on thread-state-free input.READINESS_BLOCKED): the shared counter discounts a severity marker carried in a review thread GitHub reports resolved or outdated, counting currently-open findings only. Scope: this is the mechanical resolved/outdated discount the brief specs ("severity occurrence + lifetime-vs-open state"). De-duplicating the same concern restated across re-review rounds within still-open threads is deliberately out of scope — there is no reliable mechanical "same concern" signal — so restatements still count.humanThreadsActedreported for a Bot-authored thread): count only threads whose opening author is human (via the sharedis_bot), notbotOnly(which mislabeled a bot-opened thread carrying a later human reply).orchestration.mdalready mandates trusting the deterministic engine (the brief notes the re-derivation was "real but implicit"). Strengthened that trust instruction to name the classification as now one shared, fixture-locked classifier, so "don't re-derive by eye" has teeth.report-onlydefault preserved).Verification
All commands run in the worktree with Python 3.14.6, ruff 0.15.20, jq 1.8.2.
Engine suite (unittest + ruff + guarded wrappers) — 256 tests (was 236; +17 classifier + 3 resolve-thread golden fixtures):
Readiness-gate bash suite — 53 cases, exit 0. The 47 pre-existing fixtures still pass (now through the Python counter). New: the #465 lifetime discount end-to-end, plus an explicit dual-path convergence test —
BABYSIT_READINESS_BASH_ONLY=1forces the degrade so both counts are observed in one run and asserted equal, so the severity vocabulary cannot drift between Python and bash silently:#465 end-to-end (3 lifetime severity markers — 2 resolved/outdated, 1 open — fully classified):
The bash degrade counts all 3 lifetime markers (old behavior, preserved for Python-free); the Python path discounts the 2 resolved/outdated → the false
READINESS_BLOCKEDis gone.#512 regression (
tests/test_babysit_resolve_thread.py): bot-opened thread with a human reply, acted under--include-human→humanThreadsActed: 0; human-opened →1; mixed →1. Passing.Lint:
shellcheck --rcfile .shellcheckrcclean on both gate scripts;shfmt -d(editorconfig-driven) clean; plugin manifest valid JSON at0.13.0.Closes #534
Related
Member issues folded as golden fixtures / regression proofs: #512 (bot thread counted as human — fixed here), #499 (Approve-with-nits, already fixed by #567 — regression fixture), #465 (lifetime over-count — fixed here), #497 (single-
--prself_loginsempty, already fixed on main — covered bySinglePrScopeSelfLoginTests), #473 (CLOSED, self-reply — fixture-only). #455 is a different classifier (auto-mode retry semantics, not authorship/finding/approval) and is referenced only, per the brief.babysit_resolve_thread.pytwois_botcall sites omitextra_bot_logins), pre-existing gap surfaced but not introduced by this PR, deferred out of scopeDeferred out of this PR's scope (arrived as research/triage notes, not operator-ACCEPTED like Option A; none trivially covered by the classifier built here):
COMMENTEDthreads as ADVISORY for a fast-path resolve (#534 comment 5019177827). A natural follow-up: it would slot into this same classifier.babysit_feedbackclassification gaps found on PR fix(source-control): babysit snapshot classifies Approve-with-nits bot review as non-blocking (#499) #567). Same classifier home, non-trivial policy decisions of their own.🤖 Generated with Claude Code
https://claude.ai/code/session_01KyLj6oaFVTE6xFuoYCC2KC