gsd-pi 2.78.1-dev.9d08d820b → 2.78.1-dev.a7b6e59b7

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 (101) hide show
  1. package/README.md +1 -0
  2. package/dist/cli-auto-routing.d.ts +1 -0
  3. package/dist/cli-auto-routing.js +5 -0
  4. package/dist/cli.js +5 -14
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/google-search/index.js +2 -6
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +23 -11
  8. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +55 -21
  9. package/dist/resources/extensions/gsd/auto-prompts.js +6 -0
  10. package/dist/resources/extensions/gsd/auto-worktree.js +15 -0
  11. package/dist/resources/extensions/gsd/auto.js +25 -9
  12. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +12 -0
  13. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -0
  14. package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
  15. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  16. package/dist/resources/extensions/gsd/commands/handlers/ops.js +8 -0
  17. package/dist/resources/extensions/gsd/commands-worktree.js +309 -0
  18. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  19. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +2 -0
  20. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +2 -0
  21. package/dist/resources/extensions/gsd/worktree-resolver.js +24 -0
  22. package/dist/resources/extensions/mcp-client/index.js +0 -6
  23. package/dist/resources/skills/lint/SKILL.md +4 -0
  24. package/dist/resources/skills/review/SKILL.md +4 -0
  25. package/dist/resources/skills/test/SKILL.md +3 -0
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  57. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  58. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  59. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  60. package/dist/welcome-screen.js +27 -1
  61. package/package.json +1 -1
  62. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +278 -0
  63. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  65. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.js +125 -55
  67. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  68. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +319 -0
  69. package/packages/pi-coding-agent/src/core/agent-session.ts +128 -59
  70. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  71. package/src/resources/extensions/google-search/index.ts +2 -9
  72. package/src/resources/extensions/gsd/auto/run-unit.ts +23 -11
  73. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +60 -24
  74. package/src/resources/extensions/gsd/auto-prompts.ts +6 -0
  75. package/src/resources/extensions/gsd/auto-worktree.ts +15 -0
  76. package/src/resources/extensions/gsd/auto.ts +23 -6
  77. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
  78. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -0
  79. package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
  80. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  81. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  82. package/src/resources/extensions/gsd/commands-worktree.ts +383 -0
  83. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  84. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +2 -0
  85. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +2 -0
  86. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -0
  87. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +50 -27
  88. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +48 -0
  89. package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +25 -65
  90. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +34 -0
  91. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +8 -2
  92. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +12 -6
  93. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +235 -0
  94. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +85 -0
  95. package/src/resources/extensions/gsd/worktree-resolver.ts +24 -0
  96. package/src/resources/extensions/mcp-client/index.ts +0 -7
  97. package/src/resources/skills/lint/SKILL.md +4 -0
  98. package/src/resources/skills/review/SKILL.md +4 -0
  99. package/src/resources/skills/test/SKILL.md +3 -0
  100. /package/dist/web/standalone/.next/static/{-Ukk6_YxRd4GY4iUOnRUE → GlYncvckBGG33CSoJaSnB}/_buildManifest.js +0 -0
  101. /package/dist/web/standalone/.next/static/{-Ukk6_YxRd4GY4iUOnRUE → GlYncvckBGG33CSoJaSnB}/_ssgManifest.js +0 -0
