gsd-pi 2.78.1-dev.8a893322c → 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 (79) hide show
  1. package/dist/cli-auto-routing.d.ts +1 -0
  2. package/dist/cli-auto-routing.js +5 -0
  3. package/dist/cli.js +5 -14
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +23 -11
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +55 -21
  7. package/dist/resources/extensions/gsd/auto-prompts.js +6 -0
  8. package/dist/resources/extensions/gsd/auto-worktree.js +15 -0
  9. package/dist/resources/extensions/gsd/auto.js +25 -9
  10. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  11. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +2 -0
  12. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +2 -0
  13. package/dist/resources/extensions/gsd/worktree-resolver.js +24 -0
  14. package/dist/resources/skills/lint/SKILL.md +4 -0
  15. package/dist/resources/skills/review/SKILL.md +4 -0
  16. package/dist/resources/skills/test/SKILL.md +3 -0
  17. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  18. package/dist/web/standalone/.next/BUILD_ID +1 -1
  19. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  20. package/dist/web/standalone/.next/build-manifest.json +2 -2
  21. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  22. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.html +1 -1
  39. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  46. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  48. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  49. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  50. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  51. package/package.json +1 -1
  52. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +278 -0
  53. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  55. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/agent-session.js +125 -55
  57. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  58. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +319 -0
  59. package/packages/pi-coding-agent/src/core/agent-session.ts +128 -59
  60. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  61. package/src/resources/extensions/gsd/auto/run-unit.ts +23 -11
  62. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +60 -24
  63. package/src/resources/extensions/gsd/auto-prompts.ts +6 -0
  64. package/src/resources/extensions/gsd/auto-worktree.ts +15 -0
  65. package/src/resources/extensions/gsd/auto.ts +23 -6
  66. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  67. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +2 -0
  68. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +2 -0
  69. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -0
  70. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +8 -2
  71. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +12 -6
  72. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +235 -0
  73. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +85 -0
  74. package/src/resources/extensions/gsd/worktree-resolver.ts +24 -0
  75. package/src/resources/skills/lint/SKILL.md +4 -0
  76. package/src/resources/skills/review/SKILL.md +4 -0
  77. package/src/resources/skills/test/SKILL.md +3 -0
  78. /package/dist/web/standalone/.next/static/{QK8fABiGPmonfTgboN0Y9 → GlYncvckBGG33CSoJaSnB}/_buildManifest.js +0 -0
  79. /package/dist/web/standalone/.next/static/{QK8fABiGPmonfTgboN0Y9 → GlYncvckBGG33CSoJaSnB}/_ssgManifest.js +0 -0
@@ -0,0 +1 @@
1
+ export declare function shouldRedirectAutoToHeadless(subcommand: string | undefined, stdinIsTTY: boolean | undefined, stdoutIsTTY: boolean | undefined): boolean;
@@ -0,0 +1,5 @@
1
+ export function shouldRedirectAutoToHeadless(subcommand, stdinIsTTY, stdoutIsTTY) {
2
+ if (subcommand !== 'auto')
3
+ return false;
4
+ return stdinIsTTY !== true || stdoutIsTTY !== true;
5
+ }
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
9
9
  import chalk from 'chalk';
10
10
  import { checkForUpdates } from './update-check.js';
11
11
  import { shouldBypassManagedResourceMismatchGate } from './cli-policy.js';
12
+ import { shouldRedirectAutoToHeadless } from './cli-auto-routing.js';
12
13
  import { printHelp, printSubcommandHelp } from './help-text.js';
13
14
  import { applySecurityOverrides } from './security-overrides.js';
14
15
  import { validateConfiguredModel } from './startup-model-validation.js';
@@ -409,10 +410,10 @@ function flushPendingProviderRegistrations(resourceLoader, modelRegistry) {
409
410
  }
410
411
  runtime.pendingProviderRegistrations = [];
411
412
  }
