Skip to content

feat(source-control): babysit autopilot merge tier, gate-enforced and disabled by default#665

Merged
kyle-sexton merged 17 commits into
mainfrom
feat/476-autopilot-merge-tier
Jul 20, 2026
Merged

feat(source-control): babysit autopilot merge tier, gate-enforced and disabled by default#665
kyle-sexton merged 17 commits into
mainfrom
feat/476-autopilot-merge-tier

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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.

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

… disabled by default

Add the #476 autopilot merge tier to babysit-prs: 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. Criteria are enforced deterministically in
babysit_merge.py behind the fail-closed --autopilot-merge-tier umbrella flag:

- required checks green incl. the review workflow (mergeStateStatus CLEAN;
  ruleset never bypassed) and head pinned by --expected-head as always;
- issue-linked (a closing-issue reference);
- authored by a configured pipeline lane (--lane-logins);
- no human CHANGES_REQUESTED / blocking comment / unresolved review thread;
- no configured do-not-merge label (--block-labels);
- a distinct-bot approving review on the live head (--approver-bot-logins;
  author != approver, head SHA unchanged since review).

Any criterion failing falls back to today's behavior: the PR is reported on
the human merge-ready list. The umbrella flag refuses (exit 3) unless all
three parameter sets are supplied, and every criterion predicate is reused
from the shared babysit_classify module rather than re-implemented. Absent
the flag the merge gate is byte-for-byte its prior self, so worker/autopilot's
existing gate-proven merges are unchanged.

The tier exists only while the new babysit_autopilot_merge_tier userConfig
(boolean, default off) is enabled; enabling it and any later gate-off flip is
a separate, announced operator step. safety.md's "Never do automatically:
merge" contract is updated deliberately to codify the tier and its criteria.

Tests: new test_babysit_merge.py covers each criterion with a passing and a
fall-back fixture plus the tier-absent no-network invariant; guard and
skill-contract tests cover the fail-closed CLI and the tier prose.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Jul 20, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d775a4d86c

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
…rge-tier

# Conflicts:
#	plugins/source-control/.claude-plugin/plugin.json
#	plugins/source-control/CHANGELOG.md
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 21s —— View job


PR Review

  • Gather context from PR metadata and changed files list
  • Read babysit_merge.py — core tier implementation
  • Read test_babysit_merge.py — new test coverage
  • Read test_guards.py and test_skill_contract.py — guard/contract additions
  • Read plugin.json, SKILL.md, safety.md — manifest/doc changes
  • Evaluate Codex P1 finding (superseded bot approvals)
  • Synthesize review findings and post

Overall the implementation is well-structured, fail-closed at every boundary I can trace, and clearly aligned with the decision criteria in the PR description. The new guard tests and skill-contract tests are especially solid. I have one confirmed correctness finding, one test-completeness gap, and a few smaller observations.

Finding 1 — Confirmed correctness bug: superseded bot approval not detected

babysit_merge.py L211–232find_distinct_bot_approval

Codex flagged this as P1 and the concern is valid. The loop filters to only APPROVED reviews and returns the last one chronologically:

for review in reviews:
    if str(review.get("state") or "") != "APPROVED":
        continue
    ...
    match = review

If the bot approver later submits CHANGES_REQUESTED on the same head (same commit.oid), that review is silently skipped. The result is that an obsolete approval — one the bot's own subsequent decision superseded — satisfies the tier's "distinct-bot approval on the live head" criterion.

When the base gate protects you: if the bot is a required reviewer, GitHub will set reviewDecision = CHANGES_REQUESTED, and the base gate at line 440 catches it. When it doesn't protect you: if the bot account is an optional reviewer (its approval satisfies a count requirement only) and a separate human approval already exists, GitHub may still report reviewDecision = APPROVED and mergeStateStatus = CLEAN — both base gate checks pass, the stale approval is accepted, and the tier merges against the bot's current intent.

The existing babysit_feedback.latest_reviews_by_author(..., decisive_only=True) already models this correctly: it folds reviews to each actor's latest decisive state. A straightforward fix is to deduplicate by author before scanning for approvals:

# Collapse to each bot's latest decisive review before scanning
decisive_states = {"APPROVED", "CHANGES_REQUESTED", "DISMISSED"}
by_author: dict[str, dict[str, Any]] = {}
for review in reviews:
    state = str(review.get("state") or "")
    if state not in decisive_states:
        continue
    # key = normalized login
    login_key = ...
    rank = str(review.get("submittedAt") or "")
    if login_key not in by_author or rank > by_author[login_key].get("_rank", ""):
        by_author[login_key] = {**review, "_rank": rank}

# Now filter to APPROVED among the collapsed set
for review in by_author.values():
    if str(review.get("state") or "") != "APPROVED":
        continue
    ...

Note: latest_reviews_by_author takes a PR dict (not a raw review list), so it can't be called directly here, but the same ranking logic (submittedAt descending, id as tiebreaker) can be inlined in find_distinct_bot_approval. Alternatively, evaluate_autopilot_tier could pass only the decisive-latest reviews into this function.

The test suite has no case covering [APPROVED at T1, CHANGES_REQUESTED at T2, same head] — adding one alongside the fix would close the gap.

Fix this →


Finding 2 — Test completeness gap: case-insensitive label test doesn't assert ready=False

test_babysit_merge.py L141–145

test_label_match_is_case_insensitive asserts that blockingLabels is populated but never asserts result["ready"] is False. This means if the blocking-label path stopped adding to blockers (e.g., from a refactor), the test would keep passing. The neighbouring test_blocking_label_blocks makes this assertion; adding self.assertFalse(result["ready"]) here too would close the gap.


Finding 3 — Minor: _approval helper defaults is_bot to a derived field, not the API shape

test_babysit_merge.py L63–69

The _approval fixture sets "is_bot": typename == "Bot" on the author. This field is used by actor_kind for the human-blocking check but is not a real GitHub API field — the production path uses __typename (which is_bot also checks). The fixture is fine for current tests, but it slightly over-specifies the shape. If someone adds a test where __typename and is_bot disagree, results could surprise. No action required now, but worth a note if the fixture is reused widely.


Positive observations

  • Fail-closed umbrella flag — requiring all three of --lane-logins, --approver-bot-logins, and --block-labels before the tier can run is exactly the right design; the guard tests at test_autopilot_tier_without_required_sets_refuses_exit_3 and test_autopilot_tier_partial_config_refuses_exit_3 cover both the all-missing and the partial-supply cases cleanly.
  • No predicate re-implementation — reusing actor_kind, is_bot, has_blocking_text, has_blocking_severity from babysit_classify rather than reimplementing them is the correct call; it's verified by the skill-contract tests.
  • TOCTOU guard is maintainedfind_distinct_bot_approval checks commit_oid == str(head) against the live API head, and --match-head-commit carries the same pinned SHA to GitHub's server-side guard; the head-pin discipline is consistent end-to-end.
  • TierAbsentIsInert test — verifying that fetch_pull_request_reviews and fetch_issue_comments are not called when the tier is off is a valuable network-isolation invariant.
  • safety.md update — the "Never do automatically → merge" carve-out is precise: it names every sub-criterion, explicitly ties the opt-in to operator action, and makes the gate's immutability explicit ("ruleset itself is never bypassed"). This is the right level of specificity for a safety-contract document.
  • plugin.json schema — the three required companion keys (babysit_lane_logins, babysit_approver_bot_logins, babysit_merge_block_labels) are correctly marked multiple: true and their descriptions accurately name the fail-closed behavior when absent.

