Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3ee94ed
spec: golden worktree hydration by APFS clone
m4ttheweric Sep 21, 2026
8d5bf3a
spec: golden dispose refusal and onDeck 0 scrap match existing guards
m4ttheweric Sep 21, 2026
a162312
plan: golden worktree hydration, 10 tasks
m4ttheweric Sep 21, 2026
12864df
spec+plan: round 1 review fixes (adopt skip, porcelain -z, test seams)
m4ttheweric Sep 21, 2026
f0b6181
plan: lazy volume thunk and real pick-fake API in the freshen test
m4ttheweric Sep 21, 2026
c351bf5
plan: use bunx tsc --noEmit, the command CI runs
m4ttheweric Sep 21, 2026
8070c95
plan: record bun:ffi clonefile hardened-runtime check (GO)
m4ttheweric Sep 21, 2026
51c3f3b
worktree: add golden tree kind and goldenRoot path
m4ttheweric Sep 21, 2026
59785c2
worktree: clonefile(2) wrapper with exit-code table
m4ttheweric Sep 21, 2026
2260a69
worktree: rename clonefile test, document why it skips block sharing
m4ttheweric Sep 21, 2026
9205540
worktree: hidden hydrate-clone verb wrapping clonefile
m4ttheweric Sep 21, 2026
12ef3d3
plan: correct the omitBehavior rationale for hidden leaves
m4ttheweric Sep 21, 2026
b086008
worktree: createTree target golden
m4ttheweric Sep 21, 2026
308170b
worktree: hydrateTree builds a member from the golden by clonefile
m4ttheweric Sep 21, 2026
f1c2335
worktree: check registry-first write, prove no-ready-run, tighten hyd…
m4ttheweric Sep 21, 2026
206b87c
reconciler: ensure the golden and hydrate members from it
m4ttheweric Sep 21, 2026
c50a03f
reconciler: isolate three tests from the golden's first-pass cost
m4ttheweric Sep 21, 2026
932ab21
reconciler: stop the volume probe from creating the pool root
m4ttheweric Sep 21, 2026
9448628
reconciler: prove the hydrate path never creates cfg.root either
m4ttheweric Sep 21, 2026
ef86af2
reconciler: freshen the golden first
m4ttheweric Sep 21, 2026
552c668
freshen test: create member before golden so the sort assertion is no…
m4ttheweric Sep 21, 2026
a0ba463
worktree: adopt skips the golden
m4ttheweric Sep 21, 2026
9a2c7b7
worktree cli: label the golden by kind; pin the guards for the new kind
m4ttheweric Sep 21, 2026
80176a0
worktree: tag worktree:created with kind so subscribers can tell gold…
m4ttheweric Sep 21, 2026
0f2dd5b
docs: golden worktree pointer in CLAUDE.md
m4ttheweric Sep 21, 2026
07356cf
worktree: namespace the golden branch and stop scrap deleting refs it…
m4ttheweric Sep 22, 2026
420bddb
worktree: clone the donor under the donor's own tree lock
m4ttheweric Sep 22, 2026
71e0e84
replenish: build the golden last, fall back to cold create on any hyd…
m4ttheweric Sep 22, 2026
65177c3
reconciler: sweep the golden root's trash leftovers
m4ttheweric Sep 22, 2026
9f30748
worktree each: never run a command inside the golden
m4ttheweric Sep 22, 2026
b887f15
replenish: pin the golden disk guard to a path that exists
m4ttheweric Sep 22, 2026
0ff6523
docs: golden spec matches the pass order, fallback breadth and donor …
m4ttheweric Sep 22, 2026
904d6d7
reconcile: delete an rt-owned branch when its row is miss-pruned
m4ttheweric Sep 22, 2026
34740f0
worktree each: catch the golden via branch when kind is absent
m4ttheweric Sep 22, 2026
c5f0153
replenish: skip the golden build on a cross-volume pool root
m4ttheweric Sep 22, 2026
33f8253
replenish: probe the member-loop disk guard through nearestExisting
m4ttheweric Sep 22, 2026
783c3be
names: never pick GOLDEN_NAME from a custom namePool
m4ttheweric Sep 22, 2026
c17e544
clonefile: keep both path buffers referenced across the syscall
m4ttheweric Sep 22, 2026
4401602
hydrate: warn on a dropped registry-first write, matching create
m4ttheweric Sep 22, 2026
f4293a1
replenish test: assert the onDeck-0 golden scrap deletes its branch
m4ttheweric Sep 22, 2026
b2f6b8b
hydrate test: assert scrap branch deletion on the ref, not worktree list
m4ttheweric Sep 22, 2026
2e177d1
hydrate test: tighten the donor-lock test to cover enumeration too
m4ttheweric Sep 22, 2026
2da06d0
docs: neutral placeholders for the measured repo, rt is public
m4ttheweric Sep 22, 2026
4b8c4d7
hydrate: widen the donor lock to cover the stamp re-read and worktree…
m4ttheweric Sep 22, 2026
1f23680
reconcile: keep the row when an rt-owned branch delete fails
m4ttheweric Sep 22, 2026
8a16861
docs: golden plan matches the namespaced rt/golden branch
m4ttheweric Sep 22, 2026
148af8c
reconcile test: execFileSync + scrubbed env for this branch's git calls
m4ttheweric Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ calling a repo-keyed daemon verb, or displaying a repo name, read
applies, the derivation rules, the identity-only verb guards, and the legacy
re-key/heal/prune behavior.

