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
@@ -1,13 +1,6 @@
1
1
  // GSD-2 — Deprecation stub for google-search (moved to @gsd-extensions/google-search)
2
2
  import type { ExtensionAPI } from "@gsd/pi-coding-agent";
3
3
 
4
- export default function (pi: ExtensionAPI) {
5
- pi.on("session_start", async (_event, ctx) => {
6
- ctx.ui.notify(
7
- "google_search is being extracted to @gsd-extensions/google-search " +
8
- "(not yet published to npm). This stub will be replaced once the " +
9
- "package is available. No action needed for now.",
10
- "warning",
11
- );
12
- });
4
+ export default function (_pi: ExtensionAPI) {
5
+ // Deprecation notice intentionally suppressed until @gsd-extensions/google-search ships.
13
6
  }
@@ -40,6 +40,29 @@ export async function runUnit(
40
40
  ): Promise<UnitResult> {
41
41
  debugLog("runUnit", { phase: "start", unitType, unitId });
42
42
 
43
+ // Ensure cwd matches basePath BEFORE newSession() captures it. The new
44
+ // session reads process.cwd() during construction to anchor its tool
45
+ // runtime and system prompt; if cwd has drifted (async_bash, background
46
+ // jobs, prior unit cleanup), the session would otherwise be rooted to
47
+ // the wrong directory. Must be synchronous — no awaits between chdir
48
+ // and newSession (#1389, #4762 follow-up).
49
+ try {
50
+ if (process.cwd() !== s.basePath) {
51
+ process.chdir(s.basePath);
52
+ }
53
+ } catch (e) {
54
+ const msg = `Failed to chdir to basePath before newSession (basePath: ${s.basePath}): ${String(e)}`;
55
+ logWarning("engine", msg, { basePath: s.basePath, error: String(e) });
56
+ return {
57
+ status: "cancelled",
58
+ errorContext: {
59
+ message: msg,
60
+ category: "session-failed",
61
+ isTransient: true,
62
+ },
63
+ };
64
+ }
65
+
43
66
  // ── Session creation with timeout ──
44
67
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
45
68
 
@@ -120,17 +143,6 @@ export async function runUnit(
120
143
  _setCurrentResolve(resolve);
121
144
  });
122
145
 
123
- // Ensure cwd matches basePath before dispatch (#1389).
124
- // async_bash and background jobs can drift cwd away from the worktree.
125
- // Realigning here prevents commits from landing on the wrong branch.
126
- try {
127
- if (process.cwd() !== s.basePath) {
128
- process.chdir(s.basePath);
129
- }
130
- } catch (e) {
131
- logWarning("engine", "Failed to chdir to basePath before dispatch", { basePath: s.basePath, error: String(e) });
132
- }
133
-
134
146
  // ── Provider request-readiness pre-check (#4555) ──
135
147
  // Verify the provider can accept requests before dispatching. If the token
136
148
  // has expired since bootstrap, return cancelled immediately so the unit is
@@ -30,6 +30,8 @@ import {
30
30
  import { loadEffectiveGSDPreferences } from "./preferences.js";
31
31
  import type { MinimalModelRegistry } from "./context-budget.js";
32
32
  import { pauseAuto } from "./auto.js";
33
+ import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
34
+ import { logWarning } from "./workflow-logger.js";
33
35
  import {
34
36
  getWorkflowTransportSupportError,
35
37
  getRequiredWorkflowToolsForAutoUnit,
@@ -50,6 +52,14 @@ export async function dispatchDirectPhase(
50
52
  return;
51
53
  }
52
54
 
55
+ const projectRoot = base;
56
+
57
+ // Switch the dispatch base to the canonical milestone worktree if one
58
+ // exists. Without this, /gsd dispatch invoked from the project root would
59
+ // build prompts and create a session anchored to the project root even
60
+ // though the milestone's actual code lives in the worktree.
61
+ const dispatchBase = resolveCanonicalMilestoneRoot(base, mid);
62
+
53
63
  const normalized = phase.toLowerCase();
54
64
  let unitType: string;
55
65
  let unitId: string;
@@ -70,7 +80,7 @@ export async function dispatchDirectPhase(
70
80
 
71
81
  // When require_slice_discussion is enabled, pause auto-mode before
72
82
  // each new slice so the user can discuss requirements first (#789).
73
- const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
83
+ const sliceContextFile = resolveSliceFile(dispatchBase, mid, sid, "CONTEXT");
74
84
  const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
75
85
  if (requireDiscussion && !sliceContextFile) {
76
86
  ctx.ui.notify(
@@ -83,11 +93,11 @@ export async function dispatchDirectPhase(
83
93
 
84
94
  unitType = "research-slice";
85
95
  unitId = `${mid}/${sid}`;
86
- prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
96
+ prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
87
97
  } else {
88
98
  unitType = "research-milestone";
89
99
  unitId = mid;
90
- prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
100
+ prompt = await buildResearchMilestonePrompt(mid, midTitle, dispatchBase);
91
101
  }
92
102
  break;
93
103
  }
@@ -106,7 +116,7 @@ export async function dispatchDirectPhase(
106
116
  unitType = "plan-slice";
107
117
  unitId = `${mid}/${sid}`;
108
118
  prompt = await buildPlanSlicePrompt(
109
- mid, midTitle, sid, sTitle, base, undefined,
119
+ mid, midTitle, sid, sTitle, dispatchBase, undefined,
110
120
  {
111
121
  sessionContextWindow: ctx.model?.contextWindow,
112
122
  modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
@@ -115,7 +125,7 @@ export async function dispatchDirectPhase(
115
125
  } else {
116
126
  unitType = "plan-milestone";
117
127
  unitId = mid;
118
- prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
128
+ prompt = await buildPlanMilestonePrompt(mid, midTitle, dispatchBase);
119
129
  }
120
130
  break;
121
131
  }
@@ -137,7 +147,7 @@ export async function dispatchDirectPhase(
137
147
  unitType = "execute-task";
138
148
  unitId = `${mid}/${sid}/${tid}`;
139
149
  prompt = await buildExecuteTaskPrompt(
140
- mid, sid, sTitle, tid, tTitle, base,
150
+ mid, sid, sTitle, tid, tTitle, dispatchBase,
141
151
  {
142
152
  sessionContextWindow: ctx.model?.contextWindow,
143
153
  modelRegistry: ctx.modelRegistry as MinimalModelRegistry | undefined,
@@ -159,11 +169,11 @@ export async function dispatchDirectPhase(
159
169
  }
160
170
  unitType = "complete-slice";
161
171
  unitId = `${mid}/${sid}`;
162
- prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
172
+ prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
163
173
  } else {
164
174
  unitType = "complete-milestone";
165
175
  unitId = mid;
166
- prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
176
+ prompt = await buildCompleteMilestonePrompt(mid, midTitle, dispatchBase);
167
177
  }
168
178
  break;
169
179
  }
@@ -177,7 +187,7 @@ export async function dispatchDirectPhase(
177
187
  }
178
188
  if (completedSliceIds.length === 0) {
179
189
  // File-based fallback: parse roadmap checkboxes
180
- const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
190
+ const roadmapPath = resolveMilestoneFile(dispatchBase, mid, "ROADMAP");
181
191
  if (roadmapPath) {
182
192
  const roadmapContent = await loadFile(roadmapPath);
183
193
  if (roadmapContent) {
@@ -192,7 +202,7 @@ export async function dispatchDirectPhase(
192
202
  const completedSliceId = completedSliceIds[completedSliceIds.length - 1];
193
203
  unitType = "reassess-roadmap";
194
204
  unitId = `${mid}/${completedSliceId}`;
195
- prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
205
+ prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, dispatchBase);
196
206
  break;
197
207
  }
198
208
 
@@ -208,7 +218,7 @@ export async function dispatchDirectPhase(
208
218
  }
209
219
  if (uatCompletedSliceIds.length === 0) {
210
220
  // File-based fallback: parse roadmap checkboxes
211
- const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
221
+ const roadmapPath = resolveMilestoneFile(dispatchBase, mid, "ROADMAP");
212
222
  if (roadmapPath) {
213
223
  const roadmapContent = await loadFile(roadmapPath);
214
224
  if (roadmapContent) {
@@ -221,7 +231,7 @@ export async function dispatchDirectPhase(
221
231
  return;
222
232
  }
223
233
  const sid = uatCompletedSliceIds[uatCompletedSliceIds.length - 1];
224
- const uatFile = resolveSliceFile(base, mid, sid, "UAT");
234
+ const uatFile = resolveSliceFile(dispatchBase, mid, sid, "UAT");
225
235
  if (!uatFile) {
226
236
  ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
227
237
  return;
@@ -231,10 +241,10 @@ export async function dispatchDirectPhase(
231
241
  ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
232
242
  return;
233
243
  }
234
- const uatPath = relSliceFile(base, mid, sid, "UAT");
244
+ const uatPath = relSliceFile(dispatchBase, mid, sid, "UAT");
235
245
  unitType = "run-uat";
236
246
  unitId = `${mid}/${sid}`;
237
- prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
247
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, dispatchBase);
238
248
  break;
239
249
  }
240
250
 
@@ -248,7 +258,7 @@ export async function dispatchDirectPhase(
248
258
  }
249
259
  unitType = "replan-slice";
250
260
  unitId = `${mid}/${sid}`;
251
- prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
261
+ prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
252
262
  break;
253
263
  }
254
264
 
@@ -264,7 +274,7 @@ export async function dispatchDirectPhase(
264
274
  ctx.model?.provider,
265
275
  getRequiredWorkflowToolsForAutoUnit(unitType),
266
276
  {
267
- projectRoot: base,
277
+ projectRoot,
268
278
  surface: "direct phase dispatch",
269
279
  unitType,
270
280
  authMode: ctx.model?.provider ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider) : undefined,
@@ -277,13 +287,39 @@ export async function dispatchDirectPhase(
277
287
  }
278
288
 
279
289
  ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
280
- const result = await ctx.newSession();
281
- if (result.cancelled) {
282
- ctx.ui.notify("Session creation cancelled.", "warning");
283
- return;
290
+
291
+ const originalCwd = process.cwd();
292
+
293
+ try {
294
+ // Ensure cwd matches dispatchBase BEFORE newSession() captures it. Synchronous —
295
+ // no awaits between chdir and newSession.
296
+ try {
297
+ if (process.cwd() !== dispatchBase) {
298
+ process.chdir(dispatchBase);
299
+ }
300
+ } catch (err) {
301
+ const msg = `Failed to chdir before direct-dispatch newSession (basePath: ${dispatchBase}): ${err instanceof Error ? err.message : String(err)}`;
302
+ logWarning("engine", msg, { file: "auto-direct-dispatch.ts", basePath: dispatchBase, error: err instanceof Error ? err.message : String(err) });
303
+ ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
304
+ return;
305
+ }
306
+
307
+ const result = await ctx.newSession();
308
+ if (result.cancelled) {
309
+ ctx.ui.notify("Session creation cancelled.", "warning");
310
+ return;
311
+ }
312
+ pi.sendMessage(
313
+ { customType: "gsd-dispatch", content: prompt, display: false },
314
+ { triggerTurn: true },
315
+ );
316
+ } finally {
317
+ try {
318
+ if (process.cwd() !== originalCwd) {
319
+ process.chdir(originalCwd);
320
+ }
321
+ } catch (err) {
322
+ logWarning("engine", `Failed to restore cwd after direct dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto-direct-dispatch.ts", basePath: originalCwd });
323
+ }
284
324
  }
285
- pi.sendMessage(
286
- { customType: "gsd-dispatch", content: prompt, display: false },
287
- { triggerTurn: true },
288
- );
289
325
  }
@@ -1267,6 +1267,7 @@ export async function buildDiscussMilestonePrompt(
1267
1267
  const discussTemplates = inlineTemplate("context", "Context");
1268
1268
 
1269
1269
  const basePrompt = loadPrompt("guided-discuss-milestone", {
1270
+ workingDirectory: base,
1270
1271
  milestoneId: mid,
1271
1272
  milestoneTitle: midTitle,
1272
1273
  inlinedTemplates: discussTemplates,
@@ -2645,6 +2646,7 @@ export async function buildParallelResearchSlicesPrompt(
2645
2646
  }
2646
2647
 
2647
2648
  return loadPrompt("parallel-research-slices", {
2649
+ workingDirectory: basePath,
2648
2650
  mid,
2649
2651
  midTitle,
2650
2652
  sliceCount: String(slices.length),
@@ -2681,6 +2683,7 @@ export async function buildGateEvaluatePrompt(
2681
2683
 
2682
2684
  const subagentSections: string[] = [];
2683
2685
  const gateListLines: string[] = [];
2686
+ const normalizedBase = base.replaceAll("\\", "/");
2684
2687
 
2685
2688
  for (const def of gateDefs) {
2686
2689
  gateListLines.push(`- **${def.id}**: ${def.question}`);
@@ -2688,6 +2691,8 @@ export async function buildGateEvaluatePrompt(
2688
2691
  const subPrompt = [
2689
2692
  `You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
2690
2693
  "",
2694
+ `**Working directory:** \`${normalizedBase}\`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT \`cd\` to any other directory.`,
2695
+ "",
2691
2696
  `## Question: ${def.question}`,
2692
2697
  "",
2693
2698
  def.guidance,
@@ -2804,6 +2809,7 @@ export async function buildRewriteDocsPrompt(
2804
2809
  const documentList = docList.length > 0 ? docList.join("\n") : "- No active plan documents found.";
2805
2810
 
2806
2811
  return loadPrompt("rewrite-docs", {
2812
+ workingDirectory: base,
2807
2813
  milestoneId: mid,
2808
2814
  milestoneTitle: midTitle,
2809
2815
  sliceId: sid ?? "none",
@@ -2372,5 +2372,20 @@ export function mergeMilestoneToMain(
2372
2372
  originalBase = null;
2373
2373
  nudgeGitBranchCache(previousCwd);
2374
2374
 
2375
+ // 15. Anchor cwd at the project root on success-return. Step 12 removed
2376
+ // the worktree dir; if cwd was inside it, every subsequent process.cwd()
2377
+ // would throw ENOENT and trip auto/run-unit.ts:50's session-failed cancel
2378
+ // path (the de73fb43d regression that closes headless gsd auto). Step 3
2379
+ // already chdir'd here, but defending the success-return contract makes
2380
+ // future maintainers safe against intervening chdir's between step 3 and
2381
+ // here.
2382
+ try {
2383
+ // process.cwd() can throw ENOENT when cwd was removed, so attempt
2384
+ // recovery directly.
2385
+ process.chdir(originalBasePath_);
2386
+ } catch (err) {
2387
+ logWarning("worktree", `chdir to project root after merge failed: ${err instanceof Error ? err.message : String(err)}`);
2388
+ }
2389
+
2375
2390
  return { commitMessage, pushed, prCreated, codeFilesChanged };
2376
2391
  }
@@ -1864,16 +1864,21 @@ export async function dispatchHookUnit(
1864
1864
  hookModel: string | undefined,
1865
1865
  targetBasePath: string,
1866
1866
  ): Promise<boolean> {
1867
+ const wasActive = s.active;
1868
+ const previousBasePath = s.basePath;
1869
+ const previousCurrentUnit = s.currentUnit ? { ...s.currentUnit } : null;
1870
+
1867
1871
  if (!s.active) {
1868
1872
  s.active = true;
1869
1873
  s.stepMode = true;
1870
1874
  s.cmdCtx = ctx as ExtensionCommandContext;
1871
- s.basePath = targetBasePath;
1872
1875
  s.autoStartTime = Date.now();
1873
1876
  s.currentUnit = null;
1874
1877
  s.pendingQuickTasks = [];
1875
1878
  }
1876
1879
 
1880
+ s.basePath = targetBasePath;
1881
+
1877
1882
  const hookUnitType = `hook/${hookName}`;
1878
1883
  const hookStartedAt = Date.now();
1879
1884
 
@@ -1883,6 +1888,23 @@ export async function dispatchHookUnit(
1883
1888
  startedAt: hookStartedAt,
1884
1889
  };
1885
1890
 
1891
+ // Ensure cwd matches basePath BEFORE newSession() captures it (#1389).
1892
+ // newSession() snapshots process.cwd() during construction; chdir-ing
1893
+ // afterward leaves the session rooted to whatever cwd was when the call
1894
+ // was made. Must be synchronous — no awaits between chdir and newSession.
1895
+ try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch (err) {
1896
+ const msg = `Failed to chdir before hook newSession (basePath: ${s.basePath}): ${err instanceof Error ? err.message : String(err)}`;
1897
+ logWarning("engine", msg, { file: "auto.ts", basePath: s.basePath, error: err instanceof Error ? err.message : String(err) });
1898
+ ctx.ui.notify(`${msg}. Cancelling hook dispatch to avoid running in the wrong directory.`, "error");
1899
+ if (wasActive) {
1900
+ s.basePath = previousBasePath;
1901
+ s.currentUnit = previousCurrentUnit;
1902
+ } else {
1903
+ s.reset();
1904
+ }
1905
+ return false;
1906
+ }
1907
+
1886
1908
  const result = await s.cmdCtx!.newSession();
1887
1909
  if (result.cancelled) {
1888
1910
  await stopAuto(ctx, pi);
@@ -1939,11 +1961,6 @@ export async function dispatchHookUnit(
1939
1961
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1940
1962
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1941
1963
 
1942
- // Ensure cwd matches basePath before hook dispatch (#1389)
1943
- try { if (process.cwd() !== s.basePath) process.chdir(s.basePath); } catch (err) {
1944
- logWarning("engine", `chdir failed before hook dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1945
- }
1946
-
1947
1964
  debugLog("dispatchHookUnit", {
1948
1965
  phase: "send-message",
1949
1966
  promptLength: hookPrompt.length,
@@ -451,6 +451,19 @@ export function registerHooks(
451
451
  markToolStart(event.toolCallId, event.toolName);
452
452
  safetyRecordToolCall(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
453
453
 
454
+ // Persist immediately at dispatch so a mid-unit re-dispatch — which calls
455
+ // resetEvidence() + loadEvidenceFromDisk() in runUnitPhase — cannot wipe
456
+ // the entry between tool_call and tool_execution_end. Without this, the
457
+ // race window equals the tool's runtime, producing the "no bash calls"
458
+ // false positive when the LLM clearly ran a verification command.
459
+ const callDash = getAutoRuntimeSnapshot();
460
+ if (callDash.basePath && callDash.currentUnit?.type === "execute-task") {
461
+ const { milestone: cMid, slice: cSid, task: cTid } = parseUnitId(callDash.currentUnit.id);
462
+ if (cMid && cSid && cTid) {
463
+ saveEvidenceToDisk(callDash.basePath, cMid, cSid, cTid);
464
+ }
465
+ }
466
+
454
467
  // Destructive command classification (warn only, never block)
455
468
  if (isToolCallEventType("bash", event)) {
456
469
  const classification = classifyCommand(event.input.command);
@@ -52,6 +52,17 @@ export const BUNDLED_SKILL_TRIGGERS: Array<{ trigger: string; skill: string }> =
52
52
  { trigger: "HTTP/REST/GraphQL API design — verbs, status codes, pagination, errors, idempotency, versioning", skill: "api-design" },
53
53
  { trigger: "Dependency upgrades — risk-batched, verified between batches, one major per commit", skill: "dependency-upgrade" },
54
54
  { trigger: "Agent-first observability — structured logs, persisted failure state, health surfaces, explicit failure modes", skill: "observability" },
55
+ { trigger: "React/Next.js performance — components, data fetching, bundle optimization, rendering patterns from Vercel Engineering", skill: "react-best-practices" },
56
+ { trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
57
+ { trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
58
+ { trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
59
+ { trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
60
+ { trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
61
+ { trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
62
+ { trigger: "Author or refine a GSD skill — SKILL.md structure, frontmatter, and best practices", skill: "create-skill" },
63
+ { trigger: "Create or debug a GSD extension — tools, commands, event hooks, custom TUI, providers", skill: "create-gsd-extension" },
64
+ { trigger: "Author a YAML workflow definition — steps, triggers, and templates", skill: "create-workflow" },
65
+ { trigger: "Deep code optimization audit — perf anti-patterns, memory leaks, algorithmic complexity, bundle size, I/O, caching, dead code (parallel pattern-based hunt)", skill: "code-optimizer" },
55
66
  ];
56
67
 
57
68
  function buildBundledSkillsTable(): string {
@@ -14,7 +14,7 @@ export interface GsdCommandDefinition {
14
14
  type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
15
15
 
16
16
  export const GSD_COMMAND_DESCRIPTION =
17
- "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language";
17
+ "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language|worktree";
18
18
 
19
19
  export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
20
20
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -83,6 +83,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
83
83
  { cmd: "add-tests", desc: "Generate tests for completed slices" },
84
84
  { cmd: "scan", desc: "Rapid codebase assessment — lightweight alternative to full map (--focus tech|arch|quality|concerns|tech+arch)" },
85
85
  { cmd: "language", desc: "Set or clear the global response language (e.g. /gsd language Chinese)" },
86
+ { cmd: "worktree", desc: "Manage worktrees from the TUI (list, merge, clean, remove)" },
86
87
  ];
87
88
 
88
89
  const NESTED_COMPLETIONS: CompletionMap = {
@@ -299,6 +300,12 @@ const NESTED_COMPLETIONS: CompletionMap = {
299
300
  { cmd: "off", desc: "Clear the language preference (revert to default)" },
300
301
  { cmd: "clear", desc: "Alias for off — clear the language preference" },
301
302
  ],
303
+ worktree: [
304
+ { cmd: "list", desc: "Show all worktrees with status" },
305
+ { cmd: "merge", desc: "Merge a worktree into main and clean up" },
306
+ { cmd: "clean", desc: "Remove all merged/empty worktrees" },
307
+ { cmd: "remove", desc: "Remove a worktree (--force to skip safety checks)" },
308
+ ],
302
309
  };
303
310
 
304
311
  function filterOptions(
@@ -130,6 +130,7 @@ export function showHelp(ctx: ExtensionCommandContext, args = ""): void {
130
130
  " /gsd forensics Examine execution logs and post-mortem analysis",
131
131
  " /gsd export Export milestone/slice results [--json|--markdown|--html] [--all]",
132
132
  " /gsd cleanup Remove merged branches or snapshots [branches|snapshots]",
133
+ " /gsd worktree Manage worktrees from the TUI [list|merge|clean|remove]",
133
134
  " /gsd migrate Migrate .planning/ (v1) to .gsd/ (v2) format",
134
135
  " /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
135
136
  " /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
@@ -262,5 +262,15 @@ Examples:
262
262
  await handleScan(trimmed.replace(/^scan\s*/, "").trim(), ctx, pi);
263
263
  return true;
264
264
  }
265
+ if (
266
+ trimmed === "worktree" ||
267
+ trimmed.startsWith("worktree ") ||
268
+ trimmed === "wt" ||
269
+ trimmed.startsWith("wt ")
270
+ ) {
271
+ const { handleWorktree } = await import("../../commands-worktree.js");
272
+ await handleWorktree(trimmed.replace(/^(worktree|wt)\s*/, "").trim(), ctx);
273
+ return true;
274
+ }
265
275
  return false;
266
276
  }