Summary: One correctness bug (superseded bot approval) and one minor test gap. The correctness bug has a meaningful mitigating factor in the common case (the required-reviewer path), but the edge case (optional bot reviewer with a co-approving human) is real enough to warrant a fix before merge. Everything else is solid.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9268072f6

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py Outdated
…uality cap

The #476 tier subsection duplicated the criteria that reference/safety.md
already codifies and pushed SKILL.md to 530 lines, over the skill-quality
hard cap of 500. Condense the subsection to an operational summary (disabled
by default, the review-pass-then-gated-merge flow, fail-closed, fallback) and
point to safety.md as the single home for the criteria and the safety-contract
rationale, removing the duplication. Retarget the skill-contract test at the
condensed prose and add a safety.md guard for the criteria that moved there.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 40s —— View job


PR Review

  • Check what changed in the new commit vs prior review
  • Read babysit_merge.py — evaluate Codex P1 fix: correct plugin-system inaccuracies and harden references #2 (do-not-merge comment detection)
  • Re-evaluate Finding 1 (superseded bot approval) — confirm still unaddressed
  • Read SKILL.md — verify the 500-line trim and test re-targeting
  • Synthesize and post updated review

New commit: da239d8 — SKILL.md 500-line cap fix

This is a clean editorial-only change. The 39-line tier subsection was condensed to 2 dense sentences (lines 181–182), with both sentences in the same \n\n-delimited paragraph, so _paragraph_containing still resolves all 10 markers the updated test checks. The new test_safety_md_codifies_the_tier_criteria guard is an improvement over the prior approach — criteria now have a single canonical home in safety.md rather than being duplicated across two files. No issues with this commit.


Finding 1 — CONFIRMED, still open: Superseded bot approval

babysit_merge.py L211–232find_distinct_bot_approval

Flagged in the prior review; none of the three commits on this branch address it. The loop skips non-APPROVED reviews and returns the last APPROVED one chronologically:

for review in reviews:
    if str(review.get("state") or "") != "APPROVED":
        continue
    ...
    match = review

If the bot approver later submits CHANGES_REQUESTED on the same head, that review is silently skipped. The earlier APPROVED review satisfies the tier's "distinct-bot approval on the live head" criterion despite the bot's current intent being a block.

Mitigating path: when the bot is a required reviewer, GitHub sets reviewDecision = CHANGES_REQUESTED and the base gate at line 440 catches it. Gap path: the bot is an optional reviewer that satisfies a count requirement, a human approval already exists, and GitHub still reports reviewDecision = APPROVED + mergeStateStatus = CLEAN — both base-gate checks pass, the stale approval is accepted, and the tier merges against the bot's stated intent.

The existing babysit_feedback.latest_reviews_by_author(..., decisive_only=True) models this correctly. The fix is to collapse reviews to each actor's latest decisive state (APPROVED, CHANGES_REQUESTED, DISMISSED) ranked by submittedAt before scanning for approvals. No matching test case exists for the [APPROVED at T1, CHANGES_REQUESTED at T2, same head] scenario.

Fix this →


Finding 2 — Minor, still open: case-insensitive label test missing ready=False

test_babysit_merge.py L141–145

test_label_match_is_case_insensitive asserts blockingLabels is populated but never asserts result["ready"] is False. The adjacent test_blocking_label_blocks makes that assertion; this test should too, otherwise a refactor that populates blockingLabels without adding to blockers would pass silently.

Fix this →


Codex P1 #2 — Design-bounded, not a correctness bug: "do not merge" comment text

babysit_merge.py L302

Codex flagged that has_blocking_text won't catch a plain human comment like "Please do not merge this PR." After inspecting BLOCKING_TEXT_RE in babysit_classify.py (L38–43), the pattern covers code-quality review vocabulary (blocking, required fix, must fix, regression, vulnerability, p0/p1/p2, not approving, changes requested) — it intentionally does not cover "do not merge" prose.

This is a design boundary, not a code defect: administrative "do not merge" intent is routed through the label path (--block-labels, a required parameter). The human-comment path catches cases where a reviewer is blocking on code quality but didn't submit a formal CHANGES_REQUESTED review. A reviewer who wants to block an autopilot merge at the administrative level should use the configured label.

The criterion table in the PR description ("no human blocking comment") slightly overstates the scope — it is "no human comment with blocking review language," not every conceivable stop phrase. Worth a one-line clarification in safety.md's criterion description if this distinction isn't already explicit there, but no code change is needed.


Summary

The new commit is clean and the SKILL.md restructuring is an improvement. Two items from the prior review remain open before merge:

  1. Must fix (correctness): Superseded bot approval in find_distinct_bot_approval — collapse to latest decisive state per actor before accepting an approval as valid.
  2. Should fix (test completeness): test_label_match_is_case_insensitive missing assertFalse(result["ready"]).

The Codex P1 #2 (do-not-merge comment text) is a design boundary rather than a bug, though a one-line clarification in the criterion description in safety.md would help future reviewers understand the scope.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da239d82c7

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
…nd approval checks

Address three P1 review findings on the #476 tier, all fail-closed:

- Ignore superseded bot approvals: collapse reviews to each actor's latest
  decisive state (via babysit_feedback.latest_reviews_by_author,
  decisive_only=True) before accepting a tier approval, so an approver bot that
  approved then requested changes on the same head no longer counts even when a
  separate approval keeps the base reviewDecision APPROVED.
- Recognize explicit do-not-merge comments: has_blocking_text carries no
  do-not-merge pattern, so a plain human "please do not merge" veto escaped the
  "no human blocking comment" criterion. Add a bounded merge-veto regex to the
  tier's human-comment scan.
- Scan inline review comments: a human inline veto on a since-resolved thread
  saw neither the base unresolved-thread gate nor the tier scan. Include the
  paginated pull-request review comments (fetch_pull_request_review_comments)
  with normalized author data in the human-blocking corpus.

Each fix reuses an existing shared seam; new fixtures cover all three.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 4s —— View job


PR Review

  • Read new commit fddd27c — harden human-veto and approval checks
  • Evaluate whether Finding 1 (superseded bot approval) is now addressed
  • Evaluate whether Finding 2 (label test missing ready=False) is now addressed
  • Evaluate all three new fixes for correctness
  • Synthesize and post updated review

New commit: fddd27c — Harden human-veto and approval checks

This commit addresses three P1 findings. Evaluation below.


Finding 1 — RESOLVED: Superseded bot approval

babysit_merge.py L289–299

Fixed correctly. The call now collapses reviews to each actor's latest decisive state before scanning:

decisive_reviews = latest_reviews_by_author({"reviews": reviews}, decisive_only=True)
approval = find_distinct_bot_approval(
    decisive_reviews, author_login, head, tier.approver_bot_logins
)

latest_reviews_by_author returns list[dict[str, Any]], which is type-compatible with find_distinct_bot_approval's input. Because it returns the original review dicts (not copies), find_distinct_bot_approval's review.get("commit") head-SHA check continues to work. The new test_superseded_bot_approval_is_ignored case directly covers the [APPROVED at T1, CHANGES_REQUESTED at T2, same head] scenario and asserts distinctBotApproval is None and ready=False. Approach is correct and complete.


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L154–158

Not addressed in this commit. test_label_match_is_case_insensitive still asserts only blockingLabelsself.assertFalse(result["ready"]) is absent. The adjacent test_blocking_label_blocks makes that assertion; a refactor that populates blockingLabels without pushing to blockers would pass this test silently.

Fix this →


Codex P1 #2 — RESOLVED: Explicit do-not-merge comment detection

babysit_merge.py L73–81

