Skip to content

Fan-out worktrees live in a repo-owned .worktrees.<repo-name> root (0.20.1)#89

Merged
OriNachum merged 2 commits into
mainfrom
docs/worktree-root-namespace
Jul 20, 2026
Merged

Fan-out worktrees live in a repo-owned .worktrees.<repo-name> root (0.20.1)#89
OriNachum merged 2 commits into
mainfrom
docs/worktree-root-namespace

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

What

Pins every worktree an assign-to-workforce fan-out creates to one repo-owned
root beside the repo directory
<parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>
— resolved at runtime, never hardcoded:

repo_root=$(git rev-parse --show-toplevel)
wt_root="$(dirname "$repo_root")/.worktrees.$(basename "$repo_root")"
git worktree add "$wt_root/agent-<task-id>" -b agent/<task-id>

Why

The repo carried two conflicting conventions, and both were hazardous:

Where Old path Failure mode
assign-to-workforce/SKILL.md ../worktrees/agent-<id> In a multi-repo parent the directory belongs to nobody, so it reads as deletable scratch space to any other agent or human cleaning up their own worktrees — mid-wave. And since task ids restart at t1 in every repo and every plan, two concurrent fan-outs resolve to the identical directory.
docs/assign-to-workforce-worked-example.md .claude/worktrees/agent-<id> N full checkouts inside the tree about to be committed: git add -A sweeps them into the PR, git clean -fdx destroys live agent work.

A repo-named root fixes all three: ownership is visible, collisions are
impossible, and nothing inside the repo working tree can reach it.

Cleanup is now scoped too — remove only the worktrees a run created, never
rm -rf the root itself and never another repo's root, since a concurrent
fan-out may be live inside it.

Where it's taught

The rule lands in every place an operator meets it, so no surface can drift:

  • .claude/skills/assign-to-workforce/SKILL.md — fan-out step 1, the removal
    step, a new hard rule, and the worked example.
  • scripts/assign-to-workforce.sh — the split-plan fan-out instructions the
    human reads at gate 2.
  • CLAUDE.md — new "Where worktrees live" section with both forbidden paths
    and why each fails.
  • devague/cli/_commands/learn.py — new worktree_location guidance, plus the
    learn skills authoring recipe.
  • docs/skills.md.

Pinned by test_learn_names_repo_owned_worktree_root, which asserts both the
path and its rationale survive — the reasoning is what stops a future edit
from quietly reverting to a shared directory.

Judgment calls worth reviewing

  • docs/assign-to-workforce-worked-example.md keeps its historical commands.
    It opens with "This document records a real end-to-end run" — rewriting the
    commands would falsify the record. It gains a note pointing at the current
    convention instead. Push back if you'd rather it be updated in place.
  • One pre-existing agex pr lint violation is left alone: a
    user-dotfile-reference on CHANGELOG.md (the ~/.eidetic/memory line,
    line 286 on main). It's a released changelog entry describing a real path;
    editing it would falsify shipped history. Not introduced by this branch.
  • Branch namespacing is still unfixed and out of scope. Worktree
    directories no longer collide, but branches (agent/t1, agent/t2, …)
    still do across plans within one repo, since ids restart per plan — a second
    fan-out hits fatal: a branch named 'agent/t1' already exists. Worth a
    follow-up (agent/<plan-slug>/<task-id>).

Alignment

This changes an origin skill (assign-to-workforce) that guildmaster
re-broadcasts to the mesh, plus CLAUDE.md — so consumers need the updated
skill. agex pr delta couldn't run here (no .claude/skills.local.yaml on
this machine), so the sibling sweep is unverified and should be driven from
guildmaster.

Verification

  • uv run pytest -n auto — 690 passed.
  • flake8 / black / isort clean; markdownlint-cli2 0 errors.
  • agex pr lint — clean except the pre-existing CHANGELOG line above.

Version bumped 0.20.0 → 0.20.1 with a CHANGELOG entry.

  • devague (Claude)

…ktrees.<repo-name> root (0.20.1)

Every worktree a fan-out creates now goes under one repo-owned root beside
the repo directory: <parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>,
resolved at runtime rather than hardcoded.

Two paths that were previously in use are now explicitly forbidden:

- A shared ../worktrees/ (the SKILL.md convention). In a multi-repo parent it
  belongs to nobody, so it reads as deletable scratch space to any other agent
  or human cleaning up; and because task ids restart at t1 in every repo and
  every plan, two concurrent fan-outs resolve to the identical directory.
- Any in-repo path such as .claude/worktrees/ (the worked-example convention),
  where `git add -A` sweeps N agent checkouts into the PR and `git clean -fdx`
  destroys live agent work.

Cleanup removes only the worktrees a run created — never the root itself and
never another repo's root, since a concurrent fan-out may be live inside it.

The rule is taught in every place an operator meets it: the assign-to-workforce
SKILL.md (fan-out, removal, hard rules, worked example), the split-plan
fan-out instructions, CLAUDE.md, docs/skills.md, and `devague learn` (new
worktree_location guidance + the learn skills authoring recipe), pinned by a
test that asserts both the path and its rationale.

docs/assign-to-workforce-worked-example.md records a real past run, so its
historical commands are kept intact and annotated with a note pointing at the
current convention rather than rewritten.

Not addressed here: branch names still collide across plans within a repo
(agent/t1 restarts per plan) — a separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QTCPH6y6vzAAiKDvhfzub
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Namespace fan-out worktrees under repo-owned .worktrees. root

📝 Documentation ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Standardize fan-out worktree paths under /.worktrees./agent-.
• Document forbidden legacy paths and the failure modes they cause.
• Teach and pin the convention via devague learn, tests, and 0.20.1 release notes.
Diagram

graph TD
  O["Operator"] --> G["Docs & skill guidance"] --> R[(".worktrees.<repo-name>")]
  O --> L["devague learn"] --> R
  T["CLI learn test"] --> L --> R
  M["0.20.1 release metadata"] --> G --> R
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. User-scoped root (e.g., ~/.cache/devague/worktrees/)
  • ➕ Keeps worktrees out of multi-repo parent directories entirely
  • ➕ Naturally isolated per user; easier to keep private and consistent across machines
  • ➖ Harder to discover when debugging from inside a repo checkout
  • ➖ Can accumulate stale worktrees across many repos unless tooling adds cleanup
2. Tempdir-based root (mktemp under /tmp or $TMPDIR)
  • ➕ Eliminates cross-repo collisions by construction
  • ➕ Avoids any chance of git add -A sweeping worktrees into a PR
  • ➖ Worktrees become fragile across reboots/cleaners; harder to resume a long-running wave
  • ➖ Less obvious ownership/intent; can look like disposable scratch to system cleanup
3. Configurable root via git config / env var override
  • ➕ Allows teams to standardize on a single convention while supporting special environments
  • ➕ Can preserve the repo-namespaced default while enabling power-user customization
  • ➖ Adds another surface area to document and test (defaults, precedence, validation)
  • ➖ Misconfiguration can reintroduce the exact hazards this PR is eliminating

Recommendation: Keep the PR’s chosen default: /.worktrees./... resolved at runtime. It directly addresses the two concrete historical hazards (shared parent scratch space and in-repo worktrees) while remaining discoverable (sibling of repo) and namespaced (no collisions). Consider adding an optional override later only if a real deployment environment requires it.

Files changed (10) +145 / -17

Enhancement (1) +15 / -2
learn.pyTeach repo-owned worktree root in 'devague learn' output +15/-2

Teach repo-owned worktree root in 'devague learn' output

• Adds a 'worktree_location' guidance block explaining the required path and its rationale, and includes it in the rendered 'learn' output. Updates the assign-to-workforce role description to reflect the repo-owned root requirement.

devague/cli/_commands/learn.py

Tests (1) +19 / -0
test_cli_learn.pyAdd test pinning repo-owned worktree root and rationale in 'learn' +19/-0

Add test pinning repo-owned worktree root and rationale in 'learn'

• Adds a regression test ensuring 'devague learn' mentions '.worktrees.<repo-name>' and preserves the rationale (shared '../worktrees/' collisions and in-repo 'git add -A'/'git clean -fdx' hazards).

tests/test_cli_learn.py

Documentation (6) +109 / -13
SKILL.mdMake '.worktrees.<repo-name>' the mandatory fan-out worktree root +43/-11

Make '.worktrees.<repo-name>' the mandatory fan-out worktree root

• Replaces the legacy '../worktrees/' commands with a runtime-resolved repo-namespaced root beside the repo directory. Adds explicit rationale for forbidding shared and in-repo worktree paths, and tightens cleanup guidance to remove only per-task worktrees.

.claude/skills/assign-to-workforce/SKILL.md

assign-to-workforce.shUpdate split-plan fan-out instructions to point to repo-owned worktree root +3/-1

Update split-plan fan-out instructions to point to repo-owned worktree root

• Adjusts the human-facing printed instructions to specify '<parent>/.worktrees.<repo-name>/agent-<id>' and explicitly disallow '../worktrees/' and in-repo paths.

.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh

CHANGELOG.mdAdd 0.20.1 entry documenting new worktree-root convention +19/-0

Add 0.20.1 entry documenting new worktree-root convention

• Introduces release notes for 0.20.1 describing the mandatory worktree location, the forbidden legacy paths, and the cleanup rules, and notes that the worked example remains historical.

CHANGELOG.md

CLAUDE.mdAdd mandatory worktree-location section with rationale and commands +28/-0

Add mandatory worktree-location section with rationale and commands

• Adds a dedicated section defining '.worktrees.<repo-name>' as the only valid fan-out root, including a runtime-resolved snippet and a clear explanation of why shared and in-repo locations are unsafe.

CLAUDE.md

assign-to-workforce-worked-example.mdAnnotate worked example as historical with updated worktree guidance +10/-0

Annotate worked example as historical with updated worktree guidance

• Adds a note explaining that the recorded run used an older in-repo worktree path and points readers to the current mandatory convention and rationale.

docs/assign-to-workforce-worked-example.md

skills.mdUpdate assign-to-workforce description to require repo-owned worktree root +6/-1

Update assign-to-workforce description to require repo-owned worktree root

• Extends the skill documentation to specify the mandatory repo-namespaced root and explicitly call out why shared '../worktrees/' and in-repo paths are forbidden.

docs/skills.md

Other (2) +2 / -2
pyproject.tomlBump project version to 0.20.1 +1/-1

Bump project version to 0.20.1

• Updates the project version from 0.20.0 to 0.20.1 to align with the documented behavioral guidance change.

pyproject.toml

uv.lockUpdate locked devague version to 0.20.1 +1/-1

Update locked devague version to 0.20.1

• Keeps the lockfile consistent with the project version bump to 0.20.1.

uv.lock

@qodo-code-review

qodo-code-review Bot commented Jul 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 75 rules
✅ Skills: cicd, communicate

Grey Divider


Remediation recommended

1. Undefined repo_root in guidance ✓ Resolved 🐞 Bug ≡ Correctness
Description
devague learn now teaches a wt_root=... example that references $repo_root without first
defining it, so the printed snippet is not self-contained and will typically error or produce an
invalid worktree root when copied. This contradicts the working examples in CLAUDE.md and the
assign-to-workforce skill, which do define repo_root before computing wt_root.
Code

devague/cli/_commands/learn.py[R84-93]

+    "worktree_location": (
+        "Put every worktree under one repo-owned root beside the repo "
+        "directory: <parent-of-repo>/.worktrees.<repo-name>/agent-<task-id> "
+        '(resolve it, don\'t hardcode: wt_root="$(dirname "$repo_root")'
+        '/.worktrees.$(basename "$repo_root")"). A shared ../worktrees/ '
+        "collides across repos and plans (task ids restart at t1) and looks "
+        "deletable to anyone else cleaning up; an in-repo path gets swept "
+        "into your PR by git add -A and destroyed by git clean -fdx. Remove "
+        "only the worktrees you created — never the root itself."
+    ),
Relevance

⭐⭐⭐ High

Team accepts making guidance/examples self-contained/portable (accepted similar doc/learn fixes in
PRs #54, #26).

PR-#54
PR-#26
PR-#35

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new worktree_location guidance includes a wt_root=... example that relies on $repo_root,
but that variable is not defined anywhere in that guidance string. In contrast, the skill and CLAUDE
docs include the missing repo_root=$(git rev-parse --show-toplevel) line immediately before
computing wt_root, demonstrating the intended complete snippet.

devague/cli/_commands/learn.py[84-93]
.claude/skills/assign-to-workforce/SKILL.md[155-159]
CLAUDE.md[236-240]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`devague learn` prints a worktree-location hint that includes a `wt_root="$(dirname "$repo_root")/..."` example but never defines `repo_root`, making the snippet non-self-contained for operators.

## Issue Context
Other canonical docs in this PR show the intended 2-step setup (`repo_root=...` then `wt_root=...`). Align `devague learn` with those examples so the guidance is directly usable.

## Fix Focus Areas
- devague/cli/_commands/learn.py[84-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread devague/cli/_commands/learn.py
`devague learn` printed a wt_root example referencing $repo_root without
defining it. Copied verbatim the snippet does not fail loudly: `dirname ""`
is `.` and `basename ""` is empty, so wt_root silently becomes
"./.worktrees." — a relative, in-repo path with no repo name, i.e. precisely
the layout this guidance exists to forbid.

Teach both steps (repo_root=$(git rev-parse --show-toplevel), then wt_root),
matching the working examples already in CLAUDE.md and the assign-to-workforce
skill, and name the silent-failure mode so the two-step form doesn't read as
ceremony. Pinned by test_learn_worktree_snippet_is_self_contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QTCPH6y6vzAAiKDvhfzub
@OriNachum

Copy link
Copy Markdown
Contributor Author

FIX — valid, and the failure mode is worse than "will typically error."

It fails silently. With repo_root undefined, $(dirname "") is . and $(basename "") is empty, so the snippet resolves to:

wt_root=./.worktrees.

A relative, in-repo path with no repo name — precisely the layout this whole change exists to forbid, since git add -A sweeps it into the PR and git clean -fdx destroys live agent work. An operator copying it gets no error, just the wrong root. That makes this worth more than an alignment nit.

Fixed in 2208e16: devague learn now teaches both steps, matching the working examples already in CLAUDE.md and the assign-to-workforce skill —

repo_root=$(git rev-parse --show-toplevel)
wt_root="$(dirname "$repo_root")/.worktrees.$(basename "$repo_root")"

I also named the silent-failure mode inline in the guidance text, so the two-step form doesn't read as ceremony a future editor might compress back down, and pinned it with test_learn_worktree_snippet_is_self_contained asserting repo_root=$(git rev-parse --show-toplevel) is present wherever wt_root= is taught. 691 tests pass.

  • devague (Claude)

@sonarqubecloud

Copy link
Copy Markdown

@OriNachum
OriNachum merged commit a3c835c into main Jul 20, 2026
8 checks passed
@OriNachum
OriNachum deleted the docs/worktree-root-namespace branch July 20, 2026 15:20
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