Clone or create repos for active development — the right hand of /learn. Use when user says "incubate [repo]", "work on [repo]", "clone for dev", or wants to set up a dev workflow. Use --wt SLUG to give each agent its own git worktree body when several agents develop the same repo in parallel. Do NOT trigger for study/exploration (use /learn), finding projects (use /trace), or session mining (use /dig).
SKILL.md
/incubate — Active Development Workflow
Clone or create repos for active development → set up branches, make changes, push PRs.
"/learn reads the book. /incubate writes the next chapter."
Usage
/incubate [url] # Clone via ghq, symlink, ready for dev
/incubate [slug] # Use slug from ψ/memory/slugs.yaml
/incubate [repo-name] # Finds in ghq or creates with default org
/incubate [url] --flash "fix desc" # Issue → branch → fix → PR → offload
/incubate [url] --contribute # Fork if needed → branch per feature → PRs
/incubate --status # List all active ψ/incubate/ with git status
/incubate --status --include-offloaded # Also list offloaded entries from .origins (#280)
/incubate --offload [slug] # Remove symlink, keep ghq clone
/incubate --offload [slug] --purge # Also drop entry from .origins manifest (#280)
/incubate --init # Restore all origins after git clone
# Parallel bodies — one git worktree per agent, same mother clone (#487)
/incubate [slug] --wt <name> # Claim a body on branch incubate/<name>
/incubate [slug] --wt <name> --task "…" # Same, recording who + why (worktree lock reason)
/incubate [slug] --wt <name> --from <ref># Branch from <ref> (default: origin/HEAD)
/incubate --offload [slug] --wt <name> # Retire ONE body (branch survives)
/incubate --offload [slug] --all-wt # Retire every body on this machine
Mother and bodies (#487). One ghq clone per repo per machine = one working
tree = one checked-out branch, so N agents on the same repo stomp each other.
--wt gives each agent a body: a git worktree on its own branch
incubate/<slug>, sharing the mother's object store. The mother clone is
untouched — on the --wt path it is fetch-only, never pulled, never stashed.
The claim is atomic for free: git worktree add -b creates the branch ref in a
single ref transaction, so of N agents racing for the same slug exactly one
wins and the losers touch nothing. No lockfiles, no flock (unreliable on NFS,
meaningless across machines), no claim directory.
$(ghq root)/github.com/OWNER/REPO # mother clone — one per repo per machine
$WT_ROOT/OWNER/REPO/<slug>/ # body — branch incubate/<slug>
WT_ROOT="${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}"
ψ is the committed soul; worktrees are machine-local scratch. Bodies live
outside ψ and outside $(ghq root). ψ gets one gitignored symlink per body
and nothing else.
Bodies must not live under $(ghq root) (#487). ghq decides "this is a
repository" by stat-ing <dir>/.git, and a linked worktree's .git is a
regular file that passes that test. Measured on ghq 1.10.1, with bodies under
$(ghq root)/.worktrees/:
$ ghq list
.worktrees/acme/api/fix-auth ← phantom
github.com/acme/api
Every consumer of ghq list then sees phantom repos, and .worktrees sorts
beforegithub.com, so any "first match wins" resolver returns the body
instead of the repo. That is a regression in other skills caused by this
one, so the body root lives in the XDG state directory. $INCUBATE_WT_ROOT
relocates it; any path outside $(ghq root) works, including a different
filesystem (E17).
The body link filename is literally origin — this is load-bearing. It means
the existing .gitignore rule ψ/incubate/**/origin already covers bodies (no
new rule), wt/<slug>/ contains nothing git tracks (no .gitkeep), and the
--status/--offload finders keep working unmodified.
There is no per-body manifest file. The registry is
git -C <mother> worktree list --porcelain -z, which git maintains atomically
and which cannot drift from reality. A committed .origins.d/<slug>.yaml was
rejected (#487): committed files mean N agents each git add/commit/push into
ψ, converting a benign file race into a git non-fast-forward race one layer up.
Per-body metadata (who, when, why) rides in the worktree lock reason —
machine-local at <mother>/.git/worktrees/<slug>/locked, surviving kill -9,
never committed:
incubate|<owner-id>|<iso8601>|<task text>
Read the registry with --porcelain -z | tr '\0' '\n', never plain
--porcelain (#487). git C-quotes the entirelocked line the moment the
reason holds a non-ASCII byte or a ". Measured on git 2.50.1 with
--task "แก้บั๊ก auth":
Under the quoted form awk -F'|' sees field 1 as "incubate, so every
ownership guard silently misses, the body is misreported as another tool's, and
--offload refuses it forever. -z never quotes (it terminates records with
NUL, so there is nothing to escape) and tr '\0' '\n' restores the exact shape
every existing awk expects. Requires git ≥ 2.36.
Three reason classes, and only three:
field 1
means
do
incubate
ours
parse who | when | task
initializing
git's own lock, written during worktree add and left by a kill -9 mid-checkout (E11)
report as crashed-mid-create, offer the heal, allow offload once unlocked
anything else
another tool's
report as (locked by another tool), never parse, never break
initializing is deliberately not in the "another tool" bucket — treating
it as foreign wedges the body, the slug, and the mother's offload permanently.
The full set of registry queries is in
references/worktree-recovery.md.
Offload source, keep hub:
unlink ψ/incubate/OWNER/REPO/origin # Remove symlink
# ghq clone preserved for future use
# Hub file (REPO.md) remains in ψ/incubate/OWNER/REPO/
/incubate --init
Restore all origins after cloning (like git submodule init):
ROOT="$(pwd)"
GHQ_ROOT=$(ghq root) # hoisted — ~24ms per call (#487)
WT_ROOT="${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}"
# Read .origins through a de-duplicated private copy, NEVER the shared file
# directly (#487). The guarded append at Step 0 can leave a duplicate line
# (measured: 15/30 trials with 4 concurrent runs) and every reader must absorb
# it. Redirected, not piped — a pipe would run the loop in a subshell.
ORIGINS_SNAP=$(mktemp "${TMPDIR:-/tmp}/incubate-origins.XXXXXX")
sort -u "$ROOT/ψ/incubate/.origins" > "$ORIGINS_SNAP"
while IFS= read -r repo; do
[ -z "$repo" ] && continue
OWNER=$(dirname "$repo")
REPO=$(basename "$repo")
ghq get -u "https://github.com/$repo"
MOTHER="$GHQ_ROOT/github.com/$repo"
mkdir -p "$ROOT/ψ/incubate/$OWNER/$REPO"
# ln -sfn, NEVER ln -sf: onto an EXISTING symlink-to-directory, `ln -sf`
# follows the link and creates a stray link INSIDE the old target, leaving
# the ψ link still pointing at the old path. Measured (#487).
ln -sfn "$MOTHER" "$ROOT/ψ/incubate/$OWNER/$REPO/origin"
echo "✓ Restored: $repo"
# Body relink pass (#487). Re-points ψ at any worktrees this machine already
# has. No-ops on a vault with zero bodies — `worktree list` reports only the
# mother — so old vaults behave exactly as before.
# -z: mandatory, see "Directory Structure" — plain --porcelain C-quotes.
# sub(/^worktree /,"") + $0: `print $2` truncates at the first space, and
# `$(ghq root)` on a macOS home directory with a space in its name is
# ordinary. Measured: relinking `/…/my ghq root/…` produced `/…/my` (#487).
git -C "$MOTHER" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n' \
| awk '/^worktree /{sub(/^worktree /,""); print $0}' | while IFS= read -r wt; do
[ "$wt" = "$MOTHER" ] && continue
s=$(basename "$wt")
# Registered but gone from disk (E9) — relinking would manufacture a
# DANGLING ψ symlink and report it as a restored body. Report instead.
[ -d "$wt" ] || { echo " ⋯ body wt/$s registered but gone from disk — /incubate --status"; continue; }
mkdir -p "$ROOT/ψ/incubate/$OWNER/$REPO/wt/$s"
ln -sfn "$wt" "$ROOT/ψ/incubate/$OWNER/$REPO/wt/$s/origin"
echo " ↳ relinked body wt/$s"
done
# Bodies that exist only on ANOTHER machine — report, never auto-materialize.
# A vault with 20 recorded bodies would otherwise detonate 20 `worktree add`
# calls on a fresh clone. The human opts in per body (#487).
git -C "$MOTHER" ls-remote --heads origin 'incubate/*' 2>/dev/null \
| sed 's|.*refs/heads/incubate/||' | while IFS= read -r s; do
[ -z "$s" ] && continue
[ -d "$WT_ROOT/$OWNER/$REPO/$s" ] \
|| echo " ⋯ body '$s' lives on another machine — /incubate $OWNER/$REPO --wt $s"
done
done < "$ORIGINS_SNAP"
rm -f "$ORIGINS_SNAP"
.origins keeps its exact format — flat, one OWNER/REPO per line, committed.
Bodies are never recorded in it, because .origins answers "which mothers
must --init re-clone" and a body's mother is already listed. No migration, no
dual-read, no version marker.
# Replace [URL] with actual URL
URL="[URL]"
ROOT="$(pwd)"
OWNER=$(echo "$URL" | sed -E 's|.*github.com/([^/]+)/.*|\1|')
REPO=$(echo "$URL" | sed -E 's|.*/([^/]+)(\.git)?$|\1|')
SLUG="$OWNER/$REPO"
# Auto-stash unstaged changes in source clone before pulling (#279).
# `ghq get -u` runs `git pull` under the hood and aborts on dirty trees,
# stranding the ritual. Detect + stash with a clear log + restore hint.
GHQ_ROOT_PRECHECK=$(ghq root 2>/dev/null)
SOURCE_PRECHECK="$GHQ_ROOT_PRECHECK/github.com/$SLUG"
if [ -d "$SOURCE_PRECHECK/.git" ]; then
if [ -n "$(git -C "$SOURCE_PRECHECK" status --porcelain 2>/dev/null)" ]; then
STASH_NAME="pre-incubate-$(date +%Y-%m-%d)"
echo "⚠️ Source clone has uncommitted changes — auto-stashing as '$STASH_NAME'"
git -C "$SOURCE_PRECHECK" stash push -u -m "$STASH_NAME"
echo " (run \`git -C $SOURCE_PRECHECK stash pop\` to restore)"
fi
fi
# Check if repo exists on GitHub
if gh repo view "$SLUG" --json name &>/dev/null; then
ghq get -u "https://github.com/$SLUG"
else
echo "Repo not found — creating private repo..."
NEW_REPO_CREATED=1 # our own fresh repo — commit-ignore the breadcrumb (Step 0.5)
gh repo create "$SLUG" --private --clone=false
ghq get "https://github.com/$SLUG"
GHQ_ROOT=$(ghq root)
LOCAL="$GHQ_ROOT/github.com/$SLUG"
# Seed a .gitignore that ignores the incubation breadcrumb from the very first
# commit, so the rule travels with every clone (a local .git/info/exclude does
# not). Safe here — WE just created this private repo; the #447 "don't edit a
# foreign repo's committed .gitignore" rule applies only to CLONES.
grep -qxF '.claude/INCUBATED_BY' "$LOCAL/.gitignore" 2>/dev/null \
|| echo '.claude/INCUBATED_BY' >> "$LOCAL/.gitignore"
[ -f "$LOCAL/README.md" ] || echo "# $REPO" > "$LOCAL/README.md"
git -C "$LOCAL" add README.md .gitignore
git -C "$LOCAL" diff --cached --quiet || git -C "$LOCAL" commit -m "Initial commit"
git -C "$LOCAL" push origin main 2>/dev/null || git -C "$LOCAL" push origin master
fi
GHQ_ROOT=$(ghq root)
mkdir -p "$ROOT/ψ/incubate/$OWNER/$REPO"
# ln -sfn, NEVER ln -sf (#487) — see the comment in --init above.
ln -sfn "$GHQ_ROOT/github.com/$OWNER/$REPO" "$ROOT/ψ/incubate/$OWNER/$REPO/origin"
# Auto-add gitignore pattern if missing (#250)
GITIGNORE="$ROOT/.gitignore"
if [ -f "$GITIGNORE" ]; then
if ! grep -q 'ψ/incubate/\*\*/origin' "$GITIGNORE" 2>/dev/null; then
echo 'ψ/incubate/**/origin' >> "$GITIGNORE"
echo "✓ Added ψ/incubate/**/origin to .gitignore"
fi
else
# Also check ψ/.gitignore as fallback
PSI_GITIGNORE="$ROOT/ψ/.gitignore"
if [ -f "$PSI_GITIGNORE" ] && ! grep -q 'incubate/\*\*/origin' "$PSI_GITIGNORE" 2>/dev/null; then
echo 'incubate/**/origin' >> "$PSI_GITIGNORE"
echo "✓ Added incubate/**/origin to ψ/.gitignore"
fi
fi
# Update manifest — guarded O_APPEND, never read-modify-write (#487).
# The `sort -u -o F F` that lived here re-read and rewrote the whole file:
# measured 8 concurrent writers x 10 trials -> 8/10 trials LOST entries (worst
# kept 5 of 8), and 5 trials emitted `sort: No such file or directory` because
# the file transiently does not exist — so a concurrent --init or --status read
# an EMPTY manifest. A guarded append measured 0/10 lost. Residual TOCTOU can
# only ever produce a DUPLICATE line, never a missing one, and every reader
# absorbs duplicates via `sort -u`. .origins is no longer kept sorted; nothing
# ever consumed its sortedness.
ORIGINS="$ROOT/ψ/incubate/.origins"
mkdir -p "$ROOT/ψ/incubate"
grep -qxF "$OWNER/$REPO" "$ORIGINS" 2>/dev/null || printf '%s\n' "$OWNER/$REPO" >> "$ORIGINS"
echo "✓ Ready: $ROOT/ψ/incubate/$OWNER/$REPO/origin → source"
Step 0.5: Drop INCUBATED_BY Breadcrumb (#226, #228)
After clone/symlink, write .claude/INCUBATED_BY in the target repo (not the oracle repo):
TARGET_REPO="$GHQ_ROOT/github.com/$OWNER/$REPO"
mkdir -p "$TARGET_REPO/.claude"
# Check if this repo was previously /learn'd
LEARNED_FROM=""
if [ -d "$ROOT/ψ/learn/$OWNER/$REPO" ]; then
LEARNED_FROM="learned-from: ψ/learn/$OWNER/$REPO/"
fi
cat > "$TARGET_REPO/.claude/INCUBATED_BY" << BREADCRUMB
oracle: $(basename "$ROOT")
oracle-repo: $(git -C "$ROOT" remote get-url origin 2>/dev/null || echo "local")
date: $(date +%Y-%m-%d)
mode: ${MODE:-default}
source: https://github.com/$OWNER/$REPO
${LEARNED_FROM}
BREADCRUMB
echo "✓ Breadcrumb dropped: $TARGET_REPO/.claude/INCUBATED_BY"
# Keep the breadcrumb OUT of the target repo's tracked history. Pick the mechanism
# by ownership:
# - NEW repo we just created ($NEW_REPO_CREATED): commit-ignore it in .gitignore so
# the rule travels with clones. The initial commit above already staged it.
# - CLONE we don't own (may be public/foreign): use .git/info/exclude — local-only,
# never committed, so we never edit someone else's committed .gitignore (#447).
if [ -d "$TARGET_REPO/.git" ]; then
if [ "${NEW_REPO_CREATED:-0}" = "1" ]; then
grep -qxF '.claude/INCUBATED_BY' "$TARGET_REPO/.gitignore" 2>/dev/null \
|| echo '.claude/INCUBATED_BY' >> "$TARGET_REPO/.gitignore"
echo "✓ Ignored in git: .claude/INCUBATED_BY (committed .gitignore — new repo)"
else
grep -qxF '.claude/INCUBATED_BY' "$TARGET_REPO/.git/info/exclude" 2>/dev/null \
|| echo '.claude/INCUBATED_BY' >> "$TARGET_REPO/.git/info/exclude"
echo "✓ Excluded from git: .claude/INCUBATED_BY (local .git/info/exclude)"
fi
# Either way: if a PRIOR incubation already committed the breadcrumb, an
# exclude/ignore rule won't hide an already-tracked file. Auto-untrack it
# (index-only; the file stays on disk) so "must be ignored" actually holds.
if git -C "$TARGET_REPO" ls-files --error-unmatch .claude/INCUBATED_BY >/dev/null 2>&1; then
git -C "$TARGET_REPO" rm --cached --quiet .claude/INCUBATED_BY
echo "✓ Untracked previously-committed .claude/INCUBATED_BY (git rm --cached)"
fi
fi
The breadcrumb enables:
Orphan detection: Any Claude session can check who tracks this repo
/recap awareness: /recap shows a warning when INCUBATED_BY exists (#229)
The breadcrumb stays out of the target repo's tracked history. Step 0.5 picks the
mechanism by ownership: a committed .gitignore rule for a repo we just created
(so the rule travels with every clone), or a machine-local .git/info/exclude for
a clone we don't own (#447 — never edit a foreign/public repo's committed .gitignore).
Either way it auto-untracks the file if a prior incubation already committed it
(git rm --cached, index-only — the breadcrumb stays on disk). Nothing manual is
left for you to remember.
An incubated repo that writes its own memory (retros, learnings, traces) into a
standalone ψ/ strands that brain — the parent oracle never sees it. Point the
target's ψ at the parent vault so incubated work lands in one shared brain.
# Only if the parent actually has a vault to share
if [ -e "$ROOT/ψ" ]; then
ln -sfn "$ROOT/ψ" "$TARGET_REPO/ψ" # -n: don't descend into an existing symlink
echo "✓ Vault shared: $TARGET_REPO/ψ → $ROOT/ψ"
# Keep it OUT of the target's git history via LOCAL exclude (never the
# committed .gitignore — target may be public; #447 rule).
# GOTCHA (neo, 2026-08-16): a bare symlink `ψ` is NOT matched by `ψ/` — the
# trailing slash only matches a directory. Exclude BOTH forms or the symlink
# shows up as untracked.
if [ -d "$TARGET_REPO/.git" ]; then
for pat in 'ψ' 'ψ/'; do
grep -qxF "$pat" "$TARGET_REPO/.git/info/exclude" 2>/dev/null \
|| echo "$pat" >> "$TARGET_REPO/.git/info/exclude"
done
echo "✓ Excluded from git: ψ and ψ/ (local .git/info/exclude)"
fi
fi
Consequence — the vault is now GLOBAL, not per-repo. Once the target's ψ
symlinks into the parent, incubate/, learn/, and memory/ are the same
directory on disk across every repo that shares that vault. /incubate or
/learn in one shows up for all of them. If the parent vault itself is a
symlink into a private companion repo (e.g. neo-oracle/ψ → neo-oracle-vault),
that's deliberate: it keeps memory out of open-source-bound repos while still
sharing one brain. Don't assume an incubated repo's memory is private to it.
If just a name (no slash, no URL)
Try ghq first, then create with default org:
NAME="[NAME]"
ROOT="$(pwd)"
DEFAULT_ORG="laris-co" # Configurable via --org flag
# Anchor to github.com/ (#487) — ghq enumerates dot-directories and non-GitHub
# hosts alike, so an unanchored match can select a backup tree, a gitlab clone,
# or any stray directory holding a `.git`. Measured on one machine: 54 dot-dir
# entries, 156 non-github.com. (Bodies deliberately live outside $(ghq root)
# entirely, so they never appear here — see "Directory Structure".)
MATCH=$(ghq list | grep '^github\.com/' | grep -i "/$NAME$" | head -1)
if [ -n "$MATCH" ]; then
OWNER=$(echo "$MATCH" | cut -d'/' -f2)
REPO=$(echo "$MATCH" | cut -d'/' -f3)
else
OWNER="$DEFAULT_ORG"
REPO="$NAME"
fi
# Then proceed with URL flow using OWNER/REPO
Verify
ls -la "$ROOT/ψ/incubate/$OWNER/$REPO/"
Step 1: Detect Workflow Mode
Check arguments for workflow flags:
Argument
Mode
Action
(none)
Default
Clone + symlink + show status
--wt <slug>
Body
Claim incubate/<slug> + add worktree + symlink (skip clone — the mother must already exist) (#487)
Steps 0 and 0.5 do not run for --wt, --status or --offload. Read the
mode first, then jump straight to that mode's section. Running Step 0 on the
--wt path would git stash push -u a peer's uncommitted work in the mother
and then ghq get -u (a git pull) underneath them — the exact operation the
--wt section forbids, printing the restore hint into the wrong transcript
(#487). The [ -d "$MOTHER/.git" ] check at Step W2 is the entry gate.
--wtalways takes a value. Bare --wt on create is an error — list the
existing slugs instead of guessing one. The branch is alwaysincubate/<slug>,
never configurable.
Calculate ACTUAL paths (replace variables with real values):
REPO_DIR = [ROOT]/ψ/incubate/[OWNER]/[REPO]/
SOURCE_DIR = [ROOT]/ψ/incubate/[OWNER]/[REPO]/origin/ ← symlink to mother
WORK_DIR = [GHQ_ROOT]/github.com/[OWNER]/[REPO]/ ← mother working dir
BODY_LINK = [ROOT]/ψ/incubate/[OWNER]/[REPO]/wt/[SLUG]/origin ← symlink to body
BODY_DIR = [WT_ROOT]/[OWNER]/[REPO]/[SLUG]/ ← agent working dir
WT_ROOT = ${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}
⚠️ With --wt, BODY_DIR is your working directory, not WORK_DIR. The mother
is fetch-only.
⚠️ IMPORTANT: Always use literal paths. Never pass shell variables to subagents.
Mode: Default (long-term dev)
After Step 0 (clone + symlink), the repo is ready for development.
Mode: --wt (parallel bodies, one per agent) — #487
Use when more than one agent develops the same repo at once. Each agent gets
its own worktree on its own branch; they share the mother's object store and
never touch each other's files.
Step 0 and Step 0.5 do NOT run on this path. No clone, no ghq get -u, no
auto-stash — the mother must already exist, and the [ -d "$MOTHER/.git" ]
check at the top of W2 is the entry gate. Run plain /incubate OWNER/REPO
first if it does not.
Step W1: Validate the slug
Two stages, both needed. git check-ref-formataccepts-lead (which would
be read as an option by the next command) and a/b (which would nest inside
wt/), so the case guard is load-bearing — it runs first.
SLUG="[SLUG]"
case "$SLUG" in
''|-*|*/*|*[!a-zA-Z0-9._-]*)
echo "✗ invalid slug '$SLUG' — [a-zA-Z0-9._-] only, no slashes, no leading dash"
exit 2 ;;
esac
git check-ref-format --branch "incubate/$SLUG" >/dev/null 2>&1 \
|| { echo "✗ invalid slug '$SLUG'"; exit 2; }
Step W2: Claim the body — and own it in the same command
The only atomic primitive is git worktree add. Branch creation is a ref
transaction (O_EXCL + rename(2) inside .git), so 8 concurrent adds of the
same slug yield exactly one rc=0 and seven loud fatals — measured. git worktree add also creates missing nested parent directories itself, so there is no mkdir
to race on either.
The claim and the ownership record must be one command, not two. With
add -b followed by a separate worktree lock, a body is observable unowned
for the ~10–20 ms between them, and a peer running --offload --all-wt in that
window finds an unlocked clean worktree, passes both ownership guards, and
retires it — measured, ✓ wt/<slug> retired while the claiming agent was still
running. git worktree add --lock --reason … -b … closes the window; verified
on git 2.50.1.
ROOT="$(pwd)"
GHQ_ROOT=$(ghq root)
WT_ROOT="${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}"
MOTHER="$GHQ_ROOT/github.com/$OWNER/$REPO"
[ -d "$MOTHER/.git" ] || { echo "✗ no mother clone — run: /incubate $OWNER/$REPO"; exit 1; }
# -z is MANDATORY, never plain --porcelain (#487): git C-quotes the WHOLE
# `locked` line once the reason holds a non-ASCII byte or a `"`, and every
# `awk -F'|'` guard below then silently misses. See "Directory Structure".
wtlist() { git -C "$MOTHER" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n'; }
# FETCH-ONLY mother (#487). NEVER `ghq get -u` on this path: it runs `git pull`,
# which is exactly why the auto-stash at Step 0 exists (#279) — and with N agents
# that stash silently pockets a PEER's uncommitted work, printing the restore
# hint into THIS agent's transcript where the peer will never see it.
# `git fetch` has no working tree to disturb. Measured: 8 concurrent fetches, 8x rc=0.
git -C "$MOTHER" fetch --prune origin >/dev/null 2>&1 || true
# Base the body on the remote default head — never on the mother's current
# checkout, or body #2 silently inherits body #1's work.
# FAIL CLOSED (#487): `symbolic-ref` exits 128 with EMPTY stdout when
# refs/remotes/origin/HEAD is absent, which is the case for every repo Step 0
# creates itself (`gh repo create` + `ghq get` of an empty repo never sets it,
# and neither does `fetch --prune`). The old `${FROM:+…}` then expanded to
# nothing and `worktree add -b` silently based the body on the mother's HEAD —
# measured: a body born on top of an unrelated `--flash` branch, no warning.
if [ -z "$FROM" ]; then
FROM=$(git -C "$MOTHER" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)
fi
if [ -z "$FROM" ]; then
git -C "$MOTHER" remote set-head origin -a >/dev/null 2>&1
FROM=$(git -C "$MOTHER" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)
fi
if [ -z "$FROM" ]; then
if git -C "$MOTHER" rev-parse --verify -q HEAD >/dev/null 2>&1; then
echo "✗ cannot determine origin/HEAD on $OWNER/$REPO — refusing to guess"
echo " basing on the mother's checkout would inherit whatever it has checked out"
echo " → name the base: /incubate $OWNER/$REPO --wt $SLUG --from main"
exit 1
fi
echo "ⓘ mother has no commits yet (E8) — body starts from its unborn HEAD"
fi
WT="$WT_ROOT/$OWNER/$REPO/$SLUG"
BODY="$ROOT/ψ/incubate/$OWNER/$REPO/wt/$SLUG"
ERR=$(mktemp "${TMPDIR:-/tmp}/incubate.XXXXXX")
# The ownership record is computed BEFORE the claim so `add --lock --reason`
# can write both at once. `|` is the delimiter and `"` is what makes git
# C-quote a reason, so neither survives the free-text field. Non-ASCII DOES
# survive — Thai tasks are first class, and `-z` reads them back exactly.
TASK=$(printf '%s' "${TASK:-(unstated)}" | tr '|\n"' '/ ')
OWNER_ID="${INCUBATE_AGENT:-$(id -un)@$(hostname -s)}"
REASON_NEW="incubate|$OWNER_ID|$(date -u +%Y-%m-%dT%H:%M:%SZ)|$TASK"
if wtlist | grep -qxF "worktree $WT" && [ -d "$WT" ]; then
# Already mine (E4). Re-running the identical command is NOT an error.
ACTION="↩ attached"
rm -f "$ERR"
elif wtlist | grep -qxF "worktree $WT"; then
# Registered but the directory is GONE (E9) — the 🧟 state. Testing only the
# registry here used to fall through to W3, which symlinked ψ at a path that
# does not exist and printed `cd <nothing>` as success (#487).
echo "✗ wt/$SLUG is registered but its directory is gone: $WT"
echo " heal: git -C $MOTHER worktree unlock $WT; git -C $MOTHER worktree prune"
echo " then re-run: /incubate $OWNER/$REPO --wt $SLUG"
echo " (branch incubate/$SLUG and every commit on it are untouched)"
rm -f "$ERR"; exit 1
elif [ -e "$WT" ] && [ -n "$(ls -A "$WT" 2>/dev/null)" ]; then
# E5/E10: something occupies the path and git does not own it. This guard MUST
# run before `add -b`: git creates the branch ref BEFORE it validates the path,
# so a failed `-b` LEAVES `incubate/<slug>` behind and wedges the slug forever
# — measured on git 2.50.1, `rc=128` yet `branch --list` shows it (#487).
# An EMPTY directory is harmless crash residue (E6) and falls through.
echo "✗ $WT already exists and git does not own it — nothing was touched"
echo " inspect it first (E10): ls -la $WT"
rm -f "$ERR"; exit 1
elif git -C "$MOTHER" worktree add --lock --reason "$REASON_NEW" \
-b "incubate/$SLUG" "$WT" ${FROM:+"$FROM"} >/dev/null 2>"$ERR"; then
ACTION="🌱 created"
rm -f "$ERR"
elif grep -q "already exists\|cannot lock ref\|already used by worktree" "$ERR"; then
# Lost the race, or the branch outlived a previous body.
# sub(/^worktree /,"")+$0, never $2 — `$2` truncates a path at its first space.
HOLDER=$(wtlist | awk -v b="branch refs/heads/incubate/$SLUG" \
'/^worktree /{sub(/^worktree /,""); p=$0; next} $0==b{print p; exit}')
if [ -n "$HOLDER" ]; then
# `f=0` on every `worktree` line and `exit` at the record boundary: without
# them a holder carrying no `locked` line leaks the NEXT body's reason, and
# the collision message names the wrong human and the wrong task (#487).
REASON=$(wtlist | awk -v b="branch refs/heads/incubate/$SLUG" \
'/^worktree /{f=0} $0==b{f=1} f&&/^locked /{sub(/^locked /,"");print;exit} f&&/^$/{exit}')
WHO=$(printf '%s' "$REASON" | awk -F'|' '$1=="incubate"{print $2" · "$3}')
WHY=$(printf '%s' "$REASON" | awk -F'|' '$1=="incubate"{print $4}')
if [ "$REASON" = "initializing" ]; then
WHO="(git's own lock — crashed mid-create, E11)"
WHY="git -C $MOTHER worktree unlock $HOLDER, inspect, then retry"
elif [ -n "$REASON" ] && [ -z "$WHO" ]; then
WHO="(locked by another tool)"; WHY="—"
fi
echo "✗ Slug '$SLUG' is already claimed on $OWNER/$REPO"
echo " by: ${WHO:-(unlocked)}"
echo " task: ${WHY:-(unstated)}"
echo " tree: $HOLDER"
echo " → join it: cd $HOLDER"
echo " → or branch it: /incubate $OWNER/$REPO --wt ${SLUG}-2 --task \"...\""
rm -f "$ERR"; exit 1
fi
# Branch exists but no worktree holds it — a previously offloaded body. Resume
# it. Capture THIS attempt's stderr (2>"$ERR", never 2>&1): reporting the
# stale `-b` error told operators the BRANCH was the blocker when the real
# cause was the directory, and deleting the branch did not help (#487).
if ! git -C "$MOTHER" worktree add --lock --reason "$REASON_NEW" \
"$WT" "incubate/$SLUG" >/dev/null 2>"$ERR"; then
cat "$ERR"; rm -f "$ERR"; exit 1
fi
ACTION="♻ revived"
rm -f "$ERR"
else
cat "$ERR"; rm -f "$ERR"; exit 1
fi
A slug collision is the normal outcome of N agents racing — it is a routing
decision, not a crash. That is why the message names the holder, the task, the
tree, and the two commands that resolve it.
Step W3: Reconcile ownership, then link ψ
The lock reason is machine-local, it survives kill -9, it blocks an accidental
worktree remove, and it does not block commits inside the body. A lock
protects only its own entry — pruning unrelated worktrees still works (measured).
W2 already wrote it on the create and revive paths. W3 only handles the
↩ attached case, where a claim already exists and must never be
overwritten: re-attaching to your own body keeps the ORIGINAL owner and
timestamp, and a lock written by another tool is left completely alone.
wtlist() { git -C "$MOTHER" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n'; }
EXISTING=$(wtlist | awk -v w="worktree $WT" \
'$0==w{f=1;next} f&&/^locked /{sub(/^locked /,"");print;exit} f&&/^$/{exit}')
if [ -z "$EXISTING" ]; then
# Unowned — a crash between an old `add` and its `lock`, or a peer's unlock.
# Adopt it; do not leave a body nobody owns.
git -C "$MOTHER" worktree lock --reason "$REASON_NEW" "$WT" 2>/dev/null
elif [ "$EXISTING" = "initializing" ]; then
# git's OWN lock, left by a kill -9 mid-checkout (E11) — not a foreign tool.
echo "⚠ wt/$SLUG was created but never finished (git's 'initializing' lock, E11)"
echo " the tree is half-populated. Heal, then re-run:"
echo " git -C $WT status"
echo " git -C $WT checkout -- ."
echo " git -C $MOTHER worktree unlock $WT"
exit 1
else
TASK=$(printf '%s' "$EXISTING" | awk -F'|' '$1=="incubate"{print $4}')
TASK="${TASK:-(locked by another tool)}"
fi
mkdir -p "$BODY"
ln -sfn "$WT" "$BODY/origin" # -sfn, NEVER -sf (see Anti-Patterns)
echo "${ACTION:-↩ attached} body wt/$SLUG → $WT"
echo " branch: incubate/$SLUG · task: $TASK"
echo " cd $WT"
.origins is not touched, and Step 2 does not run for --wt — the
mother is already recorded, the body's record is its lock reason, and no new
committed file is created anywhere. That is the whole point: N agents never
contend on the vault's git index.
Step W4: The one discipline
The mother's .git is genuinely shared. Concurrent fetch is safe (measured), but
git gc, git worktree prune, and git branch -D reach every sibling body.
Your worktree, your branch. No git operation outside it.
Cross-machine slugs are not coordinated: two agents on two machines can each
create incubate/fix-auth from different bases and only discover it at push time.
The namespace is per-mother, per-machine. Known limitation (#487).
PR_URL=$(gh pr create --repo "$OWNER/$REPO" \
--title "[PR title]" \
--body "$(cat <<'EOF'
## Summary
[what was fixed]
Closes #$ISSUE_NUM
---
**From**: [Oracle Name]
Rule 6: "Oracle Never Pretends to Be Human"
Written by an Oracle — AI speaking as itself.
EOF
)" --head "$BRANCH")
PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$')
echo "PR: #$PR_NUM (closes #$ISSUE_NUM)"
Step F5: Auto-offload + purge
cd "$ROOT"
MOTHER="$(ghq root)/github.com/$OWNER/$REPO"
wtlist() { git -C "$MOTHER" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n'; }
# REFUSE to purge the mother while bodies live (#487). Every body's `.git` is a
# pointer file into the mother's admin dir, and the body's COMMITS live in the
# mother's object store — so deleting the mother destroys every unpushed commit
# on every `incubate/*` branch. Measured: after `rm -rf $MOTHER` and a fresh
# re-clone, `git cat-file -t <body-commit>` → `could not get object info`, and
# `branch --list 'incubate/*'` is empty. **This guard is the only protection
# there is.** Restoring the mother does NOT heal the bodies; only PUSHED
# branches survive it.
LIVE=$(wtlist | grep -c '^worktree ')
if [ "${LIVE:-0}" -gt 1 ]; then
echo "✗ Refusing to purge $OWNER/$REPO — $((LIVE - 1)) body/bodies still live:"
wtlist | awk '/^worktree /{sub(/^worktree /,""); print " "$0}' | tail -n +2
echo " → retire them first: /incubate --offload $OWNER/$REPO --all-wt"
exit 1
fi
# Unpushed work on a body branch dies with the mother even when no worktree
# holds it any more. Never purge over the top of it.
UNPUSHED=$(git -C "$MOTHER" log --oneline --branches='incubate/*' --not --remotes 2>/dev/null | head -5)
if [ -n "$UNPUSHED" ]; then
echo "✗ Refusing to purge $OWNER/$REPO — unpushed commits on incubate/* branches:"
printf ' %s\n' "$UNPUSHED"
echo " → push them first, or accept the loss explicitly by deleting $MOTHER by hand"
exit 1
fi
unlink "$ROOT/ψ/incubate/$OWNER/$REPO/origin"
rmdir "$ROOT/ψ/incubate/$OWNER" 2>/dev/null
# Rename FIRST, then delete (#487). `rm -rf` on a large clone takes seconds, and
# a peer's `--wt` only checks `[ -d "$MOTHER/.git" ]` — anywhere in that window
# it passes, then creates a branch ref and a partial checkout inside a tree being
# deleted. `mv` is atomic and fails every peer's check instantly. Re-check the
# body count against the renamed path before the point of no return.
PURGING="$MOTHER.purging.$$"
mv "$MOTHER" "$PURGING"
LIVE=$(git -C "$PURGING" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n' | grep -c '^worktree ')
if [ "${LIVE:-0}" -gt 1 ]; then
mv "$PURGING" "$MOTHER"
echo "✗ A body was claimed while purging — mother restored, nothing deleted"
exit 1
fi
rm -rf "$PURGING"
# --flash owns the whole cycle, so it purges the manifest entry too. Same
# compare-and-swap as `--offload --purge`; see that section for why.
ORIGINS="$ROOT/ψ/incubate/.origins"
if [ -f "$ORIGINS" ]; then
I=0
while [ "$I" -lt 10 ]; do
I=$((I + 1)); SIZE=$(wc -c < "$ORIGINS" | tr -d ' ')
TMPFILE=$(mktemp "$ROOT/ψ/incubate/.origins.XXXXXX")
grep -vxF "$OWNER/$REPO" "$ORIGINS" > "$TMPFILE" || true
chmod 644 "$TMPFILE"
if [ "$(wc -c < "$ORIGINS" | tr -d ' ')" = "$SIZE" ]; then
mv "$TMPFILE" "$ORIGINS"; break
fi
rm -f "$TMPFILE"
done
fi
echo "✓ Issue #$ISSUE_NUM → PR #$PR_NUM → Offloaded & Purged"
Update hub file before offload (Step 2), then offload.
Mode: --contribute (multi-feature contribution)
For extended contribution over days/weeks. Forks if needed.
Step C1: Fork if not your repo
WORK_DIR="$ROOT/ψ/incubate/$OWNER/$REPO/origin"
ME=$(gh api user --jq '.login')
if ! gh repo view "$OWNER/$REPO" --json viewerPermission --jq '.viewerPermission' | grep -qE 'ADMIN|MAINTAIN|WRITE'; then
echo "No push access — forking..."
gh repo fork "$OWNER/$REPO" --clone=false
git -C "$WORK_DIR" remote add fork "https://github.com/$ME/$REPO.git"
echo "Fork remote added. Push to 'fork' instead of 'origin'."
fi
No clone needed. Lists every mother, every body under it, and everything that
needs attention. This is the only window a human has at 3am, so it names the
branch, the dirt, the owner, the task, and the absolute path.
Add --include-offloaded to also list entries in .origins whose symlinks
have been removed (#280) — surfaces the historical record without losing it.
State is carried by glyph shape, never colour: ● dirty, ○ clean,
⚠ needs attention, 🧟 registered-but-gone.
ROOT="$(pwd)"
INCLUDE_OFFLOADED="${1:-}" # pass "--include-offloaded" to enable
GHQ_ROOT=$(ghq root 2>/dev/null) # hoisted out of every loop — ~24ms per call (#487)
WT_ROOT="${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}"
# -z is MANDATORY, never plain --porcelain (#487) — see "Directory Structure".
wtlist() { git -C "$1" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n'; }
echo "🌱 Incubations"
# Stock /bin/bash on macOS is 3.2.57 and has NO associative arrays: `declare -A`
# there makes `${ACTIVE[acme/api]}` an ARITHMETIC subscript and aborts with
# `division by 0 (error token is "b")`, rc=1 — `--status --include-offloaded`
# was hard-broken on stock bash and only appeared to work under Homebrew bash.
# Sets now live in temp files queried with `grep -qxF` (#487).
LINKS=$(mktemp "${TMPDIR:-/tmp}/incubate-links.XXXXXX")
ACTIVE=$(mktemp "${TMPDIR:-/tmp}/incubate-active.XXXXXX")
PORC=$(mktemp "${TMPDIR:-/tmp}/incubate-porc.XXXXXX")
# `find | sort`, never `for x in $(find)` — that word-splits paths on spaces.
# Sorting full paths also makes every mother precede its own bodies, always:
# all strings sharing a prefix form one contiguous block under lexical sort.
find "$ROOT/ψ/incubate" -name origin -type l -print 2>/dev/null | sort > "$LINKS"
# ISO8601 → "2h ago"; BSD `date -j -f` first, then GNU `date -d`, else raw stamp.
incubate_age() {
[ -n "$1" ] || return 0
_e=$(date -j -u -f '%Y-%m-%dT%H:%M:%SZ' "$1" +%s 2>/dev/null \
|| date -u -d "$1" +%s 2>/dev/null) || { printf '%s' "$1"; return 0; }
_d=$(( $(date -u +%s) - _e ))
[ "$_d" -lt 0 ] && _d=0 # clocks drift across a fleet; never print "-7h ago"
if [ "$_d" -lt 3600 ]; then printf '%sm ago' "$((_d/60))"
elif [ "$_d" -lt 86400 ]; then printf '%sh ago' "$((_d/3600))"
else printf '%sd ago' "$((_d/86400))"; fi
}
REPOS=0; BODIES=0; LIVE=0; DIRTY=0; LASTBODY=""; PORC_FOR=""
# Redirected, NOT piped — a piped `while` runs in a subshell and loses counters.
while IFS= read -r link; do
DIR=$(dirname "$link")
SLUG=${DIR#"$ROOT/ψ/incubate/"} # pure shell — a `sed` here would treat
TARGET=$(readlink "$link") # regex metachars in $ROOT as syntax
case "$SLUG" in
*/wt/*)
# ---- body ----
NAME=${SLUG##*/wt/}
PARENT=${SLUG%%/wt/*}
# Derive the parent from the body's OWN path (#487). The loop used to
# carry $PORC forward from whichever mother rendered last, so a body whose
# mother has no ψ link was drawn as a child of an unrelated repo with its
# owner and task blanked — measured: acme/api's orphan body rendered under
# acme/api-gateway. Sorted full paths put a mother immediately before its
# own bodies, but only when the mother's link exists.
if [ "$PARENT" != "$PORC_FOR" ]; then
if [ -L "$ROOT/ψ/incubate/$PARENT/origin" ]; then
wtlist "$ROOT/ψ/incubate/$PARENT/origin" > "$PORC"
PORC_FOR="$PARENT"
else
BODIES=$((BODIES + 1))
echo " ⚠ orphan body wt/$NAME — mother $PARENT is not incubated"
echo " $TARGET"
echo " heal: /incubate $PARENT (or: /incubate --offload $PARENT --all-wt)"
continue
fi
fi
BODIES=$((BODIES + 1))
if [ "$link" = "$LASTBODY" ]; then TEE="└─"; CONT=" "; else TEE="├─"; CONT="│ "; fi
if [ -d "$TARGET" ]; then
LIVE=$((LIVE + 1))
BRANCH=$(git -C "$TARGET" branch --show-current 2>/dev/null)
N=$(git -C "$TARGET" status --short 2>/dev/null | wc -l | tr -d ' ')
if [ "$N" -gt 0 ]; then STATE="● dirty $N"; DIRTY=$((DIRTY + 1)); else STATE="○ clean"; fi
else
BRANCH="?"; STATE="⚠ missing"
fi
# Owner + task come from the worktree lock reason cached in $PORC.
REASON=$(awk -v w="worktree $TARGET" '$0==w{f=1;next} f&&/^locked /{sub(/^locked /,"");print;exit} f&&/^$/{exit}' "$PORC")
WHO=$(printf '%s' "$REASON" | awk -F'|' '$1=="incubate"{print $2}')
WHEN=$(printf '%s' "$REASON" | awk -F'|' '$1=="incubate"{print $3}')
TASK=$(printf '%s' "$REASON" | awk -F'|' '$1=="incubate"{print $4}')
if [ "$REASON" = "initializing" ]; then
# git's OWN lock (E11), not a foreign tool's — name it as such.
WHO="⚠ crashed mid-create — git's own lock (E11)"; TASK="(unfinished)"
elif [ -n "$REASON" ] && [ -z "$WHO" ]; then
WHO="(locked by another tool)"
fi
printf ' %s %-16s %-11s %-22s %s\n' "$TEE" "wt/$NAME" "$STATE" "${BRANCH:-?}" "\"${TASK:-(unstated)}\""
printf ' %s %-16s %s · %s · %s\n' "$CONT" "" "${WHO:-(unlocked)}" "$(incubate_age "$WHEN")" "$TARGET"
;;
*)
# ---- mother ----
REPOS=$((REPOS + 1))
printf '\n %-34s origin ' "$SLUG"
if [ -d "$TARGET" ]; then
BRANCH=$(git -C "$TARGET" branch --show-current 2>/dev/null)
N=$(git -C "$TARGET" status --short 2>/dev/null | wc -l | tr -d ' ')
if [ "$N" -gt 0 ]; then STATE="● dirty $N"; DIRTY=$((DIRTY + 1)); else STATE="○ clean"; fi
echo "✓ ${BRANCH:-(detached)} · $STATE · $TARGET"
wtlist "$TARGET" > "$PORC"
else
echo "✗ broken → $TARGET"
: > "$PORC"
fi
PORC_FOR="$SLUG"
printf '%s\n' "$SLUG" >> "$ACTIVE"
# index()==1 is a literal prefix test — `grep "^$DIR/wt/"` would read any
# regex metachar in the path as syntax.
LASTBODY=$(awk -v p="$DIR/wt/" 'index($0,p)==1' "$LINKS" | tail -1)
;;
esac
done < "$LINKS"
echo ""
# Orphans — REPORT ONLY, never removed (Rule 6: Nothing is Deleted).
while IFS= read -r rslug; do
M="$GHQ_ROOT/github.com/$rslug"
[ -d "$M/.git" ] || continue
wtlist "$M" > "$PORC"
# Registered but gone from disk. Do NOT grep for `prunable` alone: a LOCKED
# worktree is never labelled prunable and `git worktree prune` skips it — and
# /incubate locks every body, so that is the COMMON case, not the rare one
# (measured, #487). Test the directory instead, and emit the heal command that
# actually works for each case.
# sub(/^worktree /,"")+$0, never $2 — `$2` truncates a path at its first space,
# and then EVERY live body reads as gone-from-disk (measured on a ghq root
# containing a space, #487).
awk '/^worktree /{sub(/^worktree /,""); p=$0; k=0; next}
/^locked/{k=1; next}
/^$/{if(p!=""){print (k?"L":"U")"\t"p; p=""}; next}
END{if(p!="")print (k?"L":"U")"\t"p}' "$PORC" \
| while IFS="$(printf '\t')" read -r flag wtpath; do
[ "$wtpath" = "$M" ] && continue
[ -d "$wtpath" ] && continue
if [ "$flag" = "L" ]; then
echo " 🧟 $wtpath (registered, gone from disk, locked)"
echo " heal: git -C $M worktree unlock $wtpath && git -C $M worktree prune"
else
echo " 🧟 $wtpath (registered, gone from disk)"
echo " heal: git -C $M worktree prune"
fi
done
# Registered, on disk, but with NO ψ symlink — a crash between `worktree add`
# and `ln -sfn`. Invisible to the render loop above (which is driven by ψ
# links) and to both other orphan classes, yet it still blocks the mother
# offload, so --status used to contradict --offload about how many bodies
# exist (#487).
awk '/^worktree /{sub(/^worktree /,""); print $0}' "$PORC" | tail -n +2 \
| while IFS= read -r wtpath; do
[ -d "$wtpath" ] || continue
[ -L "$ROOT/ψ/incubate/$rslug/wt/$(basename "$wtpath")/origin" ] && continue
echo " ⚠ $wtpath (registered, no ψ link)"
echo " heal: /incubate $rslug --wt $(basename "$wtpath")"
done
# The invisible orphan: admin dir deleted, body kept. `worktree list` cannot
# see it at all, so it is found by set-difference against what IS listed.
for d in "$WT_ROOT/$rslug"/*; do
[ -d "$d" ] || continue
grep -qxF "worktree $d" "$PORC" || echo " ⚠ $d (on disk, unknown to git) — report only"
done
done < "$ACTIVE"
# Repos and bodies are counted SEPARATELY — the old single counter printed
# "Total: 3" for 1 repo + 2 bodies (#487).
echo ""
echo " $REPOS repo · $BODIES bodies ($LIVE live) · $DIRTY dirty"
# --include-offloaded: also list .origins entries with no live symlink (#280)
if [ "$INCLUDE_OFFLOADED" = "--include-offloaded" ] && [ -f "$ROOT/ψ/incubate/.origins" ]; then
echo ""
echo "📦 Offloaded (in .origins, no live symlink)"
echo ""
OFFLOADED_COUNT=0
# De-dup through a private snapshot, never read the shared file raw (#487).
# The guarded append can leave a duplicate line (measured: 15/30 trials, 4
# concurrent runs) and this loop used to list the repo twice and print
# `Offloaded: 3` for two repos — disagreeing with the counter below, which
# already did `sort -u`, in the same command. A `sort -u | while` PIPE would
# put OFFLOADED_COUNT in a subshell and lose it, hence the temp file.
OSNAP=$(mktemp "${TMPDIR:-/tmp}/incubate-origins.XXXXXX")
sort -u "$ROOT/ψ/incubate/.origins" > "$OSNAP"
while IFS= read -r slug; do
[ -z "$slug" ] && continue
if ! grep -qxF "$slug" "$ACTIVE"; then
if [ -d "$GHQ_ROOT/github.com/$slug" ]; then
echo " $slug (ghq preserved at $GHQ_ROOT/github.com/$slug)"
else
echo " $slug (ghq absent — fully purged)"
fi
OFFLOADED_COUNT=$((OFFLOADED_COUNT + 1))
fi
done < "$OSNAP"
rm -f "$OSNAP"
echo ""
echo " Offloaded: $OFFLOADED_COUNT"
elif [ -f "$ROOT/ψ/incubate/.origins" ]; then
# `sort -u` absorbs the duplicate lines a guarded append can produce (#487).
TOTAL_RECORDED=$(sort -u "$ROOT/ψ/incubate/.origins" | grep -cv '^$')
OFFLOADED_HIDDEN=$((TOTAL_RECORDED - REPOS))
if [ "$OFFLOADED_HIDDEN" -gt 0 ]; then
echo " ($OFFLOADED_HIDDEN offloaded — use --include-offloaded to view)"
fi
fi
rm -f "$LINKS" "$ACTIVE" "$PORC"
additionally drops .origins entry / deletes a merged branch
Add --purge to also remove the entry from .origins manifest (#280) —
useful when you want the offloaded slug to NOT count toward the total any more.
ROOT="$(pwd)"
GHQ_ROOT=$(ghq root)
WT_ROOT="${INCUBATE_WT_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees}"
SLUG="[OWNER/REPO or REPO]"
PURGE="" # "--purge" — also drop from .origins / delete a merged branch
WT="" # bare slug for ONE body (e.g. "fix-auth"), or the literal "--all-wt".
# NOT "--wt fix-auth" — see the dispatch guard below.
# ---- Resolve the repo EXACTLY (#487) ----
# The old `grep -i "$SLUG" | head -1` picked by readdir order: with acme/api and
# acme/api-gateway both present, `--offload api` selected acme/api-gateway — the
# WRONG repo, which becomes destructive once bodies exist. Exact match wins,
# suffix match is the fallback, ties are reported instead of guessed. `/wt/` is
# excluded so a body can never be mistaken for a mother.
# NO `xargs -I` (#487): BSD xargs — macOS, the primary machine — aborts with
# `command line cannot be assembled, too long` on any input line ≥ 255 bytes
# (measured: 254 ok, 255 rc=1), which a deep ghq root reaches easily. GNU xargs
# has no such limit, so this failed only on macOS. Same `while read` shape the
# --status block already uses.
CANDS=$(find "$ROOT/ψ/incubate" -name origin -type l -print 2>/dev/null \
| while IFS= read -r l; do d=$(dirname "$l"); printf '%s\n' "${d#"$ROOT/ψ/incubate/"}"; done \
| grep -v '/wt/' | sort)
# Exact first, then a LITERAL suffix match. Not `grep -xE ".*/$SLUG"`: `.` is a
# regex wildcard there too, so `--offload api.js` could select `acme/apiXjs`.
SEL=$(printf '%s\n' "$CANDS" | grep -xF "$SLUG")
[ -z "$SEL" ] && SEL=$(printf '%s\n' "$CANDS" \
| awk -v s="/$SLUG" 'length($0)>length(s) && substr($0,length($0)-length(s)+1)==s')
if [ -z "$SEL" ]; then
echo "✗ Not found: $SLUG"
# QUOTED — unquoted $CANDS word-splits any path containing a space.
echo "Active incubations:"; printf ' %s\n' "$CANDS"
exit 1
fi
if [ "$(printf '%s\n' "$SEL" | grep -c .)" -gt 1 ]; then
echo "✗ Ambiguous '$SLUG' — name the owner too:"; printf ' %s\n' "$SEL"
exit 2
fi
OWNER=$(dirname "$SEL"); REPO=$(basename "$SEL")
MOTHER="$GHQ_ROOT/github.com/$SEL"
# -z is MANDATORY, never plain --porcelain (#487) — see "Directory Structure".
# Without it a Thai or quoted --task makes every guard below misread its own
# lock as a foreign tool's and refuse the body forever.
wtlist() { git -C "$MOTHER" worktree list --porcelain -z 2>/dev/null | tr '\0' '\n'; }
# ---- Retire ONE body. Returns non-zero when it refuses; never destroys. ----
offload_body() {
_s="$1"
_wt="$WT_ROOT/$OWNER/$REPO/$_s"
_link="$ROOT/ψ/incubate/$OWNER/$REPO/wt/$_s"
if [ ! -d "$_wt" ]; then
echo " ⓘ wt/$_s — no directory on this machine"
if wtlist | grep -qxF "worktree $_wt"; then
echo " still registered — heal: git -C $MOTHER worktree unlock $_wt; git -C $MOTHER worktree prune"
return 1
fi
return 0
fi
if ! wtlist | grep -qxF "worktree $_wt"; then
echo " ⚠ wt/$_s — on disk but unknown to git; report only, not touched"; return 1
fi
_r=$(wtlist | awk -v w="worktree $_wt" \
'$0==w{f=1;next} f&&/^locked /{sub(/^locked /,"");print;exit} f&&/^$/{exit}')
_by=$(printf '%s' "$_r" | awk -F'|' '$1=="incubate"{print $2}')
_me="${INCUBATE_AGENT:-$(id -un)@$(hostname -s)}"
if [ -n "$_by" ] && [ "$_by" != "$_me" ]; then
echo " ✗ wt/$_s — held by $_by, refusing (their work, their call)"; return 1
fi
if [ "$_r" = "initializing" ]; then
# git's OWN lock, written during `worktree add` and left by a kill -9
# mid-checkout (E11). Classifying it as "another tool" wedged the body, the
# slug AND the mother offload permanently, with no --force to escape (#487).
echo " ⚠ wt/$_s — crashed mid-create (git's own 'initializing' lock, E11)"
echo " git -C $_wt status # see what landed"
echo " git -C $MOTHER worktree unlock $_wt"
echo " then re-run this offload"
return 1
fi
if [ -n "$_r" ] && [ -z "$_by" ]; then
echo " ✗ wt/$_s — locked by another tool, refusing"; return 1
fi
# Probe for dirt BEFORE unlocking (#487). `unlock` then a refused `remove`
# left the body in place with its owner/timestamp/task erased — the only
# ownership metadata the design has — so `--status` showed `(unlocked)`, a
# peer could re-claim it, and the very next `--all-wt` would delete work that
# had just been reported as "skipped, nothing was destroyed".
if [ -n "$(git -C "$_wt" status --porcelain 2>/dev/null)" ]; then
echo " ✗ wt/$_s — uncommitted work, NOT removed (still locked). Save it first:"
echo " cd $_wt && git add -A && git commit -m 'wip' && git push -u origin incubate/$_s"
return 1
fi
git -C "$MOTHER" worktree unlock "$_wt" 2>/dev/null || true
# NO --force. EVER. Plain `remove` fails closed on modified AND untracked-only
# trees and leaves the body intact — the one place Rule 6 matters most (#487).
if ! git -C "$MOTHER" worktree remove "$_wt" 2>/dev/null; then
# Lost a race with the body's own agent, or a submodule/permission problem.
# Restore the lock we took off: a refusal must leave NOTHING changed.
[ -n "$_r" ] && git -C "$MOTHER" worktree lock --reason "$_r" "$_wt" 2>/dev/null
echo " ✗ wt/$_s — could not be removed, NOT touched. Save any work first:"
echo " cd $_wt && git add -A && git commit -m 'wip' && git push -u origin incubate/$_s"
return 1
fi
unlink "$_link/origin" 2>/dev/null
rmdir "$_link" 2>/dev/null
rmdir "$ROOT/ψ/incubate/$OWNER/$REPO/wt" 2>/dev/null
echo " ✓ wt/$_s retired — branch incubate/$_s survives"
if [ "$PURGE" = "--purge" ]; then
# -d only, NEVER -D: -d refuses to delete an unmerged branch.
if git -C "$MOTHER" branch -d "incubate/$_s" 2>/dev/null; then
echo " ✂ branch incubate/$_s deleted"
else
echo " ⓘ branch incubate/$_s kept (unmerged — branch -d refuses, and we never -D)"
fi
fi
return 0
}
# ---- Dispatch ----
if [ "$WT" = "--all-wt" ]; then
# Collect outcomes, never abort the batch: with N agents, some bodies
# legitimately belong to live peers. Exit 1 if anything was skipped.
#
# The slug list is the UNION of the git registry and the disk (#487). Driving
# it from the disk glob alone made a registered-but-gone body invisible here
# while the mother guard below — which counts the REGISTRY — still saw it, so
# the refusal pointed at the command that had just silently done nothing:
# `--all-wt` rc=0 printing only its header, then `--offload` rc=1 forever.
# offload_body already handles both halves correctly once it is reached.
echo "🧹 Retiring all bodies of $SEL"
RC=0
SLUGS=$(mktemp "${TMPDIR:-/tmp}/incubate-slugs.XXXXXX")
{ wtlist | awk '/^worktree /{sub(/^worktree /,""); print $0}' | tail -n +2
for d in "$WT_ROOT/$OWNER/$REPO"/*; do [ -e "$d" ] && printf '%s\n' "$d"; done
} | while IFS= read -r p; do [ -n "$p" ] && basename "$p"; done | sort -u > "$SLUGS"
while IFS= read -r s; do
[ -z "$s" ] && continue
offload_body "$s" || RC=1
done < "$SLUGS"
rm -f "$SLUGS"
[ "$RC" -eq 0 ] || echo " ⚠ some bodies were skipped — nothing was destroyed"
exit $RC
elif [ -n "$WT" ]; then
# A flag-shaped value means the caller filled $WT the way the old comment read
# ("--wt fix-auth"). That used to fall through to a bare-slug lookup, print
# `ⓘ wt/--wt fix-auth — no directory` and exit 0 — the one silent no-op in the
# whole offload path (#487).
case "$WT" in -*) echo "✗ WT must be a bare slug (e.g. fix-auth), not a flag"; exit 2 ;; esac
offload_body "$WT"; exit $?
fi
# ---- Mother offload: refuse while bodies live (#487) ----
LIVE=$(wtlist | grep -c '^worktree ')
if [ "${LIVE:-0}" -gt 1 ]; then
echo "✗ $SEL still has $((LIVE - 1)) live body/bodies:"
wtlist | awk '/^worktree /{sub(/^worktree /,""); print " "$0}' | tail -n +2
echo " → retire them first: /incubate --offload $SEL --all-wt"
echo " (it retires what it can and names the exact heal for anything it will not touch)"
exit 1
fi
unlink "$ROOT/ψ/incubate/$SEL/origin"
rmdir "$ROOT/ψ/incubate/$OWNER" 2>/dev/null
echo "✓ Offloaded: $SEL"
echo " Hub file remains: $ROOT/ψ/incubate/$SEL/$REPO.md"
echo " ghq clone preserved for future use"
# --purge: remove from .origins manifest (#280)
if [ "$PURGE" = "--purge" ] && [ -f "$ROOT/ψ/incubate/.origins" ]; then
# RACE A, second half (#487). Step 0's append is guarded and safe, but THIS is
# still a read-modify-write of the same shared committed file, and `mv` only
# makes the swap atomic for READERS — it does nothing about lost UPDATES.
# Measured 20/20 trials: one purge against 8 concurrent appends, survivors
# ranged 0/8 to 6/8. A repo whose clone, symlink and hub file all exist
# vanished from the manifest, so --init never restored it again.
#
# Compare-and-swap: if a byte landed while we were rewriting, throw the
# rewrite away and start over; after 10 attempts give up rather than lose an
# entry. This narrows the window from the whole rewrite to a single syscall
# — it does not erase it, which is one more reason Principle 1 prefers
# --include-offloaded over --purge.
#
# mktemp in the DESTINATION directory, not $TMPDIR: $TMPDIR is often a
# different filesystem, where `mv` is copy+unlink rather than an atomic rename,
# and mktemp's 0600 mode would then land on a tracked file.
ORIGINS="$ROOT/ψ/incubate/.origins"
PURGED=""; I=0
while [ "$I" -lt 10 ]; do
I=$((I + 1))
SIZE=$(wc -c < "$ORIGINS" | tr -d ' ')
TMPFILE=$(mktemp "$ROOT/ψ/incubate/.origins.XXXXXX")
# -xF, NEVER "^${SEL}$": `.` is a regex wildcard, so purging `acme/api.js`
# would also delete `acme/apiXjs`.
grep -vxF "$SEL" "$ORIGINS" > "$TMPFILE" || true
chmod 644 "$TMPFILE"
if [ "$(wc -c < "$ORIGINS" | tr -d ' ')" = "$SIZE" ]; then
mv "$TMPFILE" "$ORIGINS"; PURGED=1; break
fi
rm -f "$TMPFILE"
done
if [ -n "$PURGED" ]; then
echo " ✂ Purged '$SEL' from .origins manifest"
echo " ⚠ This breadcrumb is gone — re-incubate to restore it (Principle 1: prefer --include-offloaded over --purge)"
else
echo " ⓘ .origins is being appended to concurrently — purge skipped, nothing lost"
echo " (the symlink is already gone; re-run --offload $SEL --purge when the fleet is idle)"
fi
fi
--all-wt is not atomic and cannot be without a journal — but every step is
idempotent, so re-running after a partial failure is safe. Every refusal path
also restores whatever it touched, so a re-run starts from the same state.
Step 2: Create/Update Hub File (REPO.md)
Runs for default, --flash and --contribute only. It does not run for
--status, --offload, or --wt.
--wt is excluded deliberately (#487): REPO.md is a single committed file
that every body would append to, so N agents claiming bodies in the same minute
each commit and push the vault, two of three pushes are rejected
non-fast-forward, and the rebase collides in one hunk and drops a session
record. That is the ψ-git contention the --wt design exists to eliminate —
worse than the .origins.d/<slug>.yaml this file already rejects, because
.origins.d was at least one file per body. A body's record is its worktree
lock reason, not a hub-file entry.
For the modes that do run it: create or update the hub file:
The actual emission lives in the bash block under Mode: --status above — every
path is resolved via readlink, so all of them are absolute (CONVENTIONS.md).
Ordering is stable (repo, then slug, lexical) so two runs diff cleanly.
Example rendering:
🌱 Incubations
acme/hermes-gateway origin ✓ main · ○ clean · /opt/Code/github.com/acme/hermes-gateway
├─ wt/fix-auth ● dirty 3 incubate/fix-auth "restore session cookie on 401 retry"
│ nat@m5 · 2h ago · ~/.local/state/incubate/worktrees/acme/hermes-gateway/fix-auth
└─ wt/rate-limit ○ clean incubate/rate-limit "add token bucket to /v1/send"
nat@m5 · 20m ago · ~/.local/state/incubate/worktrees/acme/hermes-gateway/rate-limit
🧟 …/worktrees/acme/hermes-gateway/old (registered, gone from disk) — heal: git worktree prune
⚠ …/worktrees/acme/hermes-gateway/half (registered, no ψ link) — heal: /incubate acme/hermes-gateway --wt half
⚠ …/worktrees/acme/hermes-gateway/lost (on disk, unknown to git) — report only
1 repo · 2 bodies (2 live) · 1 dirty
Four states are reported and none is auto-removed (Rule 6): live bodies in
the tree, 🧟 registered-but-gone, ⚠ registered-with-no-ψ-link, and ⚠ on
disk but unknown to git. A body that is registered but has no ψ symlink is the
crash residue of dying between worktree add and ln -sfn; it must be listed
because it still blocks the mother offload, and --status used to be the one
command that could not see it (#487).
A mother with no bodies prints one line and nothing else. Repos and bodies are
counted separately — the old single counter reported Total: 3 for 1 repo with
2 bodies (#487).
.gitignore Pattern
The pattern is auto-added to .gitignore on first /incubate run (#250). If you need to add it manually:
# Ignore origin symlinks only (source lives in ghq)
# Note: no trailing slash — origin is a symlink, not a directory
ψ/incubate/**/origin
Bodies need no new rule (#487). Because every body link is also named
origin, ψ/incubate/OWNER/REPO/wt/<slug>/origin is already matched by the
** in the existing pattern — confirmed with git check-ignore -v. Naming the
link after the slug instead would leave it untracked and unignored, which is
exactly why the filename is fixed. Do not add a wt/ rule.
Since the only thing inside wt/<slug>/ is that ignored symlink, git never sees
the directory at all — no .gitkeep, nothing committed, nothing to conflict on
when N agents work at once.
Trace Connection
After incubation work, log to Oracle so it's discoverable via /trace:
Save the lesson (two-layer pattern)
Write to ψ/memory/learnings/YYYY-MM-DD_incubate-<slug>.md with frontmatter:
Auto-creates private repos when target doesn't exist on GitHub
origin/ symlink structure allows easy offload without losing ghq clone
.origins manifest enables --init restore after fresh git clone
Mirror of /learn: learn = LEFT hand (study), incubate = RIGHT hand (work)
Multi-agent notes (#487)
Plain git worktree + POSIX shell only. No maw, no fleet, no extra
toolchain — --wt works on a fresh machine with git and ghq alone.
Needs git ≥ 2.36 for worktree list --porcelain -z.
Nothing new is committed. Bodies leave no trace in ψ except a gitignored
symlink — no .origins line, no REPO.md session block (Step 2 does not run
for --wt) — so N agents never contend on the vault's git index.
.origins format is unchanged. A vault written by the previous /incubate
reads identically here, and vice versa. No migration, mandatory or otherwise.
Never typing --wt gives the old behaviour, minus four measured bugs
(lost .origins entries, ln -sf mis-linking, the bash-3.2 --status abort,
and --offload selecting the wrong repo).
Bodies live outside $(ghq root) — $INCUBATE_WT_ROOT, defaulting to
${XDG_STATE_HOME:-$HOME/.local/state}/incubate/worktrees — because ghq reads
a linked worktree's .git file as a repository and would list every body as a
phantom entry to every other skill on the shelf.
Known limitation: slugs are not coordinated across machines. Two agents on
two machines can both create incubate/fix-auth; the conflict only surfaces at
push time.
Known limitation:--purge on .origins is compare-and-swap, not atomic.
It gives up rather than lose a concurrent append, so the worst case is a
breadcrumb that stays instead of a breadcrumb that vanishes.
A body's unpushed commits live in the mother's object store, not in the
body directory. Deleting the mother destroys them and no re-clone brings them
back — which is why --flash --purge refuses over live bodies and over
unpushed incubate/* commits. Push early.
INCUBATE_AGENT overrides the owner id. Without it the fallback is
$(id -un)@$(hostname -s), which is identical for N agents on one host — so
"is this my body?" degrades to "is this my host?".