Previously evaluated as design-bounded; the commit adds HUMAN_MERGE_VETO_RE as a tier-local addition to has_blocking_text:

HUMAN_MERGE_VETO_RE = re.compile(r"\bdo(?:n['']?t| not|-not)[\s-]*merge\b", re.I)

The pattern is correctly bounded — \b word-boundaries and the required do prefix prevent matching prose like "we should not merge A with B". It covers don't, dont, do not, and do-not variants including curly apostrophe. The new test_plain_do_not_merge_comment_blocks test covers the exact scenario from the Codex finding with the right assertions. The approach of placing this regex at the tier layer (rather than adding it to the shared has_blocking_text) is the right call given the PR description's scope — it lets the shared predicate stay focused on code-review vocabulary while the tier adds the administrative-veto sense.


Codex P1 #3 — RESOLVED: Inline review comment corpus inclusion

babysit_merge.py L310–318

Inline review thread comments are now included via:

corpus.extend(
    {"author": normalized_rest_author(row), "body": row.get("body")}
    for row in fetch_pull_request_review_comments(repo, number)
)

normalized_rest_author correctly converts the REST API user shape (type: "User" / type: "Bot") into the {"__typename": ..., "login": ..., "is_bot": ...} shape that actor_kind expects. The new test_inline_review_comment_veto_blocks test verifies that a resolved-thread inline veto still blocks, and the TierAbsentIsInert update ensures fetch_pull_request_review_comments is not called when the tier is off — the network-isolation invariant is maintained. Both fixes are correct.


Summary

Three P1 findings confirmed addressed, each via an existing shared seam and with a dedicated test case. One minor item from prior reviews remains:

  • Should fix (test completeness): test_label_match_is_case_insensitive missing self.assertFalse(result["ready"]) — a one-line addition.

The PR is otherwise substantively clean. The do-not-merge hold is intentional and the operator sign-off path is correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fddd27cf50

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
… approver bot

is_bot() accepts any [bot]/Bot-typed login before consulting approver_bot_logins,
so an unrelated installed GitHub App's APPROVED review satisfied the tier's
author != approver criterion, defeating the babysit_approver_bot_logins boundary.
find_distinct_bot_approval now also requires normalized membership in the
configured approver set, so only the designated bot's approval authorizes a tier
merge. Fail-closed: an empty approver set matches nothing (already refused by the
umbrella flag). Test: test_unconfigured_bot_approval_is_rejected.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 39s —— View job


PR Review

  • Gather context from new commit 9912da4
  • Read babysit_merge.py — evaluate Codex P1 fix (restrict approval to configured bots)
  • Read test_babysit_merge.py — verify new test coverage and check Finding 2 (label test)
  • Synthesize and post updated review

New commit: 9912da4 — Restrict babysit tier approval to the configured approver bot

This commit addresses the Codex P1 finding from the previous review: an arbitrary GitHub App bot with a structural [bot] suffix or Bot typename could satisfy the tier's "distinct-bot approval" criterion without being in the operator-configured babysit_approver_bot_logins set.


Codex P1 — RESOLVED: Unconfigured bot approval rejected

babysit_merge.py L236–242

Fixed correctly. After the existing is_bot structural/configured check, the commit adds a second, authoritative gate:

if not is_bot(login, typename, approver_bot_logins):
    continue
# A bot, but it must be the configured approver identity: `is_bot` accepts
# any `[bot]`/Bot-typed login, so without this an arbitrary installed
# App's approval would authorize a tier merge past the configured boundary.
if not (normalize_login_set([login]) & approver_norm):
    continue

The combined logic is now: an approval must come from an account that is (a) bot-typed by structure or by configured expansion AND (b) a member of the operator-supplied approver_bot_logins set. A random installed App bot passes (a) via its structural [bot] suffix but fails (b) and is correctly rejected.

The existing test_approver_matched_by_configured_login_without_bot_suffix unit test remains valid: a configured login with typename="User" passes is_bot via the configured-expansion path and passes approver_norm because it appears in the set — accepted correctly.

The new test_unconfigured_bot_approval_is_rejected (L194–201) directly covers the Codex scenario:

result = self._evaluate(
    _pr(), reviews=[_approval("random-app[bot]", HEAD)]
)
self.assertIsNone(result["autopilotMergeTier"]["distinctBotApproval"])
self.assertFalse(result["ready"])

Both assertions (distinctBotApproval is None and ready=False) are present and correct. Fix is complete.


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L154–158

Not addressed in this commit. The test asserts blockingLabels is populated but never asserts result["ready"] is False. The adjacent test_blocking_label_blocks makes that assertion; a refactor that populates blockingLabels without pushing to blockers would pass this test silently.

Fix this →


Summary

All four Codex P1 findings across the review cycle are now resolved:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)

One minor item remains: Finding 2test_label_match_is_case_insensitive missing self.assertFalse(result["ready"]). This is a one-line addition with no risk. The do-not-merge hold is intentional and the operator sign-off path is correct. The PR is substantively clean pending that small test fix.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9912da47b7

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/SKILL.md
…sit merge tier

