@afokapu/atdd-bun 0.10.100 → 0.10.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.100",
2
+ "version": "0.10.101",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -1258,10 +1258,10 @@
1258
1258
  "relationships.yaml": "fccaf745b383fe527b7db9022233037bdf5b5cc3d495bb40da3cca4746f5e241",
1259
1259
  "src/agent.ts": "6d6c6e75908af8fbab13432f0001b0374bb25cf3dee46ba218dfd67e0090cb56",
1260
1260
  "src/ci.ts": "b5c57dd87fc780b2b272156b68830d14e695a1a28300dec46a06f71864e0c59e",
1261
- "src/cli.ts": "d93c3aebb78e752fd96347a5b177c0ed0697010c6e2533e116de93e772cd65a8",
1261
+ "src/cli.ts": "395638ab87565bb10c2beb35988ff1acf686924d13515795156a2e8893b3da88",
1262
1262
  "src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
1263
1263
  "src/enforce.ts": "e0c10519aeecd7080f2900ea230fad2f09482af47c1cee885d5de71573e29732",
1264
- "src/hooks.ts": "8194c2dec14afc307b5ce9e90f64d8659210c579d8f427492823293f302e2876",
1264
+ "src/hooks.ts": "556a1c58699653c6c435b2cea705d74670afdc9ea0c99ee84c9cdd1342c9fef8",
1265
1265
  "src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
1266
1266
  "src/integrity.ts": "f1ede7cb04924ae88e11917047277e264af6596b9759e381bd9c497cd34a797d",
1267
1267
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.100",
3
+ "version": "0.10.101",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/cli.ts CHANGED
@@ -22,7 +22,7 @@ const usage = {
22
22
  "atdd-bun [profile ...] [--root <path>] [--ratchet --base <full-sha> [--ratchet-activate-profiles]]",
23
23
  "atdd-bun init [--replace]",
24
24
  "atdd-bun hooks <install|uninstall|status> [--replace]",
25
- "atdd-bun worktree <start|finish|status>",
25
+ "atdd-bun worktree <start|finish|status> [branch] [--vcs <git|jj>]",
26
26
  "atdd-bun ci <init|status> [--replace]",
27
27
  "atdd-bun agent <init|status> [--replace]",
28
28
  "atdd-bun profiles registry [--check]",
@@ -86,7 +86,10 @@ if (args[0] === "hook") {
86
86
  if (result.message) console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
87
87
  }
88
88
  if (args[0] === "worktree") {
89
- const result = args[1] === "start" ? await startWorktree(process.cwd(), args[2] ?? "") : args[1] === "finish" ? await finishWorktree(process.cwd(), args.includes("--delete-branch")) : args[1] === "status" ? await worktreeStatus(process.cwd()) : fail("worktree requires start, finish, or status");
89
+ const vcsAt = args.indexOf("--vcs"), vcsValue = vcsAt === -1 ? "git" : args[vcsAt + 1];
90
+ if (vcsValue !== "git" && vcsValue !== "jj") fail("--vcs requires git or jj");
91
+ const branch = args[1] === "start" ? args.slice(2).find((arg, index, rest) => !arg.startsWith("-") && rest[index - 1] !== "--vcs") ?? "" : "";
92
+ const result = args[1] === "start" ? await startWorktree(process.cwd(), branch, vcsValue) : args[1] === "finish" ? await finishWorktree(process.cwd(), args.includes("--delete-branch"), vcsValue) : args[1] === "status" ? await worktreeStatus(process.cwd(), vcsValue) : fail("worktree requires start, finish, or status");
90
93
  console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
91
94
  }
92
95
  if (args[0] === "ci") {
package/src/hooks.ts CHANGED
@@ -381,10 +381,171 @@ export async function hooksStatus(root = process.cwd()) {
381
381
  }
382
382
 
383
383
  const branchPath = (branch: string) => branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
384
- export async function startWorktree(root = process.cwd(), branch = "") { const cfg = await policy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled; set worktrees.enabled: true in atdd-bun.yaml"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current !== state.primary) return bad(`start must run from the primary checkout ${state.primary}`); if (!branch) return bad("branch is required: atdd-bun worktree start <branch>"); if ((await git(root, ["check-ref-format", "--branch", branch])).code) return bad(`invalid branch name ${JSON.stringify(branch)}`); if (cfg.protected_branches.includes(branch)) return bad(`cannot create a worktree for protected branch ${branch}`); const leaf = branchPath(branch); if (!leaf) return bad(`branch ${JSON.stringify(branch)} cannot form a safe worktree directory`); const target = join(state.worktreeRoot, leaf); if (existsSync(target)) return bad(`worktree path already exists: ${target}`); await mkdir(state.worktreeRoot, { recursive: true }); const created = await git(state.primary, ["worktree", "add", target, "-b", branch]); if (created.code) return bad(created.err || created.out); const hooks = await installHooks(target); if (!hooks.ok) return bad(`created ${target}, but could not install hooks: ${hooks.message}`); return { ok: true, message: `created linked worktree ${target} on ${branch}` }; }
384
+ const jj = async (root: string, args: string[]) => { const child = Bun.spawn({ cmd: ["jj", ...args], cwd: root, stdout: "pipe", stderr: "pipe" }); const out = await new Response(child.stdout).text(), err = await new Response(child.stderr).text(); return { code: await child.exited, out: out.trim(), err: err.trim() }; };
385
+ async function requireJj46(cwd: string) {
386
+ const result = await jj(cwd, ["version"]);
387
+ if (result.code) return `Jujutsu is unavailable: ${result.err || result.out}`;
388
+ const match = result.out.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
389
+ if (!match) return `could not parse Jujutsu version: ${result.out}`;
390
+ const major = Number(match[1]), minor = Number(match[2]);
391
+ if (major === 0 && minor < 46) return `Jujutsu 0.46.0 or newer is required; found ${match[0]}`;
392
+ return null;
393
+ }
394
+
395
+ async function jjPrimaryRoot(root: string) {
396
+ const common = await git(root, ["rev-parse", "--git-common-dir"]);
397
+ if (common.code || !common.out) return null;
398
+ return realpath(dirname(resolve(root, common.out))).catch(() => null);
399
+ }
400
+
401
+ async function currentCommit(root: string) {
402
+ await jj(root, ["status"]);
403
+ const result = await jj(root, ["log", "-r", "@", "--no-graph", "-T", "commit_id"]);
404
+ return result.code ? null : result.out;
405
+ }
406
+
407
+ async function branchCommit(root: string, branch: string) {
408
+ const result = await git(root, ["rev-parse", "--verify", `refs/heads/${branch}^{commit}`]);
409
+ return result.code ? null : result.out;
410
+ }
411
+
412
+ // A delivery commit must have one unambiguous non-protected exported bookmark.
413
+ // Ref ordering is never used as ownership authority.
414
+ async function branchForCommit(root: string, commit: string, protectedBranches: string[]) {
415
+ const refs = await git(root, ["for-each-ref", "--format=%(refname:short)", "--points-at", commit, "refs/heads/"]);
416
+ if (refs.code) return { error: refs.err || refs.out };
417
+ const candidates = refs.out.split("\n").map(line => line.trim()).filter(Boolean)
418
+ .filter(name => !protectedBranches.includes(name));
419
+ return candidates.length === 1
420
+ ? { branch: candidates[0]! }
421
+ : { error: `current Jujutsu revision must have exactly one non-protected exported bookmark; found ${candidates.length}` };
422
+ }
423
+
424
+ type Layout = { primary: string; worktreeRoot: string; current: string };
425
+ async function jjLayout(root: string, cfg: HookPolicy): Promise<Layout | { error: string }> {
426
+ const current = await realpath(resolve(root));
427
+ const primary = await jjPrimaryRoot(current);
428
+ if (!primary) return { error: "not inside a colocated Git-backed Jujutsu workspace" };
429
+ const expectedPrimary = cfg.worktrees.primary_directory === "." ? primary : join(dirname(primary), cfg.worktrees.primary_directory);
430
+ if (primary !== expectedPrimary) return { error: `primary checkout must be ${expectedPrimary}, not ${primary}` };
431
+ const primaryTip = await branchCommit(primary, cfg.worktrees.primary_branch);
432
+ if (!primaryTip) return { error: `primary bookmark ${cfg.worktrees.primary_branch} is not exported to Git` };
433
+ return { primary, worktreeRoot: resolve(primary, cfg.worktrees.root.replaceAll("{repo}", basename(primary))), current };
434
+ }
435
+
436
+ async function noConflicts(root: string) {
437
+ const result = await jj(root, ["log", "-r", "@ & conflicts()", "--no-graph", "-T", "commit_id"]);
438
+ return result.code ? { error: result.err || result.out } : result.out ? { error: "workspace has unresolved conflicts" } : {};
439
+ }
440
+
441
+ async function preflight(root: string, cfg: HookPolicy): Promise<Layout | { error: string }> {
442
+ if (!cfg.worktrees.enabled) return { error: "worktree policy is disabled; set worktrees.enabled: true in atdd-bun.yaml" };
443
+ const version = await requireJj46(root);
444
+ return version ? { error: version } : jjLayout(root, cfg);
445
+ }
446
+
447
+ async function startTarget(root: string, branch: string, state: Layout, cfg: HookPolicy) {
448
+ if (state.current !== state.primary) return { error: `start must run from the primary checkout ${state.primary}` };
449
+ if (!branch) return { error: "branch is required: atdd-bun worktree start <branch>" };
450
+ if ((await git(root, ["check-ref-format", "--branch", branch])).code) return { error: `invalid branch name ${JSON.stringify(branch)}` };
451
+ if (cfg.protected_branches.includes(branch)) return { error: `cannot create a worktree for protected branch ${branch}` };
452
+ if (await branchCommit(state.primary, branch)) return { error: `branch ${branch} already exists` };
453
+ const leaf = branchPath(branch);
454
+ if (!leaf) return { error: `branch ${JSON.stringify(branch)} cannot form a safe worktree directory` };
455
+ const target = join(state.worktreeRoot, leaf);
456
+ return existsSync(target) ? { error: `worktree path already exists: ${target}` } : { leaf, target };
457
+ }
458
+
459
+ // Workspace creation is two-phase. If bookmark export fails, both JJ artifacts
460
+ // are rolled back so a failed start leaves no reusable partial delivery state.
461
+ async function createWorkspace(state: Layout, target: { leaf: string; target: string }, branch: string, cfg: HookPolicy) {
462
+ await mkdir(state.worktreeRoot, { recursive: true });
463
+ const created = await jj(state.primary, ["workspace", "add", "--colocate", "--name", target.leaf, "-r", cfg.worktrees.primary_branch, target.target]);
464
+ if (created.code) return bad(created.err || created.out);
465
+ const bookmark = await jj(target.target, ["bookmark", "set", branch, "-r", "@"]);
466
+ const id = await currentCommit(target.target);
467
+ const exported = await branchCommit(target.target, branch);
468
+ if (!bookmark.code && id && exported === id) return { ok: true, message: `created linked Jujutsu workspace ${target.target} on ${branch}` };
469
+ await jj(state.primary, ["workspace", "remove", target.leaf]);
470
+ await jj(state.primary, ["bookmark", "delete", branch]);
471
+ return bad(bookmark.err || bookmark.out || `bookmark ${branch} was not exported to the colocated Git repository`);
472
+ }
473
+
474
+ async function finishCandidate(state: Layout, cfg: HookPolicy) {
475
+ if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return { error: `finish must run from a linked workspace beneath ${state.worktreeRoot}` };
476
+ const conflict = await noConflicts(state.current);
477
+ if ("error" in conflict) return conflict;
478
+ const current = await currentCommit(state.current);
479
+ if (!current) return { error: "could not resolve the current Jujutsu revision" };
480
+ let delivery = current, found = await branchForCommit(state.current, current, cfg.protected_branches);
481
+ if ("error" in found) {
482
+ const changed = await jj(state.current, ["diff", "-r", "@", "--name-only"]);
483
+ const parent = await jj(state.current, ["log", "-r", "@-", "--no-graph", "-T", "commit_id"]);
484
+ if (changed.code || changed.out || parent.code || !parent.out) return found;
485
+ const parentFound = await branchForCommit(state.current, parent.out, cfg.protected_branches);
486
+ if ("error" in parentFound) return found;
487
+ delivery = parent.out;
488
+ found = parentFound;
489
+ }
490
+ const tip = await branchCommit(state.primary, found.branch);
491
+ if (!tip || tip !== delivery) return { error: `bookmark ${found.branch} must point at the delivery revision before finish` };
492
+ if ((await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return { error: `branch ${found.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout` };
493
+ return { branch: found.branch, tip, name: branchPath(found.branch) };
494
+ }
495
+
496
+ // The sanitized bookmark name is not sufficient authority to remove a workspace;
497
+ // verify the JJ registry resolves that name back to the exact current path.
498
+ async function verifyWorkspace(state: Layout, candidate: { name: string }) {
499
+ const recorded = await jj(state.primary, ["workspace", "root", "--name", candidate.name]);
500
+ if (recorded.code) return `workspace ${candidate.name} is not registered: ${recorded.err || recorded.out}`;
501
+ const recordedPath = await realpath(recorded.out).catch(() => null);
502
+ return recordedPath === state.current ? null : `workspace ${candidate.name} resolves to ${recorded.out}, not ${state.current}`;
503
+ }
504
+
505
+ async function deleteBookmark(state: Layout, candidate: { branch: string; tip: string }, cfg: HookPolicy) {
506
+ const now = await branchCommit(state.primary, candidate.branch);
507
+ const merged = !(await git(state.primary, ["merge-base", "--is-ancestor", candidate.tip, cfg.worktrees.primary_branch])).code;
508
+ if (now !== candidate.tip || !merged) return `workspace removed, but bookmark ${candidate.branch} was not deleted: its tip changed or is no longer merged into ${cfg.worktrees.primary_branch}`;
509
+ const deleted = await jj(state.primary, ["bookmark", "delete", candidate.branch]);
510
+ return deleted.code ? `workspace removed, but bookmark deletion failed: ${deleted.err || deleted.out}` : null;
511
+ }
512
+ async function startJjWorktree(root: string, branch: string, cfg: HookPolicy) {
513
+ const state = await preflight(root, cfg);
514
+ if ("error" in state) return bad(state.error);
515
+ const target = await startTarget(root, branch, state, cfg);
516
+ if ("error" in target) return bad(target.error);
517
+ return createWorkspace(state, target, branch, cfg);
518
+ }
519
+ async function statusJjWorktrees(root: string, cfg: HookPolicy) {
520
+ if (!cfg.worktrees.enabled) return bad("worktree policy is disabled");
521
+ const version = await requireJj46(root);
522
+ if (version) return bad(version);
523
+ const state = await jjLayout(root, cfg);
524
+ if ("error" in state) return bad(state.error);
525
+ const result = await jj(state.primary, ["workspace", "list"]);
526
+ return result.code ? bad(result.err || result.out) : { ok: true, message: result.out };
527
+ }
528
+
529
+ async function finishJjWorktree(root: string, deleteBranch: boolean, cfg: HookPolicy) {
530
+ const state = await preflight(root, cfg);
531
+ if ("error" in state) return bad(state.error);
532
+ const candidate = await finishCandidate(state, cfg);
533
+ if ("error" in candidate) return bad(candidate.error);
534
+ const mismatch = await verifyWorkspace(state, candidate);
535
+ if (mismatch) return bad(mismatch);
536
+ const removed = await jj(state.primary, ["workspace", "remove", candidate.name]);
537
+ if (removed.code) return bad(removed.err || removed.out);
538
+ if (!deleteBranch) return { ok: true, message: `removed merged Jujutsu workspace ${state.current}` };
539
+ const deletionError = await deleteBookmark(state, candidate, cfg);
540
+ return deletionError ? bad(deletionError) : { ok: true, message: `removed merged Jujutsu workspace ${state.current} and bookmark ${candidate.branch}` };
541
+ }
542
+
543
+
544
+ export type VcsMode = "git" | "jj";
545
+ export async function startWorktree(root = process.cwd(), branch = "", vcs: VcsMode = "git") { const cfg = await policy(root); if (vcs === "jj") return startJjWorktree(root, branch, cfg); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled; set worktrees.enabled: true in atdd-bun.yaml"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current !== state.primary) return bad(`start must run from the primary checkout ${state.primary}`); if (!branch) return bad("branch is required: atdd-bun worktree start <branch>"); if ((await git(root, ["check-ref-format", "--branch", branch])).code) return bad(`invalid branch name ${JSON.stringify(branch)}`); if (cfg.protected_branches.includes(branch)) return bad(`cannot create a worktree for protected branch ${branch}`); const leaf = branchPath(branch); if (!leaf) return bad(`branch ${JSON.stringify(branch)} cannot form a safe worktree directory`); const target = join(state.worktreeRoot, leaf); if (existsSync(target)) return bad(`worktree path already exists: ${target}`); await mkdir(state.worktreeRoot, { recursive: true }); const created = await git(state.primary, ["worktree", "add", target, "-b", branch]); if (created.code) return bad(created.err || created.out); const hooks = await installHooks(target); if (!hooks.ok) return bad(`created ${target}, but could not install hooks: ${hooks.message}`); return { ok: true, message: `created linked worktree ${target} on ${branch}` }; }
385
546
 
386
- export async function worktreeStatus(root = process.cwd()) { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); const result = await git(state.primary, ["worktree", "list", "--porcelain"]); return result.code ? bad(result.err) : { ok: true, message: result.out }; }
547
+ export async function worktreeStatus(root = process.cwd(), vcs: VcsMode = "git") { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); if (vcs === "jj") return statusJjWorktrees(root, cfg); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); const result = await git(state.primary, ["worktree", "list", "--porcelain"]); return result.code ? bad(result.err) : { ok: true, message: result.out }; }
387
548
 
388
549
  async function generatedHooksOnly(root: string): Promise<boolean> { const directory = join(root, ".githooks"); if (!existsSync(directory)) return false; const names = (await readdir(directory)).sort(); if (names.join("\n") !== [...hookEvents].sort().join("\n")) return false; return (await Promise.all(hookEvents.map(async event => (await readFile(join(directory, event), "utf8")) === await renderHook(event, root)))).every(Boolean); }
389
550
 
390
- export async function finishWorktree(root = process.cwd(), deleteBranch = false) { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return bad(`finish must run from a linked worktree beneath ${state.worktreeRoot}`); if (!state.branch || cfg.protected_branches.includes(state.branch)) return bad("finish requires a non-protected named branch"); const status = (await git(state.current, ["status", "--porcelain"])).out, generatedHooks = await generatedHooksOnly(state.current), onlyGeneratedHooks = Boolean(status) && status.split("\n").every(line => line.endsWith(" .githooks/") || line.endsWith(" .githooks")); if (status && !onlyGeneratedHooks) return bad("worktree is dirty; commit, stash, or discard changes before finish"); const tip = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (!tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`branch ${state.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout`); const removed = await git(state.primary, ["worktree", "remove", ...(onlyGeneratedHooks && generatedHooks ? ["--force"] : []), state.current]); if (removed.code) return bad(removed.err || removed.out); if (deleteBranch) { const now = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (now !== tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`worktree removed, but branch ${state.branch} was not deleted: its tip changed or is no longer merged into ${cfg.worktrees.primary_branch}`); const deleted = await git(state.primary, ["branch", "-D", state.branch]); if (deleted.code) return bad(`worktree removed, but branch deletion failed: ${deleted.err || deleted.out}`); } return { ok: true, message: `removed merged worktree ${state.current}${deleteBranch ? ` and branch ${state.branch}` : ""}` }; }
551
+ export async function finishWorktree(root = process.cwd(), deleteBranch = false, vcs: VcsMode = "git") { const { policy: cfg, source } = await primaryPolicy(root); if (!cfg.worktrees.enabled) return disabledWorktreePolicy(source); if (vcs === "jj") return finishJjWorktree(root, deleteBranch, cfg); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return bad(`finish must run from a linked worktree beneath ${state.worktreeRoot}`); if (!state.branch || cfg.protected_branches.includes(state.branch)) return bad("finish requires a non-protected named branch"); const status = (await git(state.current, ["status", "--porcelain"])).out, generatedHooks = await generatedHooksOnly(state.current), onlyGeneratedHooks = Boolean(status) && status.split("\n").every(line => line.endsWith(" .githooks/") || line.endsWith(" .githooks")); if (status && !onlyGeneratedHooks) return bad("worktree is dirty; commit, stash, or discard changes before finish"); const tip = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (!tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`branch ${state.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout`); const removed = await git(state.primary, ["worktree", "remove", ...(onlyGeneratedHooks && generatedHooks ? ["--force"] : []), state.current]); if (removed.code) return bad(removed.err || removed.out); if (deleteBranch) { const now = (await git(state.primary, ["rev-parse", "--verify", `refs/heads/${state.branch}`])).out.trim(); if (now !== tip || (await git(state.primary, ["merge-base", "--is-ancestor", tip, cfg.worktrees.primary_branch])).code) return bad(`worktree removed, but branch ${state.branch} was not deleted: its tip changed or is no longer merged into ${cfg.worktrees.primary_branch}`); const deleted = await git(state.primary, ["branch", "-D", state.branch]); if (deleted.code) return bad(`worktree removed, but branch deletion failed: ${deleted.err || deleted.out}`); } return { ok: true, message: `removed merged worktree ${state.current}${deleteBranch ? ` and branch ${state.branch}` : ""}` }; }