Skip to content

feat(dispatch-worktree-automation): I-phase Sub-wave A — WorktreeCreate hook + paired-negative test (Candidate D2) - #279

Merged
artyhoo merged 1 commit into
stagingfrom
worktree-iphase-hook
May 29, 2026
Merged

feat(dispatch-worktree-automation): I-phase Sub-wave A — WorktreeCreate hook + paired-negative test (Candidate D2)#279
artyhoo merged 1 commit into
stagingfrom
worktree-iphase-hook

Conversation

@artyhoo

@artyhoo artyhoo commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

I-phase Sub-wave A of the dispatch-worktree-automation umbrella — implements PR #271 verdict Candidate D2 (ADAPT tfriedel/claude-worktree-hooks precedent). Adds a WorktreeCreate command hook that fires on claude -w <name>, replaces default git behaviour with project-specific setup (worktree under .claude/worktrees/<name>/, branch worktree-<name>, base ref origin/HEAD, node_modules symlinks). Companion paired-negative test asserts the CC command-hook contract verified live this PR via claude --print --worktree probe --settings '{...inline hook...}'. Closes PR #271 §8 ATTN 4 («WorktreeCreate stdin schema NOT published» → schema now empirically verified as {.session_id, .transcript_path, .cwd, .hook_event_name, .name}).

Changes

  • NEW .claude/hooks/worktree-setup.sh (107 LOC) — WorktreeCreate command hook. Behaviour: parse .name from stdin via jq; resolve project root via CLAUDE_PROJECT_DIR env (fallback chain: stdin .cwdgit rev-parse --show-toplevel); resolve base ref via origin/HEADorigin/mainmainHEAD; create worktree at $PROJECT_DIR/.claude/worktrees/<name>/ on branch worktree-<name>; symlink node_modules + packages/core/node_modules from primary checkout; print absolute path on stdout (ONLY thing on stdout per CC contract); idempotent on re-invoke (existing dir → reuse); exit 1 with stderr diagnostic on missing .name / unresolvable root / git worktree add failure (CAN BLOCK creation per CC contract).
  • NEW packages/core/hooks/worktree-setup.test.ts (237 LOC, 10 tests, all green) — 7 positive scenarios (path, both symlinks, branch name, idempotency, single-line stdout, env precedence) + 3 paired-negative (missing .name, empty .name, unresolvable project root). Per-test temporary git repo via mktemp + git init; no real-repo mutation; mirrors inject-matching-rule.test.ts pattern + describe.skipIf(!JQ) graceful jq fallback.
  • MAINTAINER-APPLIED install diff for .claude/settings.json — see §Install below. Deny-listed for agent edit per feedback_settings_json_agent_uncommittable memory → delivered as HEREDOC.

Prior-art consult

Test plan

  • npx vitest run packages/core/hooks/worktree-setup.test.ts10/10 green (positive 7, paired-negative 3)
  • npm run typecheck --workspaces --if-present → clean across @rules-as-tests/core, @rules-as-tests/meta-factory, @rules-as-tests/preset-next-15-canonical
  • npx vitest run packages/core/principles/{02-paired-negative-test,12-ai-laziness-traps,15-skill-paired-negative}.test.ts packages/core/hooks/check-hook-marker.test.ts → 21 passed / 4 skipped, no regression
  • Pre-push hook ran 21 hook test files (153 tests, 4 skipped) — green push proves no regression in scope
  • Smoke-test against synthetic tmp git repo before writing the vitest test — verified path / symlink / idempotent / missing-name failure paths empirically
  • Live stdin-schema probe — invoked claude --print --worktree wtc-schema-probe --settings '{"hooks":{"WorktreeCreate":[{"hooks":[{"type":"command","command":"bash /tmp/probe-wtc.sh"}]}]}}' "exit"; captured stdin payload: {"session_id":"...","transcript_path":"...","cwd":"/Users/art/code/rules-as-tests-aif","hook_event_name":"WorktreeCreate","name":"wtc-schema-probe"}.name field is the primary identifier; probe artefacts cleaned via git worktree remove --force + git branch -D (surgical, per T17 — preserve-before-destructive)
  • MAINTAINER: apply .claude/settings.json HEREDOC patch (§Install below), then run claude -w probe-after-install from a fresh terminal tab to verify hook fires and worktree appears at .claude/worktrees/probe-after-install/. Expected: 1 command + paste replaces the existing ~7-step manual STEP 0 paste-block. Verification: git worktree list shows new entry; symlinks present (ls -la .claude/worktrees/probe-after-install/node_modules)
  • MAINTAINER: after settings.json applied + first dispatch verified, cleanup probe via git worktree remove .claude/worktrees/probe-after-install --force && git branch -D worktree-probe-after-install