The triage lane records a defaulted (maintainer-vetoable) decision only as a
'Decision defaulted: X — veto before merge' issue comment, which a deterministic
merge gate cannot see (Codex P1 on #641). Add a tier criterion: the PR's linked
issue must carry no unratified decision-default marker. A marker is ratified
only by a human maintainer (authorAssociation OWNER/MEMBER) comment strictly
after it; reactions are not consulted (the reactions API carries no author
association, so a reaction cannot be attributed to a maintainer, and using the
operator's self-logins would let pipeline automation clear its own veto — the
same #450 attribution-drift hazard). Fail closed: an unratified marker, or a
linked issue whose comments cannot be fetched, holds the PR for the human
merge-ready list. Marker matching is deliberately loose (over-matching only
holds more for the human). Reuses fetch_issue_comments and the shared actor_kind
predicate; criterion codified in safety.md. Pass, fall-back, and fetch-error
fixtures added.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 8s —— View job


PR Review

  • Gather context from new commit a12e770
  • Read babysit_merge.py — evaluate new "decision-default veto" change
  • Read test_babysit_merge.py — check Finding 2 (label test missing ready=False) and new test coverage
  • Evaluate Codex P1 (SKILL.md tier wiring)
  • Synthesize and post updated review

New commit: a12e770 — Add decision-default veto criterion

This commit adds a tier criterion to prevent autopilot from merging a PR whose linked issue carries an unratified Decision defaulted marker — a veto window comment the triage lane produces that a deterministic gate would otherwise miss.


New criterion — CLEAN: evaluate_decision_default_veto

babysit_merge.py L251–326

All design invariants hold:

  • Fail-closed on fetch error — a RuntimeError/ValueError/JSONDecodeError from fetch_issue_comments adds a blocker and holds the PR, never merging on an unverifiable issue. ✅
  • Strict-after semantics<= marker_ts skips comments at the same timestamp as the marker; only comments strictly after ratify. ✅
  • Multi-marker casemax(marker_timestamps) means a second decision-default marker resets the window; prior ratification of an earlier marker doesn't clear the new one. ✅
  • COLLABORATOR deliberately excluded — the comment citing push-access vs. maintainer-role distinction is the right call; the RATIFYING_ASSOCIATIONS = {"OWNER", "MEMBER"} boundary prevents a per-repo collaborator from clearing a maintainer's veto. ✅
  • Bot self-clear preventedactor_kind(comment) != "human" correctly dispatches on the normalized REST author shape from fetch_issue_comments (__typename/is_bot from normalized_rest_author). Bots carrying OWNER association (a rare but possible GitHub state) cannot ratify. ✅
  • Marker regexr"decision[ -]defaulted" is deliberately loose; over-matching only holds more for the human, under-matching would be wrong. ✅

Test coverage — all four critical paths covered:

  • test_ratified_decision_default_marker_passes — OWNER comment after marker, ready=True
  • test_unratified_decision_default_marker_blocks — marker with no ratification, ready=False, correct blocker text ✅
  • test_non_maintainer_comment_does_not_ratify — later NONE-association comment doesn't clear ✅
  • test_decision_default_fetch_error_holds — fetch failure → ready=False, "could not verify" blocker ✅

Harness wiringissue_comments_side_effect in the test harness correctly separates the linked-issue comment fetch (by LINKED_ISSUE = 999) from the PR's own human-blocking comment fetch (by PR number), making the two call paths independently assertable. The updated TierAbsentIsInert check (implicit via the harness) ensures evaluate_decision_default_veto doesn't run when the tier is off, since it's gated inside evaluate_autopilot_tier. ✅

The safety.md update and test_skill_contract.py addition ("Decision defaulted") correctly propagate the new criterion to the contract tests. ✅


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L206–210

Still not addressed across five commits. The test asserts blockingLabels == ["do-not-merge"] but never asserts result["ready"] is False. A refactor that populates blockingLabels without pushing the matching entry into blockers would pass this test silently. The adjacent test_blocking_label_blocks and test_unratified_decision_default_marker_blocks both make the assertFalse(result["ready"]) assertion correctly — this test should too.

Fix this →


Codex P1 (SKILL.md tier wiring) — Design-scope, not a code bug

SKILL.md L179–182

Codex flagged that no gh pr review --approve invocation appears in code and the existing merge command at lines 153–159 has no tier flags. This conflates two architectural layers:

  • Agent instruction layer (SKILL.md prose): Lines 179–182 describe what Claude, running as the approver bot identity, does — genuine review pass, gh pr review --approve when clean, then the merge gate with --autopilot-merge-tier flags. These are natural-language instructions for an agent, not a deterministic script.
  • Deterministic gate layer (babysit_merge.py): Reads and validates existing state. It does not submit reviews — that is intentional and correct.

The architectural split (agent acts → gate validates) is the right design. The Python gate's job is to verify that the approval was submitted by the right identity against the right head — which is what find_distinct_bot_approval + approver_norm membership check enforce.

Legitimate documentation gap worth a one-liner: The SKILL's tier section says "runs a genuine review pass under a second bot account" but doesn't say explicitly that this means running gh pr review owner/repo#N --approve as the approver bot session before the merge gate. An operator reading only this section might not know the exact command sequence. This could be clarified with one example command in the tier description, but it is not a code correctness defect.


Summary

Five commits in, the implementation is substantively sound:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) Open — minor

One minor test-completeness item remains. The do-not-merge hold is intentional; the operator sign-off path is correct.

…rge-tier

# Conflicts:
#	plugins/source-control/.claude-plugin/plugin.json
#	plugins/source-control/CHANGELOG.md
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 51s —— View job


PR Review


New commits: 9d7ca0c (via merge d8b561d) — #642 fix lands on branch

d8b561d is a straight merge of origin/main into the feature branch. The only source-control content of interest in the merge is 9d7ca0c ("scope babysit classification credit to its surface"), which adds count_effective_classified to babysit_classify.py — a per-surface credit cap that fixes the fail-open where a stale PR-level classification row inflated the classified count past a live open-thread finding and emitted READINESS_OK. No changes to babysit_merge.py or its tests.

Flip-precondition status update:

The PR description listed two explicit preconditions before the tier config flip is safe:

Precondition Status
Decision-default veto added as a merge criterion ✅ In this branch (a12e770)
#642 fix (count_classified fail-open) landing ✅ Now in this branch (9d7ca0c via d8b561d)

Both preconditions are now satisfied in the branch. Tier activation remains a separate, announced operator step; the config flip is not part of this PR. The do-not-merge hold and required operator sign-off are still correct and intentional.


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L206–210

Carried across six commits now without being addressed. The test asserts blockingLabels == ["do-not-merge"] but never asserts result["ready"] is False. A refactor that populated blockingLabels without pushing the matching entry into blockers would pass this test silently. Every adjacent blocking test — test_blocking_label_blocks, test_non_lane_author_blocks, test_unratified_decision_default_marker_blocks — makes the assertFalse(result["ready"]) assertion; this test should too.

Fix this →


Summary

The merge with origin/main is a clean administrative sync — no conflicts, no code changes to the tier. Its only significance is that the #642 fix is now in the branch, completing the second of two stated flip preconditions.

Full status across the review cycle:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) Open — minor

The implementation is substantively clean. The one remaining item is a one-line test assertion that should be added before merge, but it is a test-completeness gap — not a correctness defect in the gate itself. The do-not-merge hold is intentional and the operator sign-off path is correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8b561de5f

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py Outdated
… the decision-default veto

