From 714f92aff91369cec97a2d6b6c7e961c60cef814 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:56:56 -0400 Subject: [PATCH 1/5] docs(source-control): give the nesting invariant one owner, an expiry, and a recorded fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mechanism claim justifying a machine-wide worktree-placement rule enforced by a fail-closed hook was restated as an undated absolute at 13 sites against exactly two dated statements — and the one site that asserted freshness ("It is the live constraint, not a historical one") was itself undated, so a pointer landed the reader precisely there. The fix is not thirteen updated copies. It is one owner, twelve pointers, and a test that fails when a second site states the mechanism. Three further defects in the same paragraph: - Both recheck triggers were structurally incapable of firing. #16600 has not changed state since well before the as-of date, and an event-keyed trigger cannot fire on an opaque release stanza. The stamp gains an unconditional expiry (2.1.244 or 2026-11-07), composed with docs/conventions/upstream-drift rather than a parallel mechanism. - The measurement was disputed, not refuted, and neither the original run nor its counter-reproduction recorded a fixture — so it was unadjudicable while reading as settled. It now names the dispute, carries an arm-by-arm status table (the nested-in-an-unrelated-repo arm is untested and NOT refuted), and ships a probe pinning every discriminator neither run disclosed. The probe is written and has NOT been run; that is stated at the top of the script and in the fixture record, and nothing is claimed on its authority. - "the single-string form silently never fires" contradicts the hooks docs, which document both command forms with no event-specific carve-out — and this plugin's own hooks.json uses the single-string form throughout. Replaced with the documented args rule; the genuinely unprobed question is named as unknown. The "no external prose doc" absolute was false and is now scoped, with an inbound channel so a consumer that measures a contradiction can correct the owner instead of diverging from it silently. Co-Authored-By: Claude Opus 5 (1M context) --- .../source-control/.claude-plugin/plugin.json | 4 +- plugins/source-control/CHANGELOG.md | 68 ++++++++ plugins/source-control/README.md | 2 +- .../hooks/worktree-create-gate.sh | 7 +- .../source-control/scripts/worktree-create.sh | 34 ++-- .../source-control/skills/worktree/SKILL.md | 32 +++- .../skills/worktree/context/create.md | 6 +- .../skills/worktree/fixtures/README.md | 52 ++++++ .../fixtures/nesting-invariant-probe.sh | 165 ++++++++++++++++++ .../worktree/nesting-invariant-ssot.test.sh | 87 +++++++++ 10 files changed, 430 insertions(+), 27 deletions(-) create mode 100755 plugins/source-control/skills/worktree/fixtures/nesting-invariant-probe.sh create mode 100755 plugins/source-control/skills/worktree/nesting-invariant-ssot.test.sh diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 87104dabd..4e35c1aee 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.52.1", - "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only — with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", + "version": "0.53.0", + "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop \u2014 safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only \u2014 with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply \u2014 interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep \u2014 never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", "email": "info@melodicsoftware.com" diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index b0ee77c11..53f2fdb82 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,74 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.53.0] + +### Changed + +- **The nesting-invariant claim has one owner and twelve pointers, instead of 13 undated copies** + (`skills/worktree/SKILL.md`, `skills/worktree/context/create.md`, `scripts/worktree-create.sh`, + `hooks/worktree-create-gate.sh`, `.claude-plugin/plugin.json`, `README.md`; #2213). The mechanism + claim justifying a machine-wide placement rule enforced by a fail-closed hook was restated as an + **undated absolute at 13 sites** against exactly two dated statements — and the one site asserting + freshness ("It is the live constraint, not a historical one") was itself undated, so a pointer + landed the reader precisely there. `SKILL.md` now carries the claim under an explicit + `### The nesting invariant, verified` heading and everything else points at it. Not thirteen + updated copies: one owner, and a test (`skills/worktree/nesting-invariant-ssot.test.sh`) that + fails when a second site states the mechanism, so the next person to explain it in place has to + point instead. + - **Deviation from the filed fix direction, stated so it is not read as an oversight.** The issue + asks pointers to restate the as-of at each pointer site. They do not: twelve restated dates are + twelve drift sites, which is the defect being removed. Pointers instead say the claim is dated + and measured, and name the section that carries the stamp. + - **The two exit-3 heredocs keep a short restatement** alongside their pointer. They are read at + the moment creation fails, when the reader cannot go follow a link; a pointer-only refusal there + would be a regression. Both restatements are deliberately non-causal ("can pick up") rather than + the absolute the rest of the sweep removed. +- **The nesting invariant now carries an unconditional expiry, because both of its event triggers + were structurally unable to fire** (`skills/worktree/SKILL.md`; #2213). The triggers were "a + release note naming worktree rule-file loading" and "upstream #16600 changing state". #16600 has + not changed state since well before the 2026-08-07 as-of date, and an opaque release stanza + ("Bug fixes and reliability improvements", 2.1.226) cannot fire an event-keyed trigger at all — so + the most consequential claim in this plugin was guarded by two triggers that could not go off. The + stamp now adds **2.1.244 or 2026-11-07, whichever comes first**, composed with + `docs/conventions/upstream-drift/` rather than inventing a parallel mechanism. +- **The `SKILL.md` ownership claim is no longer a false absolute, and it gained a back-channel** + (`skills/worktree/SKILL.md`; #2213). "This skill is the canonical owner … — no external prose doc" + was untrue: a consumer doc outside this repository defers mechanism to this skill *and* is more + current than it. Ownership is now scoped to this plugin fleet, and states how a consumer who + measures something contradicting the owner gets that correction back into the owner. Canonical + ownership with no inbound channel makes the owner the last to know. + +### Fixed + +- **The nesting-invariant measurement is downgraded to the modality it actually has, and its fixture + is now recorded** (`skills/worktree/SKILL.md`, `skills/worktree/fixtures/`; #2212). The 2.1.224 + leak measurement was **disputed, not refuted** — a 2.1.227 counter-reproduction did not observe + it — and *neither run recorded its fixture*, so the two results could not be compared and the + claim was not adjudicable. It read as settled anyway. The section now names the dispute, carries + an arm-by-arm status table so a fix to one arm cannot silently weaken another (the + **nested-in-an-unrelated-repo** arm is untested by anyone and **not** refuted — the dispute does + not reach it), and ships `fixtures/nesting-invariant-probe.sh`, which pins every discriminator + neither original run disclosed: creation mechanism, launch mode, the exact `paths:` glob and its + anchoring root, whether the parent's rule file was committed, and the three placements as separate + arms. **The probe is written and has NOT been run** — that is stated at the top of the script and + in `fixtures/README.md`, and nothing is claimed on its authority. It converts a recheck *trigger* + into a recheck *procedure*. +- **The reproduction guidance no longer contradicts the hooks docs** (`skills/worktree/SKILL.md`; + #2212). It claimed the single-string command shape "silently never fires". That is not what + says (raw markdown, fetched 2026-08-11): both command + forms are documented with no event-specific carve-out, and the documented rule is narrower — "Set + `args` whenever the hook references a path placeholder, since each element is passed as one + argument with no quoting." This plugin's own `hooks/hooks.json` registers all three of its hooks + in the single-string form and they fire. The guidance now states the documented rule, and the + genuinely unknown part is named as unknown: whether the single-string form fires for an + `InstructionsLoaded` hook supplied via `claude -p --settings ` is **unprobed by anyone**. +- **The 2.1.224 version basis no longer reads as a release fact** (`skills/worktree/SKILL.md`; + #2212). "which 2.1.224 already handles correctly" sat several sentences from the only "Basis:" + clause and had already been misread as a version fact by two independent readers. The basis is now + inlined at the claim: it is a **null result from the same trace**, not a release note, and the + changelog scan behind it is packet-sourced and has not been re-run. + ## [0.52.1] ### Fixed diff --git a/plugins/source-control/README.md b/plugins/source-control/README.md index 3beca1c14..6fb212828 100644 --- a/plugins/source-control/README.md +++ b/plugins/source-control/README.md @@ -307,7 +307,7 @@ reads it from. | `babysit_advisory_fix_round_cap` | number | `100` | `CLAUDE_PLUGIN_OPTION_BABYSIT_ADVISORY_FIX_ROUND_CAP` | Per-PR cap on advisory-only fix rounds (never caps blocking defects). | | `babysit_worker_concurrency_cap` | number | `10` | `CLAUDE_PLUGIN_OPTION_BABYSIT_WORKER_CONCURRENCY_CAP` | Maximum per-PR workers dispatched concurrently in one cycle. | | `babysit_worktree_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_BABYSIT_WORKTREE_ROOT` | Root directory for babysit-managed ephemeral worktrees. Absent: the worktrees/ subdirectory of the plugin data dir. | -| `worktree_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_WORKTREE_ROOT` | External root under which /worktree create places worktrees, as /-- — a path OUTSIDE every repository (on Windows, the same drive as the repo). Absent: the worktrees/ subdirectory of the plugin data dir, which the skill supplies explicitly rather than reading from the environment (not per-plugin in a Bash-tool subprocess). Deliberately outside the repository tree AND outside repository-discovery roots such as a ghq root, which a checkout-relative default would land inside. Never the in-repo .claude/worktrees/ default: from a worktree nested inside a checkout, a read matching a path-scoped rule's glob also loads the parent checkout's copy of that rule. | +| `worktree_root` | directory | *(none)* | `CLAUDE_PLUGIN_OPTION_WORKTREE_ROOT` | External root under which /worktree create places worktrees, as /-- — a path OUTSIDE every repository (on Windows, the same drive as the repo). Absent: the worktrees/ subdirectory of the plugin data dir, which the skill supplies explicitly rather than reading from the environment (not per-plugin in a Bash-tool subprocess). Deliberately outside the repository tree AND outside repository-discovery roots such as a ghq root, which a checkout-relative default would land inside. Never the in-repo .claude/worktrees/ default, whose nested placement the nesting invariant forbids — that claim is stated, measured, dated and given an expiry in exactly one place: `skills/worktree/SKILL.md` § "The nesting invariant, verified". | | `worktree_stale_days` | number
*min 1* | `14` | `CLAUDE_PLUGIN_OPTION_WORKTREE_STALE_DAYS` | Days since last commit before /worktree status classifies a worktree as stale | | `fetch_logs_max_bytes` | number
*min 1* | `52428800` | `CLAUDE_PLUGIN_OPTION_FETCH_LOGS_MAX_BYTES` | Abort a CI-log ZIP fetch larger than this | | `branch_issue_pattern` | string | *(none)* | `CLAUDE_PLUGIN_OPTION_BRANCH_ISSUE_PATTERN` | POSIX ERE for extracting the numeric GitHub issue number from the current branch name; the LAST capture group holds it and must resolve to digits (Closes #N honors only a numeric issue). Set this for a non-default branch scheme that places the number differently, e.g. '^\[^/\]+/(\[0-9\]+)-' for 'alice/1234-slug' or '-(\[0-9\]+)$' for 'feat/add-widget-1234'. Absent: the built-in '/-' (and routine-issue-) convention. | diff --git a/plugins/source-control/hooks/worktree-create-gate.sh b/plugins/source-control/hooks/worktree-create-gate.sh index d2eb41eb1..bf34c7923 100755 --- a/plugins/source-control/hooks/worktree-create-gate.sh +++ b/plugins/source-control/hooks/worktree-create-gate.sh @@ -5,8 +5,11 @@ # The `/worktree create` skill already routes through scripts/worktree-create.sh, # but three creation paths bypass the skill entirely — `claude --worktree`, a # subagent with `isolation: "worktree"`, and a background session. Those land in -# the in-repo default, where a read matching a path-scoped rule's glob also loads -# the PARENT checkout's copy of that rule. This hook is the seam that covers them, +# the in-repo default — the nested placement the nesting invariant exists to +# avoid; that claim is owned, measured and dated in exactly one place, and this +# comment does not restate it: see +# skills/worktree/SKILL.md § "The nesting invariant, verified". +# This hook is the seam that covers those three paths, # and it is a thin stdin adapter over the same helper so there is one placement # implementation rather than two. # diff --git a/plugins/source-control/scripts/worktree-create.sh b/plugins/source-control/scripts/worktree-create.sh index b8290255e..72ac8b33d 100755 --- a/plugins/source-control/scripts/worktree-create.sh +++ b/plugins/source-control/scripts/worktree-create.sh @@ -20,9 +20,11 @@ # Root-resolution contract: a configured root (--root/--root-file) wins; absent one, # the plugin data directory supplied via --data-root-file yields /worktrees; # absent both, the helper refuses (exit 3). It never falls back to Claude Code's -# in-repo `.claude/worktrees/`: from a worktree nested inside a checkout, a read -# matching a path-scoped rule's glob also loads the PARENT checkout's copy of that -# rule. The data dir is never read from the environment — see the resolution block. +# in-repo `.claude/worktrees/`, whose nested placement is what the nesting +# invariant forbids. That claim is owned, measured and dated in exactly one place +# and is not restated here: skills/worktree/SKILL.md § "The nesting invariant, +# verified". The data dir is never read from the environment — see the resolution +# block. # # Output contract: on success the created worktree path is the SOLE stdout line # (machine-parseable); all diagnostics go to stderr. @@ -59,8 +61,8 @@ Options: re-quoting of the value happens on that path). An empty value or an unexpanded \${user_config.*} token falls through to --data-root-file — never to the in-repo - .claude/worktrees/ default, whose nested placement makes a - read load the parent checkout's path-scoped rules as well. + .claude/worktrees/ default, whose nested placement the + nesting invariant forbids (skills/worktree/SKILL.md). Mutually exclusive with --root-file. --data-root-file Read the plugin's data directory from , used as the @@ -311,9 +313,10 @@ Set the source-control plugin's \`worktree_root\` directory key to an external root (a path OUTSIDE every repository), then retry. Run the worktree setup skill, or configure it via \`/plugin\`. -Not falling back to the in-repo .claude/worktrees/ default: from a worktree -nested inside a checkout, a read matching a path-scoped rule's glob also loads -the PARENT checkout's copy of that rule. +Not falling back to the in-repo .claude/worktrees/ default: a worktree nested +inside a checkout can pick up that checkout's path-scoped rules as well as its +own. Measurement, disputed arms and expiry: +skills/worktree/SKILL.md "The nesting invariant, verified". EOF exit 3 fi @@ -476,10 +479,10 @@ worktree_path=$(normalize_path "$worktree_path") # Reject placement inside any git repository — a working tree, a normal repo's # .git directory, or a bare clone. Keeping worktrees OUT of every repository is -# the helper's core purpose: from a worktree nested inside a working tree, a read -# matching a path-scoped rule's glob also loads the ancestor checkout's copy of -# that rule, and one dropped inside a .git or bare directory mixes the checkout -# into git metadata. The root resolution above does not catch a root explicitly +# the helper's core purpose — see skills/worktree/SKILL.md § "The nesting +# invariant, verified" for the measured claim, and note that a worktree dropped +# inside a .git or bare directory additionally mixes the checkout into git +# metadata, which is a separate and undisputed reason to refuse. The root resolution above does not catch a root explicitly # pointed inside a repository (e.g. the old .claude/worktrees/ path, a root under # a sibling clone, or a path beneath a .git directory), so ask git about the # target's location: walk up from the target's parent to the nearest existing @@ -522,9 +525,10 @@ Set the source-control plugin's \`worktree_root\` directory key to an external root (a path OUTSIDE every repository, on the same drive as the repo on Windows), then retry. -Not creating inside a checkout or a git directory: from there a read matching a -path-scoped rule's glob also loads the enclosing checkout's copy of that rule, -and a git-directory placement mixes the worktree into git metadata. +Not creating inside a checkout or a git directory: from there a worktree can +pick up the enclosing checkout's path-scoped rules as well as its own, and a +git-directory placement mixes the worktree into git metadata. Measurement and +expiry: skills/worktree/SKILL.md "The nesting invariant, verified". EOF exit 3 fi diff --git a/plugins/source-control/skills/worktree/SKILL.md b/plugins/source-control/skills/worktree/SKILL.md index 3cc72ecb1..b42e4c8b0 100644 --- a/plugins/source-control/skills/worktree/SKILL.md +++ b/plugins/source-control/skills/worktree/SKILL.md @@ -51,11 +51,35 @@ Orchestrate git worktree lifecycle from creation through cleanup. **Front-half** **Why this exists:** worktrees are the isolation mechanism for parallel code changes — multiple Claude Code sessions on different tasks without stepping on each other. In repos where branch protection blocks direct commits to main, every feature, fix, or refactor starts with a worktree or branch; this skill makes that seamless. -This skill is the canonical owner of the parallel-session worktree convention going forward — no external prose doc. Worktrees live at an external `worktree_root` (`/--`, outside every repository), defaulting to `/worktrees` when the key is unset, and never nested inside any repository's tree — that nesting invariant is what creation enforces. It is the live constraint, not a historical one: from a worktree nested inside a checkout, a read matching a path-scoped rule's glob also loads the parent checkout's copy of that rule. Keeping `worktree_root` clear of repository-discovery roots (such as a ghq root) is convention, not machine-checked: creation rejects only paths inside an existing repository, so a root you point at a discovery tree still pollutes repository enumeration — `ghq list` reports each worktree as a repository of its own, and a leading dot does not hide it. Choose a configured root accordingly. The default is already clear of this: the plugin data directory is harness state, never a checkout and never inside a discovery tree — which a checkout-relative default would be under a layout like `/github.com//`. +This skill is the canonical owner of the parallel-session worktree convention **for this plugin fleet**, and [§ The nesting invariant, verified](#the-nesting-invariant-verified) is the one site that states the mechanism — every other surface in this plugin points here instead of restating it. That is an ownership claim, not a census: consumer docs outside this repository also describe worktree placement, at least one of them written more recently than the as-of date below. So the ownership comes with an inbound channel — **a consumer that measures something contradicting that section should open an issue on this plugin's tracker so the owner is corrected here.** Canonical ownership with no back-channel just makes the owner the last to know. -**The nesting invariant, verified.** The eager double-load this invariant was originally written against — CLAUDE.md, commands, agents, and rules all loading twice from a nested worktree — was fixed upstream in Claude Code v2.1.69, so that basis no longer holds. What replaces it, measured on 2.1.224: from a session inside a nested worktree, a read matching a `paths:` glob emits one `path_glob_match` naming the **parent** checkout's rule file, loading it alongside the worktree's own copy — both charged at roughly their own size. The same read from an externally-placed worktree emits zero such events. +Worktrees live at an external `worktree_root` (`/--`, outside every repository), defaulting to `/worktrees` when the key is unset, and never nested inside any repository's tree — that nesting invariant is what creation enforces, on the dated measured basis recorded below rather than as a standing absolute. Keeping `worktree_root` clear of repository-discovery roots (such as a ghq root) is convention, not machine-checked: creation rejects only paths inside an existing repository, so a root you point at a discovery tree still pollutes repository enumeration — `ghq list` reports each worktree as a repository of its own, and a leading dot does not hide it. Choose a configured root accordingly. The default is already clear of this: the plugin data directory is harness state, never a checkout and never inside a discovery tree — which a checkout-relative default would be under a layout like `/github.com//`. -**Three control arms narrow what the invariant actually rests on.** The leak is **not** specific to `.claude/worktrees/`: a worktree at a plain non-dot subdirectory leaks identically, so nesting inside the parent's tree is the cause and only placement outside it avoids the leak. A worktree nested inside an **unrelated** repository is worse, not better — it inherits **all three** surfaces, `CLAUDE.md` and unconditional rules at `session_start` as well as scoped rules. The mechanism behind that asymmetry: session-start ancestor traversal is suppressed for ancestors of the worktree's **own** repository but not for a **different** one, while `path_glob_match` discovery is suppressed in **neither**. That suppression rule is what the placement convention rests on, so a change to it is a recheck trigger in its own right. Basis: `InstructionsLoaded` hook trace, which names loaded files rather than inferring them from token deltas; reproduce by registering the hook with the `args`-array command shape (`{"type":"command","command":"","args":["