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.
- package/README.md +25 -1
- package/dist/cli.js +74 -7
- package/dist/headless.d.ts +25 -0
- package/dist/headless.js +454 -0
- package/dist/help-text.js +47 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +560 -52
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +194 -11
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +76 -12
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +560 -52
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +194 -11
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +76 -12
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +85 -5
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -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 {
|
|
@@ -821,8 +896,9 @@ export async function showDiscuss(
|
|
|
821
896
|
|
|
822
897
|
if (choice === "discuss_draft") {
|
|
823
898
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
899
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
824
900
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
825
|
-
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
901
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
826
902
|
});
|
|
827
903
|
const seed = draftContent
|
|
828
904
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
@@ -831,9 +907,10 @@ export async function showDiscuss(
|
|
|
831
907
|
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
832
908
|
} else if (choice === "discuss_fresh") {
|
|
833
909
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
910
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
834
911
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
835
912
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
836
|
-
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
913
|
+
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
837
914
|
}), "gsd-discuss");
|
|
838
915
|
} else if (choice === "skip_milestone") {
|
|
839
916
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -1136,8 +1213,9 @@ export async function showSmartEntry(
|
|
|
1136
1213
|
|
|
1137
1214
|
if (choice === "discuss_draft") {
|
|
1138
1215
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1216
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1139
1217
|
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
|
1140
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1218
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1141
1219
|
});
|
|
1142
1220
|
const seed = draftContent
|
|
1143
1221
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
@@ -1146,9 +1224,10 @@ export async function showSmartEntry(
|
|
|
1146
1224
|
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
1147
1225
|
} else if (choice === "discuss_fresh") {
|
|
1148
1226
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1227
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1149
1228
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
1150
1229
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1151
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1230
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1152
1231
|
}), "gsd-discuss");
|
|
1153
1232
|
} else if (choice === "skip_milestone") {
|
|
1154
1233
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -1220,8 +1299,9 @@ export async function showSmartEntry(
|
|
|
1220
1299
|
}));
|
|
1221
1300
|
} else if (choice === "discuss") {
|
|
1222
1301
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
1302
|
+
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
1223
1303
|
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
1224
|
-
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates,
|
|
1304
|
+
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
1225
1305
|
}));
|
|
1226
1306
|
} else if (choice === "skip_milestone") {
|
|
1227
1307
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) server for the GSD extension.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the same MCP server functionality as src/mcp-server.ts
|
|
5
|
+
* but can be loaded via jiti in the extension runtime context. It enables
|
|
6
|
+
* GSD's tools to be used by external AI clients (Claude Desktop, VS Code
|
|
7
|
+
* Copilot, etc.) via the MCP standard protocol over stdin/stdout.
|
|
8
|
+
*/
|
|
7
9
|
|
|
8
10
|
interface McpTool {
|
|
9
11
|
name: string
|
|
10
12
|
description: string
|
|
11
13
|
parameters: Record<string, unknown>
|
|
12
|
-
execute(
|
|
14
|
+
execute(
|
|
15
|
+
toolCallId: string,
|
|
16
|
+
params: Record<string, unknown>,
|
|
17
|
+
signal?: AbortSignal,
|
|
18
|
+
onUpdate?: unknown,
|
|
19
|
+
): Promise<{
|
|
20
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>
|
|
21
|
+
}>
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
export async function startMcpServer(options: {
|
|
@@ -18,6 +27,16 @@ export async function startMcpServer(options: {
|
|
|
18
27
|
}): Promise<void> {
|
|
19
28
|
const { tools, version = '0.0.0' } = options
|
|
20
29
|
|
|
30
|
+
// Dynamic imports — MCP SDK subpath exports use a "./*" wildcard pattern
|
|
31
|
+
// that cannot be statically resolved by all TypeScript configurations.
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const { Server } = await import('@modelcontextprotocol/sdk/server')
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js')
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
const sdkTypes = await import('@modelcontextprotocol/sdk/types')
|
|
38
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = sdkTypes
|
|
39
|
+
|
|
21
40
|
const toolMap = new Map<string, McpTool>()
|
|
22
41
|
for (const tool of tools) {
|
|
23
42
|
toolMap.set(tool.name, tool)
|
|
@@ -28,9 +47,10 @@ export async function startMcpServer(options: {
|
|
|
28
47
|
{ capabilities: { tools: {} } },
|
|
29
48
|
)
|
|
30
49
|
|
|
50
|
+
// tools/list — return every registered GSD tool with its JSON Schema parameters
|
|
31
51
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
32
52
|
return {
|
|
33
|
-
tools: tools.map((t) => ({
|
|
53
|
+
tools: tools.map((t: McpTool) => ({
|
|
34
54
|
name: t.name,
|
|
35
55
|
description: t.description,
|
|
36
56
|
inputSchema: t.parameters,
|
|
@@ -38,6 +58,7 @@ export async function startMcpServer(options: {
|
|
|
38
58
|
}
|
|
39
59
|
})
|
|
40
60
|
|
|
61
|
+
// tools/call — execute the requested tool and return content blocks
|
|
41
62
|
server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
|
|
42
63
|
const { name, arguments: args } = request.params
|
|
43
64
|
const tool = toolMap.get(name)
|
|
@@ -56,15 +77,15 @@ export async function startMcpServer(options: {
|
|
|
56
77
|
undefined,
|
|
57
78
|
)
|
|
58
79
|
|
|
59
|
-
const content = result.content.map((block) => {
|
|
80
|
+
const content = result.content.map((block: any) => {
|
|
60
81
|
if (block.type === 'text') {
|
|
61
|
-
return { type: 'text' as const, text: block.text }
|
|
82
|
+
return { type: 'text' as const, text: block.text ?? '' }
|
|
62
83
|
}
|
|
63
84
|
if (block.type === 'image') {
|
|
64
85
|
return {
|
|
65
86
|
type: 'image' as const,
|
|
66
|
-
data: block.data,
|
|
67
|
-
mimeType: block.mimeType,
|
|
87
|
+
data: block.data ?? '',
|
|
88
|
+
mimeType: block.mimeType ?? 'image/png',
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
return { type: 'text' as const, text: JSON.stringify(block) }
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Eligibility — Milestone parallelism analysis.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes which milestones can safely run in parallel by checking
|
|
5
|
+
* dependency satisfaction and file overlap across slice plans.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deriveState } from "./state.js";
|
|
9
|
+
import { parseRoadmap, parsePlan, loadFile } from "./files.js";
|
|
10
|
+
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
11
|
+
import { findMilestoneIds } from "./guided-flow.js";
|
|
12
|
+
import type { MilestoneRegistryEntry } from "./types.js";
|
|
13
|
+
|
|
14
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface EligibilityResult {
|
|
17
|
+
milestoneId: string;
|
|
18
|
+
title: string;
|
|
19
|
+
eligible: boolean;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParallelCandidates {
|
|
24
|
+
eligible: EligibilityResult[];
|
|
25
|
+
ineligible: EligibilityResult[];
|
|
26
|
+
fileOverlaps: Array<{ mid1: string; mid2: string; files: string[] }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── File Collection ─────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect all `filesLikelyTouched` across every slice plan in a milestone.
|
|
33
|
+
* Returns a deduplicated list of file paths.
|
|
34
|
+
*/
|
|
35
|
+
async function collectTouchedFiles(
|
|
36
|
+
basePath: string,
|
|
37
|
+
milestoneId: string,
|
|
38
|
+
): Promise<string[]> {
|
|
39
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
40
|
+
if (!roadmapPath) return [];
|
|
41
|
+
|
|
42
|
+
const roadmapContent = await loadFile(roadmapPath);
|
|
43
|
+
if (!roadmapContent) return [];
|
|
44
|
+
|
|
45
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
46
|
+
const files = new Set<string>();
|
|
47
|
+
|
|
48
|
+
for (const slice of roadmap.slices) {
|
|
49
|
+
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
50
|
+
if (!planPath) continue;
|
|
51
|
+
|
|
52
|
+
const planContent = await loadFile(planPath);
|
|
53
|
+
if (!planContent) continue;
|
|
54
|
+
|
|
55
|
+
const plan = parsePlan(planContent);
|
|
56
|
+
for (const f of plan.filesLikelyTouched) {
|
|
57
|
+
files.add(f);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [...files];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Overlap Detection ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compare file sets across milestones and return pairs with overlapping files.
|
|
68
|
+
*/
|
|
69
|
+
function detectFileOverlaps(
|
|
70
|
+
fileSets: Map<string, string[]>,
|
|
71
|
+
): Array<{ mid1: string; mid2: string; files: string[] }> {
|
|
72
|
+
const overlaps: Array<{ mid1: string; mid2: string; files: string[] }> = [];
|
|
73
|
+
const ids = [...fileSets.keys()];
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < ids.length; i++) {
|
|
76
|
+
const files1 = new Set(fileSets.get(ids[i])!);
|
|
77
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
78
|
+
const files2 = fileSets.get(ids[j])!;
|
|
79
|
+
const shared = files2.filter(f => files1.has(f));
|
|
80
|
+
if (shared.length > 0) {
|
|
81
|
+
overlaps.push({ mid1: ids[i], mid2: ids[j], files: shared.sort() });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return overlaps;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Analysis ────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Analyze milestones for parallel execution eligibility.
|
|
93
|
+
*
|
|
94
|
+
* A milestone is eligible if:
|
|
95
|
+
* 1. It is not complete
|
|
96
|
+
* 2. Its dependencies (`dependsOn`) are all complete
|
|
97
|
+
* 3. It does not have file overlap with other eligible milestones
|
|
98
|
+
* (overlaps are flagged as warnings but do not disqualify)
|
|
99
|
+
*/
|
|
100
|
+
export async function analyzeParallelEligibility(
|
|
101
|
+
basePath: string,
|
|
102
|
+
): Promise<ParallelCandidates> {
|
|
103
|
+
const milestoneIds = findMilestoneIds(basePath);
|
|
104
|
+
const state = await deriveState(basePath);
|
|
105
|
+
const registry = state.registry;
|
|
106
|
+
|
|
107
|
+
// Build a lookup for quick status checks
|
|
108
|
+
const registryMap = new Map<string, MilestoneRegistryEntry>();
|
|
109
|
+
for (const entry of registry) {
|
|
110
|
+
registryMap.set(entry.id, entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const eligible: EligibilityResult[] = [];
|
|
114
|
+
const ineligible: EligibilityResult[] = [];
|
|
115
|
+
|
|
116
|
+
for (const mid of milestoneIds) {
|
|
117
|
+
const entry = registryMap.get(mid);
|
|
118
|
+
const title = entry?.title ?? mid;
|
|
119
|
+
const status = entry?.status ?? "pending";
|
|
120
|
+
|
|
121
|
+
// Rule 1: skip complete milestones
|
|
122
|
+
if (status === "complete") {
|
|
123
|
+
ineligible.push({
|
|
124
|
+
milestoneId: mid,
|
|
125
|
+
title,
|
|
126
|
+
eligible: false,
|
|
127
|
+
reason: "Already complete.",
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Rule 2: check dependency satisfaction
|
|
133
|
+
const deps = entry?.dependsOn ?? [];
|
|
134
|
+
const unsatisfied = deps.filter(dep => {
|
|
135
|
+
const depEntry = registryMap.get(dep);
|
|
136
|
+
return !depEntry || depEntry.status !== "complete";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (unsatisfied.length > 0) {
|
|
140
|
+
ineligible.push({
|
|
141
|
+
milestoneId: mid,
|
|
142
|
+
title,
|
|
143
|
+
eligible: false,
|
|
144
|
+
reason: `Blocked by incomplete dependencies: ${unsatisfied.join(", ")}.`,
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
eligible.push({
|
|
150
|
+
milestoneId: mid,
|
|
151
|
+
title,
|
|
152
|
+
eligible: true,
|
|
153
|
+
reason: "All dependencies satisfied.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Rule 3: check file overlap among eligible milestones
|
|
158
|
+
const fileSets = new Map<string, string[]>();
|
|
159
|
+
for (const result of eligible) {
|
|
160
|
+
const files = await collectTouchedFiles(basePath, result.milestoneId);
|
|
161
|
+
fileSets.set(result.milestoneId, files);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const fileOverlaps = detectFileOverlaps(fileSets);
|
|
165
|
+
|
|
166
|
+
// Annotate eligible milestones that have file overlaps
|
|
167
|
+
const overlappingIds = new Set<string>();
|
|
168
|
+
for (const overlap of fileOverlaps) {
|
|
169
|
+
overlappingIds.add(overlap.mid1);
|
|
170
|
+
overlappingIds.add(overlap.mid2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const result of eligible) {
|
|
174
|
+
if (overlappingIds.has(result.milestoneId)) {
|
|
175
|
+
result.reason = "All dependencies satisfied. WARNING: has file overlap with another eligible milestone.";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { eligible, ineligible, fileOverlaps };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Produce a human-readable report of parallel eligibility analysis.
|
|
186
|
+
*/
|
|
187
|
+
export function formatEligibilityReport(candidates: ParallelCandidates): string {
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
|
|
190
|
+
lines.push("# Parallel Eligibility Report");
|
|
191
|
+
lines.push("");
|
|
192
|
+
|
|
193
|
+
// Eligible milestones
|
|
194
|
+
lines.push(`## Eligible for Parallel Execution (${candidates.eligible.length})`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
if (candidates.eligible.length === 0) {
|
|
197
|
+
lines.push("No milestones are currently eligible for parallel execution.");
|
|
198
|
+
} else {
|
|
199
|
+
for (const e of candidates.eligible) {
|
|
200
|
+
lines.push(`- **${e.milestoneId}** — ${e.title}`);
|
|
201
|
+
lines.push(` ${e.reason}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push("");
|
|
205
|
+
|
|
206
|
+
// Ineligible milestones
|
|
207
|
+
lines.push(`## Ineligible (${candidates.ineligible.length})`);
|
|
208
|
+
lines.push("");
|
|
209
|
+
if (candidates.ineligible.length === 0) {
|
|
210
|
+
lines.push("All milestones are eligible.");
|
|
211
|
+
} else {
|
|
212
|
+
for (const e of candidates.ineligible) {
|
|
213
|
+
lines.push(`- **${e.milestoneId}** — ${e.title}`);
|
|
214
|
+
lines.push(` ${e.reason}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
|
|
219
|
+
// File overlap warnings
|
|
220
|
+
if (candidates.fileOverlaps.length > 0) {
|
|
221
|
+
lines.push(`## File Overlap Warnings (${candidates.fileOverlaps.length})`);
|
|
222
|
+
lines.push("");
|
|
223
|
+
for (const overlap of candidates.fileOverlaps) {
|
|
224
|
+
lines.push(`- **${overlap.mid1}** <-> **${overlap.mid2}** — ${overlap.files.length} shared file(s):`);
|
|
225
|
+
for (const f of overlap.files) {
|
|
226
|
+
lines.push(` - \`${f}\``);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Merge — Worktree reconciliation for parallel milestones.
|
|
3
|
+
*
|
|
4
|
+
* Handles merging completed milestone worktrees back to main branch
|
|
5
|
+
* with safety checks for parallel execution context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadFile } from "./files.js";
|
|
9
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
10
|
+
import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
11
|
+
import { MergeConflictError } from "./git-service.js";
|
|
12
|
+
import { removeSessionStatus } from "./session-status-io.js";
|
|
13
|
+
import type { WorkerInfo } from "./parallel-orchestrator.js";
|
|
14
|
+
|
|
15
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface MergeResult {
|
|
18
|
+
milestoneId: string;
|
|
19
|
+
success: boolean;
|
|
20
|
+
commitMessage?: string;
|
|
21
|
+
pushed?: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
conflictFiles?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type MergeOrder = "sequential" | "by-completion";
|
|
27
|
+
|
|
28
|
+
// ─── Merge Queue ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determine safe merge order for completed milestones.
|
|
32
|
+
* Sequential: merge in milestone ID order (M001 before M002).
|
|
33
|
+
* By-completion: merge in the order milestones finished.
|
|
34
|
+
*/
|
|
35
|
+
export function determineMergeOrder(
|
|
36
|
+
workers: WorkerInfo[],
|
|
37
|
+
order: MergeOrder = "sequential",
|
|
38
|
+
): string[] {
|
|
39
|
+
const completed = workers.filter(w => w.state === "stopped" && w.completedUnits > 0);
|
|
40
|
+
if (order === "by-completion") {
|
|
41
|
+
return completed
|
|
42
|
+
.sort((a, b) => a.startedAt - b.startedAt) // earliest first
|
|
43
|
+
.map(w => w.milestoneId);
|
|
44
|
+
}
|
|
45
|
+
return completed
|
|
46
|
+
.sort((a, b) => a.milestoneId.localeCompare(b.milestoneId))
|
|
47
|
+
.map(w => w.milestoneId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attempt to merge a single milestone's worktree back to main.
|
|
52
|
+
* Wraps mergeMilestoneToMain with error handling for parallel context.
|
|
53
|
+
*/
|
|
54
|
+
export async function mergeCompletedMilestone(
|
|
55
|
+
basePath: string,
|
|
56
|
+
milestoneId: string,
|
|
57
|
+
): Promise<MergeResult> {
|
|
58
|
+
try {
|
|
59
|
+
// Load the roadmap content (needed by mergeMilestoneToMain)
|
|
60
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
61
|
+
if (!roadmapPath) {
|
|
62
|
+
return {
|
|
63
|
+
milestoneId,
|
|
64
|
+
success: false,
|
|
65
|
+
error: `No roadmap found for ${milestoneId}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const roadmapContent = await loadFile(roadmapPath);
|
|
70
|
+
if (!roadmapContent) {
|
|
71
|
+
return {
|
|
72
|
+
milestoneId,
|
|
73
|
+
success: false,
|
|
74
|
+
error: `Could not read roadmap for ${milestoneId}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Attempt the merge
|
|
79
|
+
const result = mergeMilestoneToMain(basePath, milestoneId, roadmapContent);
|
|
80
|
+
|
|
81
|
+
// Clean up parallel session status
|
|
82
|
+
removeSessionStatus(basePath, milestoneId);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
milestoneId,
|
|
86
|
+
success: true,
|
|
87
|
+
commitMessage: result.commitMessage,
|
|
88
|
+
pushed: result.pushed,
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof MergeConflictError) {
|
|
92
|
+
return {
|
|
93
|
+
milestoneId,
|
|
94
|
+
success: false,
|
|
95
|
+
error: `Merge conflict: ${err.conflictedFiles.length} conflicting file(s)`,
|
|
96
|
+
conflictFiles: err.conflictedFiles,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
milestoneId,
|
|
101
|
+
success: false,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merge all completed milestones in sequence.
|
|
109
|
+
* Stops on first conflict and returns results so far.
|
|
110
|
+
*/
|
|
111
|
+
export async function mergeAllCompleted(
|
|
112
|
+
basePath: string,
|
|
113
|
+
workers: WorkerInfo[],
|
|
114
|
+
order: MergeOrder = "sequential",
|
|
115
|
+
): Promise<MergeResult[]> {
|
|
116
|
+
const mergeOrder = determineMergeOrder(workers, order);
|
|
117
|
+
const results: MergeResult[] = [];
|
|
118
|
+
|
|
119
|
+
for (const mid of mergeOrder) {
|
|
120
|
+
const result = await mergeCompletedMilestone(basePath, mid);
|
|
121
|
+
results.push(result);
|
|
122
|
+
|
|
123
|
+
// Stop on first conflict — later merges may depend on this one
|
|
124
|
+
if (!result.success && result.conflictFiles) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format merge results for display.
|
|
134
|
+
*/
|
|
135
|
+
export function formatMergeResults(results: MergeResult[]): string {
|
|
136
|
+
if (results.length === 0) return "No completed milestones to merge.";
|
|
137
|
+
|
|
138
|
+
const lines: string[] = ["# Merge Results\n"];
|
|
139
|
+
|
|
140
|
+
for (const r of results) {
|
|
141
|
+
if (r.success) {
|
|
142
|
+
const pushStatus = r.pushed ? " (pushed)" : "";
|
|
143
|
+
lines.push(`- **${r.milestoneId}** — merged successfully${pushStatus}`);
|
|
144
|
+
} else if (r.conflictFiles) {
|
|
145
|
+
lines.push(`- **${r.milestoneId}** — CONFLICT (${r.conflictFiles.length} file(s)):`);
|
|
146
|
+
for (const f of r.conflictFiles) {
|
|
147
|
+
lines.push(` - \`${f}\``);
|
|
148
|
+
}
|
|
149
|
+
lines.push(` Resolve conflicts manually and run \`/gsd parallel merge ${r.milestoneId}\` to retry.`);
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(`- **${r.milestoneId}** — failed: ${r.error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|