rl-workspaces 0.0.0-stage → 0.6.0

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +735 -2
  3. package/bin/rl +54 -0
  4. package/config.example.zsh +9 -0
  5. package/dist/access.js +40 -0
  6. package/dist/activity.js +90 -0
  7. package/dist/agent-claude.js +16 -0
  8. package/dist/agent-codex.js +22 -0
  9. package/dist/agent-launch.js +74 -0
  10. package/dist/agent-presets.js +70 -0
  11. package/dist/agent-registry.js +8 -0
  12. package/dist/agent-resume.js +96 -0
  13. package/dist/agent-runner.js +386 -0
  14. package/dist/agent-workflows.js +275 -0
  15. package/dist/agents.js +73 -0
  16. package/dist/automation-config.js +381 -0
  17. package/dist/base.js +66 -0
  18. package/dist/cleanup-path.js +33 -0
  19. package/dist/cli.js +57 -0
  20. package/dist/codex-sessions.js +188 -0
  21. package/dist/command-context.js +67 -0
  22. package/dist/common.js +262 -0
  23. package/dist/delete.js +113 -0
  24. package/dist/display.js +134 -0
  25. package/dist/doctor.js +145 -0
  26. package/dist/git-safety.js +24 -0
  27. package/dist/git-workflow.js +89 -0
  28. package/dist/help.js +726 -0
  29. package/dist/hooks.js +159 -0
  30. package/dist/json.js +34 -0
  31. package/dist/mcp.js +336 -0
  32. package/dist/picker.js +182 -0
  33. package/dist/pr-discovery.js +223 -0
  34. package/dist/pr-update.js +419 -0
  35. package/dist/pr-workflow.js +161 -0
  36. package/dist/repos.js +537 -0
  37. package/dist/review-policy.js +150 -0
  38. package/dist/session-workflows.js +650 -0
  39. package/dist/session.js +12 -0
  40. package/dist/state.js +250 -0
  41. package/dist/storage.js +366 -0
  42. package/dist/store.js +379 -0
  43. package/dist/summary.js +164 -0
  44. package/dist/task-state.js +65 -0
  45. package/dist/task.js +192 -0
  46. package/dist/templates.js +4 -0
  47. package/dist/workflow-collaboration.js +531 -0
  48. package/dist/workflow-config.js +177 -0
  49. package/dist/workflow.js +211 -0
  50. package/lib/rl.zsh +329 -0
  51. package/node_modules/graceful-fs/LICENSE +15 -0
  52. package/node_modules/graceful-fs/README.md +143 -0
  53. package/node_modules/graceful-fs/clone.js +23 -0
  54. package/node_modules/graceful-fs/graceful-fs.js +448 -0
  55. package/node_modules/graceful-fs/legacy-streams.js +118 -0
  56. package/node_modules/graceful-fs/package.json +53 -0
  57. package/node_modules/graceful-fs/polyfills.js +355 -0
  58. package/node_modules/proper-lockfile/LICENSE +21 -0
  59. package/node_modules/proper-lockfile/README.md +183 -0
  60. package/node_modules/proper-lockfile/index.js +40 -0
  61. package/node_modules/proper-lockfile/lib/adapter.js +85 -0
  62. package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
  63. package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
  64. package/node_modules/proper-lockfile/package.json +71 -0
  65. package/node_modules/retry/.npmignore +3 -0
  66. package/node_modules/retry/.travis.yml +15 -0
  67. package/node_modules/retry/License +21 -0
  68. package/node_modules/retry/Makefile +18 -0
  69. package/node_modules/retry/README.md +227 -0
  70. package/node_modules/retry/equation.gif +0 -0
  71. package/node_modules/retry/example/dns.js +31 -0
  72. package/node_modules/retry/example/stop.js +40 -0
  73. package/node_modules/retry/index.js +1 -0
  74. package/node_modules/retry/lib/retry.js +100 -0
  75. package/node_modules/retry/lib/retry_operation.js +158 -0
  76. package/node_modules/retry/package.json +32 -0
  77. package/node_modules/retry/test/common.js +10 -0
  78. package/node_modules/retry/test/integration/test-forever.js +24 -0
  79. package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
  80. package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
  81. package/node_modules/retry/test/integration/test-timeouts.js +69 -0
  82. package/node_modules/signal-exit/LICENSE.txt +16 -0
  83. package/node_modules/signal-exit/README.md +39 -0
  84. package/node_modules/signal-exit/index.js +202 -0
  85. package/node_modules/signal-exit/package.json +38 -0
  86. package/node_modules/signal-exit/signals.js +53 -0
  87. package/package.json +57 -4
  88. package/shell/rl.zsh +11 -0
@@ -0,0 +1,134 @@
1
+ import { activityView } from "./activity.js";
2
+ import { accessLabel } from "./access.js";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ // Keep names and remote titles on one terminal line without control sequences.
7
+ const text = (value) => stripVTControlCharacters(String(value ?? ""))
8
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, " ")
9
+ .trim();
10
+ export function prText(pr) {
11
+ if (!pr)
12
+ return "PR: none saved. Run rl pr sync to refresh.";
13
+ const number = typeof pr.number === "number" ? ` #${pr.number}` : "";
14
+ const state = [pr.isDraft ? "draft" : "", text(pr.state).toLowerCase()]
15
+ .filter(Boolean)
16
+ .join(" · ");
17
+ return [
18
+ `PR${number}${pr.title ? `: ${text(pr.title)}` : ""}${state ? ` [${state}]` : ""}`,
19
+ pr.headRefName && pr.baseRefName
20
+ ? ` ${text(pr.headRefName)} → ${text(pr.baseRefName)}`
21
+ : "",
22
+ ` ${text(pr.url)}`,
23
+ pr.syncedAt ? ` Cached ${text(pr.syncedAt)}` : "",
24
+ ]
25
+ .filter(Boolean)
26
+ .join("\n");
27
+ }
28
+ export function statusText(data, git, workflow = null) {
29
+ const color = process.env.NO_COLOR === undefined &&
30
+ process.env.FORCE_COLOR !== "0" &&
31
+ process.env.TERM !== "dumb" &&
32
+ (!!process.stdout.isTTY || !!process.env.FORCE_COLOR);
33
+ const paint = (value, code) => color ? `\x1b[${code}m${value}\x1b[0m` : value;
34
+ const muted = (value) => paint(value, 90);
35
+ const row = (label, value) => ` ${muted(label.padEnd(10))}${value}`;
36
+ const files = (count, state, code) => count ? paint(`${count} ${state} file${count === 1 ? "" : "s"}`, code) : "";
37
+ const changes = [
38
+ files(git.staged, "staged", 32),
39
+ files(git.unstaged, "modified", 33),
40
+ files(git.untracked, "untracked", 33),
41
+ files(git.conflicts, "conflicted", 31),
42
+ ].filter(Boolean);
43
+ const home = homedir();
44
+ const directory = data.worktree === home
45
+ ? "~"
46
+ : data.worktree.startsWith(home + path.sep)
47
+ ? "~" + data.worktree.slice(home.length)
48
+ : data.worktree;
49
+ const lines = [
50
+ "",
51
+ ` ${paint(text(data.id), 1)}`,
52
+ "",
53
+ ...(data.branch !== data.id ? [row("Branch", text(data.branch))] : []),
54
+ row("Path", muted(text(directory))),
55
+ ...(data.baseBranch ? [row("Base", text(data.baseBranch))] : []),
56
+ row("Git", git.availability === "ok"
57
+ ? changes.join(" · ") || paint("clean", 32)
58
+ : paint(text(git.error ?? git.availability), 31)),
59
+ ];
60
+ if (git.upstream) {
61
+ const sync = git.ahead === 0 && git.behind === 0
62
+ ? paint("up to date", 32)
63
+ : git.ahead === null || git.behind === null
64
+ ? muted("commit counts unavailable")
65
+ : `${paint(`${git.ahead} ahead`, git.ahead ? 32 : 90)} · ${paint(`${git.behind} behind`, git.behind ? 33 : 90)} ${muted("(commits)")}`;
66
+ lines.push(row("Upstream", `${sync} ${muted("· " + text(git.upstream))}`));
67
+ }
68
+ lines.push("");
69
+ const pr = data.pr;
70
+ if (pr) {
71
+ const number = typeof pr.number === "number" ? ` #${pr.number}` : "";
72
+ const state = text(pr.state).toLowerCase();
73
+ const label = [pr.isDraft ? "draft" : "", state]
74
+ .filter(Boolean)
75
+ .join(" · ");
76
+ const badge = paint(label, pr.isDraft
77
+ ? 33
78
+ : state === "open"
79
+ ? 32
80
+ : state === "merged"
81
+ ? 35
82
+ : state === "closed"
83
+ ? 31
84
+ : 33);
85
+ const target = pr.baseRefName
86
+ ? `${pr.headRefName && pr.headRefName !== data.branch ? text(pr.headRefName) + " " : ""}→ ${text(pr.baseRefName)}`
87
+ : "";
88
+ lines.push(` ${paint(`PR${number}`, 1)}${badge ? ` · ${badge}` : ""}${target ? ` ${muted("· " + target)}` : ""}`);
89
+ if (pr.title)
90
+ lines.push(` ${text(pr.title)}`);
91
+ lines.push(` ${paint(text(pr.url), 36)}`);
92
+ if (pr.syncedAt) {
93
+ const stamp = text(pr.syncedAt);
94
+ lines.push(` ${muted(`Cached ${Number.isFinite(Date.parse(stamp)) ? accessLabel(stamp) : stamp}`)}`);
95
+ }
96
+ }
97
+ else {
98
+ lines.push(` ${paint("PR", 1)} ${muted("· none saved")}`, ` ${muted("Run rl pr sync to refresh")}`);
99
+ }
100
+ lines.push("", ` ${paint("Sessions", 1)}`);
101
+ for (const agent of ["codex", "claude"]) {
102
+ const activity = activityView(data.activity?.[agent]).filter((item) => !item.childId);
103
+ const states = ["working", "idle", "stale"]
104
+ .map((state) => {
105
+ const count = activity.filter((item) => item.state === state).length;
106
+ return count
107
+ ? paint(`${count} ${state}`, state === "working" ? 32 : state === "stale" ? 33 : 90)
108
+ : "";
109
+ })
110
+ .filter(Boolean);
111
+ lines.push(row(agent === "codex" ? "Codex" : "Claude", `${data.sessions[agent].length} saved${states.length ? ` · ${states.join(" · ")}` : ""}`));
112
+ }
113
+ if (workflow) {
114
+ lines.push("", ` ${paint("Workflow", 1)} · ${text(workflow.workflow)} · ${text(workflow.status)}`, row("Steps", `${workflow.completed}/${workflow.total} completed`), ...(workflow.owner
115
+ ? [
116
+ row("Owner", `${workflow.owner.agent} · native subagents · ${workflow.owner.sessionId}`),
117
+ ]
118
+ : []), ...(workflow.verdict
119
+ ? [
120
+ row("Review", `${workflow.score === null ? "not scored" : `${workflow.score}/100`} · ${text(workflow.verdict)}`),
121
+ ]
122
+ : []));
123
+ }
124
+ const handoff = data.task?.handoffs.at(-1);
125
+ if (handoff) {
126
+ lines.push("", ` ${paint("Task", 1)} · ${paint(data.task.state, data.task.state === "paused" ? 33 : 32)}`, ` ${text(handoff.summary)}`, row("Next", text(handoff.next)));
127
+ if (handoff.blockers)
128
+ lines.push(row("Blockers", paint(text(handoff.blockers), 33)));
129
+ }
130
+ if (Object.values(data.sessions).some((sessions) => sessions.length))
131
+ lines.push("", ` ${muted("Resume with")} ${paint("rl resume", 36)}`);
132
+ lines.push("");
133
+ return lines.join("\n");
134
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { adapters, agentAdapter } from "./agent-registry.js";
4
+ import { activityView, STALE_AFTER_MS } from "./activity.js";
5
+ import { execute, isSymlink, json, message } from "./common.js";
6
+ import { hookCommand } from "./store.js";
7
+ import { isJsonObject, readJsonObject } from "./json.js";
8
+ const PROBE_TIMEOUT_MS = 2000;
9
+ const MAX_VERSION_LENGTH = 200;
10
+ export async function doctor(store, selected, asJson = false) {
11
+ const [file, data] = store.current();
12
+ const selectedAdapters = selected
13
+ ? [agentAdapter(selected)]
14
+ : Object.values(adapters);
15
+ const agents = await Promise.all(selectedAdapters.map(async (adapter) => {
16
+ const configPath = path.join(data.worktree, adapter.configPath);
17
+ const expectedEvents = Object.keys({
18
+ ...adapter.events,
19
+ ...(data.extendedHooks ? adapter.extraEvents : {}),
20
+ });
21
+ const executablePath = (process.env.PATH ?? "")
22
+ .split(path.delimiter)
23
+ .map((directory) => path.resolve(data.worktree, directory, adapter.name))
24
+ .find((candidate) => {
25
+ try {
26
+ fs.accessSync(candidate, fs.constants.X_OK);
27
+ return fs.statSync(candidate).isFile();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }) ?? null;
33
+ let configError = null;
34
+ let installedEvents = [];
35
+ try {
36
+ if (isSymlink(configPath) || isSymlink(path.dirname(configPath)))
37
+ throw new Error("Hook configuration is a symlink");
38
+ const config = readJsonObject(configPath);
39
+ if (!isJsonObject(config.hooks))
40
+ throw new Error("Missing hooks object");
41
+ const command = hookCommand(store, data, adapter.name);
42
+ const hooks = config.hooks;
43
+ installedEvents = expectedEvents.filter((event) => {
44
+ const groups = hooks[event];
45
+ if (!Array.isArray(groups))
46
+ return false;
47
+ return groups.some((group) => isJsonObject(group) &&
48
+ (group.matcher === undefined ||
49
+ group.matcher === "" ||
50
+ group.matcher === "*") &&
51
+ Array.isArray(group.hooks) &&
52
+ group.hooks.some((h) => isJsonObject(h) &&
53
+ h.type === "command" &&
54
+ h.command === command &&
55
+ h.async !== true));
56
+ });
57
+ }
58
+ catch (error) {
59
+ configError = message(error);
60
+ }
61
+ const missingEvents = expectedEvents.filter((event) => !installedEvents.includes(event));
62
+ let cliAvailable = false, version = null, cliError = null, launchVerified = false;
63
+ try {
64
+ const result = await execute([adapter.name, "--version"], data.worktree, { timeoutMs: PROBE_TIMEOUT_MS });
65
+ cliAvailable = result.code === 0;
66
+ version = result.stdout.trim().slice(0, MAX_VERSION_LENGTH) || null;
67
+ if (!cliAvailable)
68
+ cliError = "Version probe failed or timed out";
69
+ if (cliAvailable) {
70
+ const help = await execute(adapter.name === "codex"
71
+ ? [adapter.name, "exec", "--help"]
72
+ : [adapter.name, "--help"], data.worktree, { timeoutMs: PROBE_TIMEOUT_MS });
73
+ launchVerified =
74
+ help.code === 0 &&
75
+ (adapter.name === "codex"
76
+ ? /codex exec/.test(help.stdout) && /PROMPT/.test(help.stdout)
77
+ : /--print/.test(help.stdout) && /--resume/.test(help.stdout));
78
+ }
79
+ }
80
+ catch (error) {
81
+ cliError = message(error);
82
+ }
83
+ const health = data.hookHealth?.[adapter.name];
84
+ return {
85
+ agent: adapter.name,
86
+ configPath,
87
+ installed: !configError && missingEvents.length === 0,
88
+ installedEvents,
89
+ missingEvents,
90
+ configError,
91
+ cliAvailable,
92
+ executablePath,
93
+ launchVerified,
94
+ extendedHooks: !!data.extendedHooks,
95
+ delivery: health?.lastSuccess ? "observed" : "not-observed",
96
+ version,
97
+ cliError,
98
+ compatibility: "unverified",
99
+ trust: "unknown",
100
+ lastSuccess: health?.lastSuccess ?? null,
101
+ lastError: health?.lastError ?? null,
102
+ activity: activityView(data.activity?.[adapter.name]),
103
+ };
104
+ }));
105
+ let stateWritable = true;
106
+ try {
107
+ fs.accessSync(store.root, fs.constants.W_OK);
108
+ fs.accessSync(path.dirname(file), fs.constants.W_OK);
109
+ }
110
+ catch {
111
+ stateWritable = false;
112
+ }
113
+ const report = {
114
+ workspace: data.worktree,
115
+ stateDirectory: path.dirname(file),
116
+ stateWritable,
117
+ staleAfterMs: STALE_AFTER_MS,
118
+ agents,
119
+ };
120
+ if (asJson)
121
+ json(report);
122
+ else {
123
+ console.log(`RL doctor: ${data.worktree}\nState writable from this process: ${stateWritable ? "yes" : "no"}`);
124
+ for (const item of agents) {
125
+ console.log(`\n${item.agent}: hooks ${item.installed ? "installed" : "incomplete"}; CLI ${item.cliAvailable ? (item.version ?? "available") : "unavailable"}`);
126
+ console.log(` CLI: ${item.executablePath ?? "not on PATH"}; launch flags: ${item.launchVerified ? "verified in installed help" : "unverified"}; hook trust: unknown`);
127
+ console.log(` Last received event: ${item.lastSuccess ? `${item.lastSuccess.event} at ${item.lastSuccess.at}` : "none"}`);
128
+ if (item.missingEvents.length)
129
+ console.log(` Missing hooks: ${item.missingEvents.join(", ")}. Run rl adopt.`);
130
+ if (item.configError)
131
+ console.log(` Configuration error: ${item.configError}`);
132
+ if (item.cliError)
133
+ console.log(` CLI error: ${item.cliError}`);
134
+ if (item.lastError)
135
+ console.log(` Last recorded hook error: ${item.lastError.message} at ${item.lastError.at}`);
136
+ for (const activity of item.activity)
137
+ console.log(` ${activity.sessionId}${activity.childId ? ` / child ${activity.childId}` : ""}: ${activity.state}${activity.resumable ? "" : " (observation only)"}`);
138
+ }
139
+ console.log("\nInstallation does not prove hooks are enabled or trusted. Review agent hook settings and start a session here to verify delivery. Version compatibility is not certified. Activity is based on received events; after 30 minutes without an event it is stale, not confirmed running. Agent sandbox access may differ from this process.");
140
+ }
141
+ return stateWritable &&
142
+ agents.every((item) => item.installed && item.cliAvailable)
143
+ ? 0
144
+ : 1;
145
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import { git } from "./common.js";
3
+ export const GIT_WORKFLOW_DIRECTORY = ".git-workflow";
4
+ export const GIT_WORKFLOW_LOCK = ".lock";
5
+ const OPERATION_FILES = [
6
+ "MERGE_HEAD",
7
+ "CHERRY_PICK_HEAD",
8
+ "REVERT_HEAD",
9
+ "rebase-merge",
10
+ "rebase-apply",
11
+ "sequencer",
12
+ ];
13
+ /** Shared mutation guard; untracked content counts as dirty. */
14
+ export function assertReady(root, clean, expectedBranch) {
15
+ if (git(root, "branch", "--show-current") !== expectedBranch)
16
+ throw new Error("Workspace branch changed; retry from the intended branch");
17
+ for (const marker of OPERATION_FILES)
18
+ if (fs.existsSync(git(root, "rev-parse", "--path-format=absolute", "--git-path", marker)))
19
+ throw new Error(`An operation is already in progress in ${root}; finish or abort it first`);
20
+ if (git(root, "ls-files", "--unmerged"))
21
+ throw new Error(`Unresolved conflicts in ${root}`);
22
+ if (clean && git(root, "status", "--porcelain", "--untracked-files=normal"))
23
+ throw new Error(`Workspace is dirty: ${root}. Commit or stash changes explicitly first`);
24
+ }
@@ -0,0 +1,89 @@
1
+ import path from "node:path";
2
+ import { args, arity, canonical, execute, git, locked, required, } from "./common.js";
3
+ import { commandContext, contextOptions, resolveWorkspace, } from "./command-context.js";
4
+ import { worktrees } from "./repos.js";
5
+ import { assertReady, GIT_WORKFLOW_DIRECTORY, GIT_WORKFLOW_LOCK, } from "./git-safety.js";
6
+ function revision(root, ref) {
7
+ return git(root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`);
8
+ }
9
+ export async function main(argv) {
10
+ const { values, positionals } = args(argv, [...contextOptions, "message"], ["squash", "cleanup", "dry-run"], { message: "m" });
11
+ const [command, ...rest] = positionals;
12
+ if (command !== "merge")
13
+ throw new Error("Unknown Git workflow command");
14
+ arity(rest, 0, 1);
15
+ if (values.message && !values.squash)
16
+ throw new Error("Invalid merge options");
17
+ const context = commandContext(values);
18
+ const source = resolveWorkspace(context);
19
+ return locked(path.join(context.store.root, GIT_WORKFLOW_DIRECTORY), GIT_WORKFLOW_LOCK, async () => {
20
+ const selected = rest[0] ?? context.defaultBase.replace(/^origin\//, "");
21
+ const targetRecord = worktrees(context.repo).find((record) => record.branch === `refs/heads/${selected}`);
22
+ if (!targetRecord)
23
+ throw new Error(`Target must be a checked-out local branch: ${selected}`);
24
+ const target = canonical(targetRecord.worktree);
25
+ // Main and managed worktrees are the only mutation targets.
26
+ resolveWorkspace(context, target === context.repo ? "@" : target);
27
+ if (target === source.path)
28
+ throw new Error("Source and target are the same worktree");
29
+ if ("locked" in targetRecord)
30
+ throw new Error("Target worktree is locked");
31
+ assertReady(source.path, true, source.branch);
32
+ assertReady(target, true, selected);
33
+ const sourceHead = revision(source.path, "HEAD");
34
+ const targetHead = revision(target, "HEAD");
35
+ const mergeBase = git(target, "merge-base", targetHead, sourceHead);
36
+ if (!values.squash &&
37
+ mergeBase !== targetHead &&
38
+ mergeBase !== sourceHead)
39
+ throw new Error("Fast-forward merge is not possible. Rebase the feature explicitly first");
40
+ if (values.squash)
41
+ required(values, "message");
42
+ if (values["dry-run"]) {
43
+ console.log(`Would ${values.squash ? "squash merge" : "fast-forward merge"} ${source.branch} into ${selected}${values.cleanup ? " and remove the clean source worktree" : ""}`);
44
+ return 0;
45
+ }
46
+ const result = await execute([
47
+ "git",
48
+ "-C",
49
+ target,
50
+ "merge",
51
+ ...(values.squash ? ["--squash"] : ["--ff-only"]),
52
+ sourceHead,
53
+ ], target);
54
+ process.stdout.write(result.stdout);
55
+ if (result.code)
56
+ throw new Error(`${result.stderr.trim()}\nMerge did not complete in ${target}; source workspace was retained. Inspect git status before continuing`);
57
+ if (values.squash) {
58
+ const integratedTree = git(target, "write-tree");
59
+ const staged = await execute(["git", "-C", target, "diff", "--cached", "--quiet"], target);
60
+ if (staged.code > 1)
61
+ throw new Error(staged.stderr || "Cannot inspect squash result");
62
+ if (staged.code === 1)
63
+ console.log(git(target, "commit", "-m", required(values, "message")));
64
+ if (git(target, "rev-parse", "HEAD^{tree}") !== integratedTree)
65
+ throw new Error("Squash result changed during commit; source workspace retained");
66
+ }
67
+ assertReady(target, true, selected);
68
+ if (!values.squash &&
69
+ git(target, "merge-base", "HEAD", sourceHead) !== sourceHead)
70
+ throw new Error("Target no longer contains the source; cleanup skipped");
71
+ if (!values.cleanup)
72
+ return 0;
73
+ // Recheck source immediately before removal; never force worktree/branch deletion.
74
+ assertReady(source.path, true, source.branch);
75
+ if (revision(source.path, "HEAD") !== sourceHead)
76
+ throw new Error("Source changed during merge; cleanup skipped");
77
+ if (source.path === context.repo)
78
+ throw new Error("Integration succeeded; main checkout cannot be removed");
79
+ process.chdir(target);
80
+ git(context.repo, "worktree", "remove", source.path);
81
+ await context.store.locked(() => context.store.archiveWorktrees([source.path]));
82
+ const deleted = await execute(["git", "-C", target, "branch", "-d", "--", source.branch], target);
83
+ if (deleted.code)
84
+ console.log(`Worktree removed; branch ${source.branch} retained: ${deleted.stderr.trim()}`);
85
+ else
86
+ process.stdout.write(deleted.stdout);
87
+ return 0;
88
+ });
89
+ }