You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The one-task-one-worktree rule is conditional: AGENTS.md:58 ends "A lone agent in a clean checkout may still use a plain branch", ~/.claude/CLAUDE.md carries the same carve-out, and webjs-start-work steps 3 and 4 (SKILL.md L66 to L79) implement only the plain-branch path (git checkout -b in the current checkout). So when no other agent is visibly active, work lands in the primary checkout, and the carve-out's premise is unverifiable: an agent cannot know another session will not start mid-task (this repo runs several a night). The user has directed that a worktree be used ALWAYS.
Enforcement is also currently fictional: AGENTS.md:44 claims .claude/hooks/guard-branch-context.sh enforces the branch rule, but no such file exists in the repo (.claude/hooks/ has ten other scripts; the hook exists only user-globally and in the scaffold template, and it only ASKS on main, with no worktree awareness).
Design / approach
Three parts:
Skill: webjs-start-work steps 3 and 4 become worktree-first: git fetch origin, then git worktree add -b <prefix>/<slug> ../<repo>-<slug> origin/main, push, and ALL work happens in that worktree (a dirty primary no longer blocks starting, since the worktree cuts from origin/main). Point at the AGENTS.md node_modules remedy (dogfood: a fresh git worktree can't resolve @webjsdev/* (no node_modules) #954). Cleanup stays automatic post-merge via cleanup-merged-worktree.sh.
Docs: AGENTS.md retitles the section to unconditional, deletes the lone-agent sentence, updates the step-1 list (L40) from git checkout -b to the worktree recipe, and fixes the L44 hook reference. ~/.claude/CLAUDE.md item 5 gets the same unconditional rule.
Hook (real enforcement): a new repo hook .claude/hooks/require-worktree-for-edits.sh on PreToolUseWrite|Edit|MultiEdit|NotebookEdit that BLOCKS (exit 2) editing a TRACKED file that resides in a repo's PRIMARY checkout, with the worktree recipe on stderr. Untracked and gitignored files stay editable (session state, scratch); env escape WEBJS_NO_WORKTREE_GATE=1 mirrors the other gates.
Implementation notes (for the implementing agent)
Primary-checkout detection, verified empirically: in the primary, git rev-parse --git-dir equals --git-common-dir (both .git); in a linked worktree they differ and --git-dir carries /worktrees/<name>. Cheaper than the git worktree list parse cleanup-merged-worktree.sh:54 uses.
The predicate MUST run against the FILE's directory, not the session cwd (git -C "$(dirname "$fp")"): the harness resets cwd to the primary checkout between commands while edits legitimately target worktree files by absolute path. A cwd-based check would block the correct workflow.
Tracked-file test: git -C <dir> ls-files --error-unmatch <file> (~3ms measured). Tracked → block in primary; untracked or ignored → allow (.claude/settings.local.json is gitignored; note .webjs/vendor/** is tracked on purpose).
Hook contract: exit 0 allow, exit 2 block with the message on stderr (block-raw-htmlelement.sh:20); payload via jq -r '.tool_input.file_path // empty'; guard git rev-parse --is-inside-work-tree like every existing hook; exit 0 when the file is outside any repo.
Wire it in .claude/settings.json alongside the Write|Edit|MultiEdit matcher, and keep the script under the .gitignore allowlist (!.claude/hooks/** already covers it).
Test: test/hooks/require-worktree-for-edits.test.mjs, auto-discovered; copy the JSON-stdin shape from block-raw-htmlelement.test.mjs:9-30 and the temp-repo-plus-linked-worktree setup from cleanup-merged-worktree.test.mjs. Cases: tracked-in-primary blocks (the counterfactual), same file in a linked worktree allows, untracked allows, gitignored allows, env bypass allows, non-repo path allows.
Landmine, found during grounding: .agents/skills/webjs-start-work/SKILL.md is HARDLINKED (same inode) to the .claude copy, and rewrite-style edits (Write/Edit tools, open(w)) break the link, leaving the .agents copy stale. Verify both copies match at head and re-sync before editing; keep them in sync in the commit.
Landmine: webjs-instagram-post SKILL.md L112 to L131 creates its own worktree at ../webjs-ig-social relative to the CURRENT checkout; under worktree-always the session may itself sit in ../<repo>-<slug>, nesting the path one level deeper. Root its recipe at the primary checkout explicitly.
Out of scope: the scaffold template's guard-branch-context.sh (generated APPS are not multi-agent framework repos); the website conventions page mention can note the new hook in the same pass or be left, but say which in the PR body.
Invariants: AGENTS.md invariant 11 on all prose; hooks are bash with jq as an existing dependency.
Acceptance criteria
webjs-start-work steps 3 and 4 create a worktree unconditionally; no plain-branch path remains in the skill
AGENTS.md states the rule unconditionally, drops the lone-agent sentence, and its L44 enforcement claim names a hook that actually exists in the repo
~/.claude/CLAUDE.md item 5 matches
The new hook blocks a tracked-file edit in a primary checkout (proven by a counterfactual test) and allows worktree, untracked, gitignored, bypass, and non-repo cases
The hook judges by the FILE's location, not the session cwd
.agents and .claude copies of the start-work skill are byte-identical at head
Problem
The one-task-one-worktree rule is conditional:
AGENTS.md:58ends "A lone agent in a clean checkout may still use a plain branch",~/.claude/CLAUDE.mdcarries the same carve-out, andwebjs-start-worksteps 3 and 4 (SKILL.md L66 to L79) implement only the plain-branch path (git checkout -bin the current checkout). So when no other agent is visibly active, work lands in the primary checkout, and the carve-out's premise is unverifiable: an agent cannot know another session will not start mid-task (this repo runs several a night). The user has directed that a worktree be used ALWAYS.Enforcement is also currently fictional:
AGENTS.md:44claims.claude/hooks/guard-branch-context.shenforces the branch rule, but no such file exists in the repo (.claude/hooks/has ten other scripts; the hook exists only user-globally and in the scaffold template, and it only ASKS on main, with no worktree awareness).Design / approach
Three parts:
webjs-start-worksteps 3 and 4 become worktree-first:git fetch origin, thengit worktree add -b <prefix>/<slug> ../<repo>-<slug> origin/main, push, and ALL work happens in that worktree (a dirty primary no longer blocks starting, since the worktree cuts from origin/main). Point at theAGENTS.mdnode_modules remedy (dogfood: a fresh git worktree can't resolve @webjsdev/* (no node_modules) #954). Cleanup stays automatic post-merge viacleanup-merged-worktree.sh.AGENTS.mdretitles the section to unconditional, deletes the lone-agent sentence, updates the step-1 list (L40) fromgit checkout -bto the worktree recipe, and fixes the L44 hook reference.~/.claude/CLAUDE.mditem 5 gets the same unconditional rule..claude/hooks/require-worktree-for-edits.shonPreToolUseWrite|Edit|MultiEdit|NotebookEditthat BLOCKS (exit 2) editing a TRACKED file that resides in a repo's PRIMARY checkout, with the worktree recipe on stderr. Untracked and gitignored files stay editable (session state, scratch); env escapeWEBJS_NO_WORKTREE_GATE=1mirrors the other gates.Implementation notes (for the implementing agent)
git rev-parse --git-direquals--git-common-dir(both.git); in a linked worktree they differ and--git-dircarries/worktrees/<name>. Cheaper than thegit worktree listparsecleanup-merged-worktree.sh:54uses.git -C "$(dirname "$fp")"): the harness resets cwd to the primary checkout between commands while edits legitimately target worktree files by absolute path. A cwd-based check would block the correct workflow.git -C <dir> ls-files --error-unmatch <file>(~3ms measured). Tracked → block in primary; untracked or ignored → allow (.claude/settings.local.jsonis gitignored; note.webjs/vendor/**is tracked on purpose).block-raw-htmlelement.sh:20); payload viajq -r '.tool_input.file_path // empty'; guardgit rev-parse --is-inside-work-treelike every existing hook; exit 0 when the file is outside any repo..claude/settings.jsonalongside theWrite|Edit|MultiEditmatcher, and keep the script under the.gitignoreallowlist (!.claude/hooks/**already covers it).test/hooks/require-worktree-for-edits.test.mjs, auto-discovered; copy the JSON-stdin shape fromblock-raw-htmlelement.test.mjs:9-30and the temp-repo-plus-linked-worktree setup fromcleanup-merged-worktree.test.mjs. Cases: tracked-in-primary blocks (the counterfactual), same file in a linked worktree allows, untracked allows, gitignored allows, env bypass allows, non-repo path allows..agents/skills/webjs-start-work/SKILL.mdis HARDLINKED (same inode) to the.claudecopy, and rewrite-style edits (Write/Edit tools,open(w)) break the link, leaving the.agentscopy stale. Verify both copies match at head and re-sync before editing; keep them in sync in the commit.webjs-instagram-postSKILL.md L112 to L131 creates its own worktree at../webjs-ig-socialrelative to the CURRENT checkout; under worktree-always the session may itself sit in../<repo>-<slug>, nesting the path one level deeper. Root its recipe at the primary checkout explicitly.guard-branch-context.sh(generated APPS are not multi-agent framework repos); the websiteconventionspage mention can note the new hook in the same pass or be left, but say which in the PR body.jqas an existing dependency.Acceptance criteria
webjs-start-worksteps 3 and 4 create a worktree unconditionally; no plain-branch path remains in the skillAGENTS.mdstates the rule unconditionally, drops the lone-agent sentence, and its L44 enforcement claim names a hook that actually exists in the repo~/.claude/CLAUDE.mditem 5 matches.agentsand.claudecopies of the start-work skill are byte-identical at head~/.claude(diff -q)