412
- // `gsd auto [args...]` — shorthand for `gsd headless auto [args...]` (#2732)
413
- // Without this, `gsd auto` falls through to the interactive TUI which hangs
414
- // when stdin/stdout are piped (non-TTY environments).
415
- if (cliFlags.messages[0] === 'auto') {
413
+ // `gsd auto [args...]` with piped stdin/stdout — shorthand for
414
+ // `gsd headless auto [args...]` (#2732). Keep terminal TTY launches in the
415
+ // interactive path so Warp/iTerm/Terminal retain foreground ownership.
416
+ if (shouldRedirectAutoToHeadless(cliFlags.messages[0], process.stdin.isTTY, process.stdout.isTTY)) {
416
417
  await runHeadlessFromAuto(buildHeadlessAutoArgs(cliFlags));
417
418
  }
418
419
  // ---------------------------------------------------------------------------
@@ -656,16 +657,6 @@ if (!cliFlags.worktree && !isPrintMode) {
656
657
  }
657
658
  markStartup('worktreeStatusBanner');
658
659
  // ---------------------------------------------------------------------------
659
- // Auto-redirect: `gsd auto` with piped stdout → headless mode (#2732)
660
- // When stdout is not a TTY (e.g. `gsd auto | cat`, `gsd auto > file`),
661
- // the TUI cannot render and the process hangs. Redirect to headless mode
662
- // which handles non-interactive output gracefully.
663
- // ---------------------------------------------------------------------------
664
- if (cliFlags.messages[0] === 'auto' && !process.stdout.isTTY) {
665
- process.stderr.write('[gsd] stdout is not a terminal — running auto-mode in headless mode.\n');
666
- await runHeadlessFromAuto(cliFlags.messages.slice(1));
667
- }
668
- // ---------------------------------------------------------------------------
669
660
  // Interactive mode — normal TTY session
670
661
  // ---------------------------------------------------------------------------
671
662
  await ensureRtkBootstrap();
@@ -1 +1 @@
1
- 868d22f3f04d038e
1
+ 38cf2a787658e575
@@ -22,6 +22,29 @@ let sessionSwitchGeneration = 0;
22
22
  */
23
23
  export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
24
24
  debugLog("runUnit", { phase: "start", unitType, unitId });
25
+ // Ensure cwd matches basePath BEFORE newSession() captures it. The new
26
+ // session reads process.cwd() during construction to anchor its tool
27
+ // runtime and system prompt; if cwd has drifted (async_bash, background
28
+ // jobs, prior unit cleanup), the session would otherwise be rooted to
29
+ // the wrong directory. Must be synchronous — no awaits between chdir
30
+ // and newSession (#1389, #4762 follow-up).
31
+ try {
32
+ if (process.cwd() !== s.basePath) {
33
+ process.chdir(s.basePath);
34
+ }
35
+ }
36
+ catch (e) {
37
+ const msg = `Failed to chdir to basePath before newSession (basePath: ${s.basePath}): ${String(e)}`;
38
+ logWarning("engine", msg, { basePath: s.basePath, error: String(e) });
39
+ return {
40
+ status: "cancelled",
41
+ errorContext: {
42
+ message: msg,
43
+ category: "session-failed",
44
+ isTransient: true,
45
+ },
46
+ };
47
+ }
25
48
  // ── Session creation with timeout ──
26
49
  debugLog("runUnit", { phase: "session-create", unitType, unitId });
27
50
  let sessionResult;
@@ -91,17 +114,6 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
91
114
  const unitPromise = new Promise((resolve) => {
92
115
  _setCurrentResolve(resolve);
93
116
  });
94
- // Ensure cwd matches basePath before dispatch (#1389).
95
- // async_bash and background jobs can drift cwd away from the worktree.
96
- // Realigning here prevents commits from landing on the wrong branch.
97
- try {
98
- if (process.cwd() !== s.basePath) {
99
- process.chdir(s.basePath);
100
- }
101
- }
102
- catch (e) {
103
- logWarning("engine", "Failed to chdir to basePath before dispatch", { basePath: s.basePath, error: String(e) });
104
- }
105
117
  // ── Provider request-readiness pre-check (#4555) ──
106
118
  // Verify the provider can accept requests before dispatching. If the token
107
119
  // has expired since bootstrap, return cancelled immediately so the unit is
@@ -10,6 +10,8 @@ import { resolveMilestoneFile, resolveSliceFile, relSliceFile, } from "./paths.j
10
10
  import { buildResearchSlicePrompt, buildResearchMilestonePrompt, buildPlanSlicePrompt, buildPlanMilestonePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildReassessRoadmapPrompt, buildRunUatPrompt, buildReplanSlicePrompt, } from "./auto-prompts.js";
11
11
  import { loadEffectiveGSDPreferences } from "./preferences.js";
12
12
  import { pauseAuto } from "./auto.js";
13
+ import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
14
+ import { logWarning } from "./workflow-logger.js";
13
15
  import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
14
16
  export async function dispatchDirectPhase(ctx, pi, phase, base) {
15
17
  const state = await deriveState(base);
@@ -19,6 +21,12 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
19
21
  ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
20
22
  return;
21
23
  }
24
+ const projectRoot = base;
25
+ // Switch the dispatch base to the canonical milestone worktree if one
26
+ // exists. Without this, /gsd dispatch invoked from the project root would
27
+ // build prompts and create a session anchored to the project root even
28
+ // though the milestone's actual code lives in the worktree.
29
+ const dispatchBase = resolveCanonicalMilestoneRoot(base, mid);
22
30
  const normalized = phase.toLowerCase();
23
31
  let unitType;
24
32
  let unitId;
@@ -37,7 +45,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
37
45
  }
