Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions .claude/skills/assign-to-workforce/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,31 @@ stay consistent if either changes.)

Once the human approves, the main agent fans out each wave in order:

1. **Create an isolated git worktree** for each task in the current wave:
1. **Create an isolated git worktree** for each task in the current wave,
**inside this repo's own worktree root** — `.worktrees.<repo-name>`, a
sibling of the repo directory:

```bash
git worktree add ../worktrees/agent-<task-id> -b agent/<task-id>
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>
```

Never use a bare `../worktrees/` or an in-repo path. The
`.worktrees.<repo-name>` root is mandatory for three reasons:

- **Nobody else will delete it.** A shared `../worktrees/` in a multi-repo
parent directory looks like anyone's scratch space; a directory named
after *your* repo is visibly owned, so another agent or human cleaning up
their own worktrees won't sweep away a live fan-out mid-wave.
- **No cross-repo collision.** Task ids restart at `t1` in every repo and
every plan, so `../worktrees/agent-t1` from two concurrent repos is the
same path. Namespacing by repo name keeps concurrent fan-outs disjoint.
- **The repo working tree stays clean.** An in-repo path (`.worktrees/`,
`.claude/worktrees/`) puts N checkouts inside the tree you are about to
commit and PR — `git add -A` sweeps them in and `git clean -fdx` destroys
them. Outside the repo, neither can touch them.

2. **Spawn a task agent** inside that worktree (using the approved model from
the split plan), with:
- The task id, summary, working instruction, acceptance criteria, and
Expand Down Expand Up @@ -190,9 +209,13 @@ For each completed task worktree, the main agent:
4. **Removes the worktree** once the merge is accepted:

```bash
git worktree remove ../worktrees/agent-<task-id>
git worktree remove "$wt_root/agent-<task-id>"
```

Remove only the worktrees this run created — never `rm -rf` the
`.worktrees.<repo-name>` root itself, and never touch another repo's
worktree root. A concurrent fan-out may be live inside it.

The human does **not** review individual task merges. Per-task acceptance is
the main agent's responsibility — the TDD gate (tests pass before AND after
merge) plus the task's acceptance criteria. This mirrors the non-authoritative
Expand Down Expand Up @@ -242,6 +265,10 @@ These protect the human-gate contract and the TDD guarantee.
contention is managed by isolation, not by trust in the dependency graph.
The dependency graph guarantees *logical* independence within a wave, not
*file* disjointness. Conflicts surface at merge time.
- **All worktrees live under `.worktrees.<repo-name>`.** Every worktree this
skill creates goes in that one repo-owned root beside the repo directory —
never a shared `../worktrees/`, never inside the repo. Clean up only the
worktrees you created; leave the root and anyone else's worktrees alone.
- **Tests before AND after merge — no exceptions.** The TDD gate must pass on
both sides. A merge that makes tests pass only after (not before) means the
baseline was already broken — fix the baseline first.
Expand Down Expand Up @@ -285,22 +312,27 @@ a split-plan
# --- HUMAN: review the table, edit agent/model assignments if needed,
# then say "approved" to proceed ---

# 3. Fan out wave 1 (t1, t2, t3 are independent — run in parallel)
git worktree add ../worktrees/agent-t1 -b agent/t1
git worktree add ../worktrees/agent-t2 -b agent/t2
git worktree add ../worktrees/agent-t3 -b agent/t3
# 3. Fan out wave 1 (t1, t2, t3 are independent — run in parallel).
# All worktrees live under this repo's own root, beside the repo dir:
# e.g. <parent>/devague -> <parent>/.worktrees.devague/
repo_root=$(git rev-parse --show-toplevel)
wt_root="$(dirname "$repo_root")/.worktrees.$(basename "$repo_root")"

git worktree add "$wt_root/agent-t1" -b agent/t1
git worktree add "$wt_root/agent-t2" -b agent/t2
git worktree add "$wt_root/agent-t3" -b agent/t3
# ... spawn task agents in each worktree, await completion ...

# 4. TDD-gated merge for each wave-1 task (no human per task)
git merge --no-ff agent/t1 # tests pass before + after
git worktree remove ../worktrees/agent-t1
git worktree remove "$wt_root/agent-t1"
git merge --no-ff agent/t2
git worktree remove ../worktrees/agent-t2
git worktree remove "$wt_root/agent-t2"
git merge --no-ff agent/t3
git worktree remove ../worktrees/agent-t3
git worktree remove "$wt_root/agent-t3"

# 5. Advance to wave 2 (t4 depends on t1–t3 being merged)
git worktree add ../worktrees/agent-t4 -b agent/t4
git worktree add "$wt_root/agent-t4" -b agent/t4
# ... spawn, await, merge with TDD gate, remove worktree ...