The decision-default veto treated ANY later OWNER/MEMBER comment as ratifying
the marker, so an unrelated "thanks" would clear the maintainer veto (Codex P1
on #665). Ratification now requires an explicit signal in the maintainer's
comment: a closed, whole-word token set (ratify/ratified, approve/approved,
confirm/confirmed), and not a withheld-approval negation (reusing the shared
NON_APPROVAL_RE). Matching is strict/fail-closed — an unrelated comment, a
signal that predates the marker, or a negated approval do not clear, and only
over-hold to the human list. Token set documented in safety.md next to the
marker contract. Fixtures added: explicit-signal clears (existing),
unrelated-comment does not, signal-before-marker does not, negated-approval
does not.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3c9bbf38b

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py Outdated
…rge-tier

# Conflicts:
#	plugins/source-control/.claude-plugin/plugin.json
#	plugins/source-control/CHANGELOG.md
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 50s —— View job


PR Review

  • Identify new commits since last review (d3c9bbf, ec9fc5b)
  • Read babysit_merge.py — evaluate d3c9bbf ratification fix and Codex P1 (cross-repo issue fetch)
  • Read test_babysit_merge.py — check Finding 2 and new test coverage
  • Evaluate what came in from origin/main via merge commit ec9fc5b
  • Synthesize and post updated review

New commits: d3c9bbf + merge ec9fc5b

ec9fc5b is a straight origin/main sync. The only source-control content of interest is e9cef6e (cadence → ScheduleWakeup.delaySeconds mapping in loop.md) — no changes to babysit_merge.py or its tests. Merge conflicts were on plugin.json and CHANGELOG.md only.

d3c9bbf — Require explicit ratification signal — resolves Codex P1 #6.


Codex P1 #6 — RESOLVED: Explicit ratification signal

babysit_merge.py L264–295

Fixed correctly. _decision_default_ratified now applies three gates in sequence:

if association not in RATIFYING_ASSOCIATIONS:
    continue
body = str(comment.get("body") or "")
if NON_APPROVAL_RE.search(body):
    continue  # withheld/negated approval is not a ratification
if RATIFICATION_SIGNAL_RE.search(body):
    return True

RATIFICATION_SIGNAL_RE is a closed whole-word token set (ratify/ratified, approve/approved, confirm/confirmed); NON_APPROVAL_RE (imported from babysit_classify) filters negated forms (not approved, cannot approve) even when they contain an approval token. An unrelated "thanks" carries no signal and does not clear the veto. Matching is intentionally strict / fail-closed — ambiguous maintainer comments over-hold to the human list.

Three new test cases cover the key new paths:

  • test_unrelated_maintainer_comment_does_not_ratify — "thanks, nice work" (no signal) → held ✅
  • test_ratification_signal_before_marker_does_not_clear — "approved" predating the marker → held ✅
  • test_negated_approval_does_not_ratify — "not approved yet — hold this" → NON_APPROVAL_RE catches → held ✅

safety.md updated to document the ratification vocabulary and the strict semantics. All correct and complete.


Codex P1 (cross-repo issue fetch) — Still open

babysit_merge.py L321

Flagged in the previous review cycle and not addressed in either d3c9bbf or ec9fc5b. evaluate_decision_default_veto always fetches issue comments from the PR's own repository:

comments = fetch_issue_comments(repo, issue_number)

gh pr view --json closingIssuesReferences can return issues in a different repository (via Closes owner/other-repo#N links). When that happens the function reads the same issue number from the wrong repo — if the PR's repo happens to have a same-numbered issue without the marker, the veto is not detected and the tier can merge despite the actually-referenced issue having an unratified Decision defaulted veto.

The mitigating factor is that cross-repo closing links are uncommon in practice, and the failure scenario requires a coincidental issue-number collision between repos. But the gap is real: the same number in a different repo's issue list would be silently consulted instead of the referenced issue.

The fail-closed fix is straightforward: extract the repository identity from the closing-issue ref (if closingIssuesReferences includes it — the GraphQL Issue type does expose repository { nameWithOwner }) and hold the PR for the human list if the issue's repo cannot be confirmed as the PR's own repo. This does not require changing the merge semantics — it is strictly additive fail-closed behavior.

Fix this →


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L206–210

Carried across seven commits without being addressed. The test asserts blockingLabels == ["do-not-merge"] but never asserts result["ready"] is False. A refactor that populated blockingLabels without pushing the matching entry into blockers would pass this test silently. Every adjacent blocking test makes the assertFalse(result["ready"]) assertion; this one should too.

Fix this →


Summary

Full status across the review cycle:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
Any later maintainer comment ratifies the veto (implicit ratification) RESOLVED (d3c9bbf)
Cross-repo issue fetch uses wrong repo Open
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) Open — minor

The do-not-merge hold is intentional; the operator sign-off path is correct. Two items remain before merge: the cross-repo issue fetch gap (correctness, rare failure path) and the one-line test assertion gap (test completeness only, not a gate defect).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec9fc5b5b2

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
…erenced issue's own repo

The decision-default veto fetched the linked issue's comments from the PR's
repository using only the issue number, so a PR closing an issue in ANOTHER
repo would read a same-numbered issue in the wrong repo and miss its marker
(Codex P1 on #665). Each `closingIssuesReferences` entry carries its own
repository; the scan now reads from `owner/name` (falling back to the PR repo),
and the held list reports canonical `owner/repo#n` refs. Fixture added:
test_cross_repo_linked_issue_read_from_its_own_repo.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 0s —— View job


PR Review

  • Identify new commits since last review (871a9e8, d9b19d6)
  • Read babysit_merge.py — evaluate maintainer-revocation fix
  • Read test_babysit_merge.py — check Finding 2 and new test coverage
  • Synthesize and post updated review

New commits: 871a9e8 + merge d9b19d6

d9b19d6 is a straight origin/main sync — the only content of interest is d8c3d75 (planning interview docs) and other unrelated plugins. No changes to babysit_merge.py or its tests.

871a9e8 — Honor later maintainer revocations of a ratified decision default.


Codex P1 (maintainer revocations) — RESOLVED

babysit_merge.py L267–311_decision_default_ratified

Fixed correctly. The prior early-return on the first ratifying comment is replaced with a full scan that tracks the latest decisive timestamp on each side:

latest_ratify = ""
latest_revoke = ""
for comment in comments:
    ...
    if NON_APPROVAL_RE.search(body) or HUMAN_MERGE_VETO_RE.search(body):
        latest_revoke = max(latest_revoke, created_at)
    elif RATIFICATION_SIGNAL_RE.search(body):
        latest_ratify = max(latest_ratify, created_at)
return bool(latest_ratify) and latest_ratify > latest_revoke

All corner cases are handled correctly:

Scenario Outcome Correct
Only ratification exists bool(ts) and ts > "" → True
Ratify then revoke latest_ratify < latest_revoke → False
Revoke then ratify latest_ratify > latest_revoke → True
Same-timestamp tie ts > ts → False (fail-closed)
No decisive comments bool("") → False
Mixed-signal single comment elif skips ratify branch → revoke wins (fail-closed)

The safety.md update correctly documents "latest decisive signal wins" and adds the same-timestamp tie and revocation cases to the contract description. All four new test cases (test_ratified_then_revoked_holds, test_revoked_then_ratified_clears, test_non_decisive_comment_after_ratification_leaves_it_standing, test_same_timestamp_ratify_and_revoke_holds) carry the right assertions and cover the key behavioral boundary. Fix is complete.


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L208–212

Carried across all nine commits without being addressed. The test asserts blockingLabels == ["do-not-merge"] but never asserts result["ready"] is False. Every adjacent blocking test — test_blocking_label_blocks, test_non_lane_author_blocks, test_unratified_decision_default_marker_blocks, test_ratified_then_revoked_holds — makes the assertFalse(result["ready"]) assertion; a refactor that populated blockingLabels without pushing to blockers would pass this test silently.

Fix this →


Summary

Full status across the review cycle:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
Any later maintainer comment ratifies the veto (implicit ratification) RESOLVED (d3c9bbf)
Cross-repo issue fetch reads wrong repo RESOLVED (a9bb74d)
Later maintainer revocation of a ratified default not honored RESOLVED (871a9e8)
Codex P1: review-workflow status not independently gated Deferred to #675 flip precondition
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) Open — minor

Eight correctness findings resolved across the review cycle, each with a dedicated test case. The decision-default ratification logic is now fully hardened: explicit signal required, earliest revocation wins over a later ratification when they share a timestamp, and a later revocation supersedes an earlier ratification. One minor test-completeness gap remains. The do-not-merge hold is intentional; the operator sign-off path is correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9b19d6423

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py Outdated
… in the tier veto and ratification scans

An approver configured via `--approver-bot-logins` (or a pipeline lane login)
that GitHub reports as account type `User` without a `[bot]` suffix passed
`find_distinct_bot_approval` — which classifies via `is_bot`, honoring the
configured login over the non-`Bot` typename — but was read as HUMAN by the
tier's veto and ratification scans, which classify via `actor_kind`. Root
cause: `actor_kind` returned "human" on a `User` typename / `is_bot: False`
before ever consulting `extra_bot_logins`, so threading the config in was
provably inert (a populated config still returned "human"). That defeats the
field's own documented purpose — "accounts whose metadata misreports them as
users" — and disagrees with `is_bot`, which already ignores any non-`Bot`
typename. Consequence: the configured approver's own clean review body could
count as a human blocking comment against the very approval it provides, and a
configured automation identity could ratify a decision-default marker —
automation clearing its own veto, the #450 attribution-drift hazard the
ratification design explicitly excludes (Codex P2 on #665, thread
PRRT_kwDOTCGFQM6SOgye).

Fix, fail-closed: `actor_kind` now lets an explicitly configured
`extra_bot_logins` identity outrank a `User` typename, matching `is_bot` and
honoring the field's contract; default-config callers are unchanged. The tier
threads a single automation config (`approver_bot_logins | lane_logins`) into
both the human-blocking scan and the decision-default ratification scan via a
new `AutopilotMergeTierConfig.automation_actor_config` property, so a
configured identity is always automation for those scans — never a human veto,
never a ratifier. Fixtures added: classifier-level precedence contract; a
configured `User`-type approver's blocking-looking review body does not block;
a configured `User`-type lane account with an OWNER association does not ratify
a decision default; unconfigured humans unchanged.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 16s —— View job


PR Review

  • Identify new commits since last review (c404522, 07ac477, 2cc68c4)
  • Read babysit_classify.py — evaluate Codex P2 (configured approver/lane login classification)
  • Read babysit_merge.py — evaluate tier wiring of the new config
  • Read test_babysit_merge.py + test_babysit_classify.py — check Finding 2 and new test coverage
  • Synthesize and post updated review

New commits: c404522 + merge 2cc68c4

2cc68c4 is a straight origin/main sync with no changes to the source-control babysit scripts. 07ac477 fixes the review plugin's allowed-tools — unrelated to this PR.

c404522 — Classify configured approver/lane logins as bots in the tier veto and ratification scans — addresses the Codex P2 finding.


Codex P2 — RESOLVED: Configured approver/lane login classification

babysit_classify.py L230–231

Root cause was precise and the fix is correct. The prior code's User typename guard was evaluated before the extra_bot_logins fallback, which meant threading the configured set into actor_kind was entirely inert for any User-typed account: it short-circuited to "human" before ever consulting the config. The fix inserts the configured-login check in the right place — after Bot typename / is_bot: True, and before the User typename guard:

if typename == "Bot" or author.get("is_bot") is True:
    return "bot"
if config.extra_bot_logins and is_bot(login, None, config.extra_bot_logins):
    return "bot"          # <-- configured identity outranks User typename
if typename in {"Mannequin", "Organization", "User"} or author.get("is_bot") is False:
    return "human"

This matches is_bot's own contract (which already ignores a non-Bot typename for configured logins), is safe for the default-empty-config path (the config.extra_bot_logins guard prevents any change there), and resolves both consequences the Codex finding named:

Consequence 1 (self-blocking approval): A configured User-type approver whose review body carried blocking-looking prose would previously fail the actor_kind != "human" gate and count as a human veto against the very approval it provided. Fixed. Test: test_configured_user_approver_body_is_not_a_human_block — approver with typename="User" and a "blocking regression" body: ready=True, humanBlockingComments == [], distinctBotApproval non-null. ✅

Consequence 2 (automation self-ratification — #450 hazard): A configured User-type lane account with OWNER association could previously ratify a Decision defaulted marker on a linked issue — automation clearing its own veto, the attribution-drift hazard the ratification design explicitly excludes. Fixed. The tier now threads approver_bot_logins | lane_logins into both the human-blocking scan and the ratification scan via AutopilotMergeTierConfig.automation_actor_config. Test: test_configured_user_lane_does_not_ratify_decision_default — lane account typename="User", OWNER association, "Ratified — proceed." body: ready=False, held issue in decisionDefaultHeldIssues. ✅

Classifier-level contract in test_babysit_classify.py:

  • test_configured_login_outranks_user_typename — directly asserts actor_kind(item, config) == "bot" for a User-typed configured login. ✅
  • test_unconfigured_user_is_human — confirms unconfigured humans are unchanged when a different login is in the set. ✅
  • test_default_empty_config_relies_on_structure — confirms the default-config path is unchanged (returns "human" for a User-typed login not in any set). ✅

The automation_actor_config property design is clean — a single union of approver_bot_logins | lane_logins means every configured pipeline identity is treated consistently as automation across the two scan sites, and the property lives where the config already exists. No repetition, no missed call sites.

Fix is correct and complete.


Finding 2 — Still open (minor): test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L208–212

Carried across all ten commits without being addressed. The test asserts blockingLabels == ["do-not-merge"] but never asserts result["ready"] is False. Every adjacent blocking test — test_blocking_label_blocks, test_non_lane_author_blocks, test_unratified_decision_default_marker_blocks, test_configured_user_lane_does_not_ratify_decision_default — makes the assertFalse(result["ready"]) assertion; a refactor that populated blockingLabels without pushing the entry into blockers would pass this test silently. A one-line addition closes the gap.

Fix this →


Summary

Full status across the review cycle:

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
Any later maintainer comment ratifies the veto (implicit ratification) RESOLVED (d3c9bbf)
Cross-repo issue fetch reads wrong repo RESOLVED (a9bb74d)
Later maintainer revocation of a ratified default not honored RESOLVED (871a9e8)
Configured approver/lane login misclassified in veto and ratification scans RESOLVED (c404522)
Codex P1: review-workflow status not independently gated Deferred to #675 flip precondition
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) Open — minor

Nine correctness findings resolved across the review cycle, each with dedicated test cases. The decision-default veto is now fully hardened end-to-end: explicit signal required, revocations honored, cross-repo issues read from their own repo, and automation identities can never be confused for human ratifiers regardless of GitHub's reported typename. One minor test-completeness gap remains. The do-not-merge hold is intentional; the operator sign-off path is correct.

@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Jul 20, 2026
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Operator sign-off recorded (2026-07-20, in-session confirmation to control tower): safety digest verified all hardening claims (per-claim table in tower session log; digest read inline at tower after the auto-mode classifier correctly blocked delegating this review). Operator confirmed MERGE via explicit in-session prompt. Behavioral inertness at merge: tier engages only when babysit_autopilot_merge_tier is set — that flag enable, plus #675 landing and a loud announcement, remain the separate gate-off flip steps.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Babysit worker pass (safe tier, merge-gate ON — no merge/approve/label change). Classification of the open item from the latest review run (5022058112):

# Finding Classification Evidence Reacted
1 test_label_match_is_case_insensitive missing assertFalse(result["ready"]) VALID — fixed Added the assertion in ab9d9e56. Verified: augmented test passes, so the gap was test-completeness only — a mixed-case Do-Not-Merge label already blocks (ready is False); this was not a live fail-open. Full suite green (310 tests). 👍
2 Codex P1 — review-workflow status not independently gated VALID (deferred, author-adjudicated) Not re-opened. Deferred INTO #675 as an enabled-path completeness precondition; risk is structurally unreachable (tier ships disabled + PR is operator-held under the do-not-merge gate — the flip cannot happen until #675 closes). Design escalation, not a mechanical patch.

Disposition of the rest of the corpus:

  • All 10 Codex inline findings were already classified per-finding by the author and their review threads are resolved — nothing left to address there.
  • The 9 earlier claude[bot] runs are superseded by run 5022058112; not re-replied.
  • The do-not-merge hold is intentional and remains applied; mergeStateStatus is DIRTY (real conflict with main) and is reported as a blocker only — untouched by this pass (no explicit authorization to resolve conflicts on this PR).

New head: ab9d9e56.

kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…702)

## Summary

Dispatched workers assigned an out-of-tree per-issue worktree can
silently operate from the wrong checkout: the shell's working directory
does not reliably persist across separate tool calls, so a one-time `cd`
at session start does not survive into later commands. In #566 this
caused an implementation worker's edits to land in the canonical
checkout instead of its assigned worktree — caught only because that
worker happened to notice.

The fix hardens the text that briefs future dispatched workers so they
don't repeat the failure.

## Fix

`implement-dispatch`'s "Compose the brief" step
(`plugins/implementation/skills/implement-dispatch/SKILL.md`) — the
single owner of worker-brief composition mechanics, which `work` Step 5
and any plan-routed dispatch already delegate to — now requires a
worktree-cwd clause whenever a worker edits in a dedicated worktree: the
brief MUST give the worktree's absolute path and instruct the worker to
never rely on cwd persisting across separate tool calls, anchoring every
file-touching command with `git -C <worktree-path>` (or a re-`cd` per
call) rather than a one-time `cd`.

Stated as imperative discipline (the load-bearing mitigation), not as an
asserted universal tool defect — the mitigation is correct regardless of
whether the persistence gap reproduces everywhere.

`implementation` bumped 0.7.3 → 0.7.4 with a matching CHANGELOG entry.

**Deferred (parked):** `source-control`'s `babysit-prs` Worker Contract
(`reference/orchestration.md`) is a second lane that hands a worker a
worktree path and has the same latent bug. Its manifest is already being
bumped by open PR #665 (currently CONFLICTING with main), so this PR
does not race that `plugin.json`. The one-line babysit-prs mitigation
should be applied as a follow-up once #665 lands (or folded into #665).