## Worktree pool: the golden tree

Every repo with `onDeck > 0` has one `kind: "golden"` worktree under
`~/.mattstack/rt/golden/<segment>/`. New on-deck members are built by
`clonefile(2)`-ing its git-ignored artifacts and inheriting its
`readyStamp`; they never run `pnpm install` at birth. Before touching
`lib/worktree/hydrate.ts`, `lib/worktree/clonefile.ts`, or replenish, read
`docs/superpowers/specs/2026-09-21-golden-worktree-hydration-design.md`.

Three traps, each of which cost a real debugging round:

- **The golden and the pool root must share an APFS volume.** `clonefile(2)`
fails `EXDEV` across volumes, and replenish answers that by silently
cold-creating, so the symptom is slowness, not an error.
- **Nothing on the replenish path may create the pool root.** The volume
probe stats the nearest EXISTING ancestor for exactly this reason: an
existing pool root is what tells `isHeldByUnreadableMount`
(`reconcile.ts`) that a vanished mount is live again, and that hold is
all that stops a mount outage from pruning live claim state.
- **`pnpm install` on an already-current tree still reruns every lifecycle
script** (~3 min on a large pnpm monorepo), so a hydrated member must inherit the
golden's stamp rather than "verify" itself with an install.

## rt chat

Group chat and presence for the agents in the estate, over the daemon. Before
Expand Down
48 changes: 48 additions & 0 deletions commands/__tests__/worktree-hydrate-clone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, writeFileSync, existsSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { worktreeHydrateClone } from "../worktree.ts";

const realExit = process.exit;
let exitCode: number | undefined;
let stderr = "";
const realErr = console.error;

function arm() {
exitCode = undefined;
stderr = "";
process.exit = ((code?: number) => { exitCode = code ?? 0; throw new Error("__exit__"); }) as never;
console.error = (...a: unknown[]) => { stderr += a.join(" ") + "\n"; };
}
afterEach(() => { process.exit = realExit; console.error = realErr; });

async function run(args: string[]): Promise<number | undefined> {
arm();
try { await worktreeHydrateClone(args, {}); } catch (e) { if ((e as Error).message !== "__exit__") throw e; }
return exitCode;
}

describe("worktreeHydrateClone", () => {
test("usage exit 2 on missing args or src === dst", async () => {
expect(await run([])).toBe(2);
expect(await run(["/a"])).toBe(2);
expect(await run(["/a", "/a"])).toBe(2);
});

test("clones and exits 0", async () => {
const dir = mkdtempSync(join(tmpdir(), "rthc-"));
mkdirSync(join(dir, "src", "n"), { recursive: true });
writeFileSync(join(dir, "src", "n", "f"), "x");
expect(await run([join(dir, "src"), join(dir, "dst")])).toBe(0);
expect(existsSync(join(dir, "dst", "n", "f"))).toBe(true);
});

test("EEXIST exits 5 with a clonefile: line on stderr", async () => {
const dir = mkdtempSync(join(tmpdir(), "rthc-"));
mkdirSync(join(dir, "src"));
mkdirSync(join(dir, "dst"));
expect(await run([join(dir, "src"), join(dir, "dst")])).toBe(5);
expect(stderr).toMatch(/^clonefile: /m);
});
});
54 changes: 53 additions & 1 deletion commands/__tests__/worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { execSync } from "child_process";
import { mkdtempSync, realpathSync, rmSync } from "fs";
import { tmpdir } from "os";
import { basename, join } from "path";
import { repoLabel, worktreeAwaitReady, worktreeDispose, worktreeList, worktreeProvision } from "../worktree.ts";
import { repoLabel, worktreeAwaitReady, worktreeDispose, worktreeFreshen, worktreeList, worktreeProvision } from "../worktree.ts";
import { getRepoIdentity } from "../../lib/repo.ts";
import { closeStateDb } from "../../lib/state/index.ts";
import { deriveRepoIdentity, serializeIdentity } from "../../lib/settings/identity.ts";
Expand Down Expand Up @@ -302,6 +302,58 @@ describe("worktree CLI identity plumbing", () => {
l.includes("running run run-1 at implement; rt runs abandon run-1"),
)).toBe(true);
});