# 6. Open the final PR (human gate 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ print("Go/no-go: review the table above, edit the Model column as needed, then")
print("confirm: \"Approved — assign to workforce\" or \"Edit first\".")
print()
print("Once approved, fan out wave by wave:")
print(" 1. Create one git worktree per task in the wave.")
print(" 1. Create one git worktree per task in the wave, under this repo's own")
print(" root beside the repo dir: <parent>/.worktrees.<repo-name>/agent-<id>")
print(" (never a shared ../worktrees/, never inside the repo).")
print(" 2. Spawn a task agent per worktree — its brief quotes `devague plan")
print(" waves --json`'s summary, instruction, and acceptance criteria for")
print(" that task id verbatim (no paraphrasing).")
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.20.1] - 2026-07-20

### Changed

- **Fan-out worktrees now live in one repo-owned root:
`<parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>`.** The
`assign-to-workforce` skill, `CLAUDE.md`, `docs/skills.md`, the `split-plan`
fan-out instructions, and `devague learn` all teach the same mandatory path
and resolve it at runtime instead of hardcoding it. Two paths that were
previously in use are now explicitly forbidden: a shared `../worktrees/` (in
a multi-repo parent it belongs to nobody, so it reads as deletable scratch
space, and task ids restarting at `t1` in every repo and every plan make
concurrent fan-outs collide on the same directory) and any in-repo path such
as `.claude/worktrees/` (where `git add -A` sweeps agent checkouts into the
PR and `git clean -fdx` destroys live work). Cleanup removes only the
worktrees a run created — never the root itself, which a concurrent fan-out
may be using. `docs/assign-to-workforce-worked-example.md` keeps its
historical commands and gains a note pointing at the current convention.

## [0.20.0] - 2026-07-17

### Added
Expand Down
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,34 @@ Same-file overlaps between tasks (which the dependency graph does not
guarantee to exclude) surface as merge conflicts at reconcile time, never as
live races. The main/operating agent reconciles each merge.

### Where worktrees live: `.worktrees.<repo-name>` (mandatory)

**Every worktree a fan-out creates goes under one repo-owned root beside the
repo directory** — `<parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>`.
For this repo that is `../.worktrees.devague/`. Resolve it, never hardcode it:

```bash
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>
```

Two paths are **forbidden**, and each was in use here before this convention:

- **A shared `../worktrees/`** — in a multi-repo parent like `~/git/`, that
directory belongs to nobody, so it reads as scratch space another agent or
human may delete while your wave is live. Worse, task ids restart at `t1` in
every repo and every plan, so `../worktrees/agent-t1` from two concurrent
fan-outs is literally the same directory. The repo-named root makes
ownership visible and collisions impossible.
- **Anything inside the repo** (`.worktrees/`, `.claude/worktrees/`) — N full
checkouts inside the tree you are about to commit means `git add -A` sweeps
them into the PR and `git clean -fdx` destroys live agent work.

Clean up with `git worktree remove "$wt_root/agent-<task-id>"` per merged task.
Never `rm -rf` the root itself and never touch another repo's root — a
concurrent fan-out may be running inside it.

### Main-agent TDD merge gate (no human per task)

