gsd-pi 2.25.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +11 -2
  2. package/dist/headless.js +24 -4
  3. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  4. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  5. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +14 -3
  7. package/dist/resources/extensions/gsd/auto.ts +81 -12
  8. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  9. package/dist/resources/extensions/gsd/doctor.ts +24 -1
  10. package/dist/resources/extensions/gsd/files.ts +13 -2
  11. package/dist/resources/extensions/gsd/guided-flow.ts +19 -9
  12. package/dist/resources/extensions/gsd/index.ts +48 -7
  13. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  14. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  15. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  16. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  17. package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
  18. package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
  19. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  20. package/dist/resources/extensions/gsd/state.ts +17 -6
  21. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  22. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  23. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  24. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  25. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  26. package/dist/resources/extensions/gsd/types.ts +2 -0
  27. package/dist/resources/extensions/search-the-web/native-search.ts +4 -0
  28. package/dist/resources/extensions/shared/path-display.ts +19 -0
  29. package/package.json +1 -6
  30. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  31. package/packages/pi-ai/dist/providers/anthropic.js +25 -0
  32. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  33. package/packages/pi-ai/src/providers/anthropic.ts +27 -0
  34. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  35. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  37. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  39. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  42. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  45. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  47. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  49. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  51. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  53. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  56. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  59. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  61. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/index.js +5 -1
  63. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +63 -30
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  73. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  75. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  78. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  80. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  81. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  82. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  83. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  84. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  85. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  86. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  87. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  88. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  89. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  90. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  91. package/packages/pi-coding-agent/src/index.ts +15 -0
  92. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  93. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +40 -4
  94. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  95. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  96. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  97. package/src/resources/extensions/async-jobs/index.ts +9 -1
  98. package/src/resources/extensions/bg-shell/index.ts +3 -2
  99. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  100. package/src/resources/extensions/gsd/auto-worktree.ts +14 -3
  101. package/src/resources/extensions/gsd/auto.ts +81 -12
  102. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  103. package/src/resources/extensions/gsd/doctor.ts +24 -1
  104. package/src/resources/extensions/gsd/files.ts +13 -2
  105. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  106. package/src/resources/extensions/gsd/index.ts +48 -7
  107. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  108. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  109. package/src/resources/extensions/gsd/preferences.ts +2 -1
  110. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  111. package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
  112. package/src/resources/extensions/gsd/prompts/queue.md +2 -2
  113. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  114. package/src/resources/extensions/gsd/state.ts +17 -6
  115. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  116. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  117. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  118. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  119. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  120. package/src/resources/extensions/gsd/types.ts +2 -0
  121. package/src/resources/extensions/search-the-web/native-search.ts +4 -0
  122. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Cross-platform path display tests.