test("list labels the golden row by kind, not its on-deck state", async () => {
installFakeDaemon({
ok: true,
data: {
trees: [
{ name: "golden", path: "/nonexistent/golden", kind: "golden", state: "on-deck", branch: null, repoName: "github.com/acme/app", createdAt: "2026-09-21T00:00:00.000Z" },
],
},
});
const lines: string[] = [];
const origLog = console.log;
console.log = (...args: unknown[]) => { lines.push(args.join(" ")); };
try {
await worktreeList([], {});
} finally {
console.log = origLog;
}
const plain = lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, ""));
const row = plain.find((l) => l.includes("/golden "));
expect(row).toBeDefined();
expect(row).toMatch(/\/golden\s+golden\s+\(detached\)/);
expect(row).not.toContain("on-deck");
});

test("freshen's picker offers the golden alongside on-deck members", async () => {
const { installFakePick } = await import("../../lib/ui/pick-fake.ts");
const origIsTTY = process.stdin.isTTY;
Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
installFakeDaemon({
ok: true,
data: {
trees: [
{ name: "golden", path: "/g", kind: "golden", state: "on-deck", branch: null, repoName: "r1", createdAt: "2026-09-21T00:00:00.000Z" },
{ name: "lupin", path: "/l", kind: "ephemeral", state: "on-deck", branch: null, repoName: "r1", createdAt: "2026-09-21T00:00:00.000Z" },
{ name: "hedwig", path: "/h", kind: "ephemeral", state: "claimed", branch: null, repoName: "r1", createdAt: "2026-09-21T00:00:00.000Z" },
],
},
});
const fake = installFakePick([{ kind: "result", result: { action: "cancel", value: null, query: "" } }]);
try {
await worktreeFreshen([], {});
} finally {
fake.restore();
Object.defineProperty(process.stdin, "isTTY", { value: origIsTTY, configurable: true });
}
expect(fake.calls).toHaveLength(1);
const values = fake.calls[0]!.request.rows.map((r) => r.value);
expect(values).toContain("/g");
expect(values).toContain("/l");
expect(values).not.toContain("/h");
});
});