@@ -0,0 +1,383 @@
1
+ // GSD-2 — In-TUI handler for /gsd worktree commands (list, merge, clean, remove).
2
+ //
3
+ // Mirrors the CLI subcommands in src/worktree-cli.ts but emits results via
4
+ // ctx.ui.notify() instead of writing colored output to stderr. Reuses the
5
+ // same extension modules (worktree-manager, native-git-bridge, etc.) so the
6
+ // behavior is identical to the CLI surface.
7
+
8
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
+ import { existsSync } from "node:fs";
10
+
11
+ import { projectRoot } from "./commands/context.js";
12
+ import {
13
+ listWorktrees,
14
+ removeWorktree,
15
+ mergeWorktreeToMain,
16
+ diffWorktreeAll,
17
+ diffWorktreeNumstat,
18
+ worktreeBranchName,
19
+ } from "./worktree-manager.js";
20
+ import {
21
+ nativeHasChanges,
22
+ nativeDetectMainBranch,
23
+ nativeCommitCountBetween,
24
+ } from "./native-git-bridge.js";
25
+ import { inferCommitType } from "./git-service.js";
26
+ import { autoCommitCurrentBranch } from "./worktree.js";
27
+ import { GSDError, GSD_GIT_ERROR } from "./errors.js";
28
+
29
+ // ─── Types ──────────────────────────────────────────────────────────────────
30
+
31
+ export interface WorktreeStatus {
32
+ name: string;
33
+ path: string;
34
+ branch: string;
35
+ exists: boolean;
36
+ filesChanged: number;
37
+ linesAdded: number;
38
+ linesRemoved: number;
39
+ uncommitted: boolean;
40
+ commits: number;
41
+ }
42
+
43
+ // ─── Status helper ─────────────────────────────────────────────────────────
44
+
45
+ function getStatus(basePath: string, name: string, wtPath: string): WorktreeStatus {
46
+ const diff = diffWorktreeAll(basePath, name);
47
+ const numstat = diffWorktreeNumstat(basePath, name);
48
+ const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
49
+ let linesAdded = 0;
50
+ let linesRemoved = 0;
51
+ for (const s of numstat) {
52
+ linesAdded += s.added;
53
+ linesRemoved += s.removed;
54
+ }
55
+
56
+ let uncommitted = false;
57
+ try {
58
+ uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
59
+ } catch {
60
+ // native check failure → treat as clean for display purposes
61
+ }
62
+
63
+ let commits = 0;
64
+ try {
65
+ const main = nativeDetectMainBranch(basePath);
66
+ commits = nativeCommitCountBetween(basePath, main, worktreeBranchName(name));
67
+ } catch {
68
+ // commit count unavailable → leave at 0
69
+ }
70
+
71
+ return {
72
+ name,
73
+ path: wtPath,
74
+ branch: worktreeBranchName(name),
75
+ exists: existsSync(wtPath),
76
+ filesChanged,
77
+ linesAdded,
78
+ linesRemoved,
79
+ uncommitted,
80
+ commits,
81
+ };
82
+ }
83
+
84
+ // ─── Formatters (exported for tests) ────────────────────────────────────────
85
+
86
+ export function formatWorktreeList(statuses: WorktreeStatus[]): string {
87
+ if (statuses.length === 0) {
88
+ return "No worktrees.\n\nCreate one from the CLI: gsd -w <name>";
89
+ }
90
+
91
+ const lines: string[] = [`Worktrees — ${statuses.length}`, ""];
92
+ for (const s of statuses) {
93
+ const badge = s.uncommitted
94
+ ? "(uncommitted)"
95
+ : s.filesChanged > 0
96
+ ? "(unmerged)"
97
+ : "(clean)";
98
+ lines.push(` ${s.name} ${badge}`);
99
+ lines.push(` branch ${s.branch}`);
100
+ lines.push(` path ${s.path}`);
101
+ if (s.filesChanged > 0) {
102
+ lines.push(
103
+ ` diff ${s.filesChanged} file${s.filesChanged === 1 ? "" : "s"}, +${s.linesAdded} -${s.linesRemoved}, ${s.commits} commit${s.commits === 1 ? "" : "s"}`,
104
+ );
105
+ }
106
+ lines.push("");
107
+ }
108
+ lines.push("Commands:");
109
+ lines.push(" /gsd worktree merge <name> Merge into main and clean up");
110
+ lines.push(" /gsd worktree remove <name> Remove a worktree (--force to skip safety checks)");
111
+ lines.push(" /gsd worktree clean Remove all merged/empty worktrees");
112
+ return lines.join("\n");
113
+ }
114
+
115
+ export function formatCleanKeepReason(status: WorktreeStatus): string {
116
+ if (!status.exists) {
117
+ return "directory missing — run 'git worktree prune' to unregister";
118
+ }
119
+
120
+ if (status.filesChanged > 0) {
121
+ return `${status.filesChanged} changed file${status.filesChanged === 1 ? "" : "s"}${status.uncommitted ? ", uncommitted" : ""}`;
122
+ }
123
+
124
+ return "uncommitted changes";
125
+ }
126
+
127
+ // ─── Subcommand: list ───────────────────────────────────────────────────────
128
+
129
+ async function handleList(ctx: ExtensionCommandContext): Promise<void> {
130
+ const basePath = projectRoot();
131
+ const worktrees = listWorktrees(basePath);
132
+ const statuses = worktrees.map((wt) => getStatus(basePath, wt.name, wt.path));
133
+ ctx.ui.notify(formatWorktreeList(statuses), "info");
134
+ }
135
+
136
+ // ─── Subcommand: merge ──────────────────────────────────────────────────────
137
+
138
+ async function handleMerge(args: string, ctx: ExtensionCommandContext): Promise<void> {
139
+ const basePath = projectRoot();
140
+ const worktrees = listWorktrees(basePath);
141
+ const trimmed = args.trim();
142
+
143
+ let target = trimmed;
144
+ if (!target) {
145
+ if (worktrees.length === 1) {
146
+ target = worktrees[0].name;
147
+ } else if (worktrees.length === 0) {
148
+ ctx.ui.notify("No worktrees to merge.", "info");
149
+ return;
150
+ } else {
151
+ const names = worktrees.map((w) => w.name).join(", ");
152
+ ctx.ui.notify(`Usage: /gsd worktree merge <name>\n\nWorktrees: ${names}`, "warning");
153
+ return;
154
+ }
155
+ }
156
+
157
+ const wt = worktrees.find((w) => w.name === target);
158
+ if (!wt) {
159
+ const available = worktrees.map((w) => w.name).join(", ") || "(none)";
160
+ ctx.ui.notify(`Worktree "${target}" not found.\n\nAvailable: ${available}`, "error");
161
+ return;
162
+ }
163
+
164
+ const status = getStatus(basePath, target, wt.path);
165
+ if (status.filesChanged === 0 && !status.uncommitted) {
166
+ try {
167
+ removeWorktree(basePath, target, { deleteBranch: true });
168
+ ctx.ui.notify(`Removed empty worktree ${target}.`, "info");
169
+ } catch (err) {
170
+ const msg = err instanceof Error ? err.message : String(err);
171
+ ctx.ui.notify(
172
+ `Worktree partially removed: ${msg}\n\nRun 'git worktree prune' to clean up any dangling registrations.`,
173
+ "error",
174
+ );
175
+ }
176
+ return;
177
+ }
178
+
179
+ if (status.uncommitted) {
180
+ try {
181
+ autoCommitCurrentBranch(wt.path, "worktree-merge", target);
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ ctx.ui.notify(
185
+ [
186
+ `Auto-commit before merge failed: ${msg}`,
187
+ "",
188
+ `Commit or stash changes in ${wt.path}, then re-run /gsd worktree merge ${target}.`,
189
+ ].join("\n"),
190
+ "error",
191
+ );
192
+ return;
193
+ }
194
+ }
195
+
196
+ const commitType = inferCommitType(target);
197
+ const mainBranch = nativeDetectMainBranch(basePath);
198
+ const commitMessage = `${commitType}: merge worktree ${target}\n\nGSD-Worktree: ${target}`;
199
+
200
+ try {
201
+ mergeWorktreeToMain(basePath, target, commitMessage);
202
+ } catch (err) {
203
+ const msg = err instanceof Error ? err.message : String(err);
204
+ if (err instanceof GSDError && err.code === GSD_GIT_ERROR) {
205
+ ctx.ui.notify(
206
+ `Merge requires the main branch to be checked out: ${msg}\n\nSwitch to ${mainBranch} (e.g. 'git checkout ${mainBranch}'), then re-run /gsd worktree merge ${target}.`,
207
+ "error",
208
+ );
209
+ } else {
210
+ ctx.ui.notify(
211
+ `Merge failed: ${msg}\n\nResolve conflicts manually, then run /gsd worktree merge ${target} again.`,
212
+ "error",
213
+ );
214
+ }
215
+ return;
216
+ }
217
+
218
+ const successLines = [
219
+ `Merged ${target} → ${mainBranch}`,
220
+ ` ${status.filesChanged} file${status.filesChanged === 1 ? "" : "s"}, +${status.linesAdded} -${status.linesRemoved}`,
221
+ ` commit: ${commitMessage.split("\n")[0]}`,
222
+ ];
223
+
224
+ try {
225
+ removeWorktree(basePath, target, { deleteBranch: true });
226
+ ctx.ui.notify(successLines.join("\n"), "info");
227
+ } catch (err) {
228
+ const msg = err instanceof Error ? err.message : String(err);
229
+ const cleanupLines = [
230
+ ...successLines,
231
+ "",
232
+ `Cleanup failed after the merge succeeded: ${msg}`,
233
+ err instanceof GSDError && err.code === GSD_GIT_ERROR
234
+ ? `Switch to ${mainBranch} (e.g. 'git checkout ${mainBranch}'), then remove the worktree manually with /gsd worktree remove ${target} --force.`
235
+ : `Remove the worktree manually with /gsd worktree remove ${target} --force, or run 'git worktree prune' to clean up dangling registrations.`,
236
+ ];
237
+ ctx.ui.notify(cleanupLines.join("\n"), "warning");
238
+ }
239
+ }
240
+
241
+ // ─── Subcommand: clean ──────────────────────────────────────────────────────
242
+
243
+ async function handleClean(ctx: ExtensionCommandContext): Promise<void> {
244
+ const basePath = projectRoot();
245
+ const worktrees = listWorktrees(basePath);
246
+ if (worktrees.length === 0) {
247
+ ctx.ui.notify("No worktrees to clean.", "info");
248
+ return;
249
+ }
250
+
251
+ const removed: string[] = [];
252
+ const kept: string[] = [];
253
+ for (const wt of worktrees) {
254
+ const status = getStatus(basePath, wt.name, wt.path);
255
+ if (status.filesChanged === 0 && !status.uncommitted) {
256
+ try {
257
+ removeWorktree(basePath, wt.name, { deleteBranch: true });
258
+ removed.push(wt.name);
259
+ } catch (err) {
260
+ const msg = err instanceof Error ? err.message : String(err);
261
+ kept.push(`${wt.name} (failed: ${msg})`);
262
+ }
263
+ } else {
264
+ const reason = formatCleanKeepReason(status);
265
+ kept.push(`${wt.name} (${reason})`);
266
+ }
267
+ }
268
+
269
+ const lines: string[] = [`Cleaned ${removed.length} worktree${removed.length === 1 ? "" : "s"}.`];
270
+ if (removed.length > 0) {
271
+ lines.push("", "Removed:");
272
+ for (const n of removed) lines.push(` ✓ ${n}`);
273
+ }
274
+ if (kept.length > 0) {
275
+ lines.push("", "Kept:");
276
+ for (const n of kept) lines.push(` ─ ${n}`);
277
+ }
278
+ ctx.ui.notify(lines.join("\n"), "info");
279
+ }
280
+
281
+ // ─── Subcommand: remove ─────────────────────────────────────────────────────
282
+
283
+ async function handleRemove(args: string, ctx: ExtensionCommandContext): Promise<void> {
284
+ const basePath = projectRoot();
285
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
286
+ const force = tokens.includes("--force");
287
+ const name = tokens.find((t) => t !== "--force");
288
+ if (!name) {
289
+ ctx.ui.notify("Usage: /gsd worktree remove <name> [--force]", "warning");
290
+ return;
291
+ }
292
+
293
+ const worktrees = listWorktrees(basePath);
294
+ const wt = worktrees.find((w) => w.name === name);
295
+ if (!wt) {
296
+ const available = worktrees.map((w) => w.name).join(", ") || "(none)";
297
+ ctx.ui.notify(`Worktree "${name}" not found.\n\nAvailable: ${available}`, "error");
298
+ return;
299
+ }
300
+
301
+ const status = getStatus(basePath, name, wt.path);
302
+ if ((status.filesChanged > 0 || status.uncommitted) && !force) {
303
+ ctx.ui.notify(
304
+ [
305
+ `Worktree "${name}" has pending changes (${formatCleanKeepReason(status)}).`,
306
+ "",
307
+ ` Merge first: /gsd worktree merge ${name}`,
308
+ ` Or force-remove: /gsd worktree remove ${name} --force`,
309
+ ].join("\n"),
310
+ "warning",
311
+ );
312
+ return;
313
+ }
314
+
315
+ try {
316
+ removeWorktree(basePath, name, { deleteBranch: true });
317
+ ctx.ui.notify(`Removed worktree ${name}.`, "info");
318
+ } catch (err) {
319
+ const msg = err instanceof Error ? err.message : String(err);
320
+ ctx.ui.notify(
321
+ `Worktree partially removed: ${msg}\n\nRun 'git worktree prune' to clean up any dangling registrations.`,
322
+ "error",
323
+ );
324
+ }
325
+ }
326
+
327
+ // ─── Help text ──────────────────────────────────────────────────────────────
328
+
329
+ const HELP_TEXT = [
330
+ "Usage: /gsd worktree <command> [args]",
331
+ "",
332
+ "Commands:",
333
+ " list Show all worktrees with status",
334
+ " merge [name] Merge a worktree into main, then remove it",
335
+ " remove <name> [--force] Remove a worktree (refuses unmerged changes without --force)",
336
+ " clean Remove all merged/empty worktrees",
337
+ "",
338
+ "The -w flag (CLI only) creates/resumes worktrees on session start:",
339
+ " gsd -w Auto-name a new worktree, or resume the only active one",
340
+ " gsd -w my-feature Create or resume a named worktree",
341
+ ].join("\n");
342
+
343
+ // ─── Dispatcher ─────────────────────────────────────────────────────────────
344
+
345
+ export async function handleWorktree(args: string, ctx: ExtensionCommandContext): Promise<void> {
346
+ const trimmed = args.trim();
347
+ const lowered = trimmed.toLowerCase();
348
+
349
+ if (!lowered || lowered === "help" || lowered === "--help" || lowered === "-h") {
350
+ ctx.ui.notify(HELP_TEXT, "info");
351
+ return;
352
+ }
353
+
354
+ try {
355
+ if (lowered === "list" || lowered === "ls") {
356
+ await handleList(ctx);
357
+ return;
358
+ }
359
+ if (lowered === "merge" || lowered.startsWith("merge ")) {
360
+ await handleMerge(trimmed.replace(/^merge\s*/i, ""), ctx);
361
+ return;
362
+ }
363
+ if (lowered === "clean") {
364
+ await handleClean(ctx);
365
+ return;
366
+ }
367
+ if (
368
+ lowered === "remove" ||
369
+ lowered.startsWith("remove ") ||
370
+ lowered === "rm" ||
371
+ lowered.startsWith("rm ")
372
+ ) {
373
+ const stripped = trimmed.replace(/^(remove|rm)\s*/i, "");
374
+ await handleRemove(stripped, ctx);
375
+ return;
376
+ }
377
+
378
+ ctx.ui.notify(`Unknown worktree command: ${trimmed}\n\n${HELP_TEXT}`, "warning");
379
+ } catch (err) {
380
+ const msg = err instanceof Error ? err.message : String(err);
381
+ ctx.ui.notify(`Worktree command failed: ${msg}`, "error");
382
+ }
383
+ }
@@ -1,3 +1,5 @@
1
+ **Working directory:** `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
2
+
1
3
  Discuss milestone {{milestoneId}} ("{{milestoneTitle}}"). Identify gray areas, ask the user about them, and write `{{milestoneId}}-CONTEXT.md` in the milestone directory with the decisions. Use the **Context** output template below. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow; do not override required artifact rules.
2
4
 
3
5
  **Structured questions available: {{structuredQuestionsAvailable}}**
@@ -1,5 +1,7 @@
1
1
  # Parallel Slice Research
2
2
 
3
+ **Working directory:** `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
4
+
3
5
  You are dispatching parallel research agents for **{{sliceCount}} slices** in milestone **{{mid}} — {{midTitle}}**.
