fix(source-control): claim unlocked worktrees created outside the helper - #3104
Conversation
|
Claude finished @kyle-sexton's task in 2m 7s —— View job Security review — no findings
Reviewed the security-relevant surface of this PR: No security issues found. Specifically checked and confirmed safe:
No |
|
Claude finished @kyle-sexton's task in 5m 59s —— View job Code Review Complete
This PR closes the gap where a plain
Everything else — the never-rewrite guarantee, the |
|
Last security-reviewed head: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a78d3d3a38
ℹ️ 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".
|
Claude has reviewed this PR 5 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
|
Claude finished @cursor[bot]'s task in 1m 37s —— View job Code Review — skipped (no code-relevant changes since last review)
The only commit since the last review ( For completeness, here's where the substantive findings from the prior passes stand — all against unchanged code (
None of these are new — they're carried over from the existing review threads on this PR and none of the touched lines changed in |
|
No description provided. |
|
Taking over from expired work-item-lease held by session |
|
This was generated by AI during triage. Triage: verified — rework needed before mergeChecked out the branch ( Confirmed independently (not just relaying the bot review threads):
Agent BriefType: Bug Current behavior of the diff: Desired behavior:
Key interfaces:
Acceptance criteria:
Out of scope:
Priority set to |
|
Genuine merge conflict, confirmed via Note this PR also overlaps files with sibling PR #3108 ( This session's |
|
🔧 Lane claim — melo-lap-001-merge-20260823T045045Z (conflict-batch coordinator) Claiming this PR to diagnose and attempt conflict resolution ( |
|
Claude finished @kyle-sexton's task in 1m 34s —— View job Security Review — skipped (no new security-relevant changes since last review)
Skip gate hit: condition 3 — "this head already has a successful security review that still applies." The security-relevant files in this PR — Everything that changed between
None of this touches the claim/lock logic, trust boundaries, or hook wiring reviewed previously, so there's no new security-relevant surface to assess. The prior findings (no security issues in the claim/lock mechanism) still stand. Note: the correctness/logic issues raised in code review (path canonicalization in |
|
Claude finished @kyle-sexton's task in 1m 50s —— View job Code Review — skipped (no code-relevant changes since last review)
The only thing between the last-reviewed head ( Since the code itself is unchanged, the substantive findings from the prior passes on this PR still stand, unaddressed, at the same lines on
Not re-filing these as new inline comments — they're already anchored on this thread from the prior passes (Codex and Claude, 2026-08-21) and independently reproduced in the 2026-08-23 triage comment, which also opened an Agent Brief with concrete acceptance criteria for fixing them. Flagging here only so they stay visible against the current head. One thing worth noting for whoever picks up the fix: the 2026-08-23 07:42 comment reports a genuine merge conflict against |
|
Taking over from expired lane-claim held by instance |
495e395 to
efa532e
Compare
efa532e to
6746622
Compare
|
Claude finished @kyle-sexton's task in 2m 44s —— View job Security Review — in progress
|
|
Releasing lane-claim While preparing fixes for the 8 open review threads, the PR head SHA changed mid-task (pinned |
bb71af1 to
7ba0b3e
Compare
|
Claude finished @cursor[bot]'s task in 3m 55s —— View job Code Review Complete
No new findings. This head (
Also checked and clean:
Not independently verified in this sandbox: I could not execute No blocking findings on this head. |
|
Claude finished @cursor[bot]'s task in 3m 1s —— View job Code Review
Skip gate: does not apply cleanly — I independently read (not just diffed)
Also spot-checked the previously-flagged Beyond the five carried-over findings, nothing new stood out in Not independently verified: I could not execute No blocking findings from this pass. The five findings raised by Codex and earlier Claude reviews on this PR all read as correctly addressed at the current head. |
Claim non-helper worktrees with a session-distinct lock reason. The PostToolUse hook claims only the parsed git worktree add target. check-enter canonicalizes relative paths and takes --session-id. Closes #2882 Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
7ba0b3e to
9b5c0ad
Compare
…0.55.6) (#3211) Closes #3131 ## Summary `/source-control:setup`'s babysit-config section warned operators not to uninstall the plugin in order to reconfigure it, citing the `pluginConfigs` entry that an uninstall drops — and stopped there. `--keep-data` appeared nowhere in the plugin (`grep -rn 'keep-data' plugins/source-control/` returned zero hits before this change). That left a real gap for the operator who uninstalls for one of the *other* legitimate reasons — troubleshooting, changing scopes, reinstalling a version. Uninstalling from the last remaining scope deletes `${CLAUDE_PLUGIN_DATA}` by default, and this plugin keeps durable state there. ## Fix One paragraph added to `skills/setup/SKILL.md`'s "Babysit config" section, after the reconfigure bullets. It names the flag, says what the directory holds, and — the part that took the most care — states the resolution rung each worktree root must fall through before it lands there. **What the directory actually holds**, verified in-tree rather than assumed: | Path | Contents | Relocatable? | |---|---|---| | `${CLAUDE_PLUGIN_DATA}/state/babysit-prs` | queue state (`babysit_state.py`, `queue-state.json`), worker leases (`manage_babysit_lease.py`), feedback ledger (`manage_feedback_ledger.py`) | No `userConfig` key relocates it | | `${CLAUDE_PLUGIN_DATA}/worktrees` | babysit worktrees; `/source-control:worktree` trees | Only at the last resolution rung — see below | **"Left unset" is necessary but not sufficient for `worktree_root`.** `scripts/worktree-create.sh:20-32` resolves a root over five rungs, and the plugin option is rung 3, below the target repository's `melodic.worktreeroot` git config at rung 2. So `/source-control:worktree create` lands in the data directory only when *neither* resolves. `babysit_worktree_root` is the simpler case and does fall back there whenever it is unset. Stating only "unset" would have over-warned the operator who uses the recommended per-repo git key, and the paragraph now distinguishes the two. **The emphasis is on the losses that are actually irrecoverable.** Babysit's own worktrees are ephemeral scratch that rebuild from GitHub; the state directory and a `/source-control:worktree` tree holding uncommitted work are not. An earlier draft had this backwards. Follows `docs/conventions/plugin-data-report-keying/README.md` Rule 4 ("state the uninstall fragility where the artifact is the only copy"). ## Verification | Gate | Result | |---|---| | `claude plugin validate plugins/source-control` | Validation passed | | `scripts/check-changed-skills.sh` (setup) | PASS — 0 errors, 1 pre-existing soft-target warning | | `scripts/check-changelog-parity.sh` `--check` / `--check-bump` / `--check-order` / `--check-preserved` | PASS | | `scripts/sync-plugin-options-docs.py --check` | up to date | | `scripts/check-skill-count-claims.sh --check` | PASS | | `scripts/check-skill-leaf-names.sh --check` | PASS | | `scripts/check-cross-plugin-source-drift.sh --check` | PASS | | `markdownlint-cli2` (both changed markdown files) | 0 issues | | `typos` | clean | **Doc claims re-fetched at rung 1, not taken from a repo snapshot.** `curl https://code.claude.com/docs/en/plugins-reference.md` on 2026-08-23, 108305 bytes: *"By default, uninstalling from the last remaining scope also deletes the plugin's `${CLAUDE_PLUGIN_DATA}` directory. Use `--keep-data` to preserve it"*, and the `plugin uninstall` flag table carries `--keep-data` spelled exactly that way. **Rebased twice onto a moving `main`; now at 0.55.6.** `main` landed #3108 during this work, which de-slopped every source-control instruction surface under the repo's zero-tolerance em dash policy and took `0.55.4`; the worktree-claim change from #3104 / #2882 then took `0.55.5`. The added prose is written em dash free to match the rewritten file it lands in (`grep -c '—'` over the changed `SKILL.md` returns 0). Left unrebased, the version collision would have failed `check-changelog-parity.sh` and the prose would have silently reintroduced the marks #3108 had just removed. **Independent review.** A fresh-context reviewer checked the diff against the issue's acceptance criteria with the author's rationale withheld. It raised one blocker (the `worktree_root` conditional was false in both directions — it over-warned past `melodic.worktreeroot` and under-warned by omitting `/worktree create` trees from the consequence) and two should-fixes (state described as "lane and lease state", which misattributes loop-lane telemetry that actually lives in a GitHub tracking issue and survives an uninstall; and a "neither has another copy" claim that contradicted the plugin's own `reference/worktrees.md`). All three are fixed above; the reviewer's nits on length and antecedent are applied too. ## Related - Closes #3131 — F3 from the `/plugin-quality:audit` packet. The other findings in that packet are out of scope here and tracked separately. - Refs #3128 — F1 from the same run, shared with `work-items`. - Refs #3212 — **filed from this work, and materially more dangerous than the gap this PR closes.** `scripts/reap-project-plugin-records.sh:238` runs `claude plugin uninstall "$id" -s project` without `--keep-data`, non-interactively, for every plugin id with a project-scope record keyed to a worktree being torn down (`/source-control:worktree cleanup` Step 4b). For any plugin whose project-scope record was its last remaining scope, that call deletes *that* plugin's `${CLAUDE_PLUGIN_DATA}`. Verified directly against the script. It is other plugins' data and a code change rather than prose, so it sits outside #3131's prose-only acceptance criteria and is tracked on its own rather than folded in here. - **One adjacent surface deliberately left alone.** `plugins/source-control/README.md:337-338` carries a parallel uninstall warning, but it sits inside the `BEGIN GENERATED` block emitted by `scripts/sync-plugin-options-docs.py:100-101` and shared verbatim by every plugin README in the marketplace — a hand-edit is clobbered on the next sync, and editing it here would have been the mistake. Adding `--keep-data` to the generator is a one-line change that fixes roughly forty READMEs at once; it is left out of this PR because it changes every plugin's README rather than source-control's, not because it needs a decision. --- *Generated by [Claude Code](https://claude.ai/code/session_01XtbWChCVfUWAv1Pi5Qk2hA)*

Closes #2882
Summary
worktree-create.shalready armedgit worktree lockon the trees it created. A plaingit worktree adddid not, so concurrent sessions reached into those trees with no claim to read. This adds a check that reports unclaimed linked worktrees, a claim path with a session-distinct lock reason, an entry gate that surfaces a foreign live claim, and a PostToolUse hook that locks only the parsed add target after a Bash add, without rewriting helper-created reason strings.Fix
scripts/worktree-claim.shverbs:report(UNCLAIMED vs CLAIMED),claim/claim --all-unclaimed(session-distinct reason; never rewrites an existing reason;--all-unclaimedis a repair verb and preserves a lock-failure exit status),check-enter(foreign claim → exit 4 with the reason text; unclaimed → exit 3). Relative /./ symlink paths are canonicalized before matching porcelain.hooks/worktree-add-claim-gate.shis the PostToolUse sibling of the containment gate: after a Bashgit worktree addit claims only the parsed add target, composinggit -Cand wrapper chdirs the same way the containment sibling does.echo git worktree addis not a git call. It does not use--all-unclaimed(two concurrent adds must not assign both trees to whichever hook runs first).check-enterpasses--session-id "${CLAUDE_SESSION_ID}"(skill-markdown substitution). The Bash environment does not independently export that id./source-control:worktreestatus, audit, create, and cleanup now name the claim gate. source-control 0.55.4 → 0.55.5.The lock still only prevents
worktree remove/move/prune(git-worktree(1)); its value here is as a claim other agents can read.Verification
From an unrelated cwd (
/tmp), against throwaway fixtures:worktree-claim.test.sh— 52/52. Includescheck-enter .and a relative child path against a foreign claim (exit 4), and--all-unclaimedpreserving a lock-failure status.worktree-add-claim-gate.test.sh— 24/24. Echo does not lock a decoy; a sibling unclaimed tree is not stolen;git -C other-repoclaims the other repo, not the payload-cwd tree.nesting-invariant-ssot.test.sh— 18/18.shellcheck -xon the new scripts — clean.scripts/sync-plugin-options-docs.py --check— up to date.Rebased onto
origin/main(keeps the 0.55.4 de-slop changelog; this ships as 0.55.5).Related
git worktree lockliveness guard this extendslock/--reason/ porcelainlocked [reason]; lock prevents remove/move/prune, not concurrent writes