3
+ *
4
+ * Verifies that toPosixPath correctly normalizes Windows paths and that
5
+ * the system prompt builder produces forward-slash paths for LLM consumption.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { toPosixPath } from "../utils/path-display.js";
11
+ import { buildSystemPrompt } from "../core/system-prompt.js";
12
+
13
+ // ─── toPosixPath ────────────────────────────────────────────────────────────
14
+
15
+ test("toPosixPath: converts Windows backslash paths to forward slashes", () => {
16
+ assert.equal(toPosixPath("C:\\Users\\name\\project"), "C:/Users/name/project");
17
+ });
18
+
19
+ test("toPosixPath: handles mixed separators", () => {
20
+ assert.equal(toPosixPath("C:\\Users/name\\project/src"), "C:/Users/name/project/src");
21
+ });
22
+
23
+ test("toPosixPath: no-op for Unix paths", () => {
24
+ assert.equal(toPosixPath("/home/user/project"), "/home/user/project");
25
+ });
26
+
27
+ test("toPosixPath: handles empty string", () => {
28
+ assert.equal(toPosixPath(""), "");
29
+ });
30
+
31
+ test("toPosixPath: handles Windows UNC paths", () => {
32
+ assert.equal(toPosixPath("\\\\server\\share\\dir"), "//server/share/dir");
33
+ });
34
+
35
+ test("toPosixPath: handles .gsd/worktrees path on Windows", () => {
36
+ assert.equal(
37
+ toPosixPath("C:\\Users\\name\\project\\.gsd\\worktrees\\M001"),
38
+ "C:/Users/name/project/.gsd/worktrees/M001",
39
+ );
40
+ });
41
+
42
+ // ─── System prompt path normalization ───────────────────────────────────────
43
+
44
+ test("buildSystemPrompt: cwd uses forward slashes even with Windows input", () => {
45
+ const prompt = buildSystemPrompt({
46
+ cwd: "C:\\Users\\name\\development\\app-name",
47
+ });
48
+ assert.ok(
49
+ prompt.includes("C:/Users/name/development/app-name"),
50
+ "System prompt should contain forward-slash path",
51
+ );
52
+ assert.ok(
53
+ !prompt.includes("C:\\Users\\name\\development\\app-name"),
54
+ "System prompt must NOT contain backslash path",
55
+ );
56
+ });
57
+
58
+ test("buildSystemPrompt: Unix paths pass through unchanged", () => {
59
+ const prompt = buildSystemPrompt({
60
+ cwd: "/home/user/project",
61
+ });
62
+ assert.ok(prompt.includes("/home/user/project"));
63
+ });
64
+
65
+ // ─── Regression: no backslash paths in LLM-visible text ────────────────────
66
+
67
+ /**
68
+ * Pattern that matches Windows-style absolute paths with backslashes.
69
+ * Catches: C:\Users\..., D:\Projects\..., \\server\share\...
70
+ * Does not match: escaped chars in regex, JSON strings, etc.
71
+ */
72
+ const WINDOWS_ABS_PATH_RE = /[A-Z]:\\[A-Za-z]/;
73
+
74
+ test("buildSystemPrompt: no Windows absolute paths with backslashes in output", () => {
75
+ // Simulate a Windows-like cwd
76
+ const prompt = buildSystemPrompt({
77
+ cwd: "D:\\Projects\\my-app\\.gsd\\worktrees\\M002",
78
+ });
79
+ const lines = prompt.split("\n");
80
+ const violations = lines.filter(line => WINDOWS_ABS_PATH_RE.test(line));
81
+ assert.equal(
82
+ violations.length, 0,
83
+ `System prompt contains Windows backslash paths:\n${violations.join("\n")}`,
84
+ );
85
+ });
@@ -114,16 +114,43 @@ function readClipboardImageViaWlPaste(): ClipboardImage | null {
114
114
  .filter(Boolean);
115
115
 
116
116
  const selectedType = selectPreferredImageMimeType(types);
117
- if (!selectedType) {
118
- return null;
117
+ if (selectedType) {
118
+ const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
119
+ if (data.ok && data.stdout.length > 0) {
120
+ return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
121
+ }
119
122
  }
120
123
 
121
- const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
122
- if (!data.ok || data.stdout.length === 0) {
123
- return null;
124
+ // Fallback for WSLg/BMP: when only image/bmp is available, ask wl-paste
125
+ // to convert to PNG on the fly. wl-paste supports format conversion for
126
+ // some compositor types. If that fails, try reading BMP and converting
127
+ // via ImageMagick (#813).
128
+ const hasBmp = types.some((t) => baseMimeType(t) === "image/bmp");
129
+ if (!selectedType && hasBmp) {
130
+ // Try requesting PNG directly — wl-paste may convert
131
+ const pngData = runCommand("wl-paste", ["--type", "image/png", "--no-newline"]);
132
+ if (pngData.ok && pngData.stdout.length > 0) {
133
+ return { bytes: pngData.stdout, mimeType: "image/png" };
134
+ }
135
+
136
+ // Try reading BMP and converting via ImageMagick convert
137
+ const bmpData = runCommand("wl-paste", ["--type", "image/bmp", "--no-newline"]);
138
+ if (bmpData.ok && bmpData.stdout.length > 0) {
139
+ const converted = spawnSync("convert", ["bmp:-", "png:-"], {
140
+ input: bmpData.stdout,
141
+ timeout: 5000,
142
+ maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
143
+ });
144
+ if (!converted.error && converted.status === 0 && converted.stdout.length > 0) {
145
+ const stdout = Buffer.isBuffer(converted.stdout)
146
+ ? converted.stdout
147
+ : Buffer.from(converted.stdout);
148
+ return { bytes: stdout, mimeType: "image/png" };
149
+ }
150
+ }
124
151
  }
125
152
 
126
- return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
153
+ return null;
127
154
  }
128
155
 
129
156
  function readClipboardImageViaXclip(): ClipboardImage | null {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cross-platform path display utilities.
3
+ *
4
+ * Paths injected into LLM prompts, tool results, or any text the model
5
+ * processes must use forward slashes. Windows backslash paths cause bash
6
+ * failures when the model copies them into shell commands — bash interprets
7
+ * backslashes as escape characters, silently stripping them.
8
+ *
9
+ * Node's `path` module and `fs` module handle native separators correctly
10
+ * for filesystem operations. This module is ONLY for paths that enter
11
+ * text consumed by the LLM or interpreted by a shell.
12
+ *
13
+ * Usage:
14
+ * import { toPosixPath } from "./path-display.js";
15
+ * prompt += `Current working directory: ${toPosixPath(cwd)}`;
16
+ *
17
+ * NOT for:
18
+ * fs.readFile(path) — use native path as-is
19
+ * path.join(a, b) — use native path module
20
+ * spawn(cmd, { cwd: path }) — Node handles this correctly
21
+ */
22
+
23
+ /**
24
+ * Convert a filesystem path to forward-slash (POSIX) form for display.
25
+ *
26
+ * On Unix this is a no-op. On Windows it converts `C:\Users\name\project`
27
+ * to `C:/Users/name/project`, which is valid in:
28
+ * - Git Bash / MSYS2
29
+ * - WSL bash
30
+ * - PowerShell
31
+ * - Node.js APIs (which accept both separators)
32
+ * - Most Windows programs
33
+ */
34
+ export function toPosixPath(fsPath: string): string {
35
+ return fsPath.replaceAll("\\", "/");
36
+ }
@@ -54,6 +54,14 @@ export default function AsyncJobs(pi: ExtensionAPI) {
54
54
  ? output.slice(0, maxLen) + "\n\n[... truncated, use await_job for full output]"
55
55
  : output;
56
56
 
57
+ // Deliver as follow-up without triggering a new LLM turn (#875).
58
+ // When the agent is streaming: the message is queued and picked up
59
+ // by the agent loop's getFollowUpMessages() after the current turn.
60
+ // When the agent is idle: the message is appended to context so it's
61
+ // visible on the next user-initiated prompt. Previously triggerTurn:true
62
+ // caused spurious autonomous turns — the model would interpret completed
63
+ // job output as requiring action and cascade into unbounded self-reinforcing
64
+ // loops (running more commands, spawning more jobs, burning context).
57
65
  pi.sendMessage(
58
66
  {
59
67
  customType: "async_job_result",
@@ -64,7 +72,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
64
72
  ].join("\n"),
65
73
  display: true,
66
74
  },
67
- { deliverAs: "followUp", triggerTurn: true },
75
+ { deliverAs: "followUp" },
68
76
  );
69
77
  },
70
78
  });
