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
package/README.md CHANGED
@@ -32,6 +32,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
32
32
  - **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing (v2.17)
33
33
  - **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
34
34
  - **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
35
+ - **[Parallel Orchestration](./docs/parallel-orchestration.md)** — run multiple milestones simultaneously
35
36
  - **[Working in Teams](./docs/working-in-teams.md)** — unique IDs, shared artifacts
36
37
  - **[Skills](./docs/skills.md)** — bundled skills, discovery, custom authoring
37
38
  - **[Commands Reference](./docs/commands.md)** — all commands and keyboard shortcuts
@@ -112,9 +113,11 @@ Each slice flows through phases automatically:
112
113
 
113
114
  ```
114
115
  Research → Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
116
+ ↓ (all slices done)
117
+ Validate Milestone → Complete Milestone
115
118
  ```
116
119
 
117
- **Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded. **Complete** writes the summary, UAT script, marks the roadmap, and commits. **Reassess** checks if the roadmap still makes sense given what was learned.
120
+ **Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded. **Complete** writes the summary, UAT script, marks the roadmap, and commits. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
118
121
 
119
122
  ### `/gsd auto` — The Main Event
120
123
 
@@ -556,7 +559,13 @@ Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock,
556
559
 
557
560
  If you have a **Claude Max**, **Codex**, or **GitHub Copilot** subscription, you can use those directly — Pi handles the OAuth flow. No API key needed.
558
561
 
559
- > **Note:** Using OAuth tokens from subscription plans (e.g. Claude Max) outside their native applications may not be explicitly permitted by the provider's Terms of Service. GSD supports API key authentication for all providers as an alternative. Use at your own discretion.
562
+ > **⚠️ Important:** Using OAuth tokens from subscription plans outside their native applications may violate the provider's Terms of Service. In particular:
563
+ >
564
+ > - **Google Gemini** — Using Gemini CLI or Antigravity OAuth tokens in third-party tools has resulted in **Google account suspensions**. This affects your entire Google account, not just the Gemini service. **Use a Gemini API key instead.**
565
+ > - **Claude Max** — Anthropic's ToS may not explicitly permit OAuth use outside Claude's own applications.
566
+ > - **GitHub Copilot** — Usage outside GitHub's own tools may be restricted by your subscription terms.
567
+ >
568
+ > GSD supports API key authentication for all providers as the safe alternative. **We strongly recommend using API keys over OAuth for Google Gemini.**
560
569
 
561
570
  ### OpenRouter
562
571
 
package/dist/headless.js CHANGED
@@ -137,18 +137,37 @@ function formatProgress(event, verbose) {
137
137
  // ---------------------------------------------------------------------------
138
138
  // Completion Detection
139
139
  // ---------------------------------------------------------------------------
140
- const TERMINAL_KEYWORDS = ['complete', 'stopped', 'blocked'];
140
+ /**
141
+ * Detect genuine auto-mode termination notifications.
142
+ *
143
+ * Only matches the actual stop signals emitted by stopAuto():
144
+ * "Auto-mode stopped..."
145
+ * "Step-mode stopped..."
146
+ *
147
+ * Does NOT match progress notifications that happen to contain words like
148
+ * "complete" or "stopped" (e.g., "Override resolved — rewrite-docs completed",
149
+ * "All slices are complete — nothing to discuss", "Skipped 5+ completed units").
150
+ *
151
+ * Blocked detection is separate — checked via isBlockedNotification.
152
+ */
153
+ const TERMINAL_PREFIXES = ['auto-mode stopped', 'step-mode stopped'];
141
154
  const IDLE_TIMEOUT_MS = 15_000;
155
+ // new-milestone is a long-running creative task where the LLM may pause
156
+ // between tool calls (e.g. after mkdir, before writing files). Use a
157
+ // longer idle timeout to avoid killing the session prematurely (#808).
158
+ const NEW_MILESTONE_IDLE_TIMEOUT_MS = 120_000;
142
159
  function isTerminalNotification(event) {
143
160
  if (event.type !== 'extension_ui_request' || event.method !== 'notify')
144
161
  return false;
145
162
  const message = String(event.message ?? '').toLowerCase();
146
- return TERMINAL_KEYWORDS.some((kw) => message.includes(kw));
163
+ return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));
147
164
  }
148
165
  function isBlockedNotification(event) {
149
166
  if (event.type !== 'extension_ui_request' || event.method !== 'notify')
150
167
  return false;
151
- return String(event.message ?? '').toLowerCase().includes('blocked');
168
+ const message = String(event.message ?? '').toLowerCase();
169
+ // Blocked notifications come through stopAuto as "Auto-mode stopped (Blocked: ...)"
170
+ return message.includes('blocked:');
152
171
  }
153
172
  function isMilestoneReadyNotification(event) {
154
173
  if (event.type !== 'extension_ui_request' || event.method !== 'notify')
@@ -285,6 +304,7 @@ export async function runHeadless(options) {
285
304
  });
286
305
  // Idle timeout — fallback completion detection
287
306
  let idleTimer = null;
307
+ const effectiveIdleTimeout = isNewMilestone ? NEW_MILESTONE_IDLE_TIMEOUT_MS : IDLE_TIMEOUT_MS;
288
308
  function resetIdleTimer() {
289
309
  if (idleTimer)
290
310
  clearTimeout(idleTimer);
@@ -292,7 +312,7 @@ export async function runHeadless(options) {
292
312
  idleTimer = setTimeout(() => {
293
313
  completed = true;
294
314
  resolveCompletion();
295
- }, IDLE_TIMEOUT_MS);
315
+ }, effectiveIdleTimeout);
296
316
  }
297
317
  }
298
318
  // Overall timeout
@@ -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",
@@ -36,7 +36,7 @@ import { loadPrompt } from "./prompt-loader.js";
36
36
  import { deriveState } from "./state.js";
37
37
  import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js";
38
38
  import { saveActivityLog } from "./activity-log.js";
39
- import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
39
+ import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId, findMilestoneIds, nextMilestoneId } from "./guided-flow.js";
40
40
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
41
41
  import {
42
42
  loadEffectiveGSDPreferences,
@@ -59,6 +59,7 @@ import { homedir } from "node:os";
59
59
  import { shortcutDesc } from "../shared/terminal.js";
60
60
  import { Text } from "@gsd/pi-tui";
61
61
  import { pauseAutoForProviderError } from "./provider-error-pause.js";
62
+ import { toPosixPath } from "../shared/path-display.js";
62
63
 
63
64
  // ── Agent Instructions ────────────────────────────────────────────────────
64
65
  // Lightweight "always follow" files injected into every GSD agent session.
@@ -467,6 +468,46 @@ export default function (pi: ExtensionAPI) {
467
468
  },
468
469
  });
469
470
 
471
+ // ── gsd_generate_milestone_id — canonical milestone ID generation ──────
472
+ // The LLM cannot generate random suffixes for unique_milestone_ids on its
473
+ // own. This tool calls back into the TS code that owns ID generation,
474
+ // ensuring the preference is always respected and IDs are always valid.
475
+ pi.registerTool({
476
+ name: "gsd_generate_milestone_id",
477
+ label: "Generate Milestone ID",
478
+ description:
479
+ "Generate the next milestone ID for a new GSD milestone. " +
480
+ "Scans existing milestones on disk and respects the unique_milestone_ids preference. " +
481
+ "Always use this tool when creating a new milestone — never invent milestone IDs manually.",
482
+ promptSnippet: "Generate a valid milestone ID (respects unique_milestone_ids preference)",
483
+ promptGuidelines: [
484
+ "ALWAYS call gsd_generate_milestone_id before creating a new milestone directory or writing milestone files.",
485
+ "Never invent or hardcode milestone IDs like M001, M002 — always use this tool.",
486
+ "Call it once per milestone you need to create. For multi-milestone projects, call it once for each milestone in sequence.",
487
+ "The tool returns the correct format based on project preferences (e.g. M001 or M001-r5jzab).",
488
+ ],
489
+ parameters: Type.Object({}),
490
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
491
+ try {
492
+ const basePath = process.cwd();
493
+ const existingIds = findMilestoneIds(basePath);
494
+ const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
495
+ const newId = nextMilestoneId(existingIds, uniqueEnabled);
496
+ return {
497
+ content: [{ type: "text" as const, text: newId }],
498
+ details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, uniqueEnabled },
499
+ };
500
+ } catch (err) {
501
+ const msg = err instanceof Error ? err.message : String(err);
502
+ return {
503
+ content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
504
+ isError: true,
505
+ details: { operation: "generate_milestone_id", error: msg },
506
+ };
507
+ }
508
+ },
509
+ });
510
+
470
511
  // ── session_start: render branded GSD header + load tool keys + remote status ──
471
512
  pi.on("session_start", async (_event, ctx) => {
472
513
  // Theme access throws in RPC mode (no TUI) — header is decorative, skip it
@@ -608,12 +649,12 @@ export default function (pi: ExtensionAPI) {
608
649
  "",
609
650
  "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
610
651
  `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
611
- `The actual current working directory is: ${process.cwd()}`,
652
+ `The actual current working directory is: ${toPosixPath(process.cwd())}`,
612
653
  "",
613
654
  `You are working inside a GSD worktree.`,
614
655
  `- Worktree name: ${worktreeName}`,
615
- `- Worktree path (this is the real cwd): ${process.cwd()}`,
616
- `- Main project: ${worktreeMainCwd}`,
656
+ `- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
657
+ `- Main project: ${toPosixPath(worktreeMainCwd)}`,
617
658
  `- Branch: worktree/${worktreeName}`,
618
659
  "",
619
660
  "All file operations, bash commands, and GSD state resolve against the worktree path above.",
@@ -625,12 +666,12 @@ export default function (pi: ExtensionAPI) {
625
666
  "",
626
667
  "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
627
668
  `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
628
- `The actual current working directory is: ${process.cwd()}`,
669
+ `The actual current working directory is: ${toPosixPath(process.cwd())}`,
629
670
  "",
630
671
  "You are working inside a GSD auto-worktree.",
631
672
  `- Milestone worktree: ${autoWorktree.worktreeName}`,
632
- `- Worktree path (this is the real cwd): ${process.cwd()}`,
633
- `- Main project: ${autoWorktree.originalBase}`,
673
+ `- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
674
+ `- Main project: ${toPosixPath(autoWorktree.originalBase)}`,
634
675
  `- Branch: ${autoWorktree.branch}`,
635
676
  "",
636
677
  "All file operations, bash commands, and GSD state resolve against the worktree path above.",