Skip to content

feat(source-control): converge babysit-prs tiers + engine (0.8.0) - #322

Merged
kyle-sexton merged 13 commits into
mainfrom
feat/babysit-prs-convergence
Jul 18, 2026
Merged

feat(source-control): converge babysit-prs tiers + engine (0.8.0)#322
kyle-sexton merged 13 commits into
mainfrom
feat/babysit-prs-convergence

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Contributor

What

Converges the two mature babysit implementations — the personal dotfiles skill (worker/autopilot
auto-merge tiers, Python guard scripts, lease/worktree orchestration) and the extracted
source-control:babysit-prs safe-default loop — into one repo/machine/user-agnostic skill at
0.8.0. This is the capability-convergence PR (PR-B) of the babysit-prs migration; the
extraction PR (#300) shipped the safe default.

Stacked on main after #300, then merged up to reconcile with #325. Contract:
docs/topics/babysit-prs-migration/PLAN.md (Phase 2), design record
docs/topics/babysit-prs-migration/design/phase-2-engine-modules.md.

Reconciliation with main (#325) — please sanity-check

#325 (the #315 userConfig wave) merged to main while this PR was in review, bumping source-control
to 0.7.0 and adding babysit_self_logins (+ worktree_stale_days, fetch_logs_max_bytes).
This PR was rebumped to 0.8.0 and reconciled per two choices confirmed with the maintainer:

⚠️ One schema change to flag for veto: babysit_self_logins was changed from #325's plain
string (default "") to multiple: true with no default, for consistency with the other
multi-value babysit keys (comma-join substitution is unchanged). If you'd rather keep #325's exact
shape, say so and I'll revert just that key.

Codex review dispositions

Six findings across four Codex passes, all triaged with GitHub-verified evidence:

# Area Disposition
F1 multi-login discovery (--author single-valued) Fixed — per-login query + union
F2 review-gate pending-vs-failing contract Deferred → #324 (design fork)
F3 autonomous bulk thread-resolve TOCTOU Deferred → #326 (design fork, opt-in worker)
F4 ci_gateway_green false when gateway unset Fixed
F5 --state-dir ~/… not normalized across CLIs Fixed
F6 merge command missing --self-logins Fixed (additive @me resolution)

Highlights

  • Opt-in tiers. worker auto-resolves outdated bot threads and merges gate-proven PRs;
    autopilot widens author/thread scope under watched owners. The safe default is unchanged in
    spirit but narrows discovery to your own PRs under the current repo's owner (breaking, by
    minor per 0.x).
  • Decomposed stdlib engine (8 modules + 8 thin CLIs) replacing the 3,069-line monolith: one
    parameterized discovery function, one reviewThreads paginator, one subprocess runner,
    scope-aware + schema-versioned state with corrupt-file quarantine, --repo sharding with the
    deployed state-clobber bug fixed, bot-agnostic review-trigger module (dormant when
    unconfigured), L3 foreign-activity detection.
  • First-in-fleet bin/ wrappers for the two guarded mutations (bare-name allow rules survive
    auto mode; the merge wrapper refuses --allow-unpinned-head).
  • 15 userConfig keys + a /source-control:setup babysit check/apply section. Zero baked
    identities, owners, contexts, or paths.
  • Relocated + reorganized test suite in the plugin-tests lane (engine.test.sh, self-SKIP
    when Python is absent).

Gate 0 — fresh docs

Fetched the official plugins reference on 2026-07-17
(https://code.claude.com/docs/en/plugins-reference): confirmed userConfig types
(string|number|boolean|directory|file, multiple: true arrays), ${CLAUDE_PLUGIN_DATA}
semantics (resolves to ~/.claude/plugins/data/{id}/, substitutes in skill content, env-exported
to hooks only — never skill-invoked Bash), and bin/ PATH-join behavior (collision-safe names).
These shaped the flag-delivery contract (config → SKILL.md effective-config block → CLI flags) and
the wrapper naming.

Security review (plugin-acceptance re-trigger — new trust surface)

The worker/autopilot merge tier is a new trust surface, re-triggering the review across all
surfaces:

  1. Code execution. Untrusted PR/issue text never reaches a shell unquoted. Every subprocess
    goes through one runner (babysit_util.run_command, argv lists, no shell=True); guard-CLI
    argv is built only from validated refs/SHAs.
  2. MCP. None.
  3. userConfig secrets. None — all 15 keys are non-sensitive scalars/lists.
  4. Cache isolation. State and worktrees are contained under ${CLAUDE_PLUGIN_DATA};
    --state-dir/--root are flag-only and hard-error on empty/filesystem-root paths. No ../.
  5. Data egress. git/gh only.
  6. Provenance. Ported from the chezmoi source tree plus the deployed --repo delta (with its
    state-clobber bug fixed); no hand-copied opaque blobs.
  7. bin/ PATH surface. First bin/ executables in the fleet; plugin-prefixed collision-safe
    names; each wrapper self-locates its CLI and adds no capability of its own.
  8. Prompt injection via PR content. PR titles, check names, and needs_worker_reasons flow
    into worker prompts fenced as quoted data, never instructions.

Merge-gate trust chain. Readiness is gate-verified end-to-end: mergeStateStatus == CLEAN
cross-checked against the live effective branch rules, head-SHA pinned (--match-head-commit),
unresolved-thread and required-check reconciliation, never --admin, never force-push.
Dependency-manager PRs are held in every tier absent --allow-dependency; a non-self PR on an
unprotected base is held absent --allow-unprotected.

Named residual risk. The gate's scope inputs (--allowed-owners, the allow-* overrides) are
agent-supplied per call. Readiness stays gate-verified; scope relies on the wrapper's
rejected-flag set and the bare-name allow-rule surface. An out-of-band policy file the wrapper
reads is deferred (trigger: a real scope-confusion incident).

Trigger continuity (dotfiles skill → this skill)

Phase 3 retires the user-level dotfiles /babysit-prs skill; its vocabulary maps here:

Dotfiles trigger Successor
"babysit my PRs", "watch my open PRs" safe default /source-control:babysit-prs
"worker mode", "24/7 worker", "auto-merge ready PRs" /source-control:babysit-prs worker
"autopilot", "advance every PR" /source-control:babysit-prs autopilot

Lint / gap notes

  • No ruff/pyright lane exists in this repo's CI. engine.test.sh runs ruff check opportunistically
    (self-SKIP when absent); pyright is not wired — recorded gap, deferred to a future toolchain
    change (out of scope here per the Brief; ci-workflows is not this repo).
  • Python 3.11+ is a declared prerequisite for the worker/autopilot tiers only; the safe default
    runs Python-free and degrades gracefully.

Related

No linked issue (migration tracked by the PLAN contract, not a dedicated GitHub issue).

kyle-sexton and others added 9 commits July 17, 2026 19:18
Fill PLAN.md Phase 2 (PR-B capability convergence): engine module
boundaries (7 modules + lease library + thin CLIs), userConfig contract
with key-to-flag delivery matrix, tier semantics, lane structure
(E engine / D surfaces / C integration) with scope fences.

Fold both review passes: fresh-context plan reviewer (22 findings —
lease --repo pairing, delivery-matrix gaps, draft/cadence
contradictions) and formal devils-advocate (16 findings — guard-CLI
allowlist fail-closed flags, tier-keyword requirement, unprotected-repo
merge refusal, scope-aware staleness guard, schema_version, corrupt-
state quarantine, prompt-injection surface). Design record at
design/phase-2-engine-modules.md. Phase 1 tag flipped to DONE
(PR #300 merged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decompose pr_queue_snapshot's mega-module into flat stdlib siblings:
babysit_util (single subprocess core + SHA-pin constant), babysit_gh
(single parameterized discovery + single reviewThreads paginator),
babysit_state (flag-only state dir, scope-aware staleness guard,
scoped-run clobber fix, schema_version, corrupt-state quarantine),
babysit_lease (--repo scoping + snapshot pairing validation),
babysit_checks ((type,name,workflow) identity keying), babysit_feedback
(structural bot typing, config-fed logins, dependency-author detection),
babysit_review_trigger (config-driven, dormant when unconfigured),
babysit_delta (classify + 12 delta arms + L3 foreign-activity). Thin
pr_queue_snapshot + manage_babysit_lease CLIs. Zero env reads, zero
hardcoded identities; all config via CLI flags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the two privileged mutation helpers onto the decomposed engine.
Both import only util/gh/checks (+ the pure is_dependency_author) and
route every gh call through the shared runner and the single
reviewThreads paginator.

Merge gate hardening beyond the source: --allowed-owners replaces the
retired BABYSIT_OWNERS channel and fails closed (exit 3) when absent;
dependency-manager-authored PRs are held absent --allow-dependency; a
non-self PR on an unprotected base (0 required reviews AND 0 required
contexts) is held absent --allow-unprotected; check dedup keys by
(type,name,workflow) via babysit_checks. Resolve helper keeps its
bot-only bright line, isOutdated autonomous guard, and comment-count +
last-updated TOCTOU pins. Extend the shared paginator with per-comment
updatedAt (the edit-detection pin's signal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the two guarded-mutation wrappers as the fleet's first plugin bin/
executables: source-control-babysit-merge and
source-control-babysit-resolve-thread. Bare names let a non-interpreter
allow rule survive auto mode; the merge wrapper refuses
--allow-unpinned-head so no allow-rule-covered invocation merges an
unvetted head. A shared sourced babysit-python.sh resolves a working
Python >= 3.11 by functionality probe (not PATH presence, which Windows
Store aliases defeat).

Drop the typing.TypeIs annotations from babysit_util (a 3.13-only import
with zero runtime value and no type-checker in CI) so the real floor is
3.11 (datetime.UTC), widening consumer compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the four non-guard mutation CLIs onto the decomposed engine:
manage_feedback_ledger (dispose / advisory-round / worker-checkin),
prune_babysit_worktrees (--root required, no default path, dotfiles
special-case removed), refresh_pr_branch (guarded behind-base refresh,
BLOCKED compare fallback), and request_review (renamed from the codex
helper, config-driven via --trigger-phrase / --review-bot-logins,
review_request_history / review_attempt_history ledger keys). All route
subprocess through babysit_util and take state via --state-dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the skill's documentation and config for the tier system.
SKILL.md gains the safe/worker/autopilot grammar, the AC2-narrowed safe
default (own PRs under the current owner), the substituted
effective-config block, the explicit-keyword rule for merge-capable
tiers, and per-tier draft policy — with every test_skill_contract prose
literal preserved. Seven new reference files (orchestration, cadence,
freshness, review-trigger, worktrees, safety, feedback) carry the ported
fleet mechanics with untrusted PR fields fenced as data and script
invocations citing the bin/ wrapper names. plugin.json declares 13
non-sensitive userConfig keys; setup gains a babysit check/apply section
(effective config, branch-protection posture, Windows long-path);
README documents the tiers + config table. evals.json adds tier/gate/
dependency/dormant/honesty cases and revises the Phase-1 cases for the
narrowed scope. Multi-value substitution smoke recorded (csv).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump to 0.7.0 (0.x breaking-by-minor: the safe default narrows to own
PRs under the current owner). Wire the two review-downgrade userConfig
keys (approval_downgrade_logins, skip_downgrade_logins) that the engine
already accepts as flags, so all 15 keys are reachable and the design
record, manifest, SKILL effective-config block, and README table agree.
Refresh the plugin description for the tier system and regenerate the
root catalog. CHANGELOG documents the convergence, the breaking
safe-default narrowing, the state-root move, and the retired BABYSIT_*
Python env seams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate and reorganize the engine test coverage onto the decomposed
modules: per-module behavioral tests (checks, feedback, gh, lease,
delta, state), the review-trigger race suite retargeted to the
config-driven module, a near-verbatim test_skill_contract port passing
against the current SKILL.md, cross-module integration tests
(classify_pr to save_state round-trip, scoped-clobber, corrupt-state
quarantine), and guard-CLI security tests (allowlist fail-closed,
head/thread pin refusals). engine.test.sh drives the unittest suite,
an optional ruff pass, and the bash-level wrapper checks; it self-SKIPs
when Python 3.11+ is absent. 174 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(PR #322)

Co-Authored-By: Claude Opus 4.8 (1M context) <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: a58cb3e1fb

ℹ️ 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/pr_queue_snapshot.py Outdated
… scripts executable

Discovery forwarded a multi-value `self_logins` as a single `gh --author`
argument, but `--author` accepts one login, so a comma-joined value matched
no one and multi-login users silently lost their own PRs from the queue.
Resolve the author filter to a deduped, order-preserving list (new
`resolve_authors`: comma-split, per-login `@me` resolution), query `gh` once
per login, and union the results; errors dedupe across passes and
`self_logins` becomes the full set. The `--self` readiness path already
comma-split, so this closes the discovery-vs-gate asymmetry. Reported by Codex
on #322.

Also stamp the executable bit on the 18 shebang-carrying engine scripts: they
were committed on Windows without +x and tripped the repo hygiene gate, which
requires a shebang file to be mode 100755 (matching the sibling scripts).

Co-Authored-By: Claude Opus 4.8 (1M context) <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: 975118d11e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The multi-author union test left `list_repos_for_owner` unmocked, so the
owner-mode listing sweep shelled out to a real `gh`. That passed where `gh` is
on PATH but failed on CI ("gh executable not found on PATH") by appending a
repository-list error. Mock it to [] like the sibling owner-mode tests; the
search path already exercises the per-author union deterministically.

Co-Authored-By: Claude Opus 4.8 (1M context) <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: dc594c260a

ℹ️ 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 Outdated
@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): converge babysit-prs tiers + engine (0.7.0)

Tasks

  • Gather context and understand the PR scope
  • Review security-critical engine scripts (merge gate, thread resolve, subprocess runner)
  • Review state management and GitHub API layer
  • Review unresolved Codex findings (TOCTOU, CI gateway, state-dir, self-logins)
  • Review plugin manifest and SKILL.md for correctness
  • Review bin wrappers and test coverage
  • Post comprehensive review findings

Summary

This is a large, high-quality convergence PR. The security model is solid: the subprocess runner is allowlist-only with no shell=True, the merge gate uses --match-head-commit for atomic head pinning, bot identity detection is API-derived rather than hardcoded, the owner allowlist always fails closed, and both guarded mutation CLIs enforce a read-only default. The architecture — decomposed stdlib engine, thin CLIs, bin wrappers for auto mode, CLAUDE_PLUGIN_DATA state isolation — is correct and well-reasoned.

That said, I found four issues that warrant action before merge: two runtime bugs, one documentation error that causes a real behavioral gap, and one code/doc divergence in the autonomous thread-resolve path.


Confirmed Bugs

1. pr_queue_snapshot.py: resolve_state_dir return value discarded — tilde paths never expanded

pr_queue_snapshot.py:454–455

state_store.resolve_state_dir(args.state_dir)   # ← return value discarded

resolve_state_dir calls .expanduser() and returns the expanded Path, but the caller throws it away. args.state_dir stays as the raw string. Every downstream consumer — state_path_for(args.state_dir), leases.validate_snapshot_scope(args.state_dir, ...) — constructs paths with Path("~/…"), which on Python does not expand ~. A snapshot written with --state-dir ~/babysit goes to a literal ~/babysit/queue-state.json, and subsequent state-dependent CLIs that do expand the path report missing state.

Other CLIs (that the Codex reviewer noted use the returned path) are the correct pattern; this one is the exception.

Fix: store the result — args.state_dir = str(state_store.resolve_state_dir(args.state_dir)) — before any downstream use.

Fix this →


2. babysit_review_trigger.py: CI gateway permanently blocks trigger when unconfigured

babysit_review_trigger.py:292–293

"ci_gateway_green": bool(ci_gateway_checks)
    and all(check["category"] == "success" for check in ci_gateway_checks),

When ci_gateway_context is unset, ci_gateway_checks = [], so bool([]) == False and ci_gateway_green is permanently False. The candidate predicate at line 389 requires check_state["ci_gateway_green"], so the trigger can never fire for any user who configures the three required fields (trigger_phrase, reviewer_logins, gate_context) but omits the optional gateway context.

The plugin.json description for ci_gateway_context says: "Absent: gateway classification unused" — "unused" should mean satisfied (pass-through), not blocked. The ReviewTriggerConfig.configured property correctly treats ci_gateway_context as optional (line 53), but the gate logic contradicts that.

Fix: treat an absent gateway as satisfied:

"ci_gateway_green": not ci_gateway_context
    or (bool(ci_gateway_checks) and all(check["category"] == "success" for check in ci_gateway_checks)),

Fix this →


Documentation Errors That Cause Behavioral Gaps

3. SKILL.md: Wrong flag name in config table + merge invocations omit --self-logins

SKILL.md:274 / SKILL.md:184 / SKILL.md:152

The config table names the readiness-gate flag as --self, but babysit_merge.py:355 declares it as --self-logins. This is a two-part issue:

Part A — Wrong flag name in the config table:

| `self_logins` || `--author` (snapshot), `--self` (readiness gate) ||

Should be --self-logins.

Part B — Merge invocation templates omit the flag:
Every merge invocation in SKILL.md — in the Guarded mutations section, the Autopilot section, and the Operational runbook step 7 — passes only --allowed-owners. Without --self-logins, parse_allowed_owners(args.self_logins) returns , so author_is_self is always False. On any unprotected base (zero required reviews AND zero required contexts), the worker/autopilot merge gate will refuse every PR including the user's own, requiring --allow-unprotected which the wrapper (correctly) does not block but which is semantically wrong for own-author PRs.

Fix: add --self-logins <self-logins> to every merge invocation in the guarded-mutations, Autopilot, and runbook sections; correct --self--self-logins in the config table. The <self-logins> slot is already defined in the effective-configuration block.

Fix this →


Code / Doc Divergence (Addressed by Prose; Not Enforced in Code)

4. babysit_resolve_thread.py: --autonomous bulk path cannot enforce the pre-push-snapshot requirement stated in SKILL.md

babysit_resolve_thread.py:319

The TOCTOU guard at line 319 is conditioned on args.thread_id. Without it — i.e., --autonomous --resolve without --thread-id — the script resolves every isOutdated bot thread with no per-thread pin. SKILL.md says (operationally runbook step 7, and the guarded mutations prose):

"the worker path additionally requires the thread to have been outdated in the PRE-push snapshot, pinned via --expected-comment-count and --expected-last-updated"

The CLI cannot enforce "was outdated before the push" in bulk mode: isOutdated only means the referenced code changed since the thread was created — a worker's own push can make a previously-unaddressed finding isOutdated, and --autonomous --resolve will then clear it. The SKILL.md's pre-push requirement is a prose constraint on the agent, not enforced in code.

The risk is bounded by the --autonomous restriction to isOutdated threads (which do require a code change at the finding's location), but a worker that changes the file for unrelated reasons can accidentally satisfy isOutdated on an unaddressed finding. The --thread-id path with pins is strictly safer.

This is worth calling out as a tracking item (alongside #324) rather than a blocker, since it requires either a --allowed-thread-ids bulk-pin mechanism or a requirement that the agent always use --thread-id per thread in autonomous mode. The constraint is documented; the question is whether it should be enforced at the CLI level.


Acknowledged / Already Tracked


Positive Observations

  • run_command security model: allowlist-checked, shutil.which-resolved, no shell=True, always-on timeout. Clean.
  • Merge gate thoroughness: mergeStateStatus == CLEAN/HAS_HOOKS, explicit branch-rules cross-check, review decision, unresolved thread count, required-check reconciliation, --match-head-commit atomic server-side pin. No --admin. Well-reasoned.
  • parse_repo_number URL validation: scheme + hostname + no-credentials + port-None + exact path-depth checks. Correctly paranoid.
  • Bot identity: __typename == "Bot" OR [bot] suffix, no hardcoded login list, fail-closed on truncated comment pages. Good.
  • Bin wrappers: --allow-unpinned-head rejected outright; self-locating via readlink -f. Clean and minimal.
  • Error quarantine: persistent per-PR errors stopped from pinning the whole queue at the tightest cadence forever. Good design.
  • Test suite: 178 tests covering the core engine, state, checks, delta, feedback, lease, integration, and skill contract. The test_skill_contract.py prose-assertion pattern is particularly nice for catching SKILL.md drift.
  • GraphQL paginator: fail-closed on malformed pageInfo, raises on partial pages for thread connections (where a hidden page could hide an unresolved thread), but gracefully degrades for oversized comment sub-pages. Correct tradeoff.

@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): converge babysit-prs tiers + engine (0.7.0) — Updated for dc594c2

Tasks

  • Review previous findings against latest commit (dc594c2 / 975118d)
  • Verify fix: multi-login self_logins discovery (975118d)
  • Check status: resolve_state_dir return value discarded (pr_queue_snapshot.py:455)
  • Check status: CI gateway blocks trigger when unconfigured (babysit_review_trigger.py:292-293)
  • Check status: Wrong flag name + missing --self-logins in SKILL.md merge invocations
  • Post updated review

What Changed Since Last Review

The latest push (975118d) fixed the multi-login discovery bug: babysit_gh.py now has resolve_authors which comma-splits the self_logins value and queries gh once per login, unioning results. That finding is resolved. Three of the four confirmed bugs remain open. The code/doc divergence on the autonomous thread-resolve path is unchanged (still tracked).


Bugs — Still Unresolved

1. pr_queue_snapshot.py:455resolve_state_dir return value discarded

Line 455

state_store.resolve_state_dir(args.state_dir)   # return value still discarded

The expanded Path is returned but never stored. Codex confirmed this in the dc594c2 pass and the code is unchanged. Every downstream call inside build_snapshot that constructs paths from args.state_dir operates on the raw (unexpanded) string. A --state-dir ~/babysit ends up writing to a literal ~/babysit/ directory.

Fix: args.state_dir = str(state_store.resolve_state_dir(args.state_dir))

Fix this →


2. babysit_review_trigger.py:292-293 — CI gateway permanently blocks trigger when unconfigured

Lines 292-293

"ci_gateway_green": bool(ci_gateway_checks)
    and all(check["category"] == "success" for check in ci_gateway_checks),

When ci_gateway_context is unset, ci_gateway_checks = [], bool([]) is False, so ci_gateway_green is always False. The candidate predicate requires ci_gateway_green, so the review trigger never fires for users who set the three required fields but omit the optional gateway. SKILL.md line 280 says "gateway check unused" for the unset case — the code contradicts that.

Fix:

"ci_gateway_green": not ci_gateway_context
    or (bool(ci_gateway_checks) and all(check["category"] == "success" for check in ci_gateway_checks)),

Fix this →


3. SKILL.md — Wrong flag name in config table + merge invocations missing --self-logins

Three sites, all still unfixed:

Part A — Line 274: The config table lists --self (readiness gate) but babysit_merge.py declares the flag as --self-logins.

Part B — Missing --self-logins in three invocation templates:

  • Line 154-155 (Autopilot section): source-control-babysit-merge owner/repo#N --allowed-owners <watched-owners> --merge ...
  • Line 184-185 (Guarded mutations section): source-control-babysit-merge owner/repo#N --allowed-owners <watched-owners>
  • Line 383 (Runbook step 7): same pattern, no --self-logins

Without --self-logins, author_is_self is always False. On an unprotected repo (zero required reviews and contexts), the worker/autopilot merge gate refuses even the PR author's own PRs, making worker mode silently inoperative for solo developers unless they pass --allow-unprotected manually.

Fix: Add --self-logins <self-logins> to all three merge invocation templates; fix --self--self-logins in the config table at line 274.

Fix this →


Fixed ✓

Finding Fix commit
Multi-value self_logins delivered as single gh --author, silently dropping owned PRs for multi-login users 975118d

Tracked (Not Blockers)


Positive Observations (Unchanged)

The core of this PR remains well-engineered: run_command uses allowlist + shutil.which + no shell=True; the merge gate's --match-head-commit gives server-side atomic head pinning; bot identity is __typename == "Bot" OR [bot]-suffix (no hardcoded list, fail-closed on truncated pages); parse_repo_number URL validation is correctly paranoid; the GraphQL paginator fails closed on malformed pageInfo and raises on partial thread-connection pages. The architecture decomposition (8 modules + 8 CLIs + bin wrappers) is clean. The test_skill_contract.py SKILL.md prose-assertion pattern is a nice safeguard against doc/code drift.

The three open bugs are all straightforward one-liners or template additions — no design changes required.

@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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


Claude finished reviewing PR #322 — Updated for dc594c2

Tasks

  • Load tools and initialize
  • Review latest commit (dc594c2) — test stub fix
  • Verify status of 3 remaining bugs from prior reviews
  • Check pr_queue_snapshot.pyresolve_state_dir return value discarded (still open)
  • Check babysit_review_trigger.py — CI gateway blocks trigger when unconfigured (still open)
  • Check SKILL.md — wrong flag name + missing --self-logins in merge invocations (still open)

What Changed in dc594c2

test_babysit_gh.py — stub list_repos_for_owner in multi-author union test

The test_multiple_authors_are_queried_separately_and_unioned test previously stubbed only gh_json. discover_prs(owners=[...], authors=[...]) internally calls list_repos_for_owner as a fallback path when the gh search axis fails or returns nothing for an owner. Without the stub, any deviation in test environment state could trigger a real gh call, making the test non-hermetic. Adding mock.patch.object(gh, "list_repos_for_owner", return_value=[]) makes the test fully isolated. This is a correct test-reliability fix and consistent with how the other owner-axis tests (test_owner_with_zero_repositories_is_tolerated, test_owner_search_failure_with_repos_is_reported, test_owner_errors_are_deduped_across_authors) already stub that seam.


Bugs — Still Unresolved (Three Open from Prior Reviews)

1. pr_queue_snapshot.py:455resolve_state_dir return value discarded

Line 455

state_store.resolve_state_dir(args.state_dir)   # return value still discarded

resolve_state_dir calls .expanduser() and returns the expanded Path, but the return value is thrown away. Every downstream call inside build_snapshot that constructs paths from args.state_dir operates on the raw (unexpanded) string. A --state-dir ~/babysit ends up writing to a literal ~/babysit/ directory that os.path.expanduser was never applied to. All other CLIs that use the returned value are the correct pattern; this one is the lone exception.

Fix: args.state_dir = str(state_store.resolve_state_dir(args.state_dir))

Fix this →


2. babysit_review_trigger.py:292–293 — CI gateway permanently blocks trigger when unconfigured

Lines 292–293

"ci_gateway_green": bool(ci_gateway_checks)
    and all(check["category"] == "success" for check in ci_gateway_checks),

When ci_gateway_context is unset (config.ci_gateway_context == ""), ci_gateway_checks = [], so bool([]) is False and ci_gateway_green is permanently False. The candidate predicate at line 389 requires ci_gateway_green, so the review trigger can never fire for users who configure the three required keys (trigger_phrase, reviewer_logins, gate_context) but leave the optional gateway unset. Confirmed: ReviewTriggerConfig.configured (line 53) correctly treats ci_gateway_context as optional, but the gate logic at 292–293 contradicts that. SKILL.md line 280 states "gateway check unused" for the unset case — the code says the opposite.

Fix:

"ci_gateway_green": not ci_gateway_context
    or (bool(ci_gateway_checks) and all(check["category"] == "success" for check in ci_gateway_checks)),

Fix this →


3. SKILL.md — Wrong flag name in config table + merge invocations missing --self-logins

Four sites, all still unfixed:

Part A — Line 274: The config table lists --self (readiness gate) but babysit_merge.py:355 declares the flag as --self-logins (confirmed by grep). This is a documentation error that trains agents to pass the wrong flag.

Part B — Missing --self-logins in merge invocation templates:

  • Line ~155 (Autopilot step 3): source-control-babysit-merge owner/repo#N --allowed-owners <watched-owners> --merge ...
  • Line ~184 (Guarded mutations section): source-control-babysit-merge owner/repo#N --allowed-owners <watched-owners>
  • Line ~383 (Runbook step 7): same pattern, no --self-logins

Without --self-logins, parse_allowed_owners(args.self_logins) returns and author_is_self is always False. On any unprotected repo (zero required reviews and zero required status contexts), the merge gate refuses every PR absent --allow-unprotected — which includes the user's own PRs. This makes worker/autopilot mode silently inoperative for solo developers on personal repos, the most common use case for these tiers.

Fix: Add --self-logins <self-logins> to all three invocation templates; correct --self--self-logins at line 274.

Fix this →


Fixed ✓

Finding Fix commit
Multi-value self_logins delivered as single gh --author, silently dropping owned PRs for multi-login users 975118d
test_multiple_authors_are_queried_separately_and_unioned missing list_repos_for_owner stub (non-hermetic) dc594c2

Tracked (Not Blockers)


Positive Observations (Unchanged from Prior Review)

The core of this PR is well-engineered. The dc594c2 test fix is small, correct, and exactly right — it closes the only non-hermetic gap in the multi-author union test coverage. The three open bugs above are all one-liners or template additions; no design changes are required.

…ave)

Resolve the plugin.json / CHANGELOG / README conflicts from main's #325
(skill-channel userConfig migration); rebump source-control to 0.8.0 (main
shipped 0.7.0).

- Prefix all 15 babysit userConfig keys `babysit_*` to match #325's
  feature-prefixed convention; `self_logins` unifies with #325's
  `babysit_self_logins`. Keep main's `worktree_stale_days` and
  `fetch_logs_max_bytes`.
- Additive self-identity across every consumer: `--extra-self` (gate, from
  #325), `--author @me,<extras>` (discovery), `--self-logins @me,<extras>`
  (merge gate) each fold `babysit_self_logins` onto your gh login.
- Fix F4: review-trigger candidacy no longer stalls when no CI gateway context
  is configured (ci_gateway_green true when unset).
- Fix F5: pr_queue_snapshot stores the resolved --state-dir so a `~`-prefixed
  dir is shared with the other state CLIs.
- Fix F6: babysit_merge resolves `@me` in --self-logins (after the
  argument-shape refusals; fail-closed if unresolvable) so own PRs are exempt
  from the unprotected-base hold. Reported by Codex on #322.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
markdownlint MD022/MD032: the merge-reconciled CHANGELOG ran the 0.8.0
"Removed" list straight into the "## [0.7.0]" heading with no blank line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kyle-sexton kyle-sexton changed the title feat(source-control): converge babysit-prs tiers + engine (0.7.0) feat(source-control): converge babysit-prs tiers + engine (0.8.0) Jul 18, 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: c006d65365

ℹ️ 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/request_review.py
@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): converge babysit-prs tiers + engine (0.7.0) — Updated for af32fab

Tasks

  • Load tools and initialize
  • Inspect af32fab changes — verify fixes for the 3 open bugs
  • Verify pr_queue_snapshot.py resolve_state_dir fix
  • Verify babysit_review_trigger.py CI gateway fix
  • Verify SKILL.md --self-logins fix (flag name + invocation templates)
  • Scan for any new issues introduced in af32fab
  • Post updated comprehensive review

What Changed in af32fab

af32fab is a merge commit that reconciles this branch with main's #325 (userConfig scalar wave). Alongside resolving the merge conflict it carries three bug fixes (F4–F6) and a breaking rename of all babysit userConfig keys to the babysit_ prefix.

Bug fixes shipped:

  • F4 — CI gateway blocks trigger when unconfigured: babysit_review_trigger.py:292ci_gateway_green is now (not ci_gateway_context) or (…). Absent gateway → satisfied. Regression test added: test_unset_ci_gateway_context_is_treated_as_green. ✓

  • F5 — resolve_state_dir return value discarded: pr_queue_snapshot.py:455args.state_dir = str(state_store.resolve_state_dir(args.state_dir)) — tilde paths now shared with downstream CLIs. ✓

  • F6 — Merge invocations missing --self-logins: Both SKILL.md merge invocation templates (Autopilot step 3, Guarded mutations) now include --self-logins @me,<self-logins>. babysit_merge.py resolves @me via resolve_authors after all argument-shape refusals (network call deferred past the scope guard), with fail-closed fallback when @me can't resolve. Guard test added (test_self_logins_at_me_refuses_out_of_scope_before_resolving). ✓

Breaking rename (correctly documented):

All 15 babysit userConfig keys now carry the babysit_ prefix (e.g. watched_ownersbabysit_watched_owners, self_loginsbabysit_self_logins). babysit_self_logins semantics shifted from "your logins" to "extras added to gh api user login" — the self set is now always additive. BABYSIT_SELF_LOGINS env var retired; replaced by --extra-self flag on babysit-readiness-gate.sh. Plugin version bumped to 0.8.0. All changes are documented in CHANGELOG.md and SKILL.md's effective-config table.

Consistency cross-check:

The 15 babysit_* keys in plugin.json match the SKILL.md config table exactly. The --extra-self flag in babysit-readiness-gate.sh matches the config table's flag-delivery column for babysit_self_logins. resolve_authors(None) returns [] (not an error), so an absent --self-logins yields an empty self set and correct fail-closed hold behavior.


All Fixed ✓

Finding Fix commit
Multi-value self_logins delivered as single gh --author, silently dropping owned PRs for multi-login users 975118d
test_multiple_authors_are_queried_separately_and_unioned missing list_repos_for_owner stub (non-hermetic) dc594c2
F4 — CI gateway (ci_gateway_context unset) permanently blocks review trigger af32fab
F5pr_queue_snapshot.py discards resolve_state_dir return value; tilde paths written to literal ./~/… af32fab
F6 — Merge invocations omit --self-logins; own PRs held on unprotected repos af32fab

Tracked (Not Blockers)


Overall Assessment

All confirmed bugs from the two prior reviews have been fixed in this branch. The af32fab merge is clean: the @me resolution ordering is correct (scope gate fires before network access), the fail-closed fallback is tight, the breaking rename is fully documented, and the new regression tests cover the three fixed paths. No new issues identified in af32fab.

The four tracked deferrals (#324, #326, #327) are correctly scoped to an opt-in dormant-by-default module and do not affect the safe default path. The PR is ready to merge from a correctness and security standpoint.

@claude

claude Bot commented Jul 18, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): converge babysit-prs tiers + engine (0.7.0) — Updated for c006d65

Tasks

  • Load tools and initialize
  • Verify fix: resolve_state_dir return value discarded (pr_queue_snapshot.py)
  • Verify fix: CI gateway blocks trigger when unconfigured (babysit_review_trigger.py:292-293)
  • Verify fix: SKILL.md wrong flag name + missing --self-logins in merge invocations
  • Review new Codex findings from c006d65 (request_review.py:101, babysit_review_trigger.py:388)
  • Post final comprehensive review

What Changed Since Last Review

c006d65 is a pure style fix — blank line before the 0.7.0 CHANGELOG heading. No behavioral change.

af32fab (the reconciliation commit reviewed here) fixed all three previously-confirmed bugs. Verified against the current HEAD:


All Prior Bugs — Fixed ✓

1. pr_queue_snapshot.py:455resolve_state_dir return value discarded

Fixed. pr_queue_snapshot.py:455 now reads:

args.state_dir = str(state_store.resolve_state_dir(args.state_dir))

The expanded Path is stored back, so tilde paths like ~/babysit are correctly expanded before any downstream state_path_for / lease / scope validation calls.

2. babysit_review_trigger.py:292-293 — CI gateway permanently blocked trigger when unconfigured

Fixed. babysit_review_trigger.py:292-296 now reads:

"ci_gateway_green": (not ci_gateway_context)
or (
    bool(ci_gateway_checks)
    and all(check["category"] == "success" for check in ci_gateway_checks)
),

Gateway is satisfied when unset (the documented "gateway check unused" behavior), and required when configured. Regression test test_unset_ci_gateway_context_is_treated_as_green added.

3. SKILL.md — Wrong flag name + missing --self-logins in merge invocations

Fixed. All four sites verified:

  • Config table (line 277): Now correctly says --extra-self (readiness gate) and --self-logins (merge gate). The old wrong --self is gone.
  • Autopilot section (line ~155): --self-logins @me,<self-logins> now present.
  • Guarded mutations section (line ~186): --self-logins @me,<self-logins> now present. The prose explains that @me resolves to the gh login plus extras; instructions to drop the ,<self-logins> suffix when that config value is empty are correct.

The guard test in test_guards.py covers the new additive @me,extras self-identity model.


New Codex Findings (c006d65) — Confirmed Valid, Deferred to #327

Both findings were flagged on the c006d65 pass but exist in code since af32fab (c006d65 only changes the CHANGELOG). Both have been acknowledged and triaged by kyle-sexton.

F7. babysit_review_trigger.py:388 — Prior-head reactions suppress new head candidacy window

babysit_review_trigger.py:388

and not reactions

and not reactions is unconditional: fetch_reaction_signals() returns all reactions with no per-head scoping (reactions carry no commit SHA), so a reviewer-bot reaction from an earlier head permanently suppresses candidacy on every subsequent head. associated_reaction_ids is computed nearby and correctly scopes to the current head's trigger-comment reactions — but the candidacy gate doesn't use it. After a push updates the head, the review trigger will never re-post, even after the anti-flap window clears.

Bounded by opt-in scope — the review trigger module is dormant unless trigger_phrase + reviewer_logins + gate_context are all configured. The fix (use not associated_reactions instead of not reactions) is a one-liner. Tracked in #327 (F7) per kyle-sexton's triage.

F8. request_review.py:101 — BLOCKED-but-BEHIND PRs bypass the stale-head guard

request_review.py:96-107

or merge_state in {"", "BEHIND", "DIRTY", "DRAFT", "UNKNOWN"}

view_pr() enriches a BLOCKED head with _blocked_base_compare when the PR is also behind its base — because BLOCKED can mask BEHIND. This live pre-POST guard only rejects the literal BEHIND enum; a BLOCKED-but-BEHIND PR would pass the guard and receive a review trigger on a stale head, wasting the one-shot trigger request for that SHA and requiring a re-snapshot before the trigger fires again correctly.

Bounded by opt-in scope — same module as F7, dormant by default. The fix is to cross-check _blocked_base_compare or re-run compute_branch_freshness(current) before posting. Tracked in #327 (F8) per kyle-sexton's triage.


Summary

All three previously-confirmed bugs are fixed. The c006d65 push is a CHANGELOG style fix only. The two new Codex findings are valid and correctly deferred to #327 — both live in the opt-in review-trigger module (dormant for any user who doesn't set all three required config keys) and are not merge blockers for this convergence PR.

This PR is ready to merge. The architectural decomposition (8 modules + 8 CLIs + bin/ wrappers), the subprocess security model, the merge gate's --match-head-commit head pin, and the 178-test suite with test_skill_contract.py prose-assertion coverage are all solid. The fix history is clean — every confirmed bug was acknowledged, fixed, and backed by a regression test.

Finding Status Commit
resolve_state_dir return value discarded ✅ Fixed af32fab
CI gateway blocks trigger when unconfigured ✅ Fixed af32fab
SKILL.md wrong flag name + missing --self-logins ✅ Fixed af32fab
Multi-value self_logins silently dropped ✅ Fixed 975118d
Bulk --autonomous TOCTOU pin gap Tracked → #326
Failing gate never becomes trigger candidate Tracked → #324
Prior-head reactions suppress new head candidacy (F7) Tracked → #327
BLOCKED-but-BEHIND PRs bypass stale-head guard (F8) Tracked → #327

@kyle-sexton
kyle-sexton merged commit fe78acb into main Jul 18, 2026
19 of 30 checks passed
@kyle-sexton
kyle-sexton deleted the feat/babysit-prs-convergence branch July 18, 2026 05:50
kyle-sexton added a commit that referenced this pull request Jul 19, 2026
…-behind guard, stale-head reactions) (#452)

Closes #327

## Summary

Two head-staleness defects in the dormant-by-default AI review-trigger
flow (both found by Codex on #322, P2). The module only activates when
`babysit_review_trigger_phrase` + `babysit_review_bot_logins` +
`babysit_review_gate_context` are configured, so neither affects the
default babysit tiers.

### F7 — reject compare-behind BLOCKED heads before requesting review
`request_review.py`'s pre-POST freshness guard rejected only the literal
`BEHIND` merge state. GitHub reports `mergeStateStatus=BLOCKED` for a PR
that is *also* behind its base (BLOCKED masks BEHIND), so the guard
passed and posted the one-shot trigger on a **stale head**. The guard
now reuses `compute_branch_freshness(current)` — off the
`_blocked_base_compare` enrichment `view_pr` already computes — so a
compare-confirmed behind head is rejected and the branch-refresh flow
runs first.

### F8 — ignore stale-head reactions for new review windows
The candidate predicate's unconditional `and not reactions` blocked
candidacy whenever *any* reaction existed. Reactions carry no commit
SHA, so a reviewer-bot reaction left on an earlier head persisted onto
later heads; the new head never accrued `missing_observations` and the
trigger never posted for the updated head. The check is now scoped to
`associated_reactions` (reactions associated with, or newly observed
for, the current head — already computed nearby).

## Scope note
The live post-time guard at `request_review.py` (`if reaction_signals:
raise`) and the state-string branch at `babysit_review_trigger.py`
remain conservative by design — they still refuse to post while any
unproven reaction lingers. F8 restores the **observation window**
(candidacy / `missing_observations` accrual) without weakening a
fail-safe post guard. Fully unblocking end-to-end posting through a
persistent stale reaction is a larger call related to #324 and is
intentionally out of scope here.

## Verification
- `engine.test.sh` locally: 184 unittest OK, **ruff clean**,
guarded-wrapper checks pass.
- New regression tests in `test_review_trigger_race.py`: F7
(BLOCKED-masked-BEHIND rejected; BLOCKED-but-fresh still passes), F8
(stale earlier-head reaction no longer suppresses the window;
current-head reaction still suppresses).
- source-control `0.9.0 → 0.9.1` + CHANGELOG.

## Related
- #322 (PR-B), #324 (sibling review-trigger reconciliation — not pulled
in; one PR per issue).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 19, 2026
…rule, snapshot exit-code taxonomy, cross-PR dependency channel (#456)

Closes #373.

Implements the four enhancements plus one gotcha from issue #373 in the
`babysit-prs` skill. The source retrospective the issue cites
(`.work/handoffs/babysit-autopilot-retro.md`) is not present in the
checkout
(`.work/` is gitignored), so every edit was driven by the issue body
plus the
live skill files; the retro is provenance only. All doc edits are
pointer-style
(they point at the existing rule/clause, never restate it).

## (a) No-monitor hard STOP at the merge / gate-completion step

Added in both `SKILL.md` (Guarded mutations) and `reference/safety.md`
(Pinned-Command Degradation): once a PR is proven ready, or its merge is
deferred to a human, report and stop — never arm a watch/monitor. Both
point
at the existing no-background-monitor clause in the Worker Contract
(`reference/orchestration.md`) rather than restating it.

## (b) Bare-wrapper pipe rule

`reference/safety.md` Guarded Mutation Wrappers now states that the
wrapper's
JSON must be parsed in a separate step — never `wrapper | python` / `|
jq` —
because an interpreter-in-pipeline trips the auto-mode safety classifier
and
blocks the call before the wrapper runs.

## (c) Snapshot exit-code taxonomy

`scripts/pr_queue_snapshot.py` previously collapsed every non-fatal
error into
exit `1`, so a purely-advisory head-ref alias-check error was
indistinguishable from a substantive per-PR hydration failure. The
split:

| code | meaning | state written |
| --- | --- | --- |
| `0` | valid snapshot, no errors | yes |
| `1` | valid snapshot, ≥1 substantive error (per-PR hydration or
discovery failure) | yes |
| `2` | fatal — exception before a snapshot existed | no |
| `3` | valid snapshot, advisory-only (head-ref alias cross-check)
errors | yes |

Substantive errors take precedence over advisory (a run with both
returns `1`).
The advisory marker is a single-source constant
(`babysit_delta.HEAD_REF_ALIAS_ERROR_MARKER`) used at both
message-construction
sites (queue path in `babysit_delta.py`, single-PR path in
`pr_queue_snapshot.py`) so they cannot drift. Taxonomy documented in the
module
docstring; new `scripts/tests/test_pr_queue_snapshot.py` covers 0/1/2/3
including the advisory-vs-substantive distinction and precedence.

**Consumers checked** (exit codes are a caller contract): every `*.md`,
`*.sh`,
`*.py`, `engine.test.sh`, orchestration doc, and `bin/` wrapper
referencing the
snapshot. No live consumer keys on the script's `$?` — the docs
(`orchestration.md`, `SKILL.md`, `loop.md`) invoke it and parse the JSON
snapshot; `babysit-python.sh` uses `exec` (child exit code passes
through
unchanged); no `bin/` wrapper or sibling script branches on it.
Introducing `3`
therefore breaks no caller that treated `1` as "any error".

## (d) Worker→main cross-PR dependency channel

New `Cross-PR Dependency Signalling` section in
`reference/orchestration.md`
formalizing a worker signalling a discovered cross-PR coupling (e.g.
medley#1567 ↔ #1597, #224#356) back to the main agent, which owns
cross-PR
ordering. Points at the existing `SendMessage` mechanism (documented
there for
main→worker), explicitly reversed.

## Gotcha: self-blocking CI check

Recorded in the `SKILL.md` Gotchas list: a newly required check whose
own fix
PR carries that same check cannot be gate-merged and needs a one-time
human
admin-merge bootstrap. The concurrent-same-identity yield rationale is
already
covered by the Checkout/Push invariants (head-SHA recheck + worker lease
+
`foreign_activity` suppressor) and is deliberately not duplicated.

## Verification

- `bash engine.test.sh`: `Ran 186 tests … OK`; ruff `All checks
passed!`;
  guarded-wrapper behavior checks all PASS.
- markdownlint-cli2 on the four changed `.md` files: 0 issues.
- Plugin bumped `0.9.0` → `0.9.1` (`jq -e .version` confirms) with a
matching
  `## [0.9.1]` CHANGELOG entry.
- No trailing whitespace / final-newline issues in any changed file.

## Related

- #322 (PR-B) — retro source for these enhancements.
- #324 — sibling review-trigger reconciliation (F8 posting-guard
follow-on territory).
- #326, #327 — same babysit-prs safety-hardening cluster.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…sit thread resolves (#561)

## Summary

`babysit_resolve_thread.py`'s bulk resolve path (`--resolve` with no
`--thread-id`) did not require the per-thread TOCTOU pins the
single-thread path already enforces. Because an autonomous worker's own
push marks a review thread `isOutdated`, the bulk `--autonomous
--resolve` path could clear threads that changed since they were vetted
with no proof the finding was addressed — the actor signing its own
permission slip. This implements the maintainer-confirmed Option 1:
reject bulk autonomous resolves and require a per-thread vetted loop.

## Fix

- **Guard (root cause):** In `babysit_resolve_thread.py`, `--autonomous
--resolve` without `--thread-id` is now refused (exit 2, before any
network fetch), forcing the unattended-worker path through a per-thread
loop where each thread reuses the existing pin guard
(`--expected-comment-count` + `--expected-last-updated`). No second
pinning model is introduced.
- **Closed bypass (deliberate extension beyond AC1's literal "without
--thread-id"):** `--allow-unpinned-thread` is also refused in
`--autonomous` mode — otherwise `--autonomous --resolve --thread-id X
--allow-unpinned-thread` would reopen the exact "resolve with no pins"
hole. It stays an interactive-only override. Flagged here explicitly for
maintainer visibility; it is restriction-direction (auto-ratified) and
fail-closed, consistent with the Option 1 rationale.
- **Docs:** Updated the module docstring and `SKILL.md` Autopilot step 2
from a bulk call to the per-thread vetted loop, aligning them with the
pinned form already documented in `reference/orchestration.md`,
`reference/safety.md`, and the worker prompt template (those were
already correct; the code and step 2 were the stragglers).
- **Test:** Two regression tests in `test_guards.py` prove the bulk
autonomous resolve is refused and that `--allow-unpinned-thread` cannot
bypass pins in autonomous mode.
- Version bump `0.9.3 → 0.10.0` (behavior change to the
autonomous-worker contract) + CHANGELOG entry.

## Verification

Full plugin engine suite (stdlib unittest + ruff + guarded-wrapper
behavior) via `bash engine.test.sh`:

```
== unittest suite ==
Ran 219 tests in 1.293s
OK
== ruff ==
All checks passed!
== guarded-wrapper behavior ==
PASS: merge wrapper rejects --allow-unpinned-head
PASS: merge wrapper reaches fail-closed CLI (no allowlist)
PASS: resolve wrapper reaches fail-closed CLI (no allowlist)
=== EXIT: 0 ===
```

New regression tests (`python -m unittest tests.test_guards -v`):

```
test_autonomous_allow_unpinned_thread_is_refused ... ok
test_autonomous_bulk_resolve_without_thread_id_is_refused ... ok
... (11 tests) ... OK
```

markdownlint-cli2 on the two changed Markdown files: `Summary: 0
error(s)`. Plugin + catalog manifests: `All plugin manifests and the
catalog validated.`

Closes #326

## Related

- #326 — the issue this closes (maintainer decision: Option 1, reject
bulk autonomous resolves).
- #322 — the PR whose review pass originated this finding.
- #324 — sibling issue from the same review pass.
- #571 — machine-enforced fix for the displacement bypass (autonomous
worker clearing a bot thread its own push merely displaced); the
per-thread pins in this PR do not close it.

🤖 Generated with a claude-code-plugins work-2 lane implementation
subagent (model: opus)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 25, 2026
…oop.md §5.3 (#1307)

*This was generated by AI during an autonomous work-loop execution
session.*

Closes #653

## Summary

`babysit-prs`'s SKILL.md described a cadence-ownership split that no
longer exists.

#652 added the engine-backed `recommended_cadence` →
`ScheduleWakeup.delaySeconds` mapping table to
`reference/loop.md` §5.3, alongside the static Python-free degrade
ladder already living there.
`reference/cadence.md` owns only the cadence *states and thresholds* —
and has said so since #322
("`loop.md` owns the wake mechanics, this file owns the states and
thresholds behind the
recommendation"). SKILL.md still routed readers the old way:

| Surface | Before | After |
|---|---|---|
| Runbook step 9 | `recommended_cadence` per `cadence.md`; `loop.md`
§5.3 named only as the "static ladder … Python-free degrade" | `Schedule
the next wake per the cadence contract in reference/loop.md §5.3.` |
| Reporting closing line | "Recommend the exact next interval per
`cadence.md`" | same sentence, now citing `loop.md` §5.3 |
| References entry for `loop.md` | "…checklist, static cadence ladder" |
"…checklist, and the §5.3 cadence contract" |
| Step-5 progressive-disclosure trigger | load `cadence.md` "only before
**recommending cadence**" | load `cadence.md` "only before
**interpreting a cadence state**" |

The filed issue named step 9. The other three are the same defect:
leaving them would have made the
file internally inconsistent about which section owns the seconds.

**The Reporting line was a live wrong-number risk, not just
imprecision.** `cadence.md` states
`idle` = **daily**; §5.3 documents `ScheduleWakeup` clamping
`delaySeconds` to `[60, 3600]`, so
inside `/loop` `idle` and `quiet` both wake hourly. "Recommend the exact
next interval per
`cadence.md`" could therefore surface an interval the loop will never
schedule.

The `cadence.md` link is dropped from step 9 rather than kept alongside
`loop.md`: §5.3 already
hops to `cadence.md` for the states in one line, so a second pointer
here is the gratuitous
cross-reference `CLAUDE.md`'s pointer-not-copy rule forbids.
`cadence.md` keeps its own References
entry and its step-5 disclosure trigger. The new step-9 wording matches
vocabulary the file already
uses at its Step 7 checklist line ("schedule the next wake per the
cadence contract (§5.3)") and in
the sibling `babysit-loop` SKILL.md ("that mapping owns the seconds").

Docs-only. No behavior change, no script or engine change.
`source-control` `0.26.1` → `0.26.2`
with the matching CHANGELOG entry.

## Test plan

Every command below was run from the branch worktree; all passed.

- `plugins/skill-quality/scripts/check-skill.sh babysit-prs`
(`CHECK_SKILL_BASE_REF=origin/main`) —
**exit 0**, `PASS — 0 errors`. This is the gate CI runs via
`scripts/check-changed-skills.sh`.
Includes the 500-line hard cap, all 9 base-ref trigger phrases
preserved, broken-internal-ref
  check, and `scripts/engine.test.sh`.
- **SKILL.md is now 497 lines** (base `origin/main`: 499). An
intermediate revision of this branch
landed at exactly 500 and *failed* the hard cap (`LINE_COUNT >=
LINE_HARD_CAP`, cap 500) — caught
by an independent reviewer before the PR was opened. Collapsing step 9
to a single pointer and the
References bullet back to two lines both fixed the cap and made the diff
better follow
pointer-not-copy. The one remaining `WARN` is the pre-existing 200-line
soft target, untouched by
    this change.
- `scripts/check-changelog-parity.sh --check` — exit 0.
- `scripts/check-changelog-parity.sh --check-bump origin/main` — exit 0
(the manifest bump has a
  matching `## [0.26.2]` entry).
- `markdownlint-cli2` over both changed markdown files, repo config
auto-discovered — exit 0,
  0 errors.
- `jq empty` on `plugins/source-control/.claude-plugin/plugin.json` —
exit 0.
- `typos` over both changed markdown files — exit 0.
- Every relative link in the edited regions resolved by hand
(`reference/loop.md`, `cadence.md`,
`safety.md`, `orchestration.md`, `freshness.md`, `stuck-checks.md`,
`feedback.md`,
`review-trigger.md`); the `§5.3` anchor exists at
`reference/loop.md:426`. No external URLs
  introduced, so lychee's absence changes nothing.
- Repo-wide `grep` for any remaining reference sending a reader to
`cadence.md` for wake seconds, or
describing §5.3 as holding only the static ladder — none remain. The
four surviving `cadence.md`
citations in `plugins/source-control/` were each checked and are
correct.

Claims verified against the actual files rather than the issue text:
§5.3 does contain both the
mapping table (`loop.md:435-440`) and the degrade ladder
(`loop.md:456-462`); `cadence.md:4-5` does
disclaim the wake mechanics; `cadence.md:27` does state `idle` = daily
against `loop.md:449-452`'s
`[60, 3600]` clamp. The CHANGELOG entry's causal account was corrected
after `git show --stat
e9cef6e` showed #652 never touched `cadence.md`, and `git log -L4,5`
dated that file's disclaimer to
`fe78acb6` (#322) — so the references were always imprecise; #652 only
made the correct target
concrete.

Per this repo's fresh-docs mandate,
<https://code.claude.com/docs/en/skills> was fetched this
session. It confirms the progressive-disclosure model this diff operates
on — SKILL.md body plus
bundled supporting files loaded on demand — and imposes no constraint on
how a SKILL.md cites its
own `reference/` files, so the choice of pointer is a repo-convention
question, decided above by
`CLAUDE.md`'s pointer-not-copy rule.

## Related

- Refs #652 / #504 — the PR and issue that added the §5.3 mapping table
this diff points at.
- Refs #322 — where `reference/cadence.md` first disclaimed ownership of
the wake mechanics.
- Version note: open PRs #1285 and #1264 both claim `source-control`
`0.27.0`. If either merges
first this branch's `0.26.2` becomes a regression — it self-surfaces as
a git conflict on both the
manifest line and the CHANGELOG insert point, so rebase rather than
force-merge.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 25, 2026
…1392)

*This was generated by AI during autonomous /work-items:work-loop
execution.*

Closes #324

## Summary

`reference/review-trigger.md` documented the review-trigger signal as
`<review-gate-context>`
"pending **or failing**", while every `gate_state` comparison in the
engine accepts only `pending`
— the candidate predicate and `request_signal_pending` in
`babysit_review_trigger.py`, and the
"awaiting requested review" material signal in `babysit_delta.py`. #324
called this a genuine
design fork and explicitly forbade resolving it by inference.

**Decision: honor the code, narrow the doc.** Documentation and test
only; no behavior change.

### The empirical tiebreaker #324 asked for

#324 named the deciding fact: *does the `<review-gate-context>` the
cutover config wires report
FAILING or PENDING when no reviewer has engaged?* That fact was located,
and it settles the fork
in favor of the code:

- The wired context name is `codex-review` —
`babysit_review_gate_context` under
`pluginConfigs["source-control@melodic-software"]` in the dotfiles
chezmoi data
  (`melodic-software/dotfiles`, `.chezmoidata/claude.json`).
- The workflow that posts that context is `melodic-software/medley`,
`.github/workflows/comment-review-gate.yml`. Every status it writes is
either `state: "success"`
(reviewer engaged) or `state: "pending"` (not engaged). It **never**
posts `failure` or `error`:
the file's only four `state:` assignments are three `success` and one
`pending`.
- Its header states the contract directly — *"If engaged → 'success'
(review received, covers this
push) … If not → 'pending'"*, and *"its status stays 'pending' until the
next push re-checks."*

So "no reviewer engaged yet" is **PENDING**, never FAILING. The
honor-doc branch — widening the
code so a failing gate becomes a trigger candidate — would have been
correct only if the gate
reported FAILING for that state, and it does not. `review-trigger.md`'s
"or failing" was the
erroneous restatement; the code was right.

Scope note for the reviewer: the gate is deployed in `medley`, not in
this repository — recent
`claude-code-plugins` PRs carry no `StatusContext` at all, so the
evidence above comes from the
config that names the context plus the workflow that posts it, not from
a live status on this PR.

### Corroborating repo-internal evidence

Three signals inside this repository point the same way, independently
of the gate observation:

1. `review-trigger.md`'s own Engagement Gate Semantics section — the
mechanism-level definition of
the gate's vocabulary — defines exactly two states (`PENDING` = no
qualifying reviewer activity
after the polling window, `SUCCESS` = may reflect activity from an
earlier head) and gives
`FAILING` no engagement meaning anywhere in the file. The "pending or
failing" phrasing appears
only in the downstream restatement of the helper's preconditions, one
paragraph below a
separate and correct rule about never discarding "a pending or failed
StatusContext" — the
likelier provenance of the phrase than authored intent to include
failing gates.
2. A failing analogue of `request_signal_pending` (`effective_state ==
"PENDING" and not
target_url`) would require knowing what `target_url` a failing gate
carries. Nothing records
that, so honoring the doc would have meant inventing a signal shape
inside logic that posts
   comments to GitHub.
3. The motivating harm does not exist. `classify_checks` buckets the
gate `StatusContext` like any
other check, so a failing gate already raises `babysit_delta`'s "N
failing check(s)" blocker. A
failing gate is escalated to a human, not silently dropped — the
coherent reading of the split:
   an unengaged reviewer is nudged automatically, a broken gate is not.

### Changes

- **`reference/review-trigger.md`** — the trigger precondition now reads
`pending`, and Engagement
Gate Semantics states the failing case once (a failing gate is not an
engagement signal, is never
a trigger candidate, and reaches the operator through the ordinary
failing-check blocker), so the
  contract is complete rather than merely non-contradictory.
- **`test_review_trigger_race.py`** — a regression class pinning the
chosen behavior. The rollup
makes the gate `StatusContext` the only non-success check and asserts
`ci_gateway_green` and
`non_review_checks_green` are both true, so the failing gate is the
isolated variable rather than
one of several reasons `request_eligible` could be false. It also pins
the `else`-branch
pass-through (`state == "failing"`) and the failing-check bucketing from
point 3 above.
- **No code change.** All three sites named in #324, plus
`pr_queue_snapshot.py`, already read
`pending` and are correct under this decision; a grep of every
`gate_state` comparison confirms
  the enumeration is complete.
- Plugin version bump to `0.26.9` with the matching CHANGELOG entry.

### If you want to override

This is the reversible branch. Should the deployed gate ever be rewired
to report FAILING (not
PENDING) for "no reviewer engaged yet", the fix is to widen the
candidate predicate and define a
failing analogue of `request_signal_pending` with the observed
`target_url` shape in hand — and the
regression class added here is what would need inverting.

## Test plan

Run locally in the worktree:

- `python -m unittest discover` over
`plugins/source-control/skills/babysit-prs/scripts/tests` —
**375 tests, OK** after integrating `main` (includes the 3 new
regression tests).
- `scripts/validate-plugins.sh` — all plugin manifests and the catalog
validated.
- `scripts/check-changelog-parity.sh --check` and `--check-bump
origin/main` — both pass.
- `markdownlint-cli2` on both changed markdown files — 0 errors.

Left to required CI: `scripts/check-changed-skills.sh` and
`scripts/check-skill-portability.sh`,
which run in the `skill-quality-gate` and `portability-lint` jobs
respectively.

## Related

Refs #322 — PR-B (capability convergence); Codex flagged this doc/code
fork inline there.

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

Co-authored-by: Claude Opus 5 (1M context) <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.

1 participant