@@ -66,6 +66,7 @@ import { waitForReady } from "./readiness-detector.js";
66
66
  import { queryShellEnv, sendAndWait, runOnSession } from "./interaction.js";
67
67
  import { formatUptime, formatTokenCount, resolveBgShellPersistenceCwd } from "./utilities.js";
68
68
  import { BgManagerOverlay } from "./overlay.js";
69
+ import { toPosixPath } from "../shared/path-display.js";
69
70
 
70
71
  // ── Re-exports for consumers ───────────────────────────────────────────────
71
72
 
@@ -337,7 +338,7 @@ export default function (pi: ExtensionAPI) {
337
338
  text += ` type: ${bg.processType}\n`;
338
339
  text += ` status: ${bg.status}\n`;
339
340
  text += ` command: ${bg.command}\n`;
340
- text += ` cwd: ${bg.cwd}`;
341
+ text += ` cwd: ${toPosixPath(bg.cwd)}`;
341
342
 
342
343
  if (bg.group) text += `\n group: ${bg.group}`;
343
344
  if (bg.readyPort) text += `\n ready_port: ${bg.readyPort}`;
@@ -694,7 +695,7 @@ export default function (pi: ExtensionAPI) {
694
695
  }
695
696
 
696
697
  let text = `Shell environment for ${bg.id} (${bg.label}):\n`;
697
- text += ` cwd: ${envResult.cwd}\n`;
698
+ text += ` cwd: ${toPosixPath(envResult.cwd)}\n`;
698
699
  text += ` shell: ${envResult.shell}\n`;
699
700
 
700
701
  const envEntries = Object.entries(envResult.env);
@@ -90,6 +90,10 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
90
90
  const dir = resolveMilestonePath(base, mid);
91
91
  return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
92
92
  }
93
+ case "replan-slice": {
94
+ const dir = resolveSlicePath(base, mid, sid!);
95
+ return dir ? join(dir, buildSliceFileName(sid!, "REPLAN")) : null;
96
+ }
93
97
  case "rewrite-docs":
94
98
  return null;
95
99
  default:
@@ -127,10 +131,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
127
131
  }
128
132
 
129
133
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
130
- // Unit types with no verifiable artifact always pass (e.g. replan-slice).
131
- // For all other types, null means the parent directory is missing on disk
132
- // treat as stale completion state so the key gets evicted (#313).
133
- if (!absPath) return unitType === "replan-slice";
134
+ // For unit types with no verifiable artifact (null path), the parent directory
135
+ // is missing on disk treat as stale completion state so the key gets evicted (#313).
136
+ if (!absPath) return false;
134
137
  if (!existsSync(absPath)) return false;
135
138
 
136
139
  // plan-slice must produce a plan with actual task entries, not just a scaffold.
@@ -6,7 +6,7 @@
6
6
  * manages create, enter, detect, and teardown for auto-mode worktrees.
7
7
  */
8
8
 
9
- import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync } from "node:fs";
9
+ import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync, unlinkSync } from "node:fs";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
  import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
12
12
  import { execSync, execFileSync } from "node:child_process";
@@ -312,7 +312,8 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
312
312
 
313
313
  /**
314
314
  * Copy .gsd/ planning artifacts from source repo to a new worktree.
315
- * Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md.
315
+ * Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md,
316
+ * STATE.md, KNOWLEDGE.md, and OVERRIDES.md.
316
317
  * Skips runtime files (auto.lock, metrics.json, etc.) and the worktrees/ dir.
317
318
  * Best-effort — failures are non-fatal since auto-mode can recreate artifacts.
318
319
  */
@@ -330,7 +331,7 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
330
331
  }
331
332
 
332
333
  // Copy top-level planning files
333
- for (const file of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "QUEUE.md"]) {
334
+ for (const file of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "QUEUE.md", "STATE.md", "KNOWLEDGE.md", "OVERRIDES.md"]) {
334
335
  const src = join(srcGsd, file);
335
336
  if (existsSync(src)) {
336
337
  try {
@@ -559,6 +560,16 @@ export function mergeMilestoneToMain(
559
560
  // when main is already checked out in the project-root worktree, #757)
560
561
  const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_);
561
562
  if (currentBranchAtBase !== mainBranch) {
563
+ // Remove untracked .gsd/ state files that may conflict with the branch
564
+ // being checked out. These are regenerated by doctor/rebuildState and
565
+ // are not meaningful in the main working tree — the worktree had the
566
+ // real state. Without this, `git checkout main` fails with
567
+ // "Your local changes would be overwritten" (#827).
568
+ const gsdStateFiles = ["STATE.md", "completed-units.json", "auto.lock"];
569
+ for (const f of gsdStateFiles) {
570
+ const p = join(originalBasePath_, ".gsd", f);
571
+ try { unlinkSync(p); } catch { /* non-fatal — file may not exist */ }
572
+ }
562
573
  nativeCheckoutBranch(originalBasePath_, mainBranch);
563
574
  }
564
575
 
@@ -166,6 +166,41 @@ import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from ".
166
166
  // auto-mode reads stale state from the project root and re-dispatches
167
167
  // already-completed units.
168
168
 
169
+ /**
170
+ * Sync milestone artifacts from project root INTO worktree before deriveState.
171
+ * Covers the case where the LLM wrote artifacts to the main repo filesystem
172
+ * (e.g. via absolute paths) but the worktree has stale data. Also deletes
173
+ * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
174
+ * Non-fatal — sync failure should never block dispatch.
175
+ */
176
+ function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
177
+ if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
178
+ if (!milestoneId) return;
179
+
180
+ const prGsd = join(projectRoot, ".gsd");
181
+ const wtGsd = join(worktreePath, ".gsd");
182
+
183
+ // Copy milestone directory from project root to worktree if the project root
184
+ // has newer artifacts (e.g. slices that don't exist in the worktree yet)
185
+ try {
186
+ const srcMilestone = join(prGsd, "milestones", milestoneId);
187
+ const dstMilestone = join(wtGsd, "milestones", milestoneId);
188
+ if (existsSync(srcMilestone)) {
189
+ mkdirSync(dstMilestone, { recursive: true });
190
+ cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
191
+ }
192
+ } catch { /* non-fatal */ }
193
+
194
+ // Delete worktree gsd.db so it rebuilds from the freshly synced files.
195
+ // Stale DB rows are the root cause of the infinite skip loop (#853).
196
+ try {
197
+ const wtDb = join(wtGsd, "gsd.db");
198
+ if (existsSync(wtDb)) {
199
+ unlinkSync(wtDb);
200
+ }
201
+ } catch { /* non-fatal */ }
202
+ }
203
+
169
204
  /**
170
205
  * Sync dispatch-critical .gsd/ state files from worktree to project root.
171
206
  * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
@@ -261,28 +296,30 @@ const MAX_CONSECUTIVE_SKIPS = 3;
261
296
  /** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
262
297
  const completedKeySet = new Set<string>();
263
298
 
264
- /** Resource sync timestamp captured at auto-mode start. If the managed-resources
265
- * manifest changes mid-session (e.g. /gsd:update or dev edit + copy-resources),
299
+ /** Resource version captured at auto-mode start. If the managed-resources
300
+ * manifest version changes mid-session (e.g. npm update -g gsd-pi),
266
301
  * templates on disk may expect variables the in-memory code doesn't provide.
267
- * Detect this and stop gracefully instead of crashing. */
268
- let resourceSyncedAtOnStart: number | null = null;
302
+ * Detect this and stop gracefully instead of crashing.
303
+ * Uses gsdVersion (semver) instead of syncedAt (timestamp) so that
304
+ * launching a second session doesn't falsely trigger staleness (#804). */
305
+ let resourceVersionOnStart: string | null = null;
269
306
 
270
- function readResourceSyncedAt(): number | null {
307
+ function readResourceVersion(): string | null {
271
308
  const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
272
309
  const manifestPath = join(agentDir, "managed-resources.json");
273
310
  try {
274
311
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
275
- return typeof manifest?.syncedAt === "number" ? manifest.syncedAt : null;
312
+ return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
276
313
  } catch {
277
314
  return null;
278
315
  }
279
316
  }
280
317
 
281
318
  function checkResourcesStale(): string | null {
282
- if (resourceSyncedAtOnStart === null) return null;
283
- const current = readResourceSyncedAt();
319
+ if (resourceVersionOnStart === null) return null;
320
+ const current = readResourceVersion();
284
321
  if (current === null) return null;
285
- if (current !== resourceSyncedAtOnStart) {
322
+ if (current !== resourceVersionOnStart) {
286
323
  return "GSD resources were updated since this session started. Restart gsd to load the new code.";
287
324
  }
288
325
  return null;
@@ -942,6 +979,11 @@ export async function startAuto(
942
979
  ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
943
980
  }
944
981
 
982
+ // Invalidate all caches before initial state derivation to ensure we read
983
+ // fresh disk state. Without this, a stale cache from a prior session (e.g.
984
+ // after a discussion that wrote new artifacts) may cause deriveState to
985
+ // return pre-planning when the roadmap already exists (#800).
986
+ invalidateAllCaches();
945
987
  let state = await deriveState(base);
946
988
 
947
989
  // ── Stale worktree state recovery (#654) ─────────────────────────────────
@@ -1075,7 +1117,7 @@ export async function startAuto(
1075
1117
  restoreHookState(base);
1076
1118
  resetProactiveHealing();
1077
1119
  autoStartTime = Date.now();
1078
- resourceSyncedAtOnStart = readResourceSyncedAt();
1120
+ resourceVersionOnStart = readResourceVersion();
1079
1121
  completedUnits = [];
1080
1122
  pendingQuickTasks = [];
1081
1123
  currentUnit = null;
@@ -1374,10 +1416,13 @@ export async function handleAgentEnd(
1374
1416
  // fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
1375
1417
  // checkboxes). Slice/milestone completion transitions (summary stubs,
1376
1418
  // roadmap [x] marking) are left for the complete-slice dispatch unit.
1419
+ // Exception: after complete-slice itself, use fixLevel:"all" so roadmap
1420
+ // checkboxes get fixed even if complete-slice crashed (#839).
1377
1421
  try {
1378
1422
  const scopeParts = currentUnit.id.split("/").slice(0, 2);
1379
1423
  const doctorScope = scopeParts.join("/");
1380
- const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: "task" });
1424
+ const effectiveFixLevel = currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
1425
+ const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
1381
1426
  if (report.fixesApplied.length > 0) {
1382
1427
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
1383
1428
  }
@@ -2011,6 +2056,7 @@ async function dispatchNextUnit(
2011
2056
  pi: ExtensionAPI,
2012
2057
  ): Promise<void> {
2013
2058
  if (!active || !cmdCtx) {
2059
+ debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
2014
2060
  if (active && !cmdCtx) {
2015
2061
  ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
2016
2062
  }
@@ -2020,6 +2066,7 @@ async function dispatchNextUnit(
2020
2066
  // Reentrancy guard: allow recursive calls from skip paths (_skipDepth > 0)
2021
2067
  // but block concurrent external calls (watchdog, step wizard, etc.)
2022
2068
  if (_dispatching && _skipDepth === 0) {
2069
+ debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
2023
2070
  return; // Another dispatch is in progress — bail silently
2024
2071
  }
2025
2072
  _dispatching = true;
@@ -2055,7 +2102,7 @@ async function dispatchNextUnit(
2055
2102
  // Lightweight check for critical issues that would cause the next unit
2056
2103
  // to fail or corrupt state. Auto-heals what it can, blocks on the rest.
2057
2104
  try {
2058
- const healthGate = preDispatchHealthGate(basePath);
2105
+ const healthGate = await preDispatchHealthGate(basePath);
2059
2106
  if (healthGate.fixesApplied.length > 0) {
2060
2107
  ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
2061
2108
  }
@@ -2068,6 +2115,14 @@ async function dispatchNextUnit(
2068
2115
  // Non-fatal — health gate failure should never block dispatch
2069
2116
  }
2070
2117
 
2118
+ // ── Sync project root artifacts into worktree (#853) ─────────────────
2119
+ // When the LLM writes artifacts to the main repo filesystem instead of
2120
+ // the worktree, the worktree's gsd.db becomes stale. Sync before
2121
+ // deriveState to ensure the worktree has the latest artifacts.
2122
+ if (originalBasePath && basePath !== originalBasePath && currentMilestoneId) {
2123
+ syncProjectRootToWorktree(originalBasePath, basePath, currentMilestoneId);
2124
+ }
2125
+
2071
2126
  const stopDeriveTimer = debugTime("derive-state");
2072
2127
  let state = await deriveState(basePath);
2073
2128
  stopDeriveTimer({
@@ -3751,6 +3806,20 @@ export async function dispatchDirectPhase(
3751
3806
  ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
3752
3807
  return;
3753
3808
  }
3809
+
3810
+ // When require_slice_discussion is enabled, pause auto-mode before
3811
+ // each new slice so the user can discuss requirements first (#789).
3812
+ const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
3813
+ const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
3814
+ if (requireDiscussion && !sliceContextFile) {
3815
+ ctx.ui.notify(
3816
+ `Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
3817
+ "info",
3818
+ );
3819
+ await pauseAuto(ctx, pi);
3820
+ return;
3821
+ }
3822
+
3754
3823
  unitType = "research-slice";
3755
3824
  unitId = `${mid}/${sid}`;
3756
3825
  prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
@@ -19,6 +19,7 @@ import { join } from "node:path";
19
19
  import { gsdRoot, resolveGsdRootFile } from "./paths.js";
20
20
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
21
21
  import { abortAndReset } from "./git-self-heal.js";
22
+ import { rebuildState } from "./doctor.js";
22
23
 
23
24
  // ── Health Score Tracking ──────────────────────────────────────────────────
24
25
 
@@ -131,7 +132,7 @@ export interface PreDispatchHealthResult {
131
132
  *
132
133
  * Returns { proceed: true } if dispatch should continue.
133
134
  */
134
- export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult {
135
+ export async function preDispatchHealthGate(basePath: string): Promise<PreDispatchHealthResult> {
135
136
  const issues: string[] = [];
136
137
  const fixesApplied: string[] = [];
137
138
 
@@ -172,17 +173,17 @@ export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult
172
173
  }
173
174
 
174
175
  // ── STATE.md existence check ──
175
- // If STATE.md is missing, deriveState will still work but the LLM
176
- // may get confused. Rebuild it silently.
176
+ // If STATE.md is missing, rebuild it now so the next unit has accurate
177
+ // context. Non-blocking if the rebuild throws, dispatch continues anyway.
177
178
  try {
178
179
  const stateFile = resolveGsdRootFile(basePath, "STATE");
179
180
  const milestonesDir = join(gsdRoot(basePath), "milestones");
180
181
  if (existsSync(milestonesDir) && !existsSync(stateFile)) {
181
- issues.push("STATE.md missing — will rebuild after this unit");
182
- // Don't block dispatch — rebuilding happens in post-hook
182
+ await rebuildState(basePath);
183
+ fixesApplied.push("rebuilt missing STATE.md before dispatch");
183
184
  }
184
185
  } catch {
185
- // Non-fatal
186
+ // Non-fatal — dispatch continues without STATE.md if rebuild fails
186
187
  }
187
188
 
188
189
  // If we had critical issues that couldn't be auto-healed, block dispatch
@@ -1144,8 +1144,31 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1144
1144
  unitId: taskUnitId,
1145
1145
  message: `Task ${task.id} is marked done but summary is missing`,
1146
1146
  file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
1147
- fixable: false,
1147
+ fixable: true,
1148
1148
  });
1149
+ // Write a stub summary so validate-milestone can proceed.
1150
+ // This prevents infinite skip loops when tasks are marked done
1151
+ // without summaries (#820).
1152
+ if (shouldFix("task_done_missing_summary")) {
1153
+ const stubPath = join(
1154
+ basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
1155
+ `${task.id}-SUMMARY.md`,
1156
+ );
1157
+ const stubContent = [
1158
+ `---`,
1159
+ `status: done`,
1160
+ `result: unknown`,
1161
+ `doctor_generated: true`,
1162
+ `---`,
1163
+ ``,
1164
+ `# ${task.id}: ${task.title || "Unknown"}`,
1165
+ ``,
1166
+ `Summary stub generated by \`/gsd doctor\` — task was marked done but no summary existed.`,
1167
+ ``,
1168
+ ].join("\n");
1169
+ await saveFile(stubPath, stubContent);
1170
+ fixesApplied.push(`created stub summary for ${taskUnitId}`);
1171
+ }
1149
1172
  }
1150
1173
 
1151
1174
  if (!task.done && hasSummary) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { promises as fs } from 'node:fs';
7
7
  import { dirname, resolve } from 'node:path';
8
+ import { randomBytes } from 'node:crypto';
8
9
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
9
10
  import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
10
11
 
@@ -705,9 +706,19 @@ export async function saveFile(path: string, content: string): Promise<void> {
705
706
  const dir = dirname(path);
706
707
  await fs.mkdir(dir, { recursive: true });
707
708
 
708
- const tmpPath = path + '.tmp';
709
+ // Use a unique temp path per call to avoid collisions when parallel
710
+ // tool calls target the same file (e.g. concurrent gsd_save_decision).
711
+ // rename() is atomic on POSIX, so last-writer-wins is correct for
712
+ // regenerate-from-DB writes.
713
+ const tmpPath = path + `.tmp.${randomBytes(4).toString("hex")}`;
709
714
  await fs.writeFile(tmpPath, content, 'utf-8');
710
- await fs.rename(tmpPath, path);
715
+ try {
716
+ await fs.rename(tmpPath, path);
717
+ } catch (err) {
718
+ // Clean up orphaned temp file on rename failure
719
+ await fs.unlink(tmpPath).catch(() => {});
720
+ throw err;
721
+ }
711
722
  }
712
723
 
713
724
  export function parseRequirementCounts(content: string | null): RequirementCounts {
@@ -636,9 +636,11 @@ async function showQueueAdd(
636
636
  const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
637
637
 
638
638
  // ── Determine next milestone ID ─────────────────────────────────────
639
+ // Note: the LLM will use the gsd_generate_milestone_id tool to get IDs
640
+ // at creation time, but we still mention the next ID in the preamble
641
+ // for context about where the sequence is.
639
642
  const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
640
643
  const nextId = nextMilestoneId(milestoneIds, uniqueEnabled);
641
- const nextIdPlus1 = nextMilestoneId([...milestoneIds, nextId], uniqueEnabled);
642
644
 
643
645
  // ── Build preamble ──────────────────────────────────────────────────
644
646
  const activePart = state.activeMilestone
@@ -659,8 +661,6 @@ async function showQueueAdd(
659
661
  const queueInlinedTemplates = inlineTemplate("context", "Context");
660
662
  const prompt = loadPrompt("queue", {
661
663
  preamble,
662
- nextId,
663
- nextIdPlus1,
664
664
  existingMilestonesContext: existingContext,
665
665
  inlinedTemplates: queueInlinedTemplates,
666
666
  commitInstruction: buildDocsCommitInstruction("docs: queue <milestone list>"),
@@ -959,12 +959,22 @@ export async function showDiscuss(
959
959
 
960
960
  // Loop: show picker, dispatch discuss, repeat until "not_yet"
961
961
  while (true) {
962
- const actions = pendingSlices.map((s, i) => ({
963
- id: s.id,
964
- label: `${s.id}: ${s.title}`,
965
- description: state.activeSlice?.id === s.id ? "active slice" : "upcoming",
966
- recommended: i === 0,
967
- }));
962
+ const actions = pendingSlices.map((s, i) => {
963
+ // Check if this slice has already been discussed (CONTEXT file exists)
964
+ const contextFile = resolveSliceFile(basePath, mid, s.id, "CONTEXT");
965
+ const discussed = !!contextFile;
966
+ const statusParts: string[] = [];
967
+ if (state.activeSlice?.id === s.id) statusParts.push("active");
968
+ else statusParts.push("upcoming");
969
+ statusParts.push(discussed ? "discussed ✓" : "not discussed");
970
+
971
+ return {
972
+ id: s.id,
973
+ label: `${s.id}: ${s.title}`,
974
+ description: statusParts.join(" · "),
975
+ recommended: i === 0,
976
+ };
977
+ });
968
978
 
969
979
  const choice = await showNextAction(ctx, {
970
980
  title: "GSD — Discuss a slice",