fix(babysit-prs): casefold owner/repo/login compares in babysit scripts - #1329
Conversation
babysit_delta.py's head_repository_scope and babysit_feedback.py's latest_reviews_by_author used .lower() for owner/repo/login identity comparisons where the sibling pr_queue_snapshot.py already casefolds the identical concept. Converts both to .casefold() and adds case-insensitivity regression tests locking in the behavior. Closes #815 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 32s —— View job PR Review
Summary: Minimal, targeted consistency fix. The two stragglers ( CorrectnessAll five changed call sites are correct:
No other TestsThe three new tests cover all the targeted paths:
Minor nit
Version bump and changelog0.26.2 → 0.26.3 is the right semver level (patch: bug fix / consistency). The CHANGELOG entry is clear and links the issue. ✓ Overall: clean, well-scoped change. Ready to merge pending the optional docstring nit. |
|
Claude finished @kyle-sexton's task in 1m 6s —— View job Security Review
No security issues found. This PR makes a targeted normalization change ( Scope of change The two changed call sites are both in security-critical paths:
Why this is safe The PR description correctly states that GitHub owner/repo/login values are restricted to ASCII. For all-ASCII input, Allowlist input ( The No new attack surface introduced
|
…r-repo-login-babysit # Conflicts: # plugins/source-control/CHANGELOG.md
Compose both CHANGELOG sides: main's 0.26.4 gate-separation entry keeps 0.26.4; this branch's casefold entry moves to a new 0.26.5 section, and the plugin manifest bumps to 0.26.5.
|
Claude finished @kyle-sexton's task in 1m 32s —— View job PR Review
Summary: This is a clean, minimal consistency fix. The five Version bumpMain is at CorrectnessAll five changed call sites are correct:
No TestsThree new tests, all well-targeted:
SecurityNo concerns — Overall: clean, well-scoped change. Ready to merge. The missing docstring nit from the previous review round is the only outstanding item, and it's not a blocker. |
|
Claude finished @kyle-sexton's task in 1m 26s —— View job Security Review
No security issues found. This PR makes a single normalization change ( Changed attack surface
Why
|
…ed state (#1331) *This was generated by AI during work-loop execution.* Closes #816 ## Summary `prune_babysit_worktrees.py` had two related robustness gaps, both observed at queue-start prune (#657, sweep-converted into #816, operator-ratified 2026-07-23): 1. A worktree directory left behind by a lock-blocked `git worktree remove` -- git's administrative record dropped from `.git/worktrees/`, the directory itself surviving on disk, most commonly a Windows file lock -- made every subsequent prune run error `fatal: not a git repository` on that entry instead of self-healing. 2. The lock-blocked removal itself gave no signal: the residual directory was left with no report and no cleanup attempt. `git_status` failures now go through `is_missing_repo_error`, which distinguishes "this path is no longer a valid git repository" from every other git failure (permission errors, `gh` network issues, etc. still surface as real errors). An orphaned entry is self-healed by `drop_orphaned_worktree`: it drops the stale worker-lease record for that key (by the time this runs, `main`'s active-lease check has already established it isn't a live/unexpired hold -- the lease-only case with *no* matching directory stays `manage_babysit_lease.py reap`'s job, unchanged) and removes the residual directory only when it is empty and root-contained (`remove_empty_orphan_directory`) -- an orphan's `.git` pointer could be corrupted or gone while real uncommitted work still sits there, so a non-empty orphan is reported, never force-deleted. This is reported via a new `orphan_dropped` row action, and does **not** flip the run's exit code, so the same orphan stops re-erroring every cycle. `remove_worktree` now verifies the directory actually left disk after a *successful* `git worktree remove` (`attempt_directory_removal` -- safe to fully `rmtree` here, since git already confirmed the directory was removable) and reports a still-locked directory via `residual_directory` in the JSON row plus a stderr `WARNING`, instead of leaving a silent orphan for the next run to stumble over (which the `orphan_dropped` path above then self-heals). `source-control` `0.26.2` → `0.26.3` with the matching CHANGELOG entry. ## Test plan Every command below was run from the branch worktree; all passed. - `python -m pytest tests/` in `plugins/source-control/skills/babysit-prs/scripts/` -- **364 passed, 58 subtests passed** (was 358 before this change; 6 new test classes covering the orphan-detection, directory-removal, and self-heal paths, plus 2 new cases added to the existing `RemoveWorktreeIsHermetic` class). - `MissingRepoErrorDetectionTests` -- `is_missing_repo_error` fires on a real `git status` failure against a plain (non-git) directory, and not on an unrelated failure string. - `AttemptDirectoryRemovalTests` -- already-gone path reports `True`; a surviving directory (with content) is fully removed; a removal blocked by a patched `shutil.rmtree` OSError (simulating the Windows-lock scenario portably, since CI runs on `ubuntu-24.04`) reports `False` without raising. - `RemoveEmptyOrphanDirectoryTests` -- removes an empty directory under root; **leaves a non-empty directory's content untouched**; refuses a directory outside root (defense in depth, matching `remove_worktree`'s own containment guard). - `DropOrphanedWorktreeTests` -- drops the lease record and removes an empty orphan directory; is a no-op when no lease record exists; never deletes content from a non-empty orphan directory; refuses to remove an orphan directory outside root. - `RemoveWorktreeIsHermetic` (extended) -- a clean removal reports `residual_directory: False`; a cleanup that stays blocked (via a patched `attempt_directory_removal`, since a real `git worktree remove` already clears the directory in this hermetic test environment) reports `residual_directory: True`. - `MainSelfHealsAnOrphanedWorktreeEntry` -- end-to-end `main()` run against an orphaned directory plus its stale lease record: exit code `0`, `orphan_dropped` action, both the directory and the lease record gone. - `python -m py_compile prune_babysit_worktrees.py tests/test_prune_babysit_worktrees.py` -- exit 0. An advisor review (this repo's own reviewer-in-the-loop discipline) caught two real gaps in the first pass before this PR was opened: (1) an unconditional `rmtree` on the orphan path would have destroyed uncommitted work in a directory git never confirmed removable -- the issue text says *empty* directory, and I'd drifted from that while implementing; fixed by adding `remove_empty_orphan_directory`, gated on emptiness and root-containment, distinct from the unconditional `attempt_directory_removal` used only after a git-confirmed removal. (2) whether a lease-only orphan (no matching directory) needed its own sweep in this script -- confirmed via `grep` that `manage_babysit_lease.py reap` already handles that case independently and already runs adjacent to this script at queue start (`SKILL.md` runbook step 3), so no parallel mechanism was added here. ## Related - Sweep-converted from #657; ratified as `work-class: scoped` (C3) 2026-07-23. - **Version note:** open PR #1329 (`fix(babysit-prs): casefold owner/repo/login compares in babysit scripts`) also bumps `source-control` `0.26.2` → `0.26.3`. Whichever of these two merges second will conflict on the manifest version line and the CHANGELOG insert point -- resolve by rebase, not force-merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
babysit_delta.py'shead_repository_scopeused.lower()for the base-owner, head-owner,same-repository, and configured-owners comparisons;
babysit_feedback.py'slatest_reviews_by_authorused.lower()on the per-reviewer login key. The siblingpr_queue_snapshot.pyalready casefolds the identical owner/repo/login identity concept(
.casefold()), so these two were the last stragglers against that ratified convention..lower()to.casefold(). Functionallyequivalent today (GitHub owner/repo/login values are ASCII-only), but keeps every babysit
script agreeing on one normalization primitive for this concept.
head_repository_scopenow covers a differently-cased base repo,headRepository.nameWithOwner, and allowlisted owner all still resolving as a match (same-repoand cross-repo paths);
latest_reviews_by_authorcovers a differently-cased login (Revvsrev) collapsing to one latest review instead of two.source-controlto 0.26.4 with a matching CHANGELOG entry.Closes #815
Test plan
python -m pytest tests/test_babysit_delta.py tests/test_babysit_feedback.py -q— 89passed, 16 subtests passed
python -m pytest tests/ -q(fullbabysit-prssuite) — 351 passed, 58 subtests passedgrep -n "\.lower(" babysit_delta.py babysit_feedback.py— no matches remainscripts/check-changelog-parity.sh --checkand--check-bump origin/main— both pass afterthe
origin/mainmergeRelated
origin/mainwas merged in to resolve aplugins/source-control/CHANGELOG.mdconflict: main had already released0.26.3for chore(source-control): retire stale ghq-layout-sibling-pr-worktrees doc; plugin owns worktree convention #401, sothis change moved to its own
0.26.4heading and main's0.26.3entry was preserved verbatim.This was generated by AI during work-loop execution.