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.
- package/dist/cli.js +12 -3
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +118 -10
- package/dist/help-text.js +22 -7
- package/dist/resource-loader.js +64 -9
- 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 +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +123 -41
- package/dist/resources/extensions/gsd/commands.ts +176 -10
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/dist/resources/extensions/gsd/doctor.ts +56 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +75 -0
- package/dist/resources/extensions/gsd/index.ts +34 -1
- 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/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -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/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-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- 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/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/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- 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/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/scripts/postinstall.js +7 -109
- 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 +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +123 -41
- package/src/resources/extensions/gsd/commands.ts +176 -10
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/src/resources/extensions/gsd/doctor.ts +56 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +75 -0
- package/src/resources/extensions/gsd/index.ts +34 -1
- 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/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -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/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-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- 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/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/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
autoWorktreeBranch,
|
|
109
109
|
} from "./auto-worktree.js";
|
|
110
110
|
import { pruneQueueOrder } from "./queue-order.js";
|
|
111
|
+
import { consumeSignal } from "./session-status-io.js";
|
|
111
112
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
112
113
|
import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
|
|
113
114
|
import {
|
|
@@ -195,6 +196,35 @@ function syncStateToProjectRoot(worktreePath: string, projectRoot: string, miles
|
|
|
195
196
|
cpSync(srcMilestone, dstMilestone, { recursive: true, force: true });
|
|
196
197
|
}
|
|
197
198
|
} catch { /* non-fatal */ }
|
|
199
|
+
|
|
200
|
+
// 3. Merge completed-units.json (set-union of both locations)
|
|
201
|
+
// Prevents already-completed units from being re-dispatched after crash/restart.
|
|
202
|
+
const srcKeysFile = join(wtGsd, "completed-units.json");
|
|
203
|
+
const dstKeysFile = join(prGsd, "completed-units.json");
|
|
204
|
+
if (existsSync(srcKeysFile)) {
|
|
205
|
+
try {
|
|
206
|
+
const srcKeys: string[] = JSON.parse(readFileSync(srcKeysFile, "utf8"));
|
|
207
|
+
let dstKeys: string[] = [];
|
|
208
|
+
if (existsSync(dstKeysFile)) {
|
|
209
|
+
try { dstKeys = JSON.parse(readFileSync(dstKeysFile, "utf8")); } catch { /* ignore corrupt dst */ }
|
|
210
|
+
}
|
|
211
|
+
const merged = [...new Set([...dstKeys, ...srcKeys])];
|
|
212
|
+
writeFileSync(dstKeysFile, JSON.stringify(merged, null, 2));
|
|
213
|
+
} catch { /* non-fatal */ }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 4. Runtime records — unit dispatch state used by selfHealRuntimeRecords().
|
|
217
|
+
// Without this, a crash during a unit leaves the runtime record only in the
|
|
218
|
+
// worktree. If the next session resolves basePath before worktree re-entry,
|
|
219
|
+
// selfHeal can't find or clear the stale record (#769).
|
|
220
|
+
try {
|
|
221
|
+
const srcRuntime = join(wtGsd, "runtime", "units");
|
|
222
|
+
const dstRuntime = join(prGsd, "runtime", "units");
|
|
223
|
+
if (existsSync(srcRuntime)) {
|
|
224
|
+
mkdirSync(dstRuntime, { recursive: true });
|
|
225
|
+
cpSync(srcRuntime, dstRuntime, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
} catch { /* non-fatal */ }
|
|
198
228
|
}
|
|
199
229
|
|
|
200
230
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -361,11 +391,12 @@ let _sigtermHandler: (() => void) | null = null;
|
|
|
361
391
|
*/
|
|
362
392
|
const inFlightTools = new Map<string, number>();
|
|
363
393
|
|
|
364
|
-
type BudgetAlertLevel = 0 | 75 | 90 | 100;
|
|
394
|
+
type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
|
|
365
395
|
|
|
366
396
|
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
367
397
|
if (budgetPct >= 1.0) return 100;
|
|
368
398
|
if (budgetPct >= 0.90) return 90;
|
|
399
|
+
if (budgetPct >= 0.80) return 80;
|
|
369
400
|
if (budgetPct >= 0.75) return 75;
|
|
370
401
|
return 0;
|
|
371
402
|
}
|
|
@@ -552,11 +583,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
552
583
|
await dispatchNextUnit(ctx, pi);
|
|
553
584
|
} catch (retryErr) {
|
|
554
585
|
const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
555
|
-
ctx
|
|
556
|
-
`Dispatch gap recovery failed: ${message}. Stopping auto-mode.`,
|
|
557
|
-
"error",
|
|
558
|
-
);
|
|
559
|
-
await stopAuto(ctx, pi);
|
|
586
|
+
await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
|
|
560
587
|
return;
|
|
561
588
|
}
|
|
562
589
|
|
|
@@ -564,17 +591,14 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
564
591
|
// (no sendMessage called → no timeout set), auto-mode is permanently
|
|
565
592
|
// stalled. Stop cleanly instead of leaving it active but idle (#537).
|
|
566
593
|
if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
|
|
567
|
-
ctx
|
|
568
|
-
"Auto-mode stalled — no dispatchable unit found after retry. Stopping. Run /gsd auto to restart.",
|
|
569
|
-
"warning",
|
|
570
|
-
);
|
|
571
|
-
await stopAuto(ctx, pi);
|
|
594
|
+
await stopAuto(ctx, pi, "Stalled — no dispatchable unit after retry");
|
|
572
595
|
}
|
|
573
596
|
}, DISPATCH_GAP_TIMEOUT_MS);
|
|
574
597
|
}
|
|
575
598
|
|
|
576
|
-
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promise<void> {
|
|
599
|
+
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason?: string): Promise<void> {
|
|
577
600
|
if (!active && !paused) return;
|
|
601
|
+
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
578
602
|
clearUnitTimeout();
|
|
579
603
|
if (lockBase()) clearLock(lockBase());
|
|
580
604
|
clearSkillSnapshot();
|
|
@@ -626,11 +650,11 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
626
650
|
if (ledger && ledger.units.length > 0) {
|
|
627
651
|
const totals = getProjectTotals(ledger.units);
|
|
628
652
|
ctx?.ui.notify(
|
|
629
|
-
`Auto-mode stopped. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
|
|
653
|
+
`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
|
|
630
654
|
"info",
|
|
631
655
|
);
|
|
632
656
|
} else {
|
|
633
|
-
ctx?.ui.notify(
|
|
657
|
+
ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
|
|
634
658
|
}
|
|
635
659
|
|
|
636
660
|
// Sync disk state so next resume starts from accurate state
|
|
@@ -1094,6 +1118,13 @@ export async function startAuto(
|
|
|
1094
1118
|
}
|
|
1095
1119
|
// Re-register SIGTERM handler with the original basePath (lock lives there)
|
|
1096
1120
|
registerSigtermHandler(originalBasePath);
|
|
1121
|
+
|
|
1122
|
+
// After worktree entry, load completed keys from BOTH locations (project root
|
|
1123
|
+
// + worktree) so the in-memory set is the union. Prevents re-dispatch of units
|
|
1124
|
+
// completed in either location after crash/restart (#769).
|
|
1125
|
+
if (basePath !== originalBasePath) {
|
|
1126
|
+
loadPersistedKeys(basePath, completedKeySet);
|
|
1127
|
+
}
|
|
1097
1128
|
} catch (err) {
|
|
1098
1129
|
// Worktree creation is non-fatal — continue in the project root.
|
|
1099
1130
|
ctx.ui.notify(
|
|
@@ -1130,11 +1161,12 @@ export async function startAuto(
|
|
|
1130
1161
|
}
|
|
1131
1162
|
}
|
|
1132
1163
|
|
|
1133
|
-
// Initialize metrics — loads existing ledger from disk
|
|
1134
|
-
|
|
1164
|
+
// Initialize metrics — loads existing ledger from disk.
|
|
1165
|
+
// Use basePath (not base) so worktree-mode reads the worktree ledger (#769).
|
|
1166
|
+
initMetrics(basePath);
|
|
1135
1167
|
|
|
1136
1168
|
// Initialize routing history for adaptive learning
|
|
1137
|
-
initRoutingHistory(
|
|
1169
|
+
initRoutingHistory(basePath);
|
|
1138
1170
|
|
|
1139
1171
|
// Capture the session's current model at auto-mode start (#650).
|
|
1140
1172
|
// This prevents model bleed when multiple GSD instances share the
|
|
@@ -1185,8 +1217,10 @@ export async function startAuto(
|
|
|
1185
1217
|
);
|
|
1186
1218
|
}
|
|
1187
1219
|
|
|
1188
|
-
// Self-heal: clear stale runtime records where artifacts already exist
|
|
1189
|
-
|
|
1220
|
+
// Self-heal: clear stale runtime records where artifacts already exist.
|
|
1221
|
+
// Use basePath (not base) — in worktree mode, basePath points to the worktree
|
|
1222
|
+
// where runtime records and artifacts actually live (#769).
|
|
1223
|
+
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
1190
1224
|
|
|
1191
1225
|
// Self-heal: remove stale .git/index.lock from prior crash.
|
|
1192
1226
|
// A stale lock file blocks all git operations (commit, merge, checkout).
|
|
@@ -1252,6 +1286,27 @@ export async function handleAgentEnd(
|
|
|
1252
1286
|
// Unit completed — clear its timeout
|
|
1253
1287
|
clearUnitTimeout();
|
|
1254
1288
|
|
|
1289
|
+
// ── Parallel worker signal check ─────────────────────────────────────
|
|
1290
|
+
// When running as a parallel worker (GSD_MILESTONE_LOCK set), check for
|
|
1291
|
+
// coordinator signals before dispatching the next unit.
|
|
1292
|
+
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
1293
|
+
if (milestoneLock) {
|
|
1294
|
+
const signal = consumeSignal(basePath, milestoneLock);
|
|
1295
|
+
if (signal) {
|
|
1296
|
+
if (signal.signal === "stop") {
|
|
1297
|
+
_handlingAgentEnd = false;
|
|
1298
|
+
await stopAuto(ctx, pi);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (signal.signal === "pause") {
|
|
1302
|
+
_handlingAgentEnd = false;
|
|
1303
|
+
await pauseAuto(ctx, pi);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
// "resume" and "rebase" signals are handled elsewhere or no-op here
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1255
1310
|
// Invalidate all caches — the unit just completed and may have
|
|
1256
1311
|
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
1257
1312
|
invalidateAllCaches();
|
|
@@ -1485,7 +1540,7 @@ export async function handleAgentEnd(
|
|
|
1485
1540
|
const result = await cmdCtx!.newSession();
|
|
1486
1541
|
if (result.cancelled) {
|
|
1487
1542
|
resetHookState();
|
|
1488
|
-
await stopAuto(ctx, pi);
|
|
1543
|
+
await stopAuto(ctx, pi, "Hook session cancelled");
|
|
1489
1544
|
return;
|
|
1490
1545
|
}
|
|
1491
1546
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
@@ -1783,7 +1838,15 @@ async function showStepWizard(
|
|
|
1783
1838
|
|
|
1784
1839
|
// If no active milestone or everything is complete, stop
|
|
1785
1840
|
if (!mid || state.phase === "complete") {
|
|
1786
|
-
|
|
1841
|
+
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
1842
|
+
if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked") {
|
|
1843
|
+
const ids = incomplete.map(m => m.id).join(", ");
|
|
1844
|
+
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1845
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
1846
|
+
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
|
|
1847
|
+
} else {
|
|
1848
|
+
await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
|
|
1849
|
+
}
|
|
1787
1850
|
return;
|
|
1788
1851
|
}
|
|
1789
1852
|
|
|
@@ -1906,8 +1969,7 @@ async function dispatchNextUnit(
|
|
|
1906
1969
|
// doesn't provide. Stop gracefully instead of crashing.
|
|
1907
1970
|
const staleMsg = checkResourcesStale();
|
|
1908
1971
|
if (staleMsg) {
|
|
1909
|
-
await stopAuto(ctx, pi);
|
|
1910
|
-
ctx.ui.notify(staleMsg, "error");
|
|
1972
|
+
await stopAuto(ctx, pi, staleMsg);
|
|
1911
1973
|
return;
|
|
1912
1974
|
}
|
|
1913
1975
|
|
|
@@ -2054,8 +2116,25 @@ async function dispatchNextUnit(
|
|
|
2054
2116
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2055
2117
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2056
2118
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2119
|
+
|
|
2120
|
+
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
2121
|
+
if (incomplete.length === 0) {
|
|
2122
|
+
// Genuinely all complete
|
|
2123
|
+
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
2124
|
+
await stopAuto(ctx, pi, "All milestones complete");
|
|
2125
|
+
} else if (state.phase === "blocked") {
|
|
2126
|
+
// Milestones exist but are dependency-blocked
|
|
2127
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2128
|
+
await stopAuto(ctx, pi, blockerMsg);
|
|
2129
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
2130
|
+
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2131
|
+
} else {
|
|
2132
|
+
// Milestones with remaining work exist but none became active — unexpected
|
|
2133
|
+
const ids = incomplete.map(m => m.id).join(", ");
|
|
2134
|
+
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
2135
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
2136
|
+
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
2137
|
+
}
|
|
2059
2138
|
return;
|
|
2060
2139
|
}
|
|
2061
2140
|
|
|
@@ -2063,8 +2142,8 @@ async function dispatchNextUnit(
|
|
|
2063
2142
|
// The !mid check above returns early if mid is falsy; midTitle comes from
|
|
2064
2143
|
// the same object so it should always be present when mid is.
|
|
2065
2144
|
if (!midTitle) {
|
|
2066
|
-
|
|
2067
|
-
|
|
2145
|
+
midTitle = mid; // Defensive fallback: use milestone ID as title
|
|
2146
|
+
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
2068
2147
|
}
|
|
2069
2148
|
|
|
2070
2149
|
// ── Mid-merge safety check: detect leftover merge state from a prior session ──
|
|
@@ -2082,7 +2161,10 @@ async function dispatchNextUnit(
|
|
|
2082
2161
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2083
2162
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2084
2163
|
}
|
|
2085
|
-
|
|
2164
|
+
const noMilestoneReason = !mid
|
|
2165
|
+
? "No active milestone after merge reconciliation"
|
|
2166
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
2167
|
+
await stopAuto(ctx, pi, noMilestoneReason);
|
|
2086
2168
|
return;
|
|
2087
2169
|
}
|
|
2088
2170
|
|
|
@@ -2157,7 +2239,7 @@ async function dispatchNextUnit(
|
|
|
2157
2239
|
}
|
|
2158
2240
|
}
|
|
2159
2241
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
2160
|
-
await stopAuto(ctx, pi);
|
|
2242
|
+
await stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
2161
2243
|
return;
|
|
2162
2244
|
}
|
|
2163
2245
|
|
|
@@ -2167,8 +2249,8 @@ async function dispatchNextUnit(
|
|
|
2167
2249
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2168
2250
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2169
2251
|
}
|
|
2170
|
-
await stopAuto(ctx, pi);
|
|
2171
2252
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2253
|
+
await stopAuto(ctx, pi, blockerMsg);
|
|
2172
2254
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
2173
2255
|
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2174
2256
|
return;
|
|
@@ -2194,9 +2276,8 @@ async function dispatchNextUnit(
|
|
|
2194
2276
|
const msg = `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}).`;
|
|
2195
2277
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2196
2278
|
if (budgetEnforcementAction === "halt") {
|
|
2197
|
-
ctx.ui.notify(`${msg} Stopping auto-mode.`, "error");
|
|
2198
2279
|
sendDesktopNotification("GSD", msg, "error", "budget");
|
|
2199
|
-
await stopAuto(ctx, pi);
|
|
2280
|
+
await stopAuto(ctx, pi, "Budget ceiling reached");
|
|
2200
2281
|
return;
|
|
2201
2282
|
}
|
|
2202
2283
|
if (budgetEnforcementAction === "pause") {
|
|
@@ -2211,6 +2292,10 @@ async function dispatchNextUnit(
|
|
|
2211
2292
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2212
2293
|
ctx.ui.notify(`Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2213
2294
|
sendDesktopNotification("GSD", `Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2295
|
+
} else if (newBudgetAlertLevel === 80) {
|
|
2296
|
+
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2297
|
+
ctx.ui.notify(`Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2298
|
+
sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2214
2299
|
} else if (newBudgetAlertLevel === 75) {
|
|
2215
2300
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2216
2301
|
ctx.ui.notify(`Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info");
|
|
@@ -2275,8 +2360,7 @@ async function dispatchNextUnit(
|
|
|
2275
2360
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2276
2361
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2277
2362
|
}
|
|
2278
|
-
await stopAuto(ctx, pi);
|
|
2279
|
-
ctx.ui.notify(dispatchResult.reason, dispatchResult.level);
|
|
2363
|
+
await stopAuto(ctx, pi, dispatchResult.reason);
|
|
2280
2364
|
return;
|
|
2281
2365
|
}
|
|
2282
2366
|
|
|
@@ -2316,8 +2400,7 @@ async function dispatchNextUnit(
|
|
|
2316
2400
|
|
|
2317
2401
|
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
2318
2402
|
if (priorSliceBlocker) {
|
|
2319
|
-
await stopAuto(ctx, pi);
|
|
2320
|
-
ctx.ui.notify(priorSliceBlocker, "error");
|
|
2403
|
+
await stopAuto(ctx, pi, priorSliceBlocker);
|
|
2321
2404
|
return;
|
|
2322
2405
|
}
|
|
2323
2406
|
|
|
@@ -2434,9 +2517,9 @@ async function dispatchNextUnit(
|
|
|
2434
2517
|
}
|
|
2435
2518
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2436
2519
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2437
|
-
await stopAuto(ctx, pi);
|
|
2520
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId}`);
|
|
2438
2521
|
ctx.ui.notify(
|
|
2439
|
-
`Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles)
|
|
2522
|
+
`Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles).${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
|
|
2440
2523
|
"error",
|
|
2441
2524
|
);
|
|
2442
2525
|
return;
|
|
@@ -2529,7 +2612,7 @@ async function dispatchNextUnit(
|
|
|
2529
2612
|
|
|
2530
2613
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2531
2614
|
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
2532
|
-
await stopAuto(ctx, pi);
|
|
2615
|
+
await stopAuto(ctx, pi, `Loop: ${unitType} ${unitId}`);
|
|
2533
2616
|
sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
|
|
2534
2617
|
ctx.ui.notify(
|
|
2535
2618
|
`Loop detected: ${unitType} ${unitId} dispatched ${prevCount + 1} times total. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}${remediation ? `\n\n Remediation steps:\n${remediation}` : "\n Check branch state and .gsd/ artifacts."}`,
|
|
@@ -2670,8 +2753,7 @@ async function dispatchNextUnit(
|
|
|
2670
2753
|
// Fresh session
|
|
2671
2754
|
const result = await cmdCtx!.newSession();
|
|
2672
2755
|
if (result.cancelled) {
|
|
2673
|
-
await stopAuto(ctx, pi);
|
|
2674
|
-
ctx.ui.notify("Auto-mode stopped.", "info");
|
|
2756
|
+
await stopAuto(ctx, pi, "Session cancelled");
|
|
2675
2757
|
return;
|
|
2676
2758
|
}
|
|
2677
2759
|
|
|
@@ -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
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
|
|
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((
|
|
85
|
-
.map((
|
|
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)",
|
|
@@ -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;
|