Pre-existing staging failures unrelated to this PR (verified by stash + re-run on pristine staging): pre-push.test.ts:55-65 (memory project_warn_only_flip_and_ci_backstop — CI filter masks), check-doc-authority.test.ts boundary cases (3 failures on staging before this PR).

§Install (maintainer-applied — agent-uncommittable per feedback_settings_json_agent_uncommittable)

Apply this patch from the repo root to add the WorktreeCreate event entry alongside the existing UserPromptSubmit / PreToolUse / PostToolUse / Stop hook groups:

python3 <<'PY'
import json, pathlib
p = pathlib.Path('.claude/settings.json')
data = json.loads(p.read_text())
hooks = data.setdefault('hooks', {})
entry = {
    "hooks": [
        {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/worktree-setup.sh\""
        }
    ]
}
existing = hooks.get('WorktreeCreate', [])
if not any(g.get('hooks', [{}])[0].get('command') == entry['hooks'][0]['command'] for g in existing):
    existing.append(entry)
hooks['WorktreeCreate'] = existing
p.write_text(json.dumps(data, indent=2) + '\n')
print('settings.json patched.')
PY

The patched fragment (what the maintainer can also paste manually instead of running the script):

"WorktreeCreate": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/worktree-setup.sh\""
      }
    ]
  }
]

§1.7 Forward-check applied

Sweep across active .claude/rules/ for disciplines that bear on this hook + test:

§1.7 Backward-check applied

Sweep of existing artefacts under this PR's recommendation scope:

No artefact silently superseded. Follow-up scope explicitly listed (.gitignore item 7, SKILL.md / template / placeholders items 1+3+4+5, parallel-subwave-isolation.md item 6, prior-art-evaluations.md item 8 — each is a separate atomic batch per umbrella sequencing).

§Active T-traps (per ai-laziness-traps.md §3 — enumerate + extend, not blanket-reference)

Inherited canonical from PR #271 §6:

  • T3 (file:line per claim) — every behaviour claim cites either the hook source line, the test source line, or the live-probe stdout. ✓
  • T7 (run adversarial counter-prompt) — paired-negative tests execute the actual counter-prompts «what if .name missing? what if root unresolvable?» — confirmed exit non-zero with empty stdout, not asserted. ✓
  • T11 (BFR §3 6-layer) — inherited from PR research(dispatch-worktree-automation): R-phase — ADOPT claude -w + ADAPT WorktreeCreate hook #271 §2.4 6-layer execution; no new candidate introduced. ✓
  • T13 (verify ADOPTED items have upstream evidence) — tfriedel/claude-worktree-hooks DeepWiki returned «not indexed»; fell back to two phrasings WebFetch (GitHub repo page + raw README.md). Both confirm .name jq field + path-only stdout. Pattern adapted (not blind-copied): we customise project-setup step (symlinks vs npm-install) per workspace-optimisation pattern — rationale in hook comments lines 21-24. ✓
  • T15 (self-application) — hook applies to its own future use: once installed, the next sub-wave dispatch in this umbrella (.gitignore follow-up batch, SKILL.md migration batch, etc.) will go through this hook. Recursive self-application live. ✓
  • T17 (preserve before destructive) — probe artefacts (worktree /tmp/wtc-probe-87483, branch wtc-probe-87483) cleaned via git worktree remove --force + git branch -D, surgical only — never bulk-recursive delete. Hook tests use Node rmSync for tmpdir cleanup (not Bash → git-safety hook bypass safe by design). ✓
  • T19 (own cold-QA before handoff) — bash smoke-tested hook end-to-end against synthetic tmp git repo BEFORE writing the vitest test; vitest + typecheck + principle re-run before commit. ✓
  • T20 (verdict backed by evidence) — every behaviour claim in the hook is asserted in the test; live-probe payload is the evidence for the .name field choice. ✓