4
6
 
5
7
  ## Slices to Research
@@ -1,5 +1,7 @@
1
1
  You are executing GSD auto-mode.
2
2
 
3
+ **Working directory:** `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
4
+
3
5
  ## UNIT: Rewrite Documents — Apply Override(s) for Milestone {{milestoneId}} ("{{milestoneTitle}}")
4
6
 
5
7
  An override was issued by the user that changes a fundamental decision or approach. Your job is to propagate this change across all active planning documents so they are internally consistent and future tasks execute correctly.
@@ -63,6 +63,7 @@ function makeMockSession(opts?: {
63
63
  const session = {
64
64
  active: true,
65
65
  verbose: false,
66
+ basePath: process.cwd(),
66
67
  cmdCtx: {
67
68
  newSession: (options?: { abortSignal?: AbortSignal }) => {
68
69
  opts?.onNewSessionStart?.(session);
@@ -2,12 +2,56 @@
2
2
  //
3
3
  // Guards the skill-trigger table in system-context.ts against accidental
4
4
  // regression. Every entry must have a non-empty trigger + skill, and the
5
- // skills added in PR #4505 must remain present.
5
+ // skills added in PR #4505 and PR #5060 must remain present.
6
6
  import { test } from 'node:test';
7
7
  import assert from 'node:assert/strict';
8
8
 
9
9
  import { BUNDLED_SKILL_TRIGGERS } from '../bootstrap/system-context.ts';
10
10
 
11
+ const PR_4505_BUNDLED_SKILLS = [
12
+ 'review',
13
+ 'test',
14
+ 'lint',
15
+ 'make-interfaces-feel-better',
16
+ 'accessibility',
17
+ 'grill-me',
18
+ 'design-an-interface',
19
+ 'tdd',
20
+ 'write-milestone-brief',
21
+ 'decompose-into-slices',
22
+ 'spike-wrap-up',
23
+ 'verify-before-complete',
24
+ 'create-mcp-server',
25
+ 'write-docs',
26
+ 'forensics',
27
+ 'handoff',
28
+ 'security-review',
29
+ 'api-design',
30
+ 'dependency-upgrade',
31
+ 'observability',
32
+ ] as const;
33
+
34
+ const PR_5060_BUNDLED_SKILLS = [
35
+ 'react-best-practices',
36
+ 'core-web-vitals',
37
+ 'github-workflows',
38
+ 'web-quality-audit',
39
+ 'agent-browser',
40
+ 'web-design-guidelines',
41
+ 'userinterface-wiki',
42
+ 'create-skill',
43
+ 'create-gsd-extension',
44
+ 'create-workflow',
45
+ 'code-optimizer',
46
+ ] as const;
47
+
48
+ function assertBundledSkillsRegistered(label: string, expectedSkills: readonly string[]): void {
49
+ const registered = new Set(BUNDLED_SKILL_TRIGGERS.map(e => e.skill));
50
+ for (const skill of expectedSkills) {
51
+ assert.ok(registered.has(skill), `${label}: expected bundled skill "${skill}" to be registered`);
52
+ }
53
+ }
54
+
11
55
  test('BUNDLED_SKILL_TRIGGERS: every entry has a non-empty trigger and skill', () => {
12
56
  assert.ok(BUNDLED_SKILL_TRIGGERS.length > 0, 'table should not be empty');
13
57
  for (const { trigger, skill } of BUNDLED_SKILL_TRIGGERS) {
@@ -17,32 +61,11 @@ test('BUNDLED_SKILL_TRIGGERS: every entry has a non-empty trigger and skill', ()
17
61
  });
18
62
 
19
63
  test('BUNDLED_SKILL_TRIGGERS: PR #4505 bundled skills are present', () => {
20
- const expected = [
21
- 'review',
22
- 'test',
23
- 'lint',
24
- 'make-interfaces-feel-better',
25
- 'accessibility',
26
- 'grill-me',
27
- 'design-an-interface',
28
- 'tdd',
29
- 'write-milestone-brief',
30
- 'decompose-into-slices',
31
- 'spike-wrap-up',
32
- 'verify-before-complete',
33
- 'create-mcp-server',
34
- 'write-docs',
35
- 'forensics',
36
- 'handoff',
37
- 'security-review',
38
- 'api-design',
39
- 'dependency-upgrade',
40
- 'observability',
41
- ];
42
- const registered = new Set(BUNDLED_SKILL_TRIGGERS.map(e => e.skill));
43
- for (const skill of expected) {
44
- assert.ok(registered.has(skill), `expected bundled skill "${skill}" to be registered`);
45
- }
64
+ assertBundledSkillsRegistered('PR #4505', PR_4505_BUNDLED_SKILLS);
65
+ });
66
+
67
+ test('BUNDLED_SKILL_TRIGGERS: PR #5060 previously-unexposed skills are present', () => {
68
+ assertBundledSkillsRegistered('PR #5060', PR_5060_BUNDLED_SKILLS);
46
69
  });
47
70
 
48
71
  test('BUNDLED_SKILL_TRIGGERS: skill ids are unique', () => {
@@ -0,0 +1,48 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ formatCleanKeepReason,
6
+ type WorktreeStatus,
7
+ } from "../commands-worktree.ts";
8
+
9
+ function mkStatus(over: Partial<WorktreeStatus>): WorktreeStatus {
10
+ const name = over.name ?? "feat-x";
11
+ return {
12
+ name,
13
+ path: `/repo/.gsd/worktrees/${name}`,
14
+ branch: `gsd/${name}`,
15
+ exists: true,
16
+ filesChanged: 0,
17
+ linesAdded: 0,
18
+ linesRemoved: 0,
19
+ uncommitted: false,
20
+ commits: 0,
21
+ ...over,
22
+ };
23
+ }
24
+
25
+ test("clean keep reason shows uncommitted-only worktrees clearly", () => {
26
+ const reason = formatCleanKeepReason(mkStatus({ uncommitted: true }));
27
+ assert.equal(reason, "uncommitted changes");
28
+ });
29
+
30
+ test("clean keep reason includes uncommitted context with changed files", () => {
31
+ const reason = formatCleanKeepReason(mkStatus({ filesChanged: 2, uncommitted: true }));
32
+ assert.equal(reason, "2 changed files, uncommitted");
33
+ });
34
+
35
+ test("clean keep reason flags missing directory with prune hint", () => {
36
+ const reason = formatCleanKeepReason(mkStatus({ exists: false }));
37
+ assert.equal(reason, "directory missing — run 'git worktree prune' to unregister");
38
+ });
39
+
40
+ test("clean keep reason reports changed files without uncommitted suffix", () => {
41
+ const reason = formatCleanKeepReason(mkStatus({ filesChanged: 2, uncommitted: false }));
42
+ assert.equal(reason, "2 changed files");
43
+ });
44
+
45
+ test("clean keep reason uses singular form for a single changed file", () => {
46
+ const reason = formatCleanKeepReason(mkStatus({ filesChanged: 1, uncommitted: false }));
47
+ assert.equal(reason, "1 changed file");
48
+ });