## Verification

```
$ bash scripts/check-changelog-parity.sh --check-bump origin/main
Every plugin whose version changed vs origin/main has a '## [<version>]' CHANGELOG.md entry.

$ npx markdownlint-cli2 --config .markdownlint-cli2.jsonc \
    plugins/implementation/skills/implement-dispatch/SKILL.md plugins/implementation/CHANGELOG.md
Summary: 0 error(s)

$ typos --config _typos.toml plugins/implementation/
(exit 0)

$ node scripts/validate-plugin-contracts.mjs
Plugin contracts validated: 33 setup skills and 1811 plugin files checked.
```

Closes #566

## Related

- #665 — bumps `source-control`'s manifest; blocks bundling the parallel
babysit-prs fix here (see Fix).
- #572 — deferred autonomous branch/worktree provisioning seam across
`work`, `implement-dispatch`, `worktree`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
…rge-tier

# Conflicts:
#	plugins/source-control/.claude-plugin/plugin.json
#	plugins/source-control/CHANGELOG.md
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 32s —— View job


PR Review

  • Identify new commits since last review (d9b19d6423)
  • Read test_babysit_merge.py — verify Finding 2 fix (ab9d9e5)
  • Confirm merge commits from origin/main are unrelated to babysit-prs
  • Synthesize and post updated review

New commits: ab9d9e5 + merge 88ee348

88ee348 is a straight origin/main sync. The three commits it brings in (064f03b, 35515ff, 8cd71e6) touch autonomy docs, implementation docs, and consumer-config-layering docs — none touch the source-control plugin or babysit-prs scripts. Clean merge.

ab9d9e5 — assert not-ready on case-insensitive do-not-merge label match.


Finding 2 — RESOLVED: test_label_match_is_case_insensitive missing ready=False

test_babysit_merge.py L208–213

Fixed correctly. self.assertFalse(result["ready"]) is now at L210, placed before the blockingLabels assertion:

def test_label_match_is_case_insensitive(self) -> None:
    result = self._evaluate(_pr(labels=[{"name": "Do-Not-Merge"}]))
    self.assertFalse(result["ready"])                                  # ← added
    self.assertEqual(
        result["autopilotMergeTier"]["blockingLabels"], ["do-not-merge"]
    )

As kyle-sexton's pass confirmed, the gap was test-completeness only — the behavior was already correct; the mixed-case label was already blocking. The assertion now makes that invariant explicit and regression-proof.


Complete finding status across the review cycle

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
Any later maintainer comment ratifies the veto (implicit ratification) RESOLVED (d3c9bbf)
Cross-repo issue fetch reads wrong repo RESOLVED (a9bb74d)
Later maintainer revocation of a ratified default not honored RESOLVED (871a9e8)
Configured approver/lane login misclassified in veto and ratification scans RESOLVED (c404522)
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) RESOLVED (ab9d9e5)
Codex P1: review-workflow status not independently gated Deferred to #675 flip precondition

All ten correctness/test findings are now resolved. The review cycle is complete. The do-not-merge hold is intentional; operator sign-off was recorded by kyle-sexton (2026-07-20). Tier activation, #675, and the loud announcement remain as the separate gate-off flip steps.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88ee348756

ℹ️ 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".

Comment thread plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py
…ng findings

The #476 autopilot merge tier accepted a distinct-bot APPROVED review on
identity/head alone. The human-blocking corpus scan deliberately skips
bot-authored items and GitHub can still return reviewDecision=APPROVED, so an
approver bot that approved while its own body reported a P1/CRITICAL/blocking
regression sailed through the gate -- a fail-open in a merge gate.

Severity-scan the accepted approval's own body: a structured high-severity
finding (CRITICAL/IMPORTANT surviving negation redaction, or a P0-P3 severity
badge / bracketed [P0-P3] marker) invalidates it as a tier approval. Treated as
no approval, not as a human blocker, and a since-superseded earlier clean
approval is not honored past the latest blocking verdict. Only the shared
classifier's structured severity vocabulary is scanned -- prose words
("blocking"/"regression") are not, so a clean approval describing the fix it
signs off stays accepted. This is the autopilot-tier answer to the open #621
question of whether formal APPROVED-state reviews should be severity-scanned.
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 56s —— View job


PR Review

  • Identify new commit (b3bedc9 — reject approver-bot approvals that report blocking findings)
  • Read babysit_merge.py — evaluate Codex P1 (blocking findings in approver-bot review body)
  • Read test_babysit_merge.py — check new test coverage
  • Synthesize and post updated review

New commit: b3bedc9 — Reject approver-bot approvals that report blocking findings

This commit addresses the final Codex P1 finding: the tier accepted a distinct-bot APPROVED review on identity and head alone. Since the human-blocking corpus deliberately skips bot-authored items and GitHub can still return reviewDecision=APPROVED, a bot that approved while its own body reported a CRITICAL/P1 finding would sail through the gate.


Codex P1 — RESOLVED: Blocking findings in approver-bot review body

babysit_merge.py L226–249approval_reports_blocking

Fixed correctly. The new predicate uses only the shared classifier's structured severity vocabulary:

def approval_reports_blocking(body: str) -> bool:
    return (
        has_blocking_severity(body)               # CRITICAL/IMPORTANT surviving negation
        or bool(SEVERITY_BADGE_RE.search(body))   # /badge/P[0-3]- shields.io badge
        or bool(SEVERITY_PLAIN_RE.search(body))   # [P0]–[P3] bracketed marker
    )

The design decision not to include has_blocking_text() (prose: "blocking", "regression", "must fix") is the right call — a clean approval commonly describes the fix it signs off ("resolves the blocking regression"), and keying on prose would over-hold legitimate approvals. The three structured-vocabulary paths are precise enough to be reliable.

Wiring in evaluate_autopilot_tier (L484–506):

if approval is not None and approval_reports_blocking(str(approval.get("body") or "")):
    blockers.append("distinct-bot approving review reports blocking findings ...")
    approval = None
elif approval is None:
    blockers.append("no distinct-bot approving review ...")

The if/elif structure is correct — exactly one of the two "no approval" paths fires per evaluation. An approve-with-blocking-findings verdict is counted as no approval (not as a human blocker), and the "no distinct-bot approving review" message is never added alongside the "blocking findings" message.

Negation redaction is inherited correctly: has_blocking_severity applies NEGATED_SEVERITY_LIST_RE and NEGATED_SEVERITY_MARKER_RE before checking, so "No CRITICAL or IMPORTANT findings." correctly returns False. BLOCKING_SEVERITY_RE = re.compile(r"\b(?:CRITICAL|IMPORTANT)\b") is uppercase-only, so lowercase "critical path" in prose doesn't match — validated by the test_lowercase_severity_prose_approval_accepted case.

Test coverage — all paths covered via TierApprovalBodySeverityGate:

Test Path exercised Correct
test_approval_with_shields_p1_badge_body_rejected SEVERITY_BADGE_RE (/badge/P1-orange)
test_approval_with_bracketed_p1_body_rejected SEVERITY_PLAIN_RE ([P1])
test_approval_with_critical_body_rejected has_blocking_severity (CRITICAL)
test_approval_with_important_body_rejected has_blocking_severity (IMPORTANT)
test_clean_approval_body_accepted clean body → accepted
test_negated_severity_approval_body_accepted negation redaction clears CRITICAL/IMPORTANT
test_lowercase_severity_prose_approval_accepted lowercase prose doesn't match structured vocab

The _assert_rejected helper correctly enforces the full invariant: distinctBotApproval is None, ready=False, humanBlockingComments == [] (not a human blocker), and a "blocking findings in its body" message present. This is the right set of assertions — it verifies both the absence of the approval and the correct blocker category.

The predicate-level ApprovalReportsBlockingUnit class adds independent unit coverage of approval_reports_blocking(body) — confirming CRITICAL, IMPORTANT, [P0], [P1], and shields badge all return True, while empty body, LGTM, negated CRITICAL, and lowercase prose all return False. Fix is correct and complete.


Complete finding status across the review cycle

Finding Status
Superseded bot approval not detected RESOLVED (fddd27c)
Explicit do-not-merge comment not caught RESOLVED (fddd27c)
Inline review comments excluded from human-veto corpus RESOLVED (fddd27c)
Unconfigured bot approval satisfies tier RESOLVED (9912da4)
Decision-default veto criterion missing RESOLVED (a12e770)
Any later maintainer comment ratifies the veto (implicit ratification) RESOLVED (d3c9bbf)
Cross-repo issue fetch reads wrong repo RESOLVED (a9bb74d)
Later maintainer revocation of a ratified default not honored RESOLVED (871a9e8)
Configured approver/lane login misclassified in veto and ratification scans RESOLVED (c404522)
test_label_match_is_case_insensitive missing assertFalse(result["ready"]) RESOLVED (ab9d9e5)
Blocking findings in approver-bot review body accepted as clean approval RESOLVED (b3bedc9)
Codex P1: review-workflow status not independently gated Deferred to #675 flip precondition

All eleven correctness/test findings are now resolved. The do-not-merge hold is intentional; operator sign-off was recorded by kyle-sexton (2026-07-20). Tier activation, #675, and the loud announcement remain as the separate gate-off flip steps. The review cycle is complete.

kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…, not just be required (#675)

Codex P1 (re-review of 8e780ea) on #706: the review-workflow requiredness
precondition over-claimed. It said making the review workflow a required status
context "closes that hole deterministically", but requiredness alone does not
guarantee the review RAN. A conditionally-skipped review job can report a SKIPPED
conclusion that is counted as a passing state — this skill's own babysit_checks.py
classifies SKIPPED as success (CHECK_SUCCESS_STATES) — so a required-but-skipped
review still reads mergeStateStatus == CLEAN without having gated anything, and the
enabled tier would proceed on its bot approval alone.

Strengthen the operator precondition (docs-only, in #706's own scope):
- Enabling now requires the review workflow to be a required status context AND to
  always run to a non-skipped conclusion on every PR to the protected base.
- Qualify "deterministically": requiring the context closes the hole only when the
  workflow cannot conditionally skip on the tier's PRs.
- Extend "do not enable the tier" to the skippable-on-those-PRs case.

Code-side enforcement (a merge-gate predicate that verifies the review context
actually executed, or exempting the review context from SKIPPED-as-success) belongs
in the feature (#476/#665), not this docs PR, and is left for that surface.

Refs #675

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton merged commit 49c2ae5 into main Jul 20, 2026
20 checks passed
@kyle-sexton
kyle-sexton deleted the feat/476-autopilot-merge-tier branch July 20, 2026 18:15
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…, not just be required (#675)

Codex P1 (re-review of 8e780ea) on #706: the review-workflow requiredness
precondition over-claimed. It said making the review workflow a required status
context "closes that hole deterministically", but requiredness alone does not
guarantee the review RAN. A conditionally-skipped review job can report a SKIPPED
conclusion that is counted as a passing state — this skill's own babysit_checks.py
classifies SKIPPED as success (CHECK_SUCCESS_STATES) — so a required-but-skipped
review still reads mergeStateStatus == CLEAN without having gated anything, and the
enabled tier would proceed on its bot approval alone.

Strengthen the operator precondition (docs-only, in #706's own scope):
- Enabling now requires the review workflow to be a required status context AND to
  always run to a non-skipped conclusion on every PR to the protected base.
- Qualify "deterministically": requiring the context closes the hole only when the
  workflow cannot conditionally skip on the tier's PRs.
- Extend "do not enable the tier" to the skippable-on-those-PRs case.

Code-side enforcement (a merge-gate predicate that verifies the review context
actually executed, or exempting the review context from SKIPPED-as-success) belongs
in the feature (#476/#665), not this docs PR, and is left for that surface.

Refs #675

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ecify the approve mechanic and review-context precondition (#675) (#706)

Closes #675

**Gate-off flip precondition** for the autopilot merge tier (#476). All
changes are prose/contract + test-pinning: **no behavioral change** to
the merge gate or any script, and the tier still ships **DISABLED**.

Implements the operator-approved RECOMMENDED options recorded on #675:
- **§3 wiring — fork (b), reference-file restructure.** Autopilot's step
3 in `SKILL.md` no longer inlines a base-only merge command that would
ignore the tier flags when the tier is enabled. It now points at
`reference/safety.md`, the single home for both the base and the
enabled-tier merge paths, so an ENABLED config can no longer merge via
the flagless base path. Chosen over raising the skill-quality line cap
(`SKILL.md` sits at 499/500).
- **Second-account approve mechanic (specified).** The concrete
out-of-band approval the gate's distinct-bot criterion requires: `gh pr
review … --approve` under a distinct `<approver-bot-logins>` identity
(`GH_TOKEN` or `gh auth switch`, never the PR author or a lane
identity), only after a genuine clean review pass, on the live head so
`--expected-head` holds.
- **Review-context requiredness — fork (a), required-context
precondition.** Enabling the tier now carries a documented operator
precondition: the base branch ruleset must make the review workflow a
**required** status context, so `mergeStateStatus == CLEAN` actually
proves the review ran. Chosen over a merge-gate review-context config
(rejected fork b) to keep the gate deterministic with nothing new to
wire — and deliberately kept distinct from the existing snapshot-side
`babysit_review_gate_context` (a review-trigger timing signal, not a
merge predicate).

The skill-contract tests are split/extended to pin all three contracts
against drift (the worker-tier push paragraph, which has no merge tier,
keeps its inline-command assertion).

## Related

- Stacked on #665 (`feat/476-autopilot-merge-tier`) and **must land
after it** — this PR is based on that branch, not `main`; retarget to
`main` once #665 merges.
- **Version reconciliation at rebase:** bumped to `0.14.1`, continuous
with the current base (`feat/476` @ `0.14.0`). #665's plugin version is
being re-composed to `0.15.0` (collision with #660's `0.14.0`); when
#665 lands, rebase this and re-bump to one increment past whatever
`main` then carries (expected `0.15.1`).
- Precedent for the flip-precondition designation: #642, #476.

### Verification
- `engine.test.sh`: 312 tests pass, ruff clean, guarded-wrapper behavior
checks pass.
- `check-skill.sh babysit-prs`: PASS (SKILL.md 499/500, internal refs
resolve, markdownlint clean).
- markdownlint on `safety.md` + `CHANGELOG.md`: 0 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(source-control): babysit autopilot merge tier — bot-account review + criteria-gated merge

1 participant