38
46
  // When require_slice_discussion is enabled, pause auto-mode before
39
47
  // each new slice so the user can discuss requirements first (#789).
40
- const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
48
+ const sliceContextFile = resolveSliceFile(dispatchBase, mid, sid, "CONTEXT");
41
49
  const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
42
50
  if (requireDiscussion && !sliceContextFile) {
43
51
  ctx.ui.notify(`Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`, "info");
@@ -46,12 +54,12 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
46
54
  }
47
55
  unitType = "research-slice";
48
56
  unitId = `${mid}/${sid}`;
49
- prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
57
+ prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
50
58
  }
51
59
  else {
52
60
  unitType = "research-milestone";
53
61
  unitId = mid;
54
- prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
62
+ prompt = await buildResearchMilestonePrompt(mid, midTitle, dispatchBase);
55
63
  }
56
64
  break;
57
65
  }
@@ -68,7 +76,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
68
76
  }
69
77
  unitType = "plan-slice";
70
78
  unitId = `${mid}/${sid}`;
71
- prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base, undefined, {
79
+ prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase, undefined, {
72
80
  sessionContextWindow: ctx.model?.contextWindow,
73
81
  modelRegistry: ctx.modelRegistry,
74
82
  });
@@ -76,7 +84,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
76
84
  else {
77
85
  unitType = "plan-milestone";
78
86
  unitId = mid;
79
- prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
87
+ prompt = await buildPlanMilestonePrompt(mid, midTitle, dispatchBase);
80
88
  }
81
89
  break;
82
90
  }
@@ -96,7 +104,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
96
104
  }
97
105
  unitType = "execute-task";
98
106
  unitId = `${mid}/${sid}/${tid}`;
99
- prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, {
107
+ prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, dispatchBase, {
100
108
  sessionContextWindow: ctx.model?.contextWindow,
101
109
  modelRegistry: ctx.modelRegistry,
102
110
  });
@@ -115,12 +123,12 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
115
123
  }
116
124
  unitType = "complete-slice";
117
125
  unitId = `${mid}/${sid}`;
118
- prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
126
+ prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
119
127
  }
120
128
  else {
121
129
  unitType = "complete-milestone";
122
130
  unitId = mid;
123
- prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
131
+ prompt = await buildCompleteMilestonePrompt(mid, midTitle, dispatchBase);
124
132
  }
125
133
  break;
126
134
  }
@@ -133,7 +141,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
133
141
  }
134
142
  if (completedSliceIds.length === 0) {
135
143
  // File-based fallback: parse roadmap checkboxes
136
- const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
144
+ const roadmapPath = resolveMilestoneFile(dispatchBase, mid, "ROADMAP");
137
145
  if (roadmapPath) {
138
146
  const roadmapContent = await loadFile(roadmapPath);
139
147
  if (roadmapContent) {
@@ -148,7 +156,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
148
156
  const completedSliceId = completedSliceIds[completedSliceIds.length - 1];
149
157
  unitType = "reassess-roadmap";
150
158
  unitId = `${mid}/${completedSliceId}`;
151
- prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
159
+ prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, dispatchBase);
152
160
  break;
153
161
  }
154
162
  case "uat":
@@ -163,7 +171,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
163
171
  }
164
172
  if (uatCompletedSliceIds.length === 0) {
165
173
  // File-based fallback: parse roadmap checkboxes
166
- const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
174
+ const roadmapPath = resolveMilestoneFile(dispatchBase, mid, "ROADMAP");
167
175
  if (roadmapPath) {
168
176
  const roadmapContent = await loadFile(roadmapPath);
169
177
  if (roadmapContent) {
@@ -176,7 +184,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
176
184
  return;
177
185
  }
178
186
  const sid = uatCompletedSliceIds[uatCompletedSliceIds.length - 1];
179
- const uatFile = resolveSliceFile(base, mid, sid, "UAT");
187
+ const uatFile = resolveSliceFile(dispatchBase, mid, sid, "UAT");
180
188
  if (!uatFile) {
181
189
  ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
182
190
  return;
@@ -186,10 +194,10 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
186
194
  ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
187
195
  return;
188
196
  }
189
- const uatPath = relSliceFile(base, mid, sid, "UAT");
197
+ const uatPath = relSliceFile(dispatchBase, mid, sid, "UAT");
190
198
  unitType = "run-uat";
191
199
  unitId = `${mid}/${sid}`;
192
- prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
200
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, dispatchBase);
193
201
  break;
194
202
  }
195
203
  case "replan":
@@ -202,7 +210,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
202
210
  }
203
211
  unitType = "replan-slice";
204
212
  unitId = `${mid}/${sid}`;
205
- prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
213
+ prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, dispatchBase);
206
214
  break;
207
215
  }
208
216
  default:
@@ -210,7 +218,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
210
218
  return;
211
219
  }
212
220
  const compatibilityError = getWorkflowTransportSupportError(ctx.model?.provider, getRequiredWorkflowToolsForAutoUnit(unitType), {
213
- projectRoot: base,
221
+ projectRoot,
214
222
  surface: "direct phase dispatch",
215
223
  unitType,
216
224
  authMode: ctx.model?.provider ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider) : undefined,
@@ -221,10 +229,36 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
221
229
  return;
222
230
  }
223
231
  ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