The main agent gates each subagent's worktree merge with test-driven development:
Expand Down
19 changes: 17 additions & 2 deletions devague/cli/_commands/learn.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@
"This keeps file-contention safe: overlapping same-file changes "
"surface as merge conflicts at reconcile time, not live races."
),
"worktree_location": (
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
"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 it — both steps, since an undefined "
'repo_root silently yields the in-repo path "./.worktrees.": '
'repo_root=$(git rev-parse --show-toplevel) then 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."
),
"main_agent_merge_gate": (
"The main agent gates each subagent worktree merge with TDD: tests "
"pass before AND after merge. No human per-task merge decision."
Expand Down Expand Up @@ -204,6 +216,7 @@
f" Prerequisites: {ASSIGN_TO_WORKFORCE_GUIDANCE['prerequisites']}\n"
f" Human gates (3): {ASSIGN_TO_WORKFORCE_GUIDANCE['human_gates']}\n"
f" Safety: {ASSIGN_TO_WORKFORCE_GUIDANCE['worktree_isolation']}\n"
f" Worktree location: {ASSIGN_TO_WORKFORCE_GUIDANCE['worktree_location']}\n"
f" Main agent: {ASSIGN_TO_WORKFORCE_GUIDANCE['main_agent_merge_gate']}\n"
f" TDD: {ASSIGN_TO_WORKFORCE_GUIDANCE['tdd_acceptance_criteria']}\n"
f" Scope: {ASSIGN_TO_WORKFORCE_GUIDANCE['not_orchestration']}\n"
Expand Down Expand Up @@ -339,8 +352,10 @@
"leg": "plan -> parallel implementation",
"role": (
"Reads 'devague plan waves' and fans out one agent per task per wave in "
"isolated git worktrees, with main-agent TDD-gated merges. Three human "
"gates; devague never orchestrates (#20)."
"isolated git worktrees, all under the repo-owned root "
"'<parent-of-repo>/.worktrees.<repo-name>/' (never a shared "
"'../worktrees/', never in-repo), with main-agent TDD-gated merges. "
"Three human gates; devague never orchestrates (#20)."
),
"method_only": False,
},
Expand Down
10 changes: 10 additions & 0 deletions docs/assign-to-workforce-worked-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ git worktree add .claude/worktrees/agent-t4 -b agent/t4
git worktree add .claude/worktrees/agent-t5 -b agent/t5
```

> **Note — the worktree path has since changed.** This run used an in-repo
> `.claude/worktrees/` path, recorded here as it actually happened. The current
> convention (see the `assign-to-workforce` skill and `CLAUDE.md`) puts every
> worktree under one repo-owned root *beside* the repo directory —
> `<parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>` — so that no other
> agent mistakes it for shared scratch, task ids that restart at `t1` can't
> collide across repos, and `git add -A` / `git clean -fdx` can't sweep live
> agent checkouts into a PR or delete them. Follow the skill, not these
> historical commands.

Each subagent received its task id, summary, and acceptance criteria as its
brief. Each was instructed to work **test-first**: write the failing test(s)
matching the acceptance criteria before implementing.
Expand Down
7 changes: 6 additions & 1 deletion docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,12 @@ guidance carried verbatim to the workforce.
Reads `devague plan waves` (deterministic, read-only scheduling metadata) and
fans out independent tasks to **one agent per task per wave in isolated git
worktrees**, with **main-agent TDD-gated merges** (the task's tests pass before
*and* after the merge). Exactly three human gates; the final PR uses the `cicd`
*and* after the merge). Every worktree lives under one repo-owned root beside
the repo directory — `<parent-of-repo>/.worktrees.<repo-name>/agent-<task-id>`
— never a shared `../worktrees/` (which collides across repos and plans, since
task ids restart at `t1`, and reads as deletable scratch space to anyone else)
and never inside the repo (where `git add -A` sweeps checkouts into the PR and
`git clean -fdx` destroys live agent work). Exactly three human gates; the final PR uses the `cicd`
skill (`agex pr open`). Each subagent's brief quotes its task's fields verbatim
from `plan show --json` / the exported plan-md — including the per-task
`instruction` — never an operator paraphrase. The `split-plan` subcommand
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "devague"
version = "0.20.0"
version = "0.20.1"

description = "devague — turns a vague feature idea into a buildable spec, then a buildable plan."
readme = "README.md"
Expand Down
37 changes: 37 additions & 0 deletions tests/test_cli_learn.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,43 @@ def test_learn_documents_assign_to_workforce_invocation(
assert "worktree" in out


def test_learn_names_repo_owned_worktree_root(
capsys: pytest.CaptureFixture[str],
) -> None:
"""`devague learn` teaches where fan-out worktrees live: one repo-owned root
beside the repo directory (`.worktrees.<repo-name>`), never a shared
`../worktrees/` (which collides across repos and reads as deletable scratch)
and never inside the repo (where `git add -A` / `git clean -fdx` reach it).
"""
rc = main(["learn"])
assert rc == 0
out = capsys.readouterr().out
assert ".worktrees.<repo-name>" in out
# The rationale must survive, not just the path: this is why the root is
# repo-owned rather than shared.
lowered = out.lower()
assert "../worktrees/" in lowered
assert "git add -a" in lowered or "in-repo" in lowered


def test_learn_worktree_snippet_is_self_contained(
capsys: pytest.CaptureFixture[str],
) -> None:
"""The taught shell snippet must define `repo_root` before using it (#89).

An operator copies this verbatim. With `repo_root` undefined 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. exactly the layout this guidance forbids.
"""
rc = main(["learn"])
assert rc == 0
out = capsys.readouterr().out
assert "wt_root=" in out, "guidance should teach the wt_root snippet"
# Wherever wt_root is derived from repo_root, repo_root must be defined.
assert "repo_root=$(git rev-parse --show-toplevel)" in out


def test_learn_json_includes_assign_to_workforce_section(
capsys: pytest.CaptureFixture[str],
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading