@sven1103/opencode-worktree-workflow 0.3.0 → 0.5.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 (3) hide show
  1. package/README.md +25 -11
  2. package/package.json +4 -1
  3. package/src/index.js +304 -59
package/README.md CHANGED
@@ -36,6 +36,7 @@ npm install @sven1103/opencode-worktree-workflow
36
36
  OpenCode loads custom commands from either `.opencode/commands/` (project) or `~/.config/opencode/commands/` (global).
37
37
 
38
38
  This repo publishes `wt-new.md` and `wt-clean.md` as GitHub Release assets so you can install them without browsing the repository.
39
+ For a plain-language explanation of what each release contains, how it is produced, and how to verify it before installing, see `docs/releases.md`.
39
40
 
40
41
  Project install (latest release):
41
42
 
@@ -76,24 +77,34 @@ curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/
76
77
 
77
78
  ## What the plugin provides
78
79
 
79
- - `worktree_prepare`: create a worktree and matching branch from the latest default-branch commit
80
- - `worktree_cleanup`: preview or remove merged worktrees
80
+ - `worktree_prepare`: create a worktree and matching branch from the latest configured base-branch commit, or the default branch when no base branch is configured
81
+ - `worktree_cleanup`: preview all connected worktrees against the configured base branch, auto-clean safe ones, and optionally remove selected review items
81
82
 
82
83
  This package currently focuses on plugin distribution. Slash command packaging can be layered on later.
83
84
 
84
85
  ## Optional project configuration
85
86
 
86
- You can override the defaults in `opencode.json`, `opencode.jsonc`, or `.opencode/worktree-workflow.json`:
87
+ OpenCode's native `opencode.json` and `opencode.jsonc` files are schema-validated, so they can load this plugin through the standard `plugin` key but they cannot store a custom `worktreeWorkflow` block.
88
+
89
+ To override this plugin's defaults, put a sidecar config file at `.opencode/worktree-workflow.json`:
87
90
 
88
91
  ```json
89
92
  {
90
- "worktreeWorkflow": {
91
- "branchPrefix": "wt/",
92
- "remote": "origin",
93
- "worktreeRoot": ".worktrees/$REPO",
94
- "cleanupMode": "preview",
95
- "protectedBranches": ["release"]
96
- }
93
+ "branchPrefix": "wt/",
94
+ "remote": "origin",
95
+ "baseBranch": "release/v0.4.0",
96
+ "worktreeRoot": ".worktrees/$REPO",
97
+ "cleanupMode": "preview",
98
+ "protectedBranches": ["release"]
99
+ }
100
+ ```
101
+
102
+ Use `opencode.json` only to load the npm plugin itself:
103
+
104
+ ```json
105
+ {
106
+ "$schema": "https://opencode.ai/config.json",
107
+ "plugin": ["@sven1103/opencode-worktree-workflow"]
97
108
  }
98
109
  ```
99
110
 
@@ -101,6 +112,7 @@ Supported settings:
101
112
 
102
113
  - `branchPrefix`: prefix for generated worktree branches
103
114
  - `remote`: remote used to detect the default branch and fetch updates
115
+ - `baseBranch`: optional branch name used as the creation and cleanup base; defaults to the remote's default branch
104
116
  - `worktreeRoot`: destination root for new worktrees; supports `$REPO`, `$ROOT`, and `$ROOT_PARENT`
105
117
  - `cleanupMode`: default cleanup behavior, either `preview` or `apply`
106
118
  - `protectedBranches`: branches that should never be auto-cleaned
@@ -109,6 +121,8 @@ Supported settings:
109
121
 
110
122
  This repo is prepared for npm publishing from GitHub Actions using npm trusted publishing.
111
123
 
124
+ If you consume releases instead of contributing to the repo, `docs/releases.md` is the end-user guide for understanding what the published npm package and GitHub Release assets include.
125
+
112
126
  Typical release flow:
113
127
 
114
128
  1. Publish the package once manually to create it on npm.
@@ -117,7 +131,7 @@ Typical release flow:
117
131
 
118
132
  The release workflow creates a `release/v<version>` branch from `main`, updates `package.json` and `package-lock.json`, commits the version bump there, creates a matching `v<version>` tag, and pushes the branch and tag.
119
133
 
120
- The release workflow then explicitly starts the publish workflow for that tag, which verifies the tag matches `package.json` before running `npm publish` using OIDC, without storing an `NPM_TOKEN` secret. Merge the release branch back to `main` afterward if you want the version bump recorded on the default branch.
134
+ The release workflow then explicitly starts the publish workflow for that tag, which verifies the tag matches `package.json`, runs `npm publish` using OIDC without storing an `NPM_TOKEN` secret, and creates or updates the GitHub Release with generated release notes plus the command assets. Merge the release branch back to `main` afterward if you want the version bump recorded on the default branch.
121
135
 
122
136
  ## Local development
123
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sven1103/opencode-worktree-workflow",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCode plugin for creating and cleaning up git worktrees.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -32,6 +32,9 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
+ "scripts": {
36
+ "test": "node --test"
37
+ },
35
38
  "dependencies": {
36
39
  "@opencode-ai/plugin": "1.3.0",
37
40
  "jsonc-parser": "^3.3.1"
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ import { tool } from "@opencode-ai/plugin";
7
7
  const DEFAULTS = {
8
8
  branchPrefix: "wt/",
9
9
  remote: "origin",
10
+ baseBranch: null,
10
11
  worktreeRoot: ".worktrees/$REPO",
11
12
  cleanupMode: "preview",
12
13
  protectedBranches: [],
@@ -111,34 +112,74 @@ function parseWorktreeList(output) {
111
112
  return entries;
112
113
  }
113
114
 
114
- function formatPreview(candidates, defaultBranch) {
115
- if (candidates.length === 0) {
116
- return [
117
- `No merged worktrees are ready for cleanup against ${defaultBranch}.`,
118
- "",
119
- "Use `/wt-clean apply` after merged branches appear here.",
120
- ].join("\n");
115
+ function shellQuote(value) {
116
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
117
+ }
118
+
119
+ function formatWorktreeSummary(item) {
120
+ return `${item.branch || "(detached)"} -> ${item.path}${item.head ? ` (${item.head.slice(0, 12)})` : ""}`;
121
+ }
122
+
123
+ function formatCopyPasteCommands(item) {
124
+ const selector = item.branch || item.path;
125
+ const branchFlag = item.status === "safe" ? "-d" : "-D";
126
+
127
+ return [
128
+ ` copy: /wt-clean apply ${selector}`,
129
+ ` git: git worktree remove ${shellQuote(item.path)} && git branch ${branchFlag} ${shellQuote(item.branch)}`,
130
+ ];
131
+ }
132
+
133
+ function formatPreviewSection(title, items, { includeCommands = false } = {}) {
134
+ if (items.length === 0) {
135
+ return [title, "- none"];
121
136
  }
122
137
 
138
+ const lines = [title];
139
+
140
+ for (const item of items) {
141
+ lines.push(`- ${formatWorktreeSummary(item)}: ${item.reason}`);
142
+
143
+ if (includeCommands && item.branch) {
144
+ lines.push(...formatCopyPasteCommands(item));
145
+ }
146
+ }
147
+
148
+ return lines;
149
+ }
150
+
151
+ function formatPreview(grouped, defaultBranch) {
123
152
  return [
124
- `Merged worktrees ready for cleanup against ${defaultBranch}:`,
125
- ...candidates.map(
126
- (candidate) =>
127
- `- ${candidate.branch} -> ${candidate.path}${candidate.head ? ` (${candidate.head.slice(0, 12)})` : ""}`,
128
- ),
153
+ `Worktrees connected to this repository against ${defaultBranch}:`,
154
+ "",
155
+ ...formatPreviewSection("Safe to clean automatically:", grouped.safe, { includeCommands: true }),
129
156
  "",
130
- "Run `/wt-clean apply` to remove these worktrees and delete their local branches.",
157
+ ...formatPreviewSection("Needs review before cleanup:", grouped.review, { includeCommands: true }),
158
+ "",
159
+ ...formatPreviewSection("Not cleanable here:", grouped.blocked),
160
+ "",
161
+ "Run `/wt-clean apply` to remove only the safe group.",
162
+ "Run `/wt-clean apply <branch-or-path>` to also remove selected review items.",
131
163
  ].join("\n");
132
164
  }
133
165
 
134
- function formatCleanupSummary(defaultBranch, removed, failed) {
135
- const lines = [`Cleaned worktrees merged into ${defaultBranch}:`];
166
+ function formatCleanupSummary(defaultBranch, removed, failed, requestedSelectors) {
167
+ const lines = [`Cleaned worktrees relative to ${defaultBranch}:`];
136
168
 
137
169
  if (removed.length === 0) {
138
170
  lines.push("- none removed");
139
171
  } else {
140
172
  for (const item of removed) {
141
- lines.push(`- removed ${item.branch} -> ${item.path}`);
173
+ const modeLabel = item.selected ? "selected" : "auto";
174
+ lines.push(`- removed (${modeLabel}) ${item.branch} -> ${item.path}`);
175
+ }
176
+ }
177
+
178
+ if (requestedSelectors.length > 0) {
179
+ lines.push("");
180
+ lines.push("Requested selectors:");
181
+ for (const selector of requestedSelectors) {
182
+ lines.push(`- ${selector}`);
142
183
  }
143
184
  }
144
185
 
@@ -146,13 +187,166 @@ function formatCleanupSummary(defaultBranch, removed, failed) {
146
187
  lines.push("");
147
188
  lines.push("Cleanup skipped for:");
148
189
  for (const item of failed) {
149
- lines.push(`- ${item.branch} -> ${item.path}: ${item.reason}`);
190
+ lines.push(`- ${item.branch || item.selector} -> ${item.path || "(no path)"}: ${item.reason}`);
150
191
  }
151
192
  }
152
193
 
153
194
  return lines.join("\n");
154
195
  }
155
196
 
197
+ function splitCleanupToken(value) {
198
+ if (typeof value !== "string") {
199
+ return [];
200
+ }
201
+
202
+ return value
203
+ .trim()
204
+ .split(/\s+/)
205
+ .filter(Boolean);
206
+ }
207
+
208
+ function parseCleanupRawArguments(raw) {
209
+ const tokens = splitCleanupToken(raw);
210
+
211
+ if (tokens[0] === "apply") {
212
+ return {
213
+ mode: "apply",
214
+ selectors: tokens.slice(1),
215
+ };
216
+ }
217
+
218
+ if (tokens[0] === "preview") {
219
+ return {
220
+ mode: "preview",
221
+ selectors: tokens.slice(1),
222
+ };
223
+ }
224
+
225
+ return {
226
+ mode: null,
227
+ selectors: tokens,
228
+ };
229
+ }
230
+
231
+ function normalizeCleanupArgs(args, config) {
232
+ const selectors = Array.isArray(args.selectors) ? [...args.selectors] : [];
233
+ const normalizedSelectors = [];
234
+ const rawArgs = parseCleanupRawArguments(args.raw);
235
+ let explicitMode = rawArgs.mode;
236
+
237
+ if (rawArgs.selectors.length > 0) {
238
+ selectors.unshift(...rawArgs.selectors);
239
+ }
240
+
241
+ if (typeof args.mode === "string" && args.mode.trim()) {
242
+ const modeValue = args.mode.trim();
243
+ if (modeValue === "apply" || modeValue === "preview") {
244
+ explicitMode = modeValue;
245
+ } else {
246
+ selectors.unshift(...splitCleanupToken(modeValue));
247
+ }
248
+ }
249
+
250
+ for (const selector of selectors) {
251
+ if (typeof selector !== "string") {
252
+ continue;
253
+ }
254
+
255
+ if (selector.includes(" ")) {
256
+ normalizedSelectors.push(...splitCleanupToken(selector));
257
+ continue;
258
+ }
259
+
260
+ normalizedSelectors.push(selector);
261
+ }
262
+
263
+ const inlineApply = normalizedSelectors[0] === "apply";
264
+
265
+ if (inlineApply) {
266
+ normalizedSelectors.shift();
267
+ }
268
+
269
+ const mode = explicitMode === "apply" || inlineApply ? "apply" : explicitMode || config.cleanupMode;
270
+
271
+ return {
272
+ mode,
273
+ selectors: normalizedSelectors,
274
+ };
275
+ }
276
+
277
+ export const __internal = {
278
+ parseCleanupRawArguments,
279
+ normalizeCleanupArgs,
280
+ };
281
+
282
+ function selectorMatches(item, selector) {
283
+ const normalized = path.resolve(selector);
284
+ return item.branch === selector || item.path === normalized;
285
+ }
286
+
287
+ function classifyEntry(entry, repoRoot, activeWorktree, protectedBranches, mergedIntoBase) {
288
+ const entryPath = path.resolve(entry.path);
289
+ const branchName = entry.branch;
290
+ const item = {
291
+ branch: branchName,
292
+ path: entryPath,
293
+ head: entry.head,
294
+ detached: Boolean(entry.detached),
295
+ };
296
+
297
+ if (!branchName || entry.detached) {
298
+ return {
299
+ ...item,
300
+ status: "blocked",
301
+ reason: !branchName ? "no branch" : "detached HEAD",
302
+ selectable: false,
303
+ };
304
+ }
305
+
306
+ if (entryPath === path.resolve(repoRoot)) {
307
+ return {
308
+ ...item,
309
+ status: "blocked",
310
+ reason: entryPath === activeWorktree ? "repository root, current worktree, protected branch" : "repository root",
311
+ selectable: false,
312
+ };
313
+ }
314
+
315
+ if (entryPath === activeWorktree) {
316
+ return {
317
+ ...item,
318
+ status: "blocked",
319
+ reason: "current worktree",
320
+ selectable: false,
321
+ };
322
+ }
323
+
324
+ if (protectedBranches.has(branchName)) {
325
+ return {
326
+ ...item,
327
+ status: "blocked",
328
+ reason: "protected branch",
329
+ selectable: false,
330
+ };
331
+ }
332
+
333
+ if (mergedIntoBase) {
334
+ return {
335
+ ...item,
336
+ status: "safe",
337
+ reason: "merged into base branch by git ancestry",
338
+ selectable: true,
339
+ };
340
+ }
341
+
342
+ return {
343
+ ...item,
344
+ status: "review",
345
+ reason: "not merged into base branch by git ancestry",
346
+ selectable: true,
347
+ };
348
+ }
349
+
156
350
  export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
157
351
  async function git(args, options = {}) {
158
352
  const cwd = options.cwd ?? directory;
@@ -194,6 +388,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
194
388
  return {
195
389
  branchPrefix: normalizeBranchPrefix(merged.branchPrefix ?? DEFAULTS.branchPrefix),
196
390
  remote: merged.remote || DEFAULTS.remote,
391
+ baseBranch: typeof merged.baseBranch === "string" && merged.baseBranch.trim() ? merged.baseBranch.trim() : null,
197
392
  cleanupMode: merged.cleanupMode === "apply" ? "apply" : DEFAULTS.cleanupMode,
198
393
  protectedBranches: Array.isArray(merged.protectedBranches)
199
394
  ? merged.protectedBranches.filter((value) => typeof value === "string")
@@ -254,16 +449,20 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
254
449
  throw new Error("Could not determine the default branch for this repository.");
255
450
  }
256
451
 
257
- async function getBaseRef(repoRoot, remote, defaultBranch) {
258
- await git(["fetch", "--prune", remote, defaultBranch], { cwd: repoRoot });
452
+ async function resolveBaseBranch(repoRoot, remote, configuredBaseBranch) {
453
+ return configuredBaseBranch || getDefaultBranch(repoRoot, remote);
454
+ }
455
+
456
+ async function getBaseRef(repoRoot, remote, baseBranch) {
457
+ await git(["fetch", "--prune", remote, baseBranch], { cwd: repoRoot });
259
458
 
260
- const remoteRef = `refs/remotes/${remote}/${defaultBranch}`;
459
+ const remoteRef = `refs/remotes/${remote}/${baseBranch}`;
261
460
  const remoteExists = await git(["show-ref", "--verify", "--quiet", remoteRef], {
262
461
  cwd: repoRoot,
263
462
  allowFailure: true,
264
463
  });
265
464
 
266
- return remoteExists.exitCode === 0 ? `${remote}/${defaultBranch}` : defaultBranch;
465
+ return remoteExists.exitCode === 0 ? `${remote}/${baseBranch}` : baseBranch;
267
466
  }
268
467
 
269
468
  async function ensureBranchDoesNotExist(repoRoot, branchName) {
@@ -290,7 +489,8 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
290
489
  const repoRoot = await getRepoRoot();
291
490
  const config = await loadWorkflowConfig(repoRoot);
292
491
  const defaultBranch = await getDefaultBranch(repoRoot, config.remote);
293
- const baseRef = await getBaseRef(repoRoot, config.remote, defaultBranch);
492
+ const baseBranch = await resolveBaseBranch(repoRoot, config.remote, config.baseBranch);
493
+ const baseRef = await getBaseRef(repoRoot, config.remote, baseBranch);
294
494
  const baseCommit = (await git(["rev-parse", baseRef], { cwd: repoRoot })).stdout;
295
495
  const slug = slugifyTitle(args.title);
296
496
 
@@ -315,7 +515,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
315
515
  const branchCommit = (await git(["rev-parse", branchName], { cwd: repoRoot })).stdout;
316
516
  if (branchCommit !== baseCommit) {
317
517
  throw new Error(
318
- `New branch ${branchName} does not match ${defaultBranch} at ${baseCommit}. Found ${branchCommit} instead.`,
518
+ `New branch ${branchName} does not match ${baseBranch} at ${baseCommit}. Found ${branchCommit} instead.`,
319
519
  );
320
520
  }
321
521
 
@@ -324,71 +524,113 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
324
524
  `- branch: ${branchName}`,
325
525
  `- worktree: ${worktreePath}`,
326
526
  `- default branch: ${defaultBranch}`,
527
+ `- base branch: ${baseBranch}`,
327
528
  `- base ref: ${baseRef}`,
328
529
  `- base commit: ${baseCommit}`,
329
530
  ].join("\n");
330
531
  },
331
532
  }),
332
533
  worktree_cleanup: tool({
333
- description: "Preview or clean merged git worktrees",
534
+ description: "Preview or clean git worktrees",
334
535
  args: {
335
- mode: tool.schema
336
- .enum(["preview", "apply"])
337
- .default("preview")
338
- .describe("Preview cleanup candidates or remove them"),
536
+ mode: tool.schema.string().optional().describe("Preview cleanup candidates or remove them"),
537
+ raw: tool.schema.string().optional().describe("Raw cleanup arguments from slash commands"),
538
+ selectors: tool.schema
539
+ .array(tool.schema.string())
540
+ .default([])
541
+ .describe("Optional branch names or worktree paths to remove explicitly"),
339
542
  },
340
543
  async execute(args, context) {
341
- context.metadata({ title: `Clean worktrees (${args.mode})` });
342
-
343
544
  const repoRoot = await getRepoRoot();
344
545
  const config = await loadWorkflowConfig(repoRoot);
546
+ const normalizedArgs = normalizeCleanupArgs(args, config);
547
+
548
+ context.metadata({ title: `Clean worktrees (${normalizedArgs.mode})` });
549
+
345
550
  const defaultBranch = await getDefaultBranch(repoRoot, config.remote);
346
- const baseRef = await getBaseRef(repoRoot, config.remote, defaultBranch);
551
+ const baseBranch = await resolveBaseBranch(repoRoot, config.remote, config.baseBranch);
552
+ const baseRef = await getBaseRef(repoRoot, config.remote, baseBranch);
347
553
  const activeWorktree = path.resolve(context.worktree || repoRoot);
348
554
  const worktreeList = await git(["worktree", "list", "--porcelain"], { cwd: repoRoot });
349
555
  const entries = parseWorktreeList(worktreeList.stdout);
350
- const protectedBranches = new Set([defaultBranch, ...config.protectedBranches]);
351
- const candidates = [];
556
+ const protectedBranches = new Set([defaultBranch, baseBranch, ...config.protectedBranches]);
557
+ const grouped = {
558
+ safe: [],
559
+ review: [],
560
+ blocked: [],
561
+ };
352
562
 
353
563
  for (const entry of entries) {
354
- const entryPath = path.resolve(entry.path);
355
564
  const branchName = entry.branch;
565
+ let mergedIntoBase = false;
356
566
 
357
- if (!branchName || entry.detached) {
358
- continue;
567
+ if (branchName && !entry.detached) {
568
+ const merged = await git(["merge-base", "--is-ancestor", branchName, baseRef], {
569
+ cwd: repoRoot,
570
+ allowFailure: true,
571
+ });
572
+
573
+ mergedIntoBase = merged.exitCode === 0;
359
574
  }
360
575
 
361
- if (entryPath === path.resolve(repoRoot) || entryPath === activeWorktree) {
576
+ const classified = classifyEntry(
577
+ entry,
578
+ repoRoot,
579
+ activeWorktree,
580
+ protectedBranches,
581
+ mergedIntoBase,
582
+ );
583
+
584
+ grouped[classified.status].push(classified);
585
+ }
586
+
587
+ if (normalizedArgs.mode !== "apply") {
588
+ return formatPreview(grouped, baseBranch);
589
+ }
590
+
591
+ const requestedSelectors = [...new Set(normalizedArgs.selectors || [])];
592
+ const selected = [];
593
+ const failed = [];
594
+
595
+ for (const selector of requestedSelectors) {
596
+ const match = [...grouped.safe, ...grouped.review, ...grouped.blocked].find((item) =>
597
+ selectorMatches(item, selector),
598
+ );
599
+
600
+ if (!match) {
601
+ failed.push({
602
+ selector,
603
+ reason: "selector did not match any connected worktree",
604
+ });
362
605
  continue;
363
606
  }
364
607
 
365
- if (protectedBranches.has(branchName)) {
608
+ if (!match.selectable) {
609
+ failed.push({
610
+ ...match,
611
+ selector,
612
+ reason: `cannot remove via selector: ${match.reason}`,
613
+ });
366
614
  continue;
367
615
  }
368
616
 
369
- const merged = await git(["merge-base", "--is-ancestor", branchName, baseRef], {
370
- cwd: repoRoot,
371
- allowFailure: true,
372
- });
617
+ selected.push(match);
618
+ }
619
+
620
+ const targets = [...grouped.safe];
373
621
 
374
- if (merged.exitCode === 0) {
375
- candidates.push({
376
- branch: branchName,
377
- path: entryPath,
378
- head: entry.head,
622
+ for (const item of selected) {
623
+ if (!targets.some((target) => target.path === item.path)) {
624
+ targets.push({
625
+ ...item,
626
+ selected: true,
379
627
  });
380
628
  }
381
629
  }
382
630
 
383
- const requestedMode = args.mode || config.cleanupMode;
384
- if (requestedMode !== "apply") {
385
- return formatPreview(candidates, defaultBranch);
386
- }
387
-
388
631
  const removed = [];
389
- const failed = [];
390
632
 
391
- for (const candidate of candidates) {
633
+ for (const candidate of targets) {
392
634
  const removeWorktree = await git(["worktree", "remove", candidate.path], {
393
635
  cwd: repoRoot,
394
636
  allowFailure: true,
@@ -402,10 +644,13 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
402
644
  continue;
403
645
  }
404
646
 
405
- const deleteBranch = await git(["branch", "-d", candidate.branch], {
406
- cwd: repoRoot,
407
- allowFailure: true,
408
- });
647
+ const deleteBranch = await git(
648
+ ["branch", candidate.status === "safe" ? "-d" : "-D", candidate.branch],
649
+ {
650
+ cwd: repoRoot,
651
+ allowFailure: true,
652
+ },
653
+ );
409
654
 
410
655
  if (deleteBranch.exitCode !== 0) {
411
656
  failed.push({
@@ -423,7 +668,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
423
668
  allowFailure: true,
424
669
  });
425
670
 
426
- return formatCleanupSummary(defaultBranch, removed, failed);
671
+ return formatCleanupSummary(baseBranch, removed, failed, requestedSelectors);
427
672
  },
428
673
  }),
429
674
  },