Domain-specific I-phase traps:

  • T-DWA-IP-A «Field name assumed from precedent without live verification» — counter: live claude --print --worktree probe --settings '{...inline hook...}' ran 2026-05-29 confirming the {.session_id, .transcript_path, .cwd, .hook_event_name, .name} payload shape. Closes PR research(dispatch-worktree-automation): R-phase — ADOPT claude -w + ADAPT WorktreeCreate hook #271 §8 ATTN 4. ✓
  • T-DWA-IP-B «Symlinks claimed without filesystem assertion» — counter: test invokes readlinkSync on both symlink paths (cases «symlinks node_modules» + «symlinks packages/core/node_modules»), asserting both target (not just file presence). Test repo commits packages/core/.keep so git worktree add checks out the directory (production layout reproduction). ✓
  • T-DWA-IP-C «Settings.json self-edit shortcut» — counter: deny-list blocks Edit/Write(.claude/settings.json) at settings.json lines 56-57; PR body §Install delivers HEREDOC Python recipe for maintainer-applied install per feedback_settings_json_agent_uncommittable memory. No agent commit on the settings file. ✓

§Falsifier

Wrong if:

(a) .name stdin field renamed by Anthropic in a future CC release → hook exits 1 with diagnostic missing .name in WorktreeCreate stdin payload; CC reports creation failure; maintainer notices first dispatch attempt failing → re-run probe, update field name. Re-probe procedure documented in §Test plan above.

(b) origin/HEAD resolution fails on a fresh clone before first git fetch → BASE_REF fallback chain catches via origin/mainmainHEAD; worktree still created. Verified by test «valid stdin → creates worktree» on a fresh tmp repo with only local main branch.

(c) Primary checkout has no node_modules (fresh clone before install) → symlink step silently skipped ([[ -e "$PROJECT_DIR/node_modules" ]] guard); worktree still created with bare structure; first npm invocation in worktree triggers fresh install (acceptable degraded path, matches tfriedel original behaviour).

(d) WorktreeCreate hook contract changes from «print path on stdout» to a JSON output → hook continues to print path; would need migration. Mitigation: monitor code.claude.com/docs/en/hooks for contract change; the dual-channel discipline marker (# @dual-pair: if a portable agent equivalent ever exists) is currently not applicable per dual-implementation-discipline.md §3 (CC-only event).

(e) CC silently ignores hook output and uses default behaviour despite hook installation → defeats Sub-wave A intent. Maintainer post-install smoke-test (§Test plan checkbox) catches this; falls back to manual STEP 0 paste-blocks while CC contract issue is filed upstream.

Prior-art: prior-art-evaluations.md#65 (Superpowers using-git-worktrees ADOPT — native-primitive mandate operationalised by this hook).

…te hook + paired-negative test (Candidate D2)

* .claude/hooks/worktree-setup.sh (107 LOC) — WorktreeCreate command hook.
  On `claude -w <name>`: create worktree under .claude/worktrees/<name>/,
  branch `worktree-<name>`, base ref origin/HEAD with fallback chain
  (origin/main → main → HEAD), symlink node_modules + packages/core/node_modules
  from primary checkout (project D2 customisation vs tfriedel's npm install).
  Idempotent, jq-graceful, exit 1 with diagnostic on missing .name /
  unresolvable project root.
* packages/core/hooks/worktree-setup.test.ts (237 LOC) — 10 tests:
  7 positive (path, both symlinks, branch name, idempotency, single-line stdout,
  CLAUDE_PROJECT_DIR precedence) + 3 paired-negative (missing .name,
  empty .name, unresolvable project root). Runs against per-test temporary
  git repo via mktemp + git init; no real-repo mutation.

Stdin schema verified live 2026-05-29 via `claude --print --worktree probe
--settings '{...inline WorktreeCreate hook...}' "exit"` invocation — captured
payload reproduces {.session_id, .transcript_path, .cwd, .hook_event_name,
.name}. Closes PR #271 §8 ATTN 4 («schema NOT published in docs»).

Settings.json install is maintainer-applied per agent-uncommittable invariant
(deny-list blocks Edit/Write on .claude/settings.json) — HEREDOC diff in PR
body §Install.

Out of Sub-wave A scope (separate I-phase edit targets per PR #271 §8):
.gitignore line for .claude/worktrees/ (item 7), SKILL.md / template /
placeholders updates (items 1-5), prior-art-evaluations.md SSOT entries
(item 8). Hook delivers the underlying mechanism; downstream artefacts
migrate in follow-up batches per umbrella sequencing.

Prior-art: prior-art-evaluations.md#65 (Superpowers using-git-worktrees ADOPT — native-primitive mandate; this hook is the WorktreeCreate adapter that operationalises §65's Red Flag #1).
Prior-art: tfriedel/claude-worktree-hooks (ADAPT precedent per PR #271 §3 Candidate D2 — same architecture; we customise project-setup step from npm install to symlinks per meta-kickoff.template.md §4a workspace-optimisation pattern).
artyhoo added a commit that referenced this pull request May 29, 2026
….6 antipattern bug #39886 backstop + template §4a removal (PR #271 §8 item 4)

Extension of commit 193758a per amended kickoff scope. PR #279 hook
(WorktreeCreate `.claude/hooks/worktree-setup.sh`) shipped on its own
branch → unblocks PR #271 §8 item 4 «DELETE template §4a IFF D2 hook
lands first». §4a content is replaced with a brief migration note
pointing to PR #279 + parallel-subwave-isolation.md §1 fallback for
unwired-hook / non-CC harness setups (forward-going annotation per
dual-implementation-discipline.md §9 — not silent deletion).

* SKILL.md §7.6 antipattern (line 353, `#worker-dispatch-via-subagent`):
  add empirical-backstop reference to bug #39886
  (anthropics/claude-code#39886, closed-as-
  duplicate; Agent tool + isolation:"worktree" for WRITE tasks silently
  fails; read-only Agent dispatch remains OK). Applied via
  /tmp/iphase-skill-edit.py (idempotent + reversible recipe; state in
  /tmp/iphase-skill-revert-state.json; revert via
  /tmp/iphase-skill-revert.py).
* meta-kickoff.template.md §4a (was lines 126-148): REMOVED. Replaced
  with one-paragraph migration note: PR #279 hook handles symlinks when
  registered; unwired-hook / non-CC consumers fall back to
  parallel-subwave-isolation.md §1.

Stays out of scope (separate batches per PR #271 §8):
* item 6: parallel-subwave-isolation.md §1 rule edit (add `claude -w`
  preferred channel) — Class-A rule, separate atomic per Artifact
  Ownership Contract.
* item 7: .gitignore for .claude/worktrees/ — separate trivial atomic.
* item 8: 3 new SSOT entries in prior-art-evaluations.md — separate
  append-only batch.

Prior-art: skipped — prose migration following on the capability commit
in PR #279 (which already cited SSOT #65 Superpowers using-git-worktrees
ADOPT + tfriedel ADAPT); no new dep, no new ≥80 LOC file, no new
packages/core subdir. Bug-#39886 is an external evidence citation, not
a new SSOT entry.
@artyhoo
artyhoo merged commit 5278675 into staging May 29, 2026
22 checks passed
artyhoo added a commit that referenced this pull request May 29, 2026
…ceptance test (step count 7→2 verified) (#284)

Acceptance report for the dispatch-worktree-automation I-phase (PR #279 hook +
PR #282 skill prose). Measures kickoff §4 criterion 2 (step count ≤2) on two
real, independent `claude -w` dispatches:

- Primary: THIS session was spawned via `claude -w iphase-acceptance` (2 steps:
  cmd + paste), symlinks live @18:32 — self-applying measurement (T15).
- Secondary: `claude -w aif-handoff-sw-A-resume` for the parked aif-handoff SW-A
  umbrella (1 step, headless), symlinks @18:42, worktree cleaned up after verify.

Verdict: criterion 2 MET (7→2, →1 headless). STOP §10 not triggered (≤2 steps,
hook fired, flag behaved as coded). MINOR finding (§3.2): `claude -w` branches
from a stale `origin/HEAD`, not the live `origin/staging` tip — base-ref-hygiene
follow-up surfaced for worktree-setup.sh; not patched here (C = acceptance only).

CLAUDE.md §See also gains a 1-line pointer to the new dispatch convention
(pointer, not duplication — dual-implementation-discipline §7).

Prior-art: skipped — acceptance report + CLAUDE.md See-also pointer, doc-only, no new capability shipped (measures PR #279/#282 substrate).
artyhoo added a commit that referenced this pull request May 29, 2026
…285)

Retrospective for the dispatch-worktree-automation umbrella (R #271 +
I-phase A #279 / B #282 / C #284). Verdict GO, umbrella DONE.

Authored inside a worktree the umbrella's own WorktreeCreate hook created
(claude -w retro-dispatch-worktree-auto) — recursive self-application
satisfied by environment, not assertion. 4th live dispatch of the
shipped substrate; reproduced the §3.2 base-ref MINOR a 3rd time
(origin/HEAD=bb3ecec, 138 commits behind staging).

Findings carried forward: F1 worktree-setup.sh base-ref hardening
(open follow-up, surfaced not silently patched); F2 VS-Code-tab dispatch
channel DECISION-NEEDED; F3 no discipline-level principle test (deferred
per kickoff §12).

Prior-art: skipped — docs-only retro, no new capability or dependency added.

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

Co-authored-by: Claude <noreply@anthropic.com>
artyhoo added a commit that referenced this pull request Jul 23, 2026
… pre-push preflight (#1101)

* fix(worktree): SSOT provisioning helper + doctor sweep + self-healing pre-push preflight

.claude/hooks/worktree-setup.sh has NEVER been registered: `git log -S'WorktreeCreate' --
.claude/settings.json` is empty and `jq '.hooks.WorktreeCreate'` is null in both the repo and
the user-global settings. The hook shipped in PR #279 with its registration delivered as a
PR-body HEREDOC for manual application; it was never applied, and .claude/settings.json is
agent-uncommittable, so no agent could close the gap. CLAUDE.md:182 nevertheless stated the
hook "auto-creates the worktree + node_modules symlinks". PR #1072 later added a self-heal
block INSIDE that hook for this same incident class — it cannot help, because the hook it
lives in never runs.

Live census: 63 of 125 registered worktrees under .claude/worktrees/ are unprovisioned; 32 of
them hold a real node_modules directory containing nothing but vitest's .vite caches.

Root cause of the unrecoverable state: the principles section of this very hook shells out to
vitest, which materialises node_modules/.vite inside the worktree. The path then EXISTS, so
both channels' `[[ ! -e … ]]` guards are permanently false, and `ln -sfn TARGET node_modules`
against that directory creates node_modules/node_modules — a link nested INSIDE the cache.

Changes:
- scripts/worktree-node-modules.sh (new) — single source of truth. Separates "free to
  replace" (absent | symlink | dir holding only .vite* caches) from "already provisioned"
  (conflating them was a real bug: a correct symlink is both). --check never writes.
- .claude/hooks/worktree-setup.sh + scripts/create-worktree.sh — both now call the helper
  instead of carrying byte-identical copies of the block, which was #sync-by-copy-paste under
  a shared @dual-pair anchor (dual-implementation-discipline.md §7-§8). The hook's early
  "worktree exists → reuse" return now re-provisions before returning.
- scripts/worktree-doctor.sh (new) — operator sweep over `git worktree list`; --fix applies.
- packages/core/hooks/pre-push.ts — `worktree-provisioning` section, FIRST in ALL_SECTIONS so
  the symlinks land before any section can invoke vitest. Self-heals; blocks only when the
  primary itself has no node_modules, and then names the remediation.
- CLAUDE.md — states what is actually true, including that the hook is not registered and how
  to verify it.
- worktree-setup.test.ts + create-worktree.test.ts — fixtures now ship the helper the hooks
  depend on; without it both silently no-op on `|| true` and every symlink assertion fails.

Paired fixture: packages/core/hooks/worktree-node-modules.test.ts. Verified red->green — 4 of
8 arms fail against the pre-fix inline logic (cache-poison heal, dangling-symlink re-point,
unfixable-exit-2, --check-never-writes), 8/8 with the fix. Live: a real vite-poisoned worktree
went cache-dirs -> symlinks with no nesting and tsx reachable again.

Prior-art: prior-art-evaluations.md#87 (portable worktree-creation CLI landscape, verdict BUILD — unchanged). This commit extracts the shared provisioning block of that entry's own dual pair into one home and adds a sweep over it; no new capability area, no new dependency. Last reviewed updated in this commit. Superpowers `using-git-worktrees` (#65, ADOPT) remains the AI-session axis and is untouched.

* chore(install-sh): regenerate npm-stack fingerprints after pre-push.ts edit

The 8 npm-stack baselines carry a content hash of packages/core/hooks/pre-push.ts, which
ships to consumers; adding the worktree-provisioning section changed it. Diff is exactly one
hash line per baseline — the new scripts/worktree-*.sh are maintainer tooling and are NOT in
the shipped fingerprint, which is why the section guards on existsSync(helper) and is tagged
owner: 'maintainer' (composeSections filters it out of a consumer layout entirely).

Pre-push runs test:principles, not the install-sh bash battery, so this surfaced only in CI
shard C — the known regen path (SNAPSHOT_MODE=capture bash tests/install-sh/snapshot.sh).

Prior-art: skipped — mechanical baseline regeneration after a shipped-file edit, no new capability and no new dependency.
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