gsd-pi 2.22.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 (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * GSD Parallel Orchestrator — Core engine for parallel milestone orchestration.
3
+ *
4
+ * Manages worker lifecycle, budget tracking, and coordination. Workers are
5
+ * separate processes spawned via child_process, each running in its own git
6
+ * worktree with GSD_MILESTONE_LOCK env var set. The coordinator monitors
7
+ * workers via session status files (see session-status-io.ts).
8
+ */
9
+
10
+ import { spawn, type ChildProcess } from "node:child_process";
11
+ import { existsSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { gsdRoot } from "./paths.js";
15
+ import { createWorktree, worktreePath } from "./worktree-manager.js";
16
+ import { autoWorktreeBranch, runWorktreePostCreateHook } from "./auto-worktree.js";
17
+ import { nativeBranchExists } from "./native-git-bridge.js";
18
+ import { readIntegrationBranch } from "./git-service.js";
19
+ import { resolveParallelConfig } from "./preferences.js";
20
+ import type { GSDPreferences } from "./preferences.js";
21
+ import type { ParallelConfig } from "./types.js";
22
+ import {
23
+ writeSessionStatus,
24
+ readAllSessionStatuses,
25
+ removeSessionStatus,
26
+ sendSignal,
27
+ cleanupStaleSessions,
28
+ type SessionStatus,
29
+ } from "./session-status-io.js";
30
+ import {
31
+ analyzeParallelEligibility,
32
+ type ParallelCandidates,
33
+ } from "./parallel-eligibility.js";
34
+
35
+ // ─── Types ─────────────────────────────────────────────────────────────────
36
+
37
+ export interface WorkerInfo {
38
+ milestoneId: string;
39
+ title: string;
40
+ pid: number;
41
+ process: ChildProcess | null; // null after process exits
42
+ worktreePath: string;
43
+ startedAt: number;
44
+ state: "running" | "paused" | "stopped" | "error";
45
+ completedUnits: number;
46
+ cost: number;
47
+ }
48
+
49
+ export interface OrchestratorState {
50
+ active: boolean;
51
+ workers: Map<string, WorkerInfo>;
52
+ config: ParallelConfig;
53
+ totalCost: number;
54
+ startedAt: number;
55
+ }
56
+
57
+ // ─── Module State ──────────────────────────────────────────────────────────
58
+
59
+ let state: OrchestratorState | null = null;
60
+
61
+ // ─── Accessors ─────────────────────────────────────────────────────────────
62
+
63
+ /** Returns true if the orchestrator is active and has been initialized. */
64
+ export function isParallelActive(): boolean {
65
+ return state?.active ?? false;
66
+ }
67
+
68
+ /** Returns the current orchestrator state, or null if not initialized. */
69
+ export function getOrchestratorState(): OrchestratorState | null {
70
+ return state;
71
+ }
72
+
73
+ /** Returns a snapshot of all tracked workers as an array. */
74
+ export function getWorkerStatuses(): WorkerInfo[] {
75
+ if (!state) return [];
76
+ return [...state.workers.values()];
77
+ }
78
+
79
+ // ─── Preparation ───────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Analyze eligibility and prepare for parallel start.
83
+ * Returns the candidates report without actually starting workers.
84
+ */
85
+ export async function prepareParallelStart(
86
+ basePath: string,
87
+ _prefs: GSDPreferences | undefined,
88
+ ): Promise<ParallelCandidates> {
89
+ return analyzeParallelEligibility(basePath);
90
+ }
91
+
92
+ // ─── Start ─────────────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Start parallel execution with the given eligible milestones.
96
+ * Creates worktrees, spawns worker processes, and begins monitoring.
97
+ */
98
+ export async function startParallel(
99
+ basePath: string,
100
+ milestoneIds: string[],
101
+ prefs: GSDPreferences | undefined,
102
+ ): Promise<{ started: string[]; errors: Array<{ mid: string; error: string }> }> {
103
+ // Prevent workers from spawning nested parallel sessions
104
+ if (process.env.GSD_PARALLEL_WORKER) {
105
+ return { started: [], errors: [{ mid: "all", error: "Cannot start parallel from within a parallel worker" }] };
106
+ }
107
+
108
+ const config = resolveParallelConfig(prefs);
109
+ const now = Date.now();
110
+
111
+ // Initialize orchestrator state
112
+ state = {
113
+ active: true,
114
+ workers: new Map(),
115
+ config,
116
+ totalCost: 0,
117
+ startedAt: now,
118
+ };
119
+
120
+ const started: string[] = [];
121
+ const errors: Array<{ mid: string; error: string }> = [];
122
+
123
+ // Cap to max_workers
124
+ const toStart = milestoneIds.slice(0, config.max_workers);
125
+
126
+ for (const mid of toStart) {
127
+ try {
128
+ // Create the worktree (without chdir — coordinator stays in project root)
129
+ let wtPath: string;
130
+ try {
131
+ wtPath = createMilestoneWorktree(basePath, mid);
132
+ } catch {
133
+ // Worktree creation may fail in test environments or when git
134
+ // is not available. Fall back to a placeholder path.
135
+ wtPath = worktreePath(basePath, mid);
136
+ }
137
+
138
+ const worker: WorkerInfo = {
139
+ milestoneId: mid,
140
+ title: mid,
141
+ pid: process.pid,
142
+ process: null,
143
+ worktreePath: wtPath,
144
+ startedAt: now,
145
+ state: "running",
146
+ completedUnits: 0,
147
+ cost: 0,
148
+ };
149
+
150
+ state.workers.set(mid, worker);
151
+
152
+ // Write initial session status
153
+ const sessionStatus: SessionStatus = {
154
+ milestoneId: mid,
155
+ pid: worker.pid,
156
+ state: "running",
157
+ currentUnit: null,
158
+ completedUnits: 0,
159
+ cost: 0,
160
+ lastHeartbeat: now,
161
+ startedAt: now,
162
+ worktreePath: wtPath,
163
+ };
164
+ writeSessionStatus(basePath, sessionStatus);
165
+
166
+ // Attempt to spawn the worker process.
167
+ // Spawning may fail if the CLI binary is not available (e.g., in tests).
168
+ // The worker is still tracked and can be spawned later via spawnWorker().
169
+ const spawned = spawnWorker(basePath, mid);
170
+ if (!spawned) {
171
+ // Worker tracked but not yet running a process.
172
+ // State stays "running" so coordinator can retry or user can investigate.
173
+ }
174
+
175
+ started.push(mid);
176
+ } catch (err) {
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ errors.push({ mid, error: message });
179
+ }
180
+ }
181
+
182
+ // If nothing started successfully, deactivate
183
+ if (started.length === 0) {
184
+ state.active = false;
185
+ }
186
+
187
+ return { started, errors };
188
+ }
189
+
190
+ // ─── Worktree Creation ────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Create a git worktree for a milestone without changing the coordinator's cwd.
194
+ * Uses milestone/<MID> branch naming (same as auto-worktree.ts).
195
+ */
196
+ function createMilestoneWorktree(basePath: string, milestoneId: string): string {
197
+ const branch = autoWorktreeBranch(milestoneId);
198
+ const branchExists = nativeBranchExists(basePath, branch);
199
+
200
+ let info: { name: string; path: string; branch: string; exists: boolean };
201
+ if (branchExists) {
202
+ info = createWorktree(basePath, milestoneId, { branch, reuseExistingBranch: true });
203
+ } else {
204
+ const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
205
+ info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
206
+ }
207
+
208
+ // Run post-create hook if configured
209
+ runWorktreePostCreateHook(basePath, info.path);
210
+
211
+ return info.path;
212
+ }
213
+
214
+ // ─── Worker Spawning ───────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Spawn a worker process for a milestone.
218
+ * The worker runs `gsd --print "/gsd auto"` in the milestone's worktree
219
+ * with GSD_MILESTONE_LOCK set to isolate state derivation.
220
+ */
221
+ export function spawnWorker(
222
+ basePath: string,
223
+ milestoneId: string,
224
+ ): boolean {
225
+ if (!state) return false;
226
+ const worker = state.workers.get(milestoneId);
227
+ if (!worker) return false;
228
+ if (worker.process) return true; // already spawned
229
+
230
+ // Resolve the GSD CLI binary path
231
+ const binPath = resolveGsdBin();
232
+ if (!binPath) return false;
233
+
234
+ let child: ChildProcess;
235
+ try {
236
+ child = spawn(process.execPath, [binPath, "--print", "/gsd auto"], {
237
+ cwd: worker.worktreePath,
238
+ env: {
239
+ ...process.env,
240
+ GSD_MILESTONE_LOCK: milestoneId,
241
+ // Prevent workers from spawning their own parallel sessions
242
+ GSD_PARALLEL_WORKER: "1",
243
+ },
244
+ stdio: ["ignore", "pipe", "pipe"],
245
+ detached: false,
246
+ });
247
+ } catch {
248
+ return false;
249
+ }
250
+
251
+ // Handle spawn errors (e.g., ENOENT when binary doesn't exist)
252
+ child.on("error", () => {
253
+ if (!state) return;
254
+ const w = state.workers.get(milestoneId);
255
+ if (w) {
256
+ w.process = null;
257
+ // Don't change state — spawn failure is non-fatal, coordinator can retry
258
+ }
259
+ });
260
+
261
+ worker.process = child;
262
+ worker.pid = child.pid ?? 0;
263
+
264
+ if (!child.pid) {
265
+ // Spawn returned but no PID — process failed to start
266
+ worker.process = null;
267
+ return false;
268
+ }
269
+
270
+ // Update session status with real PID
271
+ writeSessionStatus(basePath, {
272
+ milestoneId,
273
+ pid: worker.pid,
274
+ state: "running",
275
+ currentUnit: null,
276
+ completedUnits: worker.completedUnits,
277
+ cost: worker.cost,
278
+ lastHeartbeat: Date.now(),
279
+ startedAt: worker.startedAt,
280
+ worktreePath: worker.worktreePath,
281
+ });
282
+
283
+ // Handle worker exit
284
+ child.on("exit", (code) => {
285
+ if (!state) return;
286
+ const w = state.workers.get(milestoneId);
287
+ if (!w) return;
288
+
289
+ w.process = null;
290
+ if (w.state === "stopped") return; // graceful stop, already handled
291
+
292
+ if (code === 0) {
293
+ w.state = "stopped";
294
+ } else {
295
+ w.state = "error";
296
+ }
297
+
298
+ // Update session status
299
+ writeSessionStatus(basePath, {
300
+ milestoneId,
301
+ pid: w.pid,
302
+ state: w.state,
303
+ currentUnit: null,
304
+ completedUnits: w.completedUnits,
305
+ cost: w.cost,
306
+ lastHeartbeat: Date.now(),
307
+ startedAt: w.startedAt,
308
+ worktreePath: w.worktreePath,
309
+ });
310
+ });
311
+
312
+ return true;
313
+ }
314
+
315
+ /**
316
+ * Resolve the GSD CLI binary path.
317
+ * Uses GSD_BIN_PATH env var (set by loader.ts) or falls back to
318
+ * finding the binary relative to the current module.
319
+ */
320
+ function resolveGsdBin(): string | null {
321
+ // GSD_BIN_PATH is set by loader.ts to the absolute path of dist/loader.js
322
+ if (process.env.GSD_BIN_PATH && existsSync(process.env.GSD_BIN_PATH)) {
323
+ return process.env.GSD_BIN_PATH;
324
+ }
325
+
326
+ // Fallback: try to find loader.js relative to this file
327
+ // This file is at dist/resources/extensions/gsd/parallel-orchestrator.js
328
+ // loader.js is at dist/loader.js
329
+ let thisDir: string;
330
+ try {
331
+ thisDir = dirname(fileURLToPath(import.meta.url));
332
+ } catch {
333
+ thisDir = process.cwd();
334
+ }
335
+ const candidates = [
336
+ join(thisDir, "..", "..", "..", "loader.js"),
337
+ join(thisDir, "..", "..", "..", "..", "dist", "loader.js"),
338
+ ];
339
+ for (const candidate of candidates) {
340
+ if (existsSync(candidate)) return candidate;
341
+ }
342
+
343
+ return null;
344
+ }
345
+
346
+ // ─── Stop ──────────────────────────────────────────────────────────────────
347
+
348
+ /**
349
+ * Stop all workers or a specific milestone's worker.
350
+ * Sends stop signals and updates tracking state.
351
+ */
352
+ export async function stopParallel(
353
+ basePath: string,
354
+ milestoneId?: string,
355
+ ): Promise<void> {
356
+ if (!state) return;
357
+
358
+ const targets = milestoneId
359
+ ? [milestoneId]
360
+ : [...state.workers.keys()];
361
+
362
+ for (const mid of targets) {
363
+ const worker = state.workers.get(mid);
364
+ if (!worker) continue;
365
+
366
+ // Send stop signal via file-based IPC (worker checks on next dispatch)
367
+ sendSignal(basePath, mid, "stop");
368
+
369
+ // Also send SIGTERM to the process for immediate response
370
+ if (worker.process && worker.pid > 0) {
371
+ try {
372
+ worker.process.kill("SIGTERM");
373
+ } catch { /* process may already be dead */ }
374
+ }
375
+
376
+ // Update in-memory state
377
+ worker.state = "stopped";
378
+ worker.process = null;
379
+
380
+ // Clean up session status file
381
+ removeSessionStatus(basePath, mid);
382
+ }
383
+
384
+ // If stopping all workers, deactivate the orchestrator
385
+ if (!milestoneId) {
386
+ state.active = false;
387
+ }
388
+ }
389
+
390
+ // ─── Pause / Resume ────────────────────────────────────────────────────────
391
+
392
+ /** Pause a specific worker or all workers. */
393
+ export function pauseWorker(
394
+ basePath: string,
395
+ milestoneId?: string,
396
+ ): void {
397
+ if (!state) return;
398
+
399
+ const targets = milestoneId
400
+ ? [milestoneId]
401
+ : [...state.workers.keys()];
402
+
403
+ for (const mid of targets) {
404
+ const worker = state.workers.get(mid);
405
+ if (!worker || worker.state !== "running") continue;
406
+
407
+ sendSignal(basePath, mid, "pause");
408
+ worker.state = "paused";
409
+ }
410
+ }
411
+
412
+ /** Resume a specific worker or all workers. */
413
+ export function resumeWorker(
414
+ basePath: string,
415
+ milestoneId?: string,
416
+ ): void {
417
+ if (!state) return;
418
+
419
+ const targets = milestoneId
420
+ ? [milestoneId]
421
+ : [...state.workers.keys()];
422
+
423
+ for (const mid of targets) {
424
+ const worker = state.workers.get(mid);
425
+ if (!worker || worker.state !== "paused") continue;
426
+
427
+ sendSignal(basePath, mid, "resume");
428
+ worker.state = "running";
429
+ }
430
+ }
431
+
432
+ // ─── Status Refresh ────────────────────────────────────────────────────────
433
+
434
+ /**
435
+ * Poll worker statuses from disk and update orchestrator state.
436
+ * Call this periodically from the dashboard refresh cycle.
437
+ */
438
+ export function refreshWorkerStatuses(basePath: string): void {
439
+ if (!state) return;
440
+
441
+ // Clean up stale sessions first
442
+ const staleIds = cleanupStaleSessions(basePath);
443
+ for (const mid of staleIds) {
444
+ const worker = state.workers.get(mid);
445
+ if (worker) {
446
+ worker.state = "error";
447
+ worker.process = null;
448
+ }
449
+ }
450
+
451
+ // Read all live session statuses from disk
452
+ const statuses = readAllSessionStatuses(basePath);
453
+ const statusMap = new Map<string, SessionStatus>();
454
+ for (const s of statuses) {
455
+ statusMap.set(s.milestoneId, s);
456
+ }
457
+
458
+ // Update in-memory worker state from disk data
459
+ for (const [mid, worker] of state.workers) {
460
+ const diskStatus = statusMap.get(mid);
461
+ if (!diskStatus) continue;
462
+
463
+ worker.state = diskStatus.state;
464
+ worker.completedUnits = diskStatus.completedUnits;
465
+ worker.cost = diskStatus.cost;
466
+ worker.pid = diskStatus.pid;
467
+ }
468
+
469
+ // Recalculate aggregate cost
470
+ state.totalCost = 0;
471
+ for (const worker of state.workers.values()) {
472
+ state.totalCost += worker.cost;
473
+ }
474
+ }
475
+
476
+ // ─── Budget ────────────────────────────────────────────────────────────────
477
+
478
+ /** Get aggregate cost across all workers. */
479
+ export function getAggregateCost(): number {
480
+ if (!state) return 0;
481
+ return state.totalCost;
482
+ }
483
+
484
+ /** Check if budget ceiling has been reached. */
485
+ export function isBudgetExceeded(): boolean {
486
+ if (!state) return false;
487
+ if (state.config.budget_ceiling == null) return false;
488
+ return state.totalCost >= state.config.budget_ceiling;
489
+ }
490
+
491
+ // ─── Reset ─────────────────────────────────────────────────────────────────
492
+
493
+ /** Reset orchestrator state. Called on clean shutdown. */
494
+ export function resetOrchestrator(): void {
495
+ state = null;
496
+ }
@@ -60,7 +60,8 @@ export function checkPostUnitHooks(
60
60
 
61
61
  // Don't trigger hooks for other hook units (prevent hook-on-hook chains)
62
62
  // Don't trigger hooks for triage units (prevent hook-on-triage chains)
63
- if (completedUnitType.startsWith("hook/") || completedUnitType === "triage-captures") return null;
63
+ // Don't trigger hooks for quick-task units (lightweight one-offs from captures)
64
+ if (completedUnitType.startsWith("hook/") || completedUnitType === "triage-captures" || completedUnitType === "quick-task") return null;
64
65
 
65
66
  // Check if any hooks are configured for this unit type
66
67
  const hooks = resolvePostUnitHooks().filter(h =>
@@ -75,6 +75,7 @@ const KNOWN_PREFERENCE_KEYS = new Set<string>([
75
75
  "token_profile",
76
76
  "phases",
77
77
  "auto_visualize",
78
+ "parallel",
78
79
  ]);
79
80
 
80
81
  export interface GSDSkillRule {
@@ -171,6 +172,7 @@ export interface GSDPreferences {
171
172
  token_profile?: TokenProfile;
172
173
  phases?: PhaseSkipPreferences;
173
174
  auto_visualize?: boolean;
175
+ parallel?: import("./types.js").ParallelConfig;
174
176
  }
175
177
 
176
178
  export interface LoadedGSDPreferences {
@@ -688,6 +690,7 @@ export function resolveProfileDefaults(profile: TokenProfile): Partial<GSDPrefer
688
690
  skip_research: true,
689
691
  skip_reassess: true,
690
692
  skip_slice_research: true,
693
+ skip_milestone_validation: true,
691
694
  },
692
695
  };
693
696
  case "balanced":
@@ -767,6 +770,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
767
770
  phases: (base.phases || override.phases)
768
771
  ? { ...(base.phases ?? {}), ...(override.phases ?? {}) }
769
772
  : undefined,
773
+ parallel: (base.parallel || override.parallel)
774
+ ? { ...(base.parallel ?? {}), ...(override.parallel ?? {}) } as import("./types.js").ParallelConfig
775
+ : undefined,
770
776
  };
771
777
  }
772
778
 
@@ -909,8 +915,9 @@ export function validatePreferences(preferences: GSDPreferences): {
909
915
  if (p.skip_research !== undefined) validatedPhases.skip_research = !!p.skip_research;
910
916
  if (p.skip_reassess !== undefined) validatedPhases.skip_reassess = !!p.skip_reassess;
911
917
  if (p.skip_slice_research !== undefined) validatedPhases.skip_slice_research = !!p.skip_slice_research;
918
+ if (p.skip_milestone_validation !== undefined) validatedPhases.skip_milestone_validation = !!p.skip_milestone_validation;
912
919
  // Warn on unknown phase keys
913
- const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research"]);
920
+ const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research", "skip_milestone_validation"]);
914
921
  for (const key of Object.keys(p)) {
915
922
  if (!knownPhaseKeys.has(key)) {
916
923
  warnings.push(`unknown phases key "${key}" — ignored`);
@@ -1152,6 +1159,51 @@ export function validatePreferences(preferences: GSDPreferences): {
1152
1159
  }
1153
1160
  }
1154
1161
 
1162
+ // ─── Parallel Config ────────────────────────────────────────────────────
1163
+ if (preferences.parallel && typeof preferences.parallel === "object") {
1164
+ const p = preferences.parallel as unknown as Record<string, unknown>;
1165
+ const parallel: Record<string, unknown> = {};
1166
+
1167
+ if (p.enabled !== undefined) {
1168
+ if (typeof p.enabled === "boolean") parallel.enabled = p.enabled;
1169
+ else errors.push("parallel.enabled must be a boolean");
1170
+ }
1171
+ if (p.max_workers !== undefined) {
1172
+ if (typeof p.max_workers === "number" && p.max_workers >= 1 && p.max_workers <= 4) {
1173
+ parallel.max_workers = Math.floor(p.max_workers);
1174
+ } else {
1175
+ errors.push("parallel.max_workers must be a number between 1 and 4");
1176
+ }
1177
+ }
1178
+ if (p.budget_ceiling !== undefined) {
1179
+ if (typeof p.budget_ceiling === "number" && p.budget_ceiling > 0) {
1180
+ parallel.budget_ceiling = p.budget_ceiling;
1181
+ } else {
1182
+ errors.push("parallel.budget_ceiling must be a positive number");
1183
+ }
1184
+ }
1185
+ if (p.merge_strategy !== undefined) {
1186
+ const validStrategies = new Set(["per-slice", "per-milestone"]);
1187
+ if (typeof p.merge_strategy === "string" && validStrategies.has(p.merge_strategy)) {
1188
+ parallel.merge_strategy = p.merge_strategy;
1189
+ } else {
1190
+ errors.push("parallel.merge_strategy must be one of: per-slice, per-milestone");
1191
+ }
1192
+ }
1193
+ if (p.auto_merge !== undefined) {
1194
+ const validModes = new Set(["auto", "confirm", "manual"]);
1195
+ if (typeof p.auto_merge === "string" && validModes.has(p.auto_merge)) {
1196
+ parallel.auto_merge = p.auto_merge;
1197
+ } else {
1198
+ errors.push("parallel.auto_merge must be one of: auto, confirm, manual");
1199
+ }
1200
+ }
1201
+
1202
+ if (Object.keys(parallel).length > 0) {
1203
+ validated.parallel = parallel as unknown as import("./types.js").ParallelConfig;
1204
+ }
1205
+ }
1206
+
1155
1207
  // ─── Git Preferences ───────────────────────────────────────────────────
1156
1208
  if (preferences.git && typeof preferences.git === "object") {
1157
1209
  const git: Record<string, unknown> = {};
@@ -1369,3 +1421,15 @@ export function updatePreferencesModels(models: GSDModelConfigV2): void {
1369
1421
 
1370
1422
  writeFileSync(prefsPath, content, "utf-8");
1371
1423
  }
1424
+
1425
+ // ─── Parallel Config Resolver ──────────────────────────────────────────────
1426
+
1427
+ export function resolveParallelConfig(prefs: GSDPreferences | undefined): import("./types.js").ParallelConfig {
1428
+ return {
1429
+ enabled: prefs?.parallel?.enabled ?? false,
1430
+ max_workers: Math.max(1, Math.min(4, prefs?.parallel?.max_workers ?? 2)),
1431
+ budget_ceiling: prefs?.parallel?.budget_ceiling,
1432
+ merge_strategy: prefs?.parallel?.merge_strategy ?? "per-milestone",
1433
+ auto_merge: prefs?.parallel?.auto_merge ?? "confirm",
1434
+ };
1435
+ }