/*
* `ensure_worktree` — the worktree invariant as a verb.
*
* `git worktree add` lives on five cards as a RECIPE, which means it is followed
* from memory and skipped under pressure. The failure it prevents is not
* theoretical: a shared checkout is how two agents' edits land in one tree, and
* the primary checkout is the one everybody reaches for because it is the path
* they already have.
*
* CUT FROM `origin/`, NEVER A LOCAL BRANCH. A local `main` is whatever the
* last person left there; `origin/` is what everyone else will merge into.
* This is the same rule as `land`'s target-tip check, one step earlier: the
* question is always "what does the thing I am merging into have?".
*/
import { execFileSync } from "node:child_process";
import { existsSync, realpathSync } from "node:fs";
import path from "node:path";
import { z } from "zod";
/**
* Compare paths by their REAL path, never by string.
*
* git reports `/private/var/...` where a caller passes `/var/...` — macOS's
* symlink — so `path.resolve` comparison silently MISSES the primary and the
* guard that refuses it never fires. The same symlink cost a carrier check its
* main-module guard earlier today: it exited 0 having done nothing.
*/
const samePath = (a: string, b: string): boolean => {
const real = (x: string) => {
try {
return realpathSync(x);
} catch {
return path.resolve(x);
}
};
return real(a) === real(b);
};
const git = (repo: string, args: string[]): string =>
execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
/** Every worktree git knows about: `{path, branch, bare}` in list order. The
* FIRST entry is the primary checkout — that is what `--porcelain` guarantees. */
export function listWorktrees(repo: string): { path: string; branch: string | null }[] {
const out: { path: string; branch: string | null }[] = [];
let cur: { path: string; branch: string | null } | null = null;
for (const line of git(repo, ["worktree", "list", "--porcelain"]).split("\n")) {
if (line.startsWith("worktree ")) {
if (cur) out.push(cur);
cur = { path: line.slice(9), branch: null };
} else if (line.startsWith("branch ") && cur) {
cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
}
}
if (cur) out.push(cur);
return out;
}
/** The primary checkout — the tree nobody may be given for a slice. */
export function primaryOf(repo: string): string | null {
const all = listWorktrees(repo);
return all.length ? (all[0] as { path: string }).path : null;
}
const isDirty = (repo: string): boolean => {
try {
return git(repo, ["status", "--porcelain"]).length > 0;
} catch {
return true; // unreadable is not clean
}
};
export const ensureWorktreeSchema = {
agentId: z.string().min(1),
repo: z.string().min(1),
base: z.string().min(1),
task: z.string().optional(),
ephemeral: z.boolean().optional(),
parent: z.string().optional(),
};
export async function ensureWorktreeTool(args: {
agentId: string;
repo: string;
base: string;
task?: string;
ephemeral?: boolean;
parent?: string;
}) {
const { agentId, repo, base } = args;
if (!path.isAbsolute(repo)) return { ok: false as const, error: `repo must be an absolute path, got '${repo}'` };
if (!existsSync(repo)) return { ok: false as const, error: `no such repo: ${repo}` };
let primary: string | null;
try {
primary = primaryOf(repo);
} catch (e) {
return { ok: false as const, error: `not a git repository (${String((e as Error).message).split("\n")[0]})` };
}
// `repo` IS the primary checkout in normal use — that is how you address the
// repository. What must never happen is HANDING AN AGENT THE PRIMARY as its
// slice tree, so the refusal is on the computed TARGET, not on the input.
//
// Reading spec 1.4 literally ("calling with the primary path exits non-zero")
// would refuse every call and make the verb unusable, since `repo` is always
// the primary. The invariant it protects is the one implemented here: two
// agents editing one tree. Flagged in the PR rather than silently chosen.
return await create({ ...args, primary: primary ?? repo });
}
async function create(a: {
agentId: string;
repo: string;
base: string;
task?: string;
ephemeral?: boolean;
parent?: string;
primary: string;
}) {
const { repo, base, agentId, primary } = a;
// The BASE must exist as a REMOTE ref. A missing `origin/` is refused
// rather than silently falling back to a local branch of the same name — the
// local one is whatever the last person left there.
const ref = `origin/${base}`;
let sha: string;
try {
git(repo, ["fetch", "--quiet", "origin", base]);
} catch {
/* offline or no remote: fall through to the rev-parse, which is the real test */
}
try {
sha = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
} catch {
return {
ok: false as const,
error: `${ref} does not resolve — refusing to cut a tree from a local '${base}', which is whatever the last person left there. Fetch the remote, or name a base that exists on origin.`,
};
}
// ONE PROJECT PARENT: trees live beside the primary, named for the agent, so a
// stray tree is identifiable without opening it.
const parent = a.parent ?? path.dirname(primary);
const leaf = `${path.basename(primary)}-${agentId}${a.ephemeral ? "-ephemeral" : ""}`;
const target = path.join(parent, leaf);
const branch = a.task ? `${agentId}/${a.task}` : `${agentId}/work`;
// DEFENCE IN DEPTH, AND IT IS CURRENTLY UNREACHABLE — stated because a guard
// that cannot fire is worth nothing until someone knows it cannot.
//
// The leaf is always `-`, so the target can never
// equal the primary while that naming holds. I tried to write a test that
// reaches this branch and could not without symlink contrivance; the honest
// conclusion is that the NAMING is the real invariant and this is a backstop
// for the day someone changes it. It is kept rather than deleted for exactly
// that day, and labelled rather than left looking load-bearing.
//
// What actually protects the invariant is tested instead: the target always
// differs from the primary, for any agent id.
if (samePath(target, primary)) {
return {
ok: false as const,
error:
`refusing to hand back the PRIMARY checkout (${primary}) as a slice tree — that is the path everyone already has, ` +
`so it is the one two agents end up editing at once. Pass a different parent or agentId.`,
};
}
const existing = listWorktrees(repo).find((w) => samePath(w.path, target));
if (existing) {
// IDEMPOTENT, and it reports what it FOUND rather than what it would have
// made: a verb that silently returns a tree on a different branch than asked
// for is the adjacent-answer shape.
const head = git(existing.path, ["rev-parse", "HEAD"]);
// IS THE REUSED TREE ACTUALLY AT THE BASE IT CLAIMS?
//
// The verb warned when an existing tree was on a different BRANCH and said
// nothing about it being behind the BASE — so a tree left on last week's
// main was handed back as ready. Starting a new task there produces the
// "stale main -> confidently-wrong inventories" failure the worker card
// already warns about, and nothing about the result looks stale.
//
// Reported here, not refused: an existing tree legitimately holds
// in-progress work mid-slice (1.3). The refusal belongs to `claim`, which
// is the verb that means "start something new".
let atBase = false;
let behindBy: number | null = null;
try {
const baseSha = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
atBase = head === baseSha;
if (!atBase) behindBy = Number(git(existing.path, ["rev-list", "--count", `HEAD..${ref}`])) || 0;
} catch {
// NOT MEASURED IS NOT AT-BASE. Leaving `atBase` false is the safe
// direction: it makes `claim` ask rather than assume.
atBase = false;
}
return {
ok: true as const,
path: existing.path,
sha: head,
branch: existing.branch,
created: false,
base: ref,
atBase,
behindBy: behindBy ?? 0,
...(existing.branch !== branch
? { warning: `existing tree is on '${existing.branch}', not the '${branch}' this call would have created — reusing it, NOT re-pointing it` }
: {}),
};
}
try {
git(repo, ["worktree", "add", "-q", "-b", branch, target, sha]);
} catch (e) {
return { ok: false as const, error: `git worktree add failed: ${String((e as Error).message).split("\n")[0]}` };
}
// ⟨q-7b2f6c04⟩ — THE EMPTY PUSH AT CLAIM. The branch goes to origin at the
// base's tip, so the lane has a ref on the remote from its first minute: the
// VCS axis sees it, and the stall clock scores it on the CLAIM axis (a ref
// sitting on the base has no commits of its own to date). Best-effort and
// REPORTED, never fatal — a tree cut offline is still a tree.
let pushed: { ok: true; ref: string } | { ok: false; why: string };
try {
execFileSync("git", ["push", "-q", "-u", "origin", `${branch}:refs/heads/${branch}`], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 20_000 });
pushed = { ok: true, ref: `origin/${branch}` };
} catch (e) {
pushed = { ok: false, why: `the empty claim-time push did not land: ${String((e as Error).message).split("\n").find((l) => l.trim()) ?? "unknown"} — the lane is observable on the claim axis from the board's history only` };
}
return {
ok: true as const,
path: target,
sha,
branch,
created: true,
pushed,
base: ref,
// Cut from `sha`, which IS origin/ — true by construction here, and
// stated so callers need not special-case "created" to know it.
atBase: true,
behindBy: 0,
...(a.ephemeral
? {
ephemeral: true,
// A VERB NAME, NOT A COMMAND LINE. This used to hand back the shell to
// paste — the tool knew the repo, path and branch and spent the agent's
// tokens on work it could do itself.
releaseWith: { verb: "release_worktree", args: { repo, path: target, apply: true } },
}
: {}),
};
}
// ---------- shared: is this tree's work already in the base? ----------
/**
* Are the commits HEAD has beyond `tip` ALREADY LANDED there?
*
* `rev-list --count ..HEAD` is pure ANCESTRY, and under squash merge a landed
* branch's own shas never enter the base's history — so `ahead` stays > 0 FOREVER
* for work that shipped (q-5c40db91; 20 of the last 20 merges on this repo's main
* are single-parent). Reading `ahead > 0` as "this tree holds unlanded work" makes
* the verb whose job is fast-forwarding idle trees classify a FINISHED tree as
* mid-slice, permanently.
*
* MEASURED, and it cost a lane cycle: a `claim` was refused because a worker's tree
* "reads as 1 commit not on origin/main" while that branch had been squash-merged an
* hour earlier. Same inverted alarm as the item's headline — landed work reported as
* unmerged, inviting preservation of work already shipped — inside our own tooling.
*
* `git cherry` compares PATCH IDS, so every line marked `-` means every commit's patch
* is already upstream. It is NOT complete: several commits squashed into one carry a
* different combined patch id, so a negative is INCONCLUSIVE and is reported as
* mid-slice exactly as before. Only a positive changes the verdict, which keeps the
* failure direction the same as today's for everything this cannot prove.
*/
function aheadIsLanded(at: string, tip: string): boolean {
// `git` here THROWS rather than returning null, so the catch is what keeps an
// UNREADABLE answer out of the landed bucket: not measured is not landed, and
// the tree stays mid-slice, which is the direction that refuses rather than
// the direction that tells someone their work is safe to discard.
let cherry: string;
try {
cherry = git(at, ["cherry", tip, "HEAD"]);
} catch {
return false;
}
if (cherry.trim().length === 0) return false;
return cherry
.trim()
.split("\n")
.every((l) => l.trim().startsWith("-"));
}
// ---------- 1.2 / 1.3: refresh idle trees, never mid-slice ----------
export const refreshWorktreesSchema = {
repo: z.string().min(1),
base: z.string().min(1),
apply: z.boolean().optional(),
};
/**
* Fast-forward IDLE trees onto `origin/`. Never `--force`, never a
* mid-slice tree.
*
* A tree is MID-SLICE when it is dirty or holds commits the base does not.
* Pulling under someone's feet is worse than staleness: staleness is visible in
* a diff, a clobbered work-in-progress is not. So dirty or diverged is REFUSED
* per tree and named, and the run continues for the others.
*/
export async function refreshWorktreesTool(args: { repo: string; base: string; apply?: boolean }) {
const { repo, base } = args;
const ref = `origin/${base}`;
try {
git(repo, ["fetch", "--quiet", "origin", base]);
} catch {
/* reported per tree below */
}
let tip: string;
try {
tip = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
} catch {
return { ok: false as const, error: `${ref} does not resolve — nothing to fast-forward onto` };
}
const results = [];
for (const w of listWorktrees(repo)) {
const at = w.path;
if (samePath(at, primaryOf(repo) ?? "")) {
// THE PRIMARY CHECKOUT IS REPORTED, ALWAYS — it used to be skipped with
// "not a slice tree", which is true and useless. It is the tree David
// reads, the one every hand-run measurement runs in, and a silent skip
// let it sit arbitrarily far behind while this verb reported a clean
// sweep. "Not ours to fast-forward" and "nothing to say about it" are
// different claims and only the first one was intended.
let behind = "unknown";
let ahead = "unknown";
try {
behind = git(at, ["rev-list", "--count", `HEAD..${tip}`]);
ahead = git(at, ["rev-list", "--count", `${tip}..HEAD`]);
} catch {
/* left as unknown — an unreadable count must not read as zero */
}
const dirty = isDirty(at);
// FAST-FORWARD ONLY, AND ONLY WHEN CLEAN. A ff-only merge cannot rewrite
// history or resolve a conflict, so the worst case is a refusal. Dirty is
// refused for the same reason a slice tree is: uncommitted work is what a
// diff cannot show and git cannot give back.
const canFf = !dirty && ahead === "0" && behind !== "0" && behind !== "unknown";
if (args.apply && canFf) {
try {
git(at, ["merge", "--ff-only", ref]);
results.push({ path: w.path, action: "fast-forwarded", why: `primary checkout advanced ${behind} commit(s) to ${ref}` });
continue;
} catch (e) {
results.push({ path: w.path, action: "refused", why: `primary checkout: ff-only merge failed — ${(e as Error).message}` });
continue;
}
}
results.push({
path: w.path,
action: behind === "0" ? "current" : "stale",
why:
behind === "0"
? `primary checkout is at ${ref}`
: `primary checkout is ${behind} commit(s) behind ${ref}` +
(dirty
? " and DIRTY — not fast-forwarded; uncommitted work is what a diff cannot show"
: ahead !== "0"
? aheadIsLanded(at, tip)
? ` and ${ahead} ahead whose patches are ALREADY UPSTREAM — squash-merged, so ancestry will never agree; not fast-forwarded, but nothing here needs preserving`
: ` and ${ahead} ahead — not fast-forwarded; it has commits ${ref} does not`
: args.apply
? ""
: " — pass apply:true to fast-forward it"),
});
continue;
}
if (isDirty(at)) {
results.push({ path: w.path, action: "refused", why: "MID-SLICE: uncommitted changes. Pulling here would clobber work a diff cannot show." });
continue;
}
let head: string;
try {
head = git(at, ["rev-parse", "HEAD"]);
} catch {
results.push({ path: w.path, action: "refused", why: "unreadable HEAD" });
continue;
}
if (head === tip) {
results.push({ path: w.path, action: "current", why: `already at ${ref}` });
continue;
}
let ahead = "0";
try {
ahead = git(at, ["rev-list", "--count", `${tip}..HEAD`]);
} catch {
/* treated as diverged below */
}
if (ahead !== "0") {
// LANDED, NOT MID-SLICE — the ancestry count cannot tell these apart.
if (aheadIsLanded(at, tip)) {
results.push({
path: w.path,
action: "landed",
why:
`${ahead} commit(s) are not ancestors of ${ref}, but every one of their PATCHES is already upstream — ` +
`this branch was SQUASH-MERGED and its work has shipped. Ancestry can never say so: a squash writes a new ` +
`commit, so these shas will read as "not on ${ref}" forever. Nothing here needs preserving; detach to ${ref} ` +
`(or delete the branch) and this tree is reusable. NOT fast-forwarded automatically — moving a tree off ` +
`committed work is a decision, not a refresh.`,
});
continue;
}
results.push({
path: w.path,
action: "refused",
why:
`MID-SLICE: ${ahead} commit(s) not on ${ref}, and their patches are NOT upstream. Refusing rather than --force. ` +
`(A squash of SEVERAL commits into one changes the combined patch id, so this cannot prove the negative — ` +
`it reports mid-slice, which is the same direction it always failed in.)`,
});
continue;
}
if (!args.apply) {
results.push({ path: w.path, action: "would-fast-forward", why: `behind ${ref}` });
continue;
}
try {
git(at, ["merge", "--ff-only", tip]);
results.push({ path: w.path, action: "fast-forwarded", why: `to ${tip.slice(0, 8)}` });
} catch (e) {
results.push({ path: w.path, action: "refused", why: `ff-only failed: ${String((e as Error).message).split("\n")[0]}` });
}
}
return { ok: true as const, base: ref, tip: tip.slice(0, 8), applied: args.apply === true, trees: results };
}
// ---------- the counterpart to ensure_worktree: give the tree back ----------
/*
* `ensure_worktree` CREATED trees and nothing RELEASED them, so teardown stayed a
* hand step in five role cards — `git worktree prune` in coord-audit, coord-ci,
* coord-qa and coord-worker, `git worktree remove` twice in coordinator, which
* says so itself: "it never removes, so `git worktree remove`/`prune` remain hand
* steps". Seven commands for one act the bus already had the inputs for.
*
* WORSE THAN ABSENT: `ensure_worktree` COMPUTED the teardown command and handed it
* back as a string for the agent to paste (`removeWith`). The tool knew the repo,
* the path and the branch, and spent the agent's tokens on shell it could have run.
* That is the shape David ruled against: "where possible we should hide the
* plumbing from the agents".
*
* THE REFUSALS ARE THE POINT, and they inherit `refresh_worktrees`' direction —
* report by default, refuse per tree with the reason named, never clobber:
*
* - THE PRIMARY CHECKOUT IS NEVER RELEASED. It is the path everyone already has.
* - A DIRTY TREE IS REFUSED. Uncommitted work is what a diff cannot show and git
* cannot give back, so this is the one refusal with no override.
* - A TREE HOLDING UNLANDED COMMITS IS REFUSED — and "unlanded" is decided by
* `aheadIsLanded` (patch ids), NOT by ancestry. Under squash merge a landed
* branch's shas never enter the base, so ancestry calls every finished tree
* mid-slice forever (q-5c40db91). Using ancestry here would refuse to release
* exactly the trees that are safe to release, which is how the hand step
* survived: the guard would have been wrong and everyone would have forced it.
*
* A NEGATIVE FROM `aheadIsLanded` IS INCONCLUSIVE, NOT A NO. Several commits squashed
* into one carry a different combined patch id, so it cannot prove landedness for
* every shape. It therefore refuses and says the check was inconclusive rather than
* claiming the work is unlanded — the caller lands it, or passes `force:true`, which
* is recorded in the result so a forced release is findable afterwards.
*/
export const releaseWorktreeSchema = {
repo: z.string().min(1),
agentId: z.string().min(1).optional(),
path: z.string().min(1).optional(),
base: z.string().min(1).optional(),
apply: z.boolean().optional(),
force: z.boolean().optional(),
};
export async function releaseWorktreeTool(args: {
repo: string;
agentId?: string;
path?: string;
base?: string;
apply?: boolean;
force?: boolean;
}) {
const { repo } = args;
if (!args.agentId && !args.path) {
return { ok: false as const, error: "name the tree to release: pass `agentId` or `path`" };
}
const trees = listWorktrees(repo);
const primary = primaryOf(repo) ?? "";
const target = args.path
? trees.find((w) => samePath(w.path, args.path as string))
: trees.find((w) => (w.branch ?? "").includes(args.agentId as string) || w.path.includes(args.agentId as string));
if (!target) {
return {
ok: false as const,
error: args.path
? `no worktree at '${args.path}' — nothing to release`
: `no worktree found for '${args.agentId}' — nothing to release`,
// An ABSENCE is reported with what WAS found, so a misaimed lookup is
// distinguishable from an empty world (the positive-control rule).
known: trees.map((w) => ({ path: w.path, branch: w.branch })),
};
}
const at = target.path;
const branch = target.branch;
if (samePath(at, primary)) {
return {
ok: false as const,
error: `refusing to release ${at}: that is the PRIMARY checkout, the path everyone already has`,
path: at,
};
}
if (isDirty(at)) {
return {
ok: false as const,
error:
`refusing to release ${at}: the tree is DIRTY. Uncommitted work is what a diff cannot show and ` +
`git cannot give back, so this refusal has no override — commit it, stash it, or delete it by hand.`,
path: at,
branch,
dirty: true,
};
}
const base = args.base ?? "main";
const ref = `origin/${base}`;
let tip: string | null = null;
try {
git(repo, ["fetch", "--quiet", "origin", base]);
} catch {
/* a stale tip is reported below rather than guessed at */
}
try {
tip = git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]);
} catch {
tip = null;
}
let ahead = "unknown";
if (tip) {
try {
ahead = git(at, ["rev-list", "--count", `${tip}..HEAD`]);
} catch {
/* left unknown — an unreadable count must not read as zero */
}
}
const holdsWork = ahead !== "0";
const landed = tip && holdsWork ? aheadIsLanded(at, tip) : true;
if (!tip) {
return {
ok: false as const,
error: `refusing to release ${at}: ${ref} does not resolve, so whether this tree's work landed cannot be measured`,
path: at,
branch,
};
}
if (holdsWork && !landed && !args.force) {
return {
ok: false as const,
error:
`refusing to release ${at}: it is ${ahead} commit(s) ahead of ${ref} and the patch-id check was ` +
`INCONCLUSIVE — not proof the work is unlanded, but not proof it landed either (several commits ` +
`squashed into one carry a different combined patch id). Land it, or pass force:true, which is recorded.`,
path: at,
branch,
ahead,
landedCheck: "inconclusive",
};
}
const plan = {
path: at,
branch,
ahead,
landedCheck: holdsWork ? (landed ? "landed" : "inconclusive") : "nothing-ahead",
...(args.force && holdsWork && !landed ? { forced: true } : {}),
};
if (!args.apply) {
return {
ok: true as const,
applied: false,
would: "remove the worktree and delete its branch",
...plan,
note: "reports by default; pass apply:true to remove",
};
}
try {
git(repo, ["worktree", "remove", "--force", at]);
} catch (e) {
return {
ok: false as const,
error: `worktree remove failed: ${String((e as Error).message).split("\n")[0]}`,
...plan,
};
}
// PRUNE IS PART OF THE ACT, not a step the caller remembers. Every card that
// said `git worktree remove` also said `git worktree prune`, which is the
// clearest evidence they were one operation split across two lines of prose.
try {
git(repo, ["worktree", "prune"]);
} catch {
/* removal already succeeded; a failed prune is not worth failing the call */
}
let branchDeleted = false;
if (branch) {
try {
git(repo, ["branch", "-D", branch.replace(/^refs\/heads\//, "")]);
branchDeleted = true;
} catch {
/* a branch that will not delete is reported, not fatal */
}
}
return { ok: true as const, applied: true, removed: true, branchDeleted, ...plan };
}