Git worktree lifecycle for task-isolated execution via the `yaco worktree` CLI — create/reuse, merge up the worktree/branch DAG, clean up. Use when a task carries a `worktree` slug or /orchestrate needs an isolated checkout.
2 stars
0 votes
0 copies
1 view
Added September 2, 2026
ai-agentspythonrustgobashnodegit
Works with
terminal
cli
Security analysis
B84/100
mediumInstalls packages at runtime which could introduce malicious dependencies
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Yaco Worktree?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/imoonkey-yaco-worktree)
---
name: yaco-worktree
description: Git worktree lifecycle for task-isolated execution via the `yaco worktree` CLI — create/reuse, merge up the worktree/branch DAG, clean up. Use when a task carries a `worktree` slug or /orchestrate needs an isolated checkout.
metadata:
yaco-dependent: "true"
---
Operation manual for `yaco worktree` — each worktree is an isolated git checkout (its own working
tree, branch, and git index) keyed by a **slug**. `/orchestrate` drives this lifecycle; tasks
declare a slug through the `worktree` field (see `/yaco-task`). The model is **task DAG ≅
worktree/branch DAG**:
- **1 runnable leaf = 1 worktree = 1 branch.** No shared mutable checkout, no inherited slug.
- **1 integration milestone = 1 integration worktree/branch** — a milestone whose children must
be verified *together* (non-empty `acceptCriteria`) owns a `task/<slug>` tree that children
merge into. A pure grouping milestone owns no tree.
The convention is fixed: `<primary>/.yaco/worktrees/<slug>` on branch `task/<slug>`; `create`
excludes `/.yaco/worktrees/` in the host's `info/exclude`. Pass `--json` on every invocation so output flows
through the `{ok,data}/{ok,error}` envelope.
## CWD resolution
A runnable leaf **always executes in its own worktree** — the slug defaults to the task id; an
explicit `worktree` field only overrides it (and must be unique — no two runnable leaves share a slug):
| slug source | CWD | Branch |
|-------------|-----|--------|
| default | `<repoRoot>/<resolved-worktrees>/<task-id>/` | `task/<task-id>` |
| explicit `worktree: "<slug>"` | `<repoRoot>/<resolved-worktrees>/<slug>/` | `task/<slug>` |
The worktree is created **off its merge-target branch at dispatch** (see Merge up), not always
`main` — so the base already contains every predecessor that has merged up. The **main checkout** is
the orchestrator's home and the →main merge transport, **never a leaf's execution cwd**.
## Create
```bash
worktree_path="$(yaco worktree create <slug> --base <target-branch> --json | jq -r .data.path)"
```
`yaco worktree create <slug> [--base <branch>]` creates the worktree on branch `task/<slug>` off
`--base` (default `main`), provisions the plan (see Provisioning), runs
`scripts/worktree-provision.sh` if present (see Provisioning), and **reuses** an existing
worktree of the same slug. Reuse also repairs a missing plan link without recreating the
worktree. An existing path not registered by git fails closed; create never recursively
deletes it. Without `--json` it prints the path on stdout.
**Cross-repo:** if work spans multiple repos, create a worktree in each repo using the **same
slug**. Each repo manages its own `.yaco/worktrees/` directory independently.
## Merge up
A finished leaf merges **up** the DAG into a **target branch**. The target is determined by one rule:
> **target = the nearest ancestor that owns an integration worktree (non-empty `acceptCriteria`);
> if none, `main`.**
`acceptCriteria` being non-empty is a *field semantic* ("verify the children's combined result"),
not a text heuristic — don't author `"all children done"` rollup boilerplate; leave a pure grouping
milestone's `acceptCriteria` empty so its children ship independently.
**The target branch must exist before a child bases off it.** If the target is an integration
milestone, create/reuse its worktree first — `yaco worktree create <milestone-slug> --base
<parent-target> --json` (idempotent) — then create each child off `task/<milestone-slug>`. If the
target is `main`, children base off `main` directly. This create is part of the per-target serialized
writes (below).
Two transports, one state machine — pick by target type:
| target | transport |
|--------|-----------|
| **integration milestone** (`task/<milestone-slug>`) | **native git** in the target's checkout: `cd <target-worktree> && git merge task/<leaf>` |
| **`main`** | existing `yaco worktree merge <slug>` path (below) |
```bash
yaco worktree merge <slug> --mode pr --json # push branch + open PR
yaco worktree merge <slug> --mode local --json # rebase + fast-forward merge into main
```
- **child→parent uses native git, never `yaco worktree merge`.** `--mode local` is
primary-checkout-centric (`git checkout <base>` + `--ff-only` *in repoRoot*, and refuses a dirty
primary); git also refuses to check out a branch already live in another worktree. It is built for
**→main** only.
- **→main, autonomous local:** `--mode local` requires the **primary checkout parked on a clean
base** (it switches the primary's branch). If you can't guarantee that, use `--mode pr`, or route
leaves through a **root integration branch/worktree** that later merges/PRs to main — don't assume
`main` can be checked out in a second worktree.
- **`pr` opened ≠ integrated** — a leaf is terminal only after its PR **merges**.
- **Serialize writes per target:** one checkout has one writer. Branch-create / merge / resolver on
the *same* target run one at a time; different targets run in parallel.
Native git merges fast-forward when they can; a target advanced by a sibling produces an ordinary
merge commit; a textual conflict triggers the resolver.
## Conflict resolution
A merge-up conflict is decided by **git hunks**, not the `scope` field. The common source is two
leaves with **no `depends`, same target, real edits to the same region** — exactly the overlap this
model parallelizes. A serial chain can't conflict with its own predecessors (a dependent dispatches
only after its predecessor is terminal); different targets don't share a checkout.
When `git merge` conflicts, **dispatch a resolver agent in the target's checkout** (`/yaco-agent`),
default strategy **keep both intents, make both work**:
- **Context** — give the resolver: the incoming leaf's task contract, the contracts of the relevant
already-merged leaves, the `git merge-base` diffs of both sides, the conflicted files, and the
current target diff. Not just conflict markers.
- **Resolver gate** — after resolution, re-run: `/verify` + the incoming leaf's acceptCriteria + the
relevant merged leaves' acceptCriteria + the integration milestone's acceptCriteria (if any) +
affected `/qa` + an independent review of the resolution diff. The evidence-gate is a floor, not a
proof: if an intent has no observable criterion, the resolver must write one or escalate — never let
"green" swallow a dropped intent.
- **Escalate rarely** — 3 rounds with no real progress → set the **triggering leaf** `blocked`,
`blockReason: "merge-conflict"`. Reserve `requireHumanReview`/`blocked` for irreversible/outward
actions, product calls, external credentials, or an unobservable intent.
## Cleanup
```bash
yaco worktree cleanup <slug> --json # safe: refuses an unmerged branch (git branch -d)
yaco worktree cleanup <slug> --force --json # force: git worktree remove --force + git branch -D
```
Cleanup removes the worktree directory **first**, then deletes the branch (`git branch -d`, which
refuses an unmerged branch). The safe path only succeeds when the leaf is merged into the **primary's
HEAD** — i.e. a →main leaf after `--mode local`. A leaf merged into an **integration target** (not
main) isn't an ancestor of main yet, so `git branch -d` refuses: confirm integration with `git
merge-base --is-ancestor task/<leaf> <target-branch>`, then `cleanup --force` (the worktree is clean
post-merge, so force only force-deletes the already-integrated branch) — or simply defer leaf-branch
cleanup until the integration milestone lands in main, when the safe path works again. After `--mode
pr` the branch is unmerged, so leave the worktree on disk until the PR merges, then cleanup.
`--force` discards local state regardless — use it only deliberately.
## Completion
Completion is **per leaf**, not a per-slug batch:
1. **Leaf** — when a leaf passes its gate, **merge it up** into its target (above). Only then is the
leaf terminal (`done`). A merge conflict the resolver can't converge → `blocked`. A `cancelled`
leaf does not merge.
2. **Integration milestone** — when **all** its children are integrated-terminal, run the milestone's
`acceptCriteria` in the integration worktree; on pass, merge the milestone branch **up to its own
target** (the same rule, one level higher). On fail, treat like any gate miss (bounce/resolve;
don't mark the milestone done).
3. **Cleanup** the leaf/milestone worktree once its branch has landed (per Cleanup).
4. **Cross-repo:** merge (and later cleanup) each repo independently under the same slug.
Don't set a milestone parent's state by hand on a merge failure — set the **triggering leaf**
`blocked`; the parent's state derives from its children.
## Provisioning (shared deps)
Before the repository hook, create gives the worktree the plan per its privacy state
(`/yaco-paths`): a private `.yaco/plan` (it has `.git`) is linked from `<worktree>/.yaco/plan` to
the primary's plan; a tracked plan is the branch's own copy and needs nothing. The link target is
relative, so moving the whole repo keeps it valid. For a private plan, an existing directory or
stale link fails with `CONFLICT`; migrate its content and remove it before re-running create.
Never hand-link the plan location, and never recursively remove `<worktree>/.yaco/plan/` with a
trailing slash — that dereferences the link into the shared task graph. Whole-worktree removal and `yaco worktree cleanup` are safe.
`yaco worktree create` runs `<repoRoot>/scripts/worktree-provision.sh` after adding the worktree
**if it exists and is executable** (silently skipped otherwise), with the new worktree path as `$1`
and the worktree as cwd. The hook is a **generic mechanism; which heavy dirs to share is each repo's
policy** — the CLI hardcodes nothing, because shareable state is stack-specific.
**Pattern:** symlink shares **read-mostly heavy deps** (cheap + fast worktree creation); a task that
**mutates** them is not isolated, so it must declare `resources` to serialize against peers.
- **node_modules** (Node): `ln -s <main>/node_modules` per workspace dir. A task running
`npm install <dep>` mutates the shared tree → declare `resources: ["node_modules"]`.
**Not in an npm/pnpm/yarn workspaces monorepo.** The tree holds the workspace self-links
(`@scope/pkg -> ../../pkg`), written *relative*, so sharing it whole makes every
workspace import in the worktree resolve to the **main checkout, on another branch** —
silently, with the suite still green. Share the third-party tree but give the worktree
its own copy of those links; assert it with the resolver, not by reading the symlink.
- **`.venv`** (Python): symlink shares one venv — works, but **not isolated** (a `pip install` leaks)
and a venv is **not relocatable**, so never *copy* it per-worktree; isolate dep changes via
`resources`, not duplication.
- Other stacks follow the same shape: `target/` (Rust), `vendor/` (Go), build caches.
A minimal hook resolves the main checkout from `git worktree list`, then for each heavy dir symlinks
`<main>/<dir>` into the new worktree when the source exists and the destination doesn't. Port/other
runtime isolation belongs in code (e.g. derive a per-worktree port from the path), not the hook.
Whatever it shares, it should end by **asking the toolchain's own resolver** where the
worktree's imports land and failing loudly when the answer is another checkout — a share
that is wrong in one direction produces green runs of the wrong source.
The hook runs from the **main checkout's** copy, so an edit to it reaches new worktrees only
once it lands on the base branch; existing worktrees are repaired by running it in place.