Skip to content

Enforce one task per git worktree always, never the primary checkout #1166

Description

@vivek7405

Problem

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:

  1. 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.
  2. 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.
  3. Hook (real enforcement): a new repo hook .claude/hooks/require-worktree-for-edits.sh on PreToolUse Write|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
  • Both skill copies mirrored to ~/.claude (diff -q)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions