gsd-pi 2.23.0 → 2.24.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 (121) hide show
  1. package/dist/cli.js +12 -3
  2. package/dist/headless.d.ts +4 -0
  3. package/dist/headless.js +118 -10
  4. package/dist/help-text.js +22 -7
  5. package/dist/resource-loader.js +64 -9
  6. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  10. package/dist/resources/extensions/gsd/auto.ts +123 -41
  11. package/dist/resources/extensions/gsd/commands.ts +176 -10
  12. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  14. package/dist/resources/extensions/gsd/doctor.ts +56 -11
  15. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  16. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  17. package/dist/resources/extensions/gsd/guided-flow.ts +75 -0
  18. package/dist/resources/extensions/gsd/index.ts +34 -1
  19. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  20. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  21. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  22. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  23. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  24. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  26. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  27. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  28. package/dist/resources/extensions/gsd/state.ts +72 -30
  29. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  30. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  31. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  32. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
  33. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  34. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  35. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  36. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  37. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  38. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  39. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  40. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  41. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  42. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  43. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  44. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  45. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  46. package/dist/resources/extensions/gsd/types.ts +15 -1
  47. package/dist/resources/extensions/subagent/index.ts +5 -0
  48. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  49. package/dist/update-check.d.ts +9 -0
  50. package/dist/update-check.js +97 -0
  51. package/package.json +6 -1
  52. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  53. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  54. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  55. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  56. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  57. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  58. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  60. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  61. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  62. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  63. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  64. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  65. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  66. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  67. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  68. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  69. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  70. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  71. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  72. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  73. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  75. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  76. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  77. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  78. package/scripts/postinstall.js +7 -109
  79. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  80. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  81. package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
  82. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  83. package/src/resources/extensions/gsd/auto.ts +123 -41
  84. package/src/resources/extensions/gsd/commands.ts +176 -10
  85. package/src/resources/extensions/gsd/complexity.ts +1 -0
  86. package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  87. package/src/resources/extensions/gsd/doctor.ts +56 -11
  88. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  89. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  90. package/src/resources/extensions/gsd/guided-flow.ts +75 -0
  91. package/src/resources/extensions/gsd/index.ts +34 -1
  92. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  93. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  94. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  95. package/src/resources/extensions/gsd/preferences.ts +65 -1
  96. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  97. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  98. package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  99. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  100. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  101. package/src/resources/extensions/gsd/state.ts +72 -30
  102. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  103. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  104. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  105. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
  106. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  107. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  108. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  109. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  110. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  111. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  112. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  113. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  114. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  115. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  116. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  117. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  118. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  119. package/src/resources/extensions/gsd/types.ts +15 -1
  120. package/src/resources/extensions/subagent/index.ts +5 -0
  121. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -6,14 +6,14 @@
6
6
 
7
7
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
8
8
  import { AuthStorage } from "@gsd/pi-coding-agent";
9
- import { existsSync, readFileSync, mkdirSync } from "node:fs";
9
+ import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
10
10
  import { join, dirname } from "node:path";
11
11
  import { enableDebug, isDebugEnabled } from "./debug-logger.js";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { deriveState } from "./state.js";
14
14
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
15
15
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
16
- import { showQueue, showDiscuss } from "./guided-flow.js";
16
+ import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
17
17
  import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
18
18
  import { resolveProjectRoot } from "./worktree.js";
19
19
  import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
@@ -42,6 +42,14 @@ import { handleQuick } from "./quick.js";
42
42
  import { handleHistory } from "./history.js";
43
43
  import { handleUndo } from "./undo.js";
44
44
  import { handleExport } from "./export.js";
45
+ import {
46
+ isParallelActive, getOrchestratorState, getWorkerStatuses,
47
+ prepareParallelStart, startParallel, stopParallel,
48
+ pauseWorker, resumeWorker,
49
+ } from "./parallel-orchestrator.js";
50
+ import { formatEligibilityReport } from "./parallel-eligibility.js";
51
+ import { mergeAllCompleted, mergeCompletedMilestone, formatMergeResults } from "./parallel-merge.js";
52
+ import { resolveParallelConfig } from "./preferences.js";
45
53
  import { nativeBranchList, nativeDetectMainBranch, nativeBranchListMerged, nativeBranchDelete, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js";
46
54
 
47
55
  export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
@@ -69,20 +77,53 @@ function projectRoot(): string {
69
77
 
70
78
  export function registerGSDCommand(pi: ExtensionAPI): void {
71
79
  pi.registerCommand("gsd", {
72
- description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge",
80
+ description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel",
73
81
  getArgumentCompletions: (prefix: string) => {
74
82
  const subcommands = [
75
- "help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "quick", "discuss",
76
- "capture", "triage", "dispatch",
77
- "history", "undo", "skip", "export", "cleanup", "mode", "prefs",
78
- "config", "hooks", "run-hook", "skill-health", "doctor", "forensics", "migrate", "remote", "steer", "inspect", "knowledge",
83
+ { cmd: "help", desc: "Categorized command reference with descriptions" },
84
+ { cmd: "next", desc: "Explicit step mode (same as /gsd)" },
85
+ { cmd: "auto", desc: "Autonomous mode — research, plan, execute, commit, repeat" },
86
+ { cmd: "stop", desc: "Stop auto mode gracefully" },
87
+ { cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
88
+ { cmd: "status", desc: "Progress dashboard" },
89
+ { cmd: "visualize", desc: "Open workflow visualizer (progress, deps, metrics, timeline)" },
90
+ { cmd: "queue", desc: "Queue and reorder future milestones" },
91
+ { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
92
+ { cmd: "discuss", desc: "Discuss architecture and decisions" },
93
+ { cmd: "capture", desc: "Fire-and-forget thought capture" },
94
+ { cmd: "triage", desc: "Manually trigger triage of pending captures" },
95
+ { cmd: "dispatch", desc: "Dispatch a specific phase directly" },
96
+ { cmd: "history", desc: "View execution history" },
97
+ { cmd: "undo", desc: "Revert last completed unit" },
98
+ { cmd: "skip", desc: "Prevent a unit from auto-mode dispatch" },
99
+ { cmd: "export", desc: "Export milestone/slice results" },
100
+ { cmd: "cleanup", desc: "Remove merged branches or snapshots" },
101
+ { cmd: "mode", desc: "Switch workflow mode (solo/team)" },
102
+ { cmd: "prefs", desc: "Manage preferences (model selection, timeouts, etc.)" },
103
+ { cmd: "config", desc: "Set API keys for external tools" },
104
+ { cmd: "hooks", desc: "Show configured post-unit and pre-dispatch hooks" },
105
+ { cmd: "run-hook", desc: "Manually trigger a specific hook" },
106
+ { cmd: "skill-health", desc: "Skill lifecycle dashboard" },
107
+ { cmd: "doctor", desc: "Runtime health checks with auto-fix" },
108
+ { cmd: "forensics", desc: "Examine execution logs" },
109
+ { cmd: "migrate", desc: "Migrate a v1 .planning directory to .gsd format" },
110
+ { cmd: "remote", desc: "Control remote auto-mode" },
111
+ { cmd: "steer", desc: "Hard-steer plan documents during execution" },
112
+ { cmd: "inspect", desc: "Show SQLite DB diagnostics" },
113
+ { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
114
+ { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
115
+ { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
79
116
  ];
80
117
  const parts = prefix.trim().split(/\s+/);
81
118
 
82
119
  if (parts.length <= 1) {
83
120
  return subcommands
84
- .filter((cmd) => cmd.startsWith(parts[0] ?? ""))
85
- .map((cmd) => ({ value: cmd, label: cmd }));
121
+ .filter((item) => item.cmd.startsWith(parts[0] ?? ""))
122
+ .map((item) => ({
123
+ value: item.cmd,
124
+ label: item.cmd,
125
+ description: item.desc
126
+ }));
86
127
  }
87
128
 
88
129
  if (parts[0] === "auto" && parts.length <= 2) {
@@ -99,6 +140,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
99
140
  .map((cmd) => ({ value: `mode ${cmd}`, label: cmd }));
100
141
  }
101
142
 
143
+ if (parts[0] === "parallel" && parts.length <= 2) {
144
+ const subPrefix = parts[1] ?? "";
145
+ return ["start", "status", "stop", "pause", "resume", "merge"]
146
+ .filter((cmd) => cmd.startsWith(subPrefix))
147
+ .map((cmd) => ({ value: `parallel ${cmd}`, label: cmd }));
148
+ }
149
+
102
150
  if (parts[0] === "prefs" && parts.length <= 2) {
103
151
  const subPrefix = parts[1] ?? "";
104
152
  return ["global", "project", "status", "wizard", "setup", "import-claude"]
@@ -251,7 +299,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
251
299
  }
252
300
  return;
253
301
  }
254
- await stopAuto(ctx, pi);
302
+ await stopAuto(ctx, pi, "User requested stop");
255
303
  return;
256
304
  }
257
305
 
@@ -288,6 +336,108 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
288
336
  return;
289
337
  }
290
338
 
339
+ // ─── Parallel Orchestration ────────────────────────────────────────
340
+ if (trimmed.startsWith("parallel")) {
341
+ const parallelArgs = trimmed.slice("parallel".length).trim();
342
+ const [subCmd = "", ...restParts] = parallelArgs.split(/\s+/);
343
+ const rest = restParts.join(" ");
344
+
345
+ if (subCmd === "start" || subCmd === "") {
346
+ const loaded = loadEffectiveGSDPreferences();
347
+ const config = resolveParallelConfig(loaded?.preferences);
348
+ if (!config.enabled) {
349
+ pi.sendMessage({
350
+ customType: "gsd-parallel",
351
+ content: "Parallel mode is not enabled. Set `parallel.enabled: true` in your preferences.",
352
+ display: false,
353
+ });
354
+ return;
355
+ }
356
+ const candidates = await prepareParallelStart(projectRoot(), loaded?.preferences);
357
+ const report = formatEligibilityReport(candidates);
358
+ if (candidates.eligible.length === 0) {
359
+ pi.sendMessage({ customType: "gsd-parallel", content: report + "\n\nNo milestones are eligible for parallel execution.", display: false });
360
+ return;
361
+ }
362
+ const result = await startParallel(
363
+ projectRoot(),
364
+ candidates.eligible.map(e => e.milestoneId),
365
+ loaded?.preferences,
366
+ );
367
+ const lines = [`Parallel orchestration started.`, `Workers: ${result.started.join(", ")}`];
368
+ if (result.errors.length > 0) {
369
+ lines.push(`Errors: ${result.errors.map(e => `${e.mid}: ${e.error}`).join("; ")}`);
370
+ }
371
+ pi.sendMessage({ customType: "gsd-parallel", content: report + "\n\n" + lines.join("\n"), display: false });
372
+ return;
373
+ }
374
+
375
+ if (subCmd === "status") {
376
+ if (!isParallelActive()) {
377
+ pi.sendMessage({ customType: "gsd-parallel", content: "No parallel orchestration is currently active.", display: false });
378
+ return;
379
+ }
380
+ const workers = getWorkerStatuses();
381
+ const lines = ["# Parallel Workers\n"];
382
+ for (const w of workers) {
383
+ lines.push(`- **${w.milestoneId}** (${w.title}) — ${w.state} — ${w.completedUnits} units — $${w.cost.toFixed(2)}`);
384
+ }
385
+ const orchState = getOrchestratorState();
386
+ if (orchState) {
387
+ lines.push(`\nTotal cost: $${orchState.totalCost.toFixed(2)}`);
388
+ }
389
+ pi.sendMessage({ customType: "gsd-parallel", content: lines.join("\n"), display: false });
390
+ return;
391
+ }
392
+
393
+ if (subCmd === "stop") {
394
+ const mid = rest.trim() || undefined;
395
+ await stopParallel(projectRoot(), mid);
396
+ pi.sendMessage({ customType: "gsd-parallel", content: mid ? `Stopped worker for ${mid}.` : "All parallel workers stopped.", display: false });
397
+ return;
398
+ }
399
+
400
+ if (subCmd === "pause") {
401
+ const mid = rest.trim() || undefined;
402
+ pauseWorker(projectRoot(), mid);
403
+ pi.sendMessage({ customType: "gsd-parallel", content: mid ? `Paused worker for ${mid}.` : "All parallel workers paused.", display: false });
404
+ return;
405
+ }
406
+
407
+ if (subCmd === "resume") {
408
+ const mid = rest.trim() || undefined;
409
+ resumeWorker(projectRoot(), mid);
410
+ pi.sendMessage({ customType: "gsd-parallel", content: mid ? `Resumed worker for ${mid}.` : "All parallel workers resumed.", display: false });
411
+ return;
412
+ }
413
+
414
+ if (subCmd === "merge") {
415
+ const mid = rest.trim() || undefined;
416
+ if (mid) {
417
+ // Merge a specific milestone
418
+ const result = await mergeCompletedMilestone(projectRoot(), mid);
419
+ pi.sendMessage({ customType: "gsd-parallel", content: formatMergeResults([result]), display: false });
420
+ return;
421
+ }
422
+ // Merge all completed milestones
423
+ const workers = getWorkerStatuses();
424
+ if (workers.length === 0) {
425
+ pi.sendMessage({ customType: "gsd-parallel", content: "No parallel workers to merge.", display: false });
426
+ return;
427
+ }
428
+ const results = await mergeAllCompleted(projectRoot(), workers);
429
+ pi.sendMessage({ customType: "gsd-parallel", content: formatMergeResults(results), display: false });
430
+ return;
431
+ }
432
+
433
+ pi.sendMessage({
434
+ customType: "gsd-parallel",
435
+ content: `Unknown parallel subcommand "${subCmd}". Usage: /gsd parallel [start|status|stop|pause|resume|merge]`,
436
+ display: false,
437
+ });
438
+ return;
439
+ }
440
+
291
441
  if (trimmed === "cleanup") {
292
442
  await handleCleanupBranches(ctx, projectRoot());
293
443
  await handleCleanupSnapshots(ctx, projectRoot());
@@ -314,6 +464,21 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
314
464
  return;
315
465
  }
316
466
 
467
+ if (trimmed === "new-milestone") {
468
+ const basePath = projectRoot();
469
+ const headlessContextPath = join(basePath, ".gsd", "runtime", "headless-context.md");
470
+ if (existsSync(headlessContextPath)) {
471
+ const seedContext = readFileSync(headlessContextPath, "utf-8");
472
+ try { unlinkSync(headlessContextPath); } catch { /* non-fatal */ }
473
+ await showHeadlessMilestoneCreation(ctx, pi, basePath, seedContext);
474
+ } else {
475
+ // No headless context — fall back to interactive smart entry
476
+ const { showSmartEntry } = await import("./guided-flow.js");
477
+ await showSmartEntry(ctx, pi, basePath);
478
+ }
479
+ return;
480
+ }
481
+
317
482
  if (trimmed.startsWith("capture ") || trimmed === "capture") {
318
483
  await handleCapture(trimmed.replace(/^capture\s*/, "").trim(), ctx);
319
484
  return;
@@ -434,6 +599,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
434
599
  " /gsd stop Stop auto-mode gracefully",
435
600
  " /gsd pause Pause auto-mode (preserves state, /gsd auto to resume)",
436
601
  " /gsd discuss Start guided milestone/slice discussion",
602
+ " /gsd new-milestone Create milestone from headless context (used by gsd headless)",
437
603
  "",
438
604
  "VISIBILITY",
439
605
  " /gsd status Show progress dashboard (Ctrl+Alt+G)",
@@ -87,6 +87,7 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
87
87
  "execute-task": "standard",
88
88
  "replan-slice": "heavy",
89
89
  "reassess-roadmap": "heavy",
90
+ "validate-milestone": "heavy",
90
91
  "complete-milestone": "standard",
91
92
  };
92
93
 
@@ -19,6 +19,7 @@ import {
19
19
  } from "./metrics.js";
20
20
  import { loadEffectiveGSDPreferences } from "./preferences.js";
21
21
  import { getActiveWorktreeName } from "./worktree-command.js";
22
+ import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagent/worker-registry.js";
22
23
 
23
24
  function formatDuration(ms: number): string {
24
25
  const s = Math.floor(ms / 1000);
@@ -363,6 +364,43 @@ export class GSDDashboardOverlay {
363
364
  lines.push(blank());
364
365
  }
365
366
 
367
+ // Parallel workers section — shows active subagent sessions
368
+ if (hasActiveWorkers()) {
369
+ lines.push(hr());
370
+ lines.push(row(th.fg("text", th.bold("Parallel Workers"))));
371
+ lines.push(blank());
372
+
373
+ const batches = getWorkerBatches();
374
+ for (const [batchId, workers] of batches) {
375
+ const running = workers.filter(w => w.status === "running").length;
376
+ const done = workers.filter(w => w.status === "completed").length;
377
+ const failed = workers.filter(w => w.status === "failed").length;
378
+ const total = workers[0]?.batchSize ?? workers.length;
379
+
380
+ lines.push(row(joinColumns(
381
+ ` ${th.fg("accent", "⟐")} ${th.fg("text", `Batch ${batchId.slice(0, 8)}`)}`,
382
+ th.fg("dim", `${done + failed}/${total} done`),
383
+ contentWidth,
384
+ )));
385
+
386
+ for (const w of workers) {
387
+ const icon = w.status === "running"
388
+ ? th.fg("accent", "▸")
389
+ : w.status === "completed"
390
+ ? th.fg("success", "✓")
391
+ : th.fg("error", "✗");
392
+ const elapsed = th.fg("dim", formatDuration(Date.now() - w.startedAt));
393
+ const taskPreview = truncateToWidth(w.task, Math.max(20, contentWidth - 30));
394
+ lines.push(row(joinColumns(
395
+ ` ${icon} ${th.fg("text", w.agent)} ${th.fg("dim", taskPreview)}`,
396
+ elapsed,
397
+ contentWidth,
398
+ )));
399
+ }
400
+ }
401
+ lines.push(blank());
402
+ }
403
+
366
404
  // Pending captures badge — only shown when captures are waiting for triage
367
405
  if (this.dashData.pendingCaptureCount > 0) {
368
406
  const count = this.dashData.pendingCaptureCount;
@@ -11,6 +11,7 @@ import { RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
11
11
  import { nativeIsRepo, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
12
12
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
13
13
  import { ensureGitignore } from "./gitignore.js";
14
+ import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
14
15
 
15
16
  export type DoctorSeverity = "info" | "warning" | "error";
16
17
  export type DoctorIssueCode =
@@ -24,6 +25,7 @@ export type DoctorIssueCode =
24
25
  | "all_tasks_done_roadmap_not_checked"
25
26
  | "slice_checked_missing_summary"
26
27
  | "slice_checked_missing_uat"
28
+ | "all_slices_done_missing_milestone_validation"
27
29
  | "all_slices_done_missing_milestone_summary"
28
30
  | "task_done_must_haves_not_verified"
29
31
  | "active_requirement_missing_owner"
@@ -36,6 +38,7 @@ export type DoctorIssueCode =
36
38
  | "tracked_runtime_files"
37
39
  | "legacy_slice_branches"
38
40
  | "stale_crash_lock"
41
+ | "stale_parallel_session"
39
42
  | "orphaned_completed_units"
40
43
  | "stale_hook_state"
41
44
  | "activity_log_bloat"
@@ -710,6 +713,31 @@ async function checkRuntimeHealth(
710
713
  // Non-fatal — crash lock check failed
711
714
  }
712
715
 
716
+ // ── Stale parallel sessions ────────────────────────────────────────────
717
+ try {
718
+ const parallelStatuses = readAllSessionStatuses(basePath);
719
+ for (const status of parallelStatuses) {
720
+ if (isSessionStale(status)) {
721
+ issues.push({
722
+ severity: "warning",
723
+ code: "stale_parallel_session",
724
+ scope: "project",
725
+ unitId: status.milestoneId,
726
+ message: `Stale parallel session for ${status.milestoneId} (PID ${status.pid}, started ${new Date(status.startedAt).toISOString()}, last heartbeat ${new Date(status.lastHeartbeat).toISOString()}) — process is no longer running`,
727
+ file: `.gsd/parallel/${status.milestoneId}.status.json`,
728
+ fixable: true,
729
+ });
730
+
731
+ if (shouldFix("stale_parallel_session")) {
732
+ removeSessionStatus(basePath, status.milestoneId);
733
+ fixesApplied.push(`cleaned up stale parallel session for ${status.milestoneId}`);
734
+ }
735
+ }
736
+ }
737
+ } catch {
738
+ // Non-fatal — parallel session check failed
739
+ }
740
+
713
741
  // ── Orphaned completed-units keys ─────────────────────────────────────
714
742
  try {
715
743
  const completedKeysFile = join(root, "completed-units.json");
@@ -1066,11 +1094,13 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1066
1094
  const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
1067
1095
  if (!tasksDir) {
1068
1096
  issues.push({
1069
- severity: "error",
1097
+ severity: slice.done ? "warning" : "error",
1070
1098
  code: "missing_tasks_dir",
1071
1099
  scope: "slice",
1072
1100
  unitId,
1073
- message: `Missing tasks directory for ${unitId}`,
1101
+ message: slice.done
1102
+ ? `Missing tasks directory for ${unitId} (slice is complete — cosmetic only)`
1103
+ : `Missing tasks directory for ${unitId}`,
1074
1104
  file: relSlicePath(basePath, milestoneId, slice.id),
1075
1105
  fixable: true,
1076
1106
  });
@@ -1084,15 +1114,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1084
1114
  const planContent = planPath ? await loadFile(planPath) : null;
1085
1115
  const plan = planContent ? parsePlan(planContent) : null;
1086
1116
  if (!plan) {
1087
- issues.push({
1088
- severity: "warning",
1089
- code: "missing_slice_plan",
1090
- scope: "slice",
1091
- unitId,
1092
- message: `Slice ${unitId} has no plan file`,
1093
- file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
1094
- fixable: false,
1095
- });
1117
+ if (!slice.done) {
1118
+ issues.push({
1119
+ severity: "warning",
1120
+ code: "missing_slice_plan",
1121
+ scope: "slice",
1122
+ unitId,
1123
+ message: `Slice ${unitId} has no plan file`,
1124
+ file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
1125
+ fixable: false,
1126
+ });
1127
+ }
1096
1128
  continue;
1097
1129
  }
1098
1130
 
@@ -1255,6 +1287,19 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1255
1287
  }
1256
1288
  }
1257
1289
 
1290
+ // Milestone-level check: all slices done but no validation file
1291
+ if (isMilestoneComplete(roadmap) && !resolveMilestoneFile(basePath, milestoneId, "VALIDATION") && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
1292
+ issues.push({
1293
+ severity: "info",
1294
+ code: "all_slices_done_missing_milestone_validation",
1295
+ scope: "milestone",
1296
+ unitId: milestoneId,
1297
+ message: `All slices are done but ${milestoneId}-VALIDATION.md is missing — milestone is in validating-milestone phase`,
1298
+ file: relMilestoneFile(basePath, milestoneId, "VALIDATION"),
1299
+ fixable: false,
1300
+ });
1301
+ }
1302
+
1258
1303
  // Milestone-level check: all slices done but no milestone summary
1259
1304
  if (isMilestoneComplete(roadmap) && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
1260
1305
  issues.push({
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
2
 
3
- type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI) => Promise<void>;
3
+ type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
4
4
 
5
5
  export function registerExitCommand(
6
6
  pi: ExtensionAPI,
@@ -11,7 +11,7 @@ export function registerExitCommand(
11
11
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
12
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
13
  const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
14
- await stopAuto(ctx, pi);
14
+ await stopAuto(ctx, pi, "Graceful exit");
15
15
  ctx.shutdown();
16
16
  },
17
17
  });
@@ -19,6 +19,7 @@ const GSD_RUNTIME_PATTERNS = [
19
19
  ".gsd/forensics/",
20
20
  ".gsd/runtime/",
21
21
  ".gsd/worktrees/",
22
+ ".gsd/parallel/",
22
23
  ".gsd/auto.lock",
23
24
  ".gsd/metrics.json",
24
25
  ".gsd/completed-units.json",
@@ -201,6 +201,81 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string)
201
201
  });
202
202
  }
203
203
 
204
+ /**
205
+ * Build the discuss prompt for headless milestone creation.
206
+ * Uses the discuss-headless prompt template with seed context injected.
207
+ */
208
+ function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePath: string): string {
209
+ const milestoneRel = `.gsd/milestones/${nextId}`;
210
+ const inlinedTemplates = [
211
+ inlineTemplate("project", "Project"),
212
+ inlineTemplate("requirements", "Requirements"),
213
+ inlineTemplate("context", "Context"),
214
+ inlineTemplate("roadmap", "Roadmap"),
215
+ inlineTemplate("decisions", "Decisions"),
216
+ ].join("\n\n---\n\n");
217
+ return loadPrompt("discuss-headless", {
218
+ milestoneId: nextId,
219
+ seedContext,
220
+ contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
221
+ roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
222
+ inlinedTemplates,
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Bootstrap a .gsd/ project from scratch for headless use.
228
+ * Ensures git repo, .gsd/ structure, gitignore, and preferences all exist.
229
+ */
230
+ function bootstrapGsdProject(basePath: string): void {
231
+ if (!nativeIsRepo(basePath)) {
232
+ const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
233
+ nativeInit(basePath, mainBranch);
234
+ }
235
+
236
+ const root = gsdRoot(basePath);
237
+ mkdirSync(join(root, "milestones"), { recursive: true });
238
+ mkdirSync(join(root, "runtime"), { recursive: true });
239
+
240
+ const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
241
+ ensureGitignore(basePath, { commitDocs });
242
+ ensurePreferences(basePath);
243
+ untrackRuntimeFiles(basePath);
244
+ }
245
+
246
+ /**
247
+ * Headless milestone creation from a seed specification document.
248
+ * Bootstraps the project if needed, generates the next milestone ID,
249
+ * and dispatches the headless discuss prompt (no Q&A rounds).
250
+ */
251
+ export async function showHeadlessMilestoneCreation(
252
+ ctx: ExtensionCommandContext,
253
+ pi: ExtensionAPI,
254
+ basePath: string,
255
+ seedContext: string,
256
+ ): Promise<void> {
257
+ // Ensure .gsd/ is bootstrapped
258
+ bootstrapGsdProject(basePath);
259
+
260
+ // Generate next milestone ID
261
+ const existingIds = findMilestoneIds(basePath);
262
+ const prefs = loadEffectiveGSDPreferences();
263
+ const nextId = nextMilestoneId(existingIds, prefs?.preferences?.unique_milestone_ids ?? false);
264
+
265
+ // Create milestone directory
266
+ const milestoneDir = join(basePath, ".gsd", "milestones", nextId, "slices");
267
+ mkdirSync(milestoneDir, { recursive: true });
268
+
269
+ // Build and dispatch the headless discuss prompt
270
+ const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
271
+
272
+ // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
273
+ pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
274
+
275
+ // Dispatch
276
+ dispatchWorkflow(pi, prompt);
277
+ }
278
+
204
279
  export function findMilestoneIds(basePath: string): string[] {
205
280
  const dir = milestonesDir(basePath);
206
281
  try {
@@ -129,6 +129,23 @@ export default function (pi: ExtensionAPI) {
129
129
  registerWorktreeCommand(pi);
130
130
  registerExitCommand(pi);
131
131
 
132
+ // ── EPIPE guard — prevent crash when stdout/stderr pipe closes unexpectedly ──
133
+ // Node.js throws a fatal `Error: write EPIPE` when the parent process closes
134
+ // its end of the stdio pipe (e.g. during shell/IPC teardown) while auto-mode
135
+ // is still writing diagnostics. Catching this here gives auto-mode a clean
136
+ // chance to persist state and pause instead of crashing (see issue #739).
137
+ if (!process.listeners("uncaughtException").some(l => l.name === "_gsdEpipeGuard")) {
138
+ const _gsdEpipeGuard = (err: Error): void => {
139
+ if ((err as NodeJS.ErrnoException).code === "EPIPE") {
140
+ // Pipe closed — nothing we can write; just exit cleanly
141
+ process.exit(0);
142
+ }
143
+ // Re-throw anything that isn't EPIPE so real crashes still surface
144
+ throw err;
145
+ };
146
+ process.on("uncaughtException", _gsdEpipeGuard);
147
+ }
148
+
132
149
  // ── /kill — immediate exit (bypass cleanup) ─────────────────────────────
133
150
  pi.registerCommand("kill", {
134
151
  description: "Exit GSD immediately (no cleanup)",
@@ -699,7 +716,23 @@ export default function (pi: ExtensionAPI) {
699
716
  }
700
717
  }
701
718
 
702
- await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi));
719
+ // Detect rate-limit errors and extract retry delay for auto-resume
720
+ const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
721
+ const isRateLimit = /rate.?limit|too many requests|429/i.test(errorMsg);
722
+ const retryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number")
723
+ ? lastMsg.retryAfterMs
724
+ : (() => { const m = errorMsg.match(/reset in (\d+)s/i); return m ? Number(m[1]) * 1000 : undefined; })();
725
+
726
+ await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi), {
727
+ isRateLimit,
728
+ retryAfterMs,
729
+ resume: () => {
730
+ pi.sendMessage(
731
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution \u2014 rate limit window elapsed.", display: false },
732
+ { triggerTurn: true },
733
+ );
734
+ },
735
+ });
703
736
  return;
704
737
  }
705
738