describe("repoLabel", () => {
Expand Down
42 changes: 33 additions & 9 deletions commands/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { maybeOfferClaudeHook } from "./worktree-hook.ts";
import { writeReadyApproval } from "../lib/worktree/ready-approval.ts";
import { daemonQuery, lastQueryTimedOut, type DaemonResponse } from "../lib/daemon-client.ts";
import { listWorktrees } from "../lib/git-worktrees.ts";
import { clonePath, cloneExitCode, CLONE_EXIT } from "../lib/worktree/clonefile.ts";
import {
parseEachArgs,
filterTargets,
Expand Down Expand Up @@ -253,6 +254,11 @@ interface TreeRow {
duplicateBranch?: boolean;
}

/** The golden's state is readiness bookkeeping; its kind is what a human needs to see. */
function rowLabel(r: { kind: string; state?: string }): string {
return r.kind === "golden" ? "golden" : (r.state ?? r.kind);
}

async function fetchTreeRows(json: boolean, repoName?: string): Promise<TreeRow[]> {
const res = await daemonQuery("worktree:list", repoName ? { repoName } : undefined);
const ok = requireQueryResult(json, res);
Expand Down Expand Up @@ -311,7 +317,7 @@ async function pickOneTree(rows: TreeRow[], message: string, breadcrumb: string[
const trailingByPath = await enrichTrailingByPath(rows);
const nameWidth = Math.max(...rows.map((r) => r.name.length));
const options = rows.map((r) => {
const state = r.state ?? r.kind;
const state = rowLabel(r);
const base =
state === "disposable"
? r.disposableReason
Expand Down Expand Up @@ -618,7 +624,7 @@ export async function worktreeList(args: string[], _ctx: unknown): Promise<void>
const dupPart = r.duplicateBranch ? ` ${yellow}duplicate branch${reset}` : "";
const ownerPart = r.owner ? ` ${dim}${r.owner}${reset}` : "";
console.log(
` ${bold}${repoLabel(r.repoName)}/${r.name}${reset} ${dim}${r.state ?? r.kind}${reset} ${cyan}${r.branch ?? "(detached)"}${reset}${ownerPart}${mrPart}${dupPart}`,
` ${bold}${repoLabel(r.repoName)}/${r.name}${reset} ${dim}${rowLabel(r)}${reset} ${cyan}${r.branch ?? "(detached)"}${reset}${ownerPart}${mrPart}${dupPart}`,
);
}
console.log("");
Expand Down Expand Up @@ -722,10 +728,11 @@ export async function worktreeFreshen(args: string[], _ctx: unknown): Promise<vo

if (!treeName && process.stdin.isTTY) {
// Mirrors freshenCandidate (lib/daemon/worktree-reconciler.ts): only
// on-deck ephemeral trees and the main clone are ever freshened — a
// claimed tree is someone's active work and always comes back ran:[].
// on-deck ephemeral or golden trees and the main clone are ever
// freshened... a claimed tree is someone's active work and always comes
// back ran:[].
const rows = (await fetchTreeRows(parsed.json, repoName))
.filter((r) => (r.kind === "ephemeral" && r.state === "on-deck") || r.kind === "main")
.filter((r) => ((r.kind === "ephemeral" || r.kind === "golden") && r.state === "on-deck") || r.kind === "main")
.sort((a, b) => a.name.localeCompare(b.name));
const picked = await pickOneTree(rows, "Freshen which worktree?", ["rt", "worktree", "freshen"]);
if (!picked) { console.log(`\n ${dim}nothing selected${reset}\n`); return; }
Expand Down Expand Up @@ -799,7 +806,7 @@ async function bindingsFromDaemon(repoName: string): Promise<WorktreeBinding[] |
const res = await daemonQuery("worktree:list", { repoName });
if (res === null || !res.ok) return null;
const rows = (res.data?.trees ?? []) as TreeRow[];
return rows.map((r) => ({ path: r.path, branch: r.branch, state: r.state }));
return rows.map((r) => ({ path: r.path, branch: r.branch, state: r.state, kind: r.kind }));
}

/** Read-only git fallback — each is the one lifecycle command allowed this, since it never mutates. */
Expand Down Expand Up @@ -829,8 +836,13 @@ export async function worktreeEach(args: string[], _ctx: unknown): Promise<void>
if (!process.stdin.isTTY) {
fail("no --all/--on-deck flag and no TTY for the picker — pass --all or --on-deck");
}
const widest = Math.max(...bindings.map(b => relWorktreeName(repoPath, b.path).length));
const options = bindings.map(b => ({
const pickable = filterTargets(bindings, "pick");
if (pickable.length === 0) {
console.log(`\n ${dim}no worktrees to run in${reset}\n`);
return;
}
const widest = Math.max(...pickable.map(b => relWorktreeName(repoPath, b.path).length));
const options = pickable.map(b => ({
value: b.path,
label: relWorktreeName(repoPath, b.path).padEnd(widest),
hint: b.branch ?? "(detached)",
Expand All @@ -845,7 +857,7 @@ export async function worktreeEach(args: string[], _ctx: unknown): Promise<void>
return;
}
const set = new Set(selected);
targets = bindings.filter(b => set.has(b.path));
targets = pickable.filter(b => set.has(b.path));
} else {
targets = filterTargets(bindings, parsed.mode);
if (targets.length === 0) {
Expand Down Expand Up @@ -880,3 +892,15 @@ export async function worktreeEach(args: string[], _ctx: unknown): Promise<void>
console.log(` ${hasFailures(results) ? red : green}${summary}${reset}\n`);
if (hasFailures(results)) process.exit(1);
}

/** Child-process body for hydration: one clonefile(2) of <src> at <dst>. Exit codes are the daemon's contract; see lib/worktree/clonefile.ts. */
export async function worktreeHydrateClone(args: string[], _ctx: unknown): Promise<void> {
const [src, dst] = args.filter((a) => !a.startsWith("--"));
if (!src || !dst || src === dst) {
console.error("usage: rt worktree hydrate-clone <src> <dst>");
process.exit(CLONE_EXIT.usage);
}
const r = clonePath(src, dst);
if (!r.ok) console.error(`clonefile: ${r.message}`);
process.exit(cloneExitCode(r));
}
Loading
Loading