224
- const result = await ctx.newSession();
225
- if (result.cancelled) {
226
- ctx.ui.notify("Session creation cancelled.", "warning");
227
- return;
232
+ const originalCwd = process.cwd();
233
+ try {
234
+ // Ensure cwd matches dispatchBase BEFORE newSession() captures it. Synchronous —
235
+ // no awaits between chdir and newSession.
236
+ try {
237
+ if (process.cwd() !== dispatchBase) {
238
+ process.chdir(dispatchBase);
239
+ }
240
+ }
241
+ catch (err) {
242
+ const msg = `Failed to chdir before direct-dispatch newSession (basePath: ${dispatchBase}): ${err instanceof Error ? err.message : String(err)}`;
243
+ logWarning("engine", msg, { file: "auto-direct-dispatch.ts", basePath: dispatchBase, error: err instanceof Error ? err.message : String(err) });
244
+ ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
245
+ return;
246
+ }
247
+ const result = await ctx.newSession();
248
+ if (result.cancelled) {
249
+ ctx.ui.notify("Session creation cancelled.", "warning");
250
+ return;
251
+ }
252
+ pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
253
+ }
254
+ finally {
255
+ try {
256
+ if (process.cwd() !== originalCwd) {
257
+ process.chdir(originalCwd);
258
+ }
259
+ }
260
+ catch (err) {
261
+ logWarning("engine", `Failed to restore cwd after direct dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto-direct-dispatch.ts", basePath: originalCwd });
262
+ }
228
263
  }
229
- pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
230
264
  }
@@ -1120,6 +1120,7 @@ export async function checkNeedsRunUat(base, mid, state, prefs) {
1120
1120
  export async function buildDiscussMilestonePrompt(mid, midTitle, base, structuredQuestionsAvailable = "false") {
1121
1121
  const discussTemplates = inlineTemplate("context", "Context");
1122
1122
  const basePrompt = loadPrompt("guided-discuss-milestone", {
1123
+ workingDirectory: base,
1123
1124
  milestoneId: mid,
1124
1125
  milestoneTitle: midTitle,
1125
1126
  inlinedTemplates: discussTemplates,
@@ -2323,6 +2324,7 @@ export async function buildParallelResearchSlicesPrompt(mid, midTitle, slices, b
2323
2324
  ].join("\n"));
2324
2325
  }
2325
2326
  return loadPrompt("parallel-research-slices", {
2327
+ workingDirectory: basePath,
2326
2328
  mid,
2327
2329
  midTitle,
2328
2330
  sliceCount: String(slices.length),
@@ -2350,11 +2352,14 @@ export async function buildGateEvaluatePrompt(mid, midTitle, sid, sTitle, base,
2350
2352
  const gateDefs = getGatesForTurn("gate-evaluate").filter((def) => pendingIds.has(def.id));
2351
2353
  const subagentSections = [];
2352
2354
  const gateListLines = [];
2355
+ const normalizedBase = base.replaceAll("\\", "/");
2353
2356
  for (const def of gateDefs) {
2354
2357
  gateListLines.push(`- **${def.id}**: ${def.question}`);
2355
2358
  const subPrompt = [
2356
2359
  `You are evaluating quality gate **${def.id}** for slice ${sid} (${sTitle}).`,
2357
2360
  "",
2361
+ `**Working directory:** \`${normalizedBase}\`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT \`cd\` to any other directory.`,
2362
+ "",
2358
2363
  `## Question: ${def.question}`,
2359
2364
  "",
2360
2365
  def.guidance,
@@ -2462,6 +2467,7 @@ export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, o
2462
2467
  ].join("\n")).join("\n\n");
2463
2468
  const documentList = docList.length > 0 ? docList.join("\n") : "- No active plan documents found.";
2464
2469
  return loadPrompt("rewrite-docs", {
2470
+ workingDirectory: base,
2465
2471
  milestoneId: mid,
2466
2472
  milestoneTitle: midTitle,
2467
2473
  sliceId: sid ?? "none",
@@ -2076,5 +2076,20 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
2076
2076
  // 14. Clear module state
2077
2077
  originalBase = null;
2078
2078
  nudgeGitBranchCache(previousCwd);
2079
+ // 15. Anchor cwd at the project root on success-return. Step 12 removed
2080
+ // the worktree dir; if cwd was inside it, every subsequent process.cwd()
2081
+ // would throw ENOENT and trip auto/run-unit.ts:50's session-failed cancel
2082
+ // path (the de73fb43d regression that closes headless gsd auto). Step 3
2083
+ // already chdir'd here, but defending the success-return contract makes
2084
+ // future maintainers safe against intervening chdir's between step 3 and
2085
+ // here.
2086
+ try {
2087
+ // process.cwd() can throw ENOENT when cwd was removed, so attempt
2088
+ // recovery directly.
2089
+ process.chdir(originalBasePath_);
2090
+ }
2091
+ catch (err) {
2092
+ logWarning("worktree", `chdir to project root after merge failed: ${err instanceof Error ? err.message : String(err)}`);
2093
+ }
2079
2094
  return { commitMessage, pushed, prCreated, codeFilesChanged };
2080
2095
  }
@@ -1441,15 +1441,18 @@ export function ensurePreconditions(unitType, unitId, base, state) {
1441
1441
  }
1442
1442
  }
1443
1443
  export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, triggerUnitId, hookPrompt, hookModel, targetBasePath) {
1444
+ const wasActive = s.active;
1445
+ const previousBasePath = s.basePath;
1446
+ const previousCurrentUnit = s.currentUnit ? { ...s.currentUnit } : null;
1444
1447
  if (!s.active) {
1445
1448
  s.active = true;
1446
1449
  s.stepMode = true;
1447
1450
  s.cmdCtx = ctx;
1448
- s.basePath = targetBasePath;
1449
1451
  s.autoStartTime = Date.now();
1450
1452
  s.currentUnit = null;
1451
1453
  s.pendingQuickTasks = [];
1452
1454
  }
1455
+ s.basePath = targetBasePath;
1453
1456
  const hookUnitType = `hook/${hookName}`;
1454
1457
  const hookStartedAt = Date.now();
1455
1458
  s.currentUnit = {
@@ -1457,6 +1460,27 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1457
1460
  id: triggerUnitId,
1458
1461
  startedAt: hookStartedAt,
1459
1462
  };
1463
+ // Ensure cwd matches basePath BEFORE newSession() captures it (#1389).
1464
+ // newSession() snapshots process.cwd() during construction; chdir-ing
1465
+ // afterward leaves the session rooted to whatever cwd was when the call
1466
+ // was made. Must be synchronous — no awaits between chdir and newSession.
1467
+ try {
1468
+ if (process.cwd() !== s.basePath)
1469
+ process.chdir(s.basePath);
1470
+ }
1471
+ catch (err) {
1472
+ const msg = `Failed to chdir before hook newSession (basePath: ${s.basePath}): ${err instanceof Error ? err.message : String(err)}`;
1473
+ logWarning("engine", msg, { file: "auto.ts", basePath: s.basePath, error: err instanceof Error ? err.message : String(err) });
1474
+ ctx.ui.notify(`${msg}. Cancelling hook dispatch to avoid running in the wrong directory.`, "error");
1475
+ if (wasActive) {
1476
+ s.basePath = previousBasePath;
1477
+ s.currentUnit = previousCurrentUnit;
1478
+ }
1479
+ else {
1480
+ s.reset();
1481
+ }
1482
+ return false;
1483
+ }
1460
1484
  const result = await s.cmdCtx.newSession();
1461
1485
  if (result.cancelled) {
1462
1486
  await stopAuto(ctx, pi);
@@ -1499,14 +1523,6 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1499
1523
  }, hookHardTimeoutMs);
1500
1524
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1501
1525
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1502
- // Ensure cwd matches basePath before hook dispatch (#1389)
1503
- try {
1504
- if (process.cwd() !== s.basePath)
1505
- process.chdir(s.basePath);
1506
- }
1507
- catch (err) {
1508
- logWarning("engine", `chdir failed before hook dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1509
- }
1510
1526
  debugLog("dispatchHookUnit", {
1511
1527
  phase: "send-message",
1512
1528
  promptLength: hookPrompt.length,
@@ -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.
@@ -297,6 +297,30 @@ export class WorktreeResolver {
297
297
  */
298
298
  mergeAndExit(milestoneId, ctx) {
299
299
  this.validateMilestoneId(milestoneId);
300
+ // Anchor cwd at the project root before any merge work. Some merge code
301
+ // paths (mergeMilestoneToMain, slice-cadence) chdir explicitly; others
302
+ // (branch-mode, isolation-degraded skip, missing-original-base skip)
303
+ // do not. If the worktree dir is later torn down while cwd still points
304
+ // into it, every subsequent process.cwd() throws ENOENT — and after
305
+ // de73fb43d that surfaces as a session-failed cancel and (in headless
306
+ // mode) terminates the whole gsd process. Best-effort: silent on
307
+ // failure so existing test fixtures that use synthetic paths still pass.
308
+ if (this.s.originalBasePath) {
309
+ try {
310
+ // process.cwd() can throw ENOENT when cwd was removed, so attempt
311
+ // recovery directly.
312
+ process.chdir(this.s.originalBasePath);
313
+ }
314
+ catch (err) {
315
+ debugLog("WorktreeResolver", {
316
+ action: "mergeAndExit",
317
+ phase: "pre-merge-chdir-failed",
318
+ milestoneId,
319
+ originalBasePath: this.s.originalBasePath,
320
+ error: err instanceof Error ? err.message : String(err),
321
+ });
322
+ }
323
+ }
300
324
  // #4764 — telemetry: record start timestamp so we can emit merge duration.
301
325
  const mergeStartedAt = new Date().toISOString();
302
326
  const mergeStartMs = Date.now();
@@ -7,6 +7,10 @@ description: Lint and format code. Auto-detects ESLint, Biome, Prettier, or lang
7
7
  Lint and format code in the current project. Auto-detect the project's linter and formatter toolchain, run them against the target files, and report results grouped by severity with actionable fix suggestions.
8
8
  </objective>
9
9
 
10
+ <working_directory_awareness>
11
+ **Before running any `git` or build command:** check whether your dispatch context specifies a working directory (look for "Working directory:" in your initial prompt). If it does and `pwd` does not match it, prefix every git invocation with `-C <that path>` (e.g. `git -C /path/to/worktree diff --name-only`) and run linters/formatters with the explicit path argument. Linting the wrong directory is a silent failure mode.
12
+ </working_directory_awareness>
13
+
10
14
  <arguments>
11
15
  This skill accepts optional arguments after `/lint`:
12
16
 
@@ -23,6 +23,10 @@ The reviewer reads both the diff and the surrounding source files to understand
23
23
  The purpose is to review and report findings. Making changes during review conflates the reviewer and author roles. Present findings and let the user decide what to act on.
24
24
  </analysis_only_rule>
25
25
 
26
+ <working_directory_awareness>
27
+ **Before running any `git` command:** check whether your dispatch context specifies a working directory (look for "Working directory:" in your initial prompt). If it does and `pwd` does not match it, prefix every git invocation with `-C <that path>` (e.g. `git -C /path/to/worktree diff --cached`). Reviewing the wrong directory's diff is a silent failure mode — the review will look correct but cover the wrong code.
28
+ </working_directory_awareness>
29
+
26
30
  <quick_start>
27
31
 
28
32
  <determine_review_scope>
@@ -151,6 +151,9 @@ Failures:
151
151
  **Suggest what to test when no arguments are given.**
152
152
 
153
153
  **A. Check recent changes:**
154
+
155
+ > **Working directory check:** if your dispatch context specifies a working directory and `pwd` does not match it, prefix the git commands below with `-C <that path>` (e.g. `git -C /path/to/worktree diff --name-only HEAD~5`).
156
+
154
157
  - Run `git diff --name-only HEAD~5` to find recently changed files
155
158
  - Run `git diff --name-only --cached` for staged files
156
159
  - Filter to source files (exclude configs, docs, lockfiles)