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
|
@@ -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 {
|
|
@@ -125,6 +126,18 @@ import {
|
|
|
125
126
|
reconcileMergeState,
|
|
126
127
|
} from "./auto-recovery.js";
|
|
127
128
|
import { resolveDispatch, resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
129
|
+
import {
|
|
130
|
+
buildResearchSlicePrompt,
|
|
131
|
+
buildResearchMilestonePrompt,
|
|
132
|
+
buildPlanSlicePrompt,
|
|
133
|
+
buildPlanMilestonePrompt,
|
|
134
|
+
buildExecuteTaskPrompt,
|
|
135
|
+
buildCompleteSlicePrompt,
|
|
136
|
+
buildCompleteMilestonePrompt,
|
|
137
|
+
buildReassessRoadmapPrompt,
|
|
138
|
+
buildRunUatPrompt,
|
|
139
|
+
buildReplanSlicePrompt,
|
|
140
|
+
} from "./auto-prompts.js";
|
|
128
141
|
import {
|
|
129
142
|
type AutoDashboardData,
|
|
130
143
|
updateProgressWidget as _updateProgressWidget,
|
|
@@ -183,6 +196,35 @@ function syncStateToProjectRoot(worktreePath: string, projectRoot: string, miles
|
|
|
183
196
|
cpSync(srcMilestone, dstMilestone, { recursive: true, force: true });
|
|
184
197
|
}
|
|
185
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 */ }
|
|
186
228
|
}
|
|
187
229
|
|
|
188
230
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -211,6 +253,11 @@ const MAX_LIFETIME_DISPATCHES = 6;
|
|
|
211
253
|
/** Tracks recovery attempt count per unit for backoff and diagnostics. */
|
|
212
254
|
const unitRecoveryCount = new Map<string, number>();
|
|
213
255
|
|
|
256
|
+
/** Track consecutive skips per unit — catches infinite skip loops where deriveState
|
|
257
|
+
* keeps returning the same already-completed unit. Reset on any real dispatch. */
|
|
258
|
+
const unitConsecutiveSkips = new Map<string, number>();
|
|
259
|
+
const MAX_CONSECUTIVE_SKIPS = 3;
|
|
260
|
+
|
|
214
261
|
/** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
|
|
215
262
|
const completedKeySet = new Set<string>();
|
|
216
263
|
|
|
@@ -297,6 +344,9 @@ let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
|
297
344
|
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
298
345
|
let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
|
|
299
346
|
|
|
347
|
+
/** Queue of quick-task captures awaiting dispatch after triage resolution */
|
|
348
|
+
let pendingQuickTasks: import("./captures.js").CaptureEntry[] = [];
|
|
349
|
+
|
|
300
350
|
/**
|
|
301
351
|
* Model captured at auto-mode start. Used to prevent model bleed between
|
|
302
352
|
* concurrent GSD instances sharing the same global settings.json (#650).
|
|
@@ -334,14 +384,19 @@ let lastBaselineCharCount: number | undefined;
|
|
|
334
384
|
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
335
385
|
let _sigtermHandler: (() => void) | null = null;
|
|
336
386
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Tool calls currently being executed — prevents false idle detection during long-running tools.
|
|
389
|
+
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
390
|
+
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
391
|
+
*/
|
|
392
|
+
const inFlightTools = new Map<string, number>();
|
|
339
393
|
|
|
340
|
-
type BudgetAlertLevel = 0 | 75 | 90 | 100;
|
|
394
|
+
type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
|
|
341
395
|
|
|
342
396
|
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
343
397
|
if (budgetPct >= 1.0) return 100;
|
|
344
398
|
if (budgetPct >= 0.90) return 90;
|
|
399
|
+
if (budgetPct >= 0.80) return 80;
|
|
345
400
|
if (budgetPct >= 0.75) return 75;
|
|
346
401
|
return 0;
|
|
347
402
|
}
|
|
@@ -414,11 +469,11 @@ export function isAutoPaused(): boolean {
|
|
|
414
469
|
|
|
415
470
|
/**
|
|
416
471
|
* Mark a tool execution as in-flight. Called from index.ts on tool_execution_start.
|
|
417
|
-
*
|
|
472
|
+
* Records start time so the idle watchdog can detect tools hung longer than the idle timeout.
|
|
418
473
|
*/
|
|
419
474
|
export function markToolStart(toolCallId: string): void {
|
|
420
475
|
if (!active) return;
|
|
421
|
-
inFlightTools.
|
|
476
|
+
inFlightTools.set(toolCallId, Date.now());
|
|
422
477
|
}
|
|
423
478
|
|
|
424
479
|
/**
|
|
@@ -428,6 +483,16 @@ export function markToolEnd(toolCallId: string): void {
|
|
|
428
483
|
inFlightTools.delete(toolCallId);
|
|
429
484
|
}
|
|
430
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Returns the age (ms) of the oldest currently in-flight tool, or 0 if none.
|
|
488
|
+
* Exported for testing.
|
|
489
|
+
*/
|
|
490
|
+
export function getOldestInFlightToolAgeMs(): number {
|
|
491
|
+
if (inFlightTools.size === 0) return 0;
|
|
492
|
+
const oldestStart = Math.min(...inFlightTools.values());
|
|
493
|
+
return Date.now() - oldestStart;
|
|
494
|
+
}
|
|
495
|
+
|
|
431
496
|
/**
|
|
432
497
|
* Return the base path to use for the auto.lock file.
|
|
433
498
|
* Always uses the original project root (not the worktree) so that
|
|
@@ -518,11 +583,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
518
583
|
await dispatchNextUnit(ctx, pi);
|
|
519
584
|
} catch (retryErr) {
|
|
520
585
|
const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
521
|
-
ctx
|
|
522
|
-
`Dispatch gap recovery failed: ${message}. Stopping auto-mode.`,
|
|
523
|
-
"error",
|
|
524
|
-
);
|
|
525
|
-
await stopAuto(ctx, pi);
|
|
586
|
+
await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
|
|
526
587
|
return;
|
|
527
588
|
}
|
|
528
589
|
|
|
@@ -530,17 +591,14 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
530
591
|
// (no sendMessage called → no timeout set), auto-mode is permanently
|
|
531
592
|
// stalled. Stop cleanly instead of leaving it active but idle (#537).
|
|
532
593
|
if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
|
|
533
|
-
ctx
|
|
534
|
-
"Auto-mode stalled — no dispatchable unit found after retry. Stopping. Run /gsd auto to restart.",
|
|
535
|
-
"warning",
|
|
536
|
-
);
|
|
537
|
-
await stopAuto(ctx, pi);
|
|
594
|
+
await stopAuto(ctx, pi, "Stalled — no dispatchable unit after retry");
|
|
538
595
|
}
|
|
539
596
|
}, DISPATCH_GAP_TIMEOUT_MS);
|
|
540
597
|
}
|
|
541
598
|
|
|
542
|
-
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promise<void> {
|
|
599
|
+
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason?: string): Promise<void> {
|
|
543
600
|
if (!active && !paused) return;
|
|
601
|
+
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
544
602
|
clearUnitTimeout();
|
|
545
603
|
if (lockBase()) clearLock(lockBase());
|
|
546
604
|
clearSkillSnapshot();
|
|
@@ -592,11 +650,11 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
592
650
|
if (ledger && ledger.units.length > 0) {
|
|
593
651
|
const totals = getProjectTotals(ledger.units);
|
|
594
652
|
ctx?.ui.notify(
|
|
595
|
-
`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`,
|
|
596
654
|
"info",
|
|
597
655
|
);
|
|
598
656
|
} else {
|
|
599
|
-
ctx?.ui.notify(
|
|
657
|
+
ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
|
|
600
658
|
}
|
|
601
659
|
|
|
602
660
|
// Sync disk state so next resume starts from accurate state
|
|
@@ -621,6 +679,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
621
679
|
stepMode = false;
|
|
622
680
|
unitDispatchCount.clear();
|
|
623
681
|
unitRecoveryCount.clear();
|
|
682
|
+
unitConsecutiveSkips.clear();
|
|
624
683
|
inFlightTools.clear();
|
|
625
684
|
lastBudgetAlertLevel = 0;
|
|
626
685
|
unitLifetimeDispatches.clear();
|
|
@@ -629,6 +688,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
629
688
|
currentMilestoneId = null;
|
|
630
689
|
originalBasePath = "";
|
|
631
690
|
completedUnits = [];
|
|
691
|
+
pendingQuickTasks = [];
|
|
632
692
|
clearSliceProgressCache();
|
|
633
693
|
clearActivityLogState();
|
|
634
694
|
resetProactiveHealing();
|
|
@@ -710,6 +770,7 @@ export async function startAuto(
|
|
|
710
770
|
basePath = base;
|
|
711
771
|
unitDispatchCount.clear();
|
|
712
772
|
unitLifetimeDispatches.clear();
|
|
773
|
+
unitConsecutiveSkips.clear();
|
|
713
774
|
// Re-initialize metrics in case ledger was lost during pause
|
|
714
775
|
if (!getLedger()) initMetrics(base);
|
|
715
776
|
// Ensure milestone ID is set on git service for integration branch resolution
|
|
@@ -782,6 +843,9 @@ export async function startAuto(
|
|
|
782
843
|
pausedSessionFile = null;
|
|
783
844
|
}
|
|
784
845
|
|
|
846
|
+
// Write lock on resume so cross-process status detection works (#723).
|
|
847
|
+
writeLock(lockBase(), "resuming", currentMilestoneId ?? "unknown", completedUnits.length);
|
|
848
|
+
|
|
785
849
|
await dispatchNextUnit(ctx, pi);
|
|
786
850
|
return;
|
|
787
851
|
}
|
|
@@ -988,6 +1052,7 @@ export async function startAuto(
|
|
|
988
1052
|
basePath = base;
|
|
989
1053
|
unitDispatchCount.clear();
|
|
990
1054
|
unitRecoveryCount.clear();
|
|
1055
|
+
unitConsecutiveSkips.clear();
|
|
991
1056
|
lastBudgetAlertLevel = 0;
|
|
992
1057
|
unitLifetimeDispatches.clear();
|
|
993
1058
|
completedKeySet.clear();
|
|
@@ -998,6 +1063,7 @@ export async function startAuto(
|
|
|
998
1063
|
autoStartTime = Date.now();
|
|
999
1064
|
resourceSyncedAtOnStart = readResourceSyncedAt();
|
|
1000
1065
|
completedUnits = [];
|
|
1066
|
+
pendingQuickTasks = [];
|
|
1001
1067
|
currentUnit = null;
|
|
1002
1068
|
currentMilestoneId = state.activeMilestone?.id ?? null;
|
|
1003
1069
|
originalModelId = ctx.model?.id ?? null;
|
|
@@ -1052,6 +1118,13 @@ export async function startAuto(
|
|
|
1052
1118
|
}
|
|
1053
1119
|
// Re-register SIGTERM handler with the original basePath (lock lives there)
|
|
1054
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
|
+
}
|
|
1055
1128
|
} catch (err) {
|
|
1056
1129
|
// Worktree creation is non-fatal — continue in the project root.
|
|
1057
1130
|
ctx.ui.notify(
|
|
@@ -1088,11 +1161,12 @@ export async function startAuto(
|
|
|
1088
1161
|
}
|
|
1089
1162
|
}
|
|
1090
1163
|
|
|
1091
|
-
// Initialize metrics — loads existing ledger from disk
|
|
1092
|
-
|
|
1164
|
+
// Initialize metrics — loads existing ledger from disk.
|
|
1165
|
+
// Use basePath (not base) so worktree-mode reads the worktree ledger (#769).
|
|
1166
|
+
initMetrics(basePath);
|
|
1093
1167
|
|
|
1094
1168
|
// Initialize routing history for adaptive learning
|
|
1095
|
-
initRoutingHistory(
|
|
1169
|
+
initRoutingHistory(basePath);
|
|
1096
1170
|
|
|
1097
1171
|
// Capture the session's current model at auto-mode start (#650).
|
|
1098
1172
|
// This prevents model bleed when multiple GSD instances share the
|
|
@@ -1116,6 +1190,11 @@ export async function startAuto(
|
|
|
1116
1190
|
: "Will loop until milestone complete.";
|
|
1117
1191
|
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
|
|
1118
1192
|
|
|
1193
|
+
// Write initial lock file immediately so cross-process status detection
|
|
1194
|
+
// works even before the first unit is dispatched (#723).
|
|
1195
|
+
// The lock is updated with unit-specific info on each dispatch and cleared on stop.
|
|
1196
|
+
writeLock(lockBase(), "starting", currentMilestoneId ?? "unknown", 0);
|
|
1197
|
+
|
|
1119
1198
|
// Secrets collection gate — collect pending secrets before first dispatch
|
|
1120
1199
|
const mid = state.activeMilestone!.id;
|
|
1121
1200
|
try {
|
|
@@ -1138,8 +1217,10 @@ export async function startAuto(
|
|
|
1138
1217
|
);
|
|
1139
1218
|
}
|
|
1140
1219
|
|
|
1141
|
-
// Self-heal: clear stale runtime records where artifacts already exist
|
|
1142
|
-
|
|
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);
|
|
1143
1224
|
|
|
1144
1225
|
// Self-heal: remove stale .git/index.lock from prior crash.
|
|
1145
1226
|
// A stale lock file blocks all git operations (commit, merge, checkout).
|
|
@@ -1205,6 +1286,27 @@ export async function handleAgentEnd(
|
|
|
1205
1286
|
// Unit completed — clear its timeout
|
|
1206
1287
|
clearUnitTimeout();
|
|
1207
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
|
+
|
|
1208
1310
|
// Invalidate all caches — the unit just completed and may have
|
|
1209
1311
|
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
1210
1312
|
invalidateAllCaches();
|
|
@@ -1297,6 +1399,53 @@ export async function handleAgentEnd(
|
|
|
1297
1399
|
}
|
|
1298
1400
|
}
|
|
1299
1401
|
|
|
1402
|
+
// ── Post-triage: execute actionable resolutions (inject, replan, queue quick-tasks) ──
|
|
1403
|
+
// After a triage-captures unit completes, the LLM has classified captures and
|
|
1404
|
+
// updated CAPTURES.md. Now we execute those classifications: inject tasks into
|
|
1405
|
+
// the plan, write replan triggers, and queue quick-tasks for dispatch.
|
|
1406
|
+
if (currentUnit.type === "triage-captures") {
|
|
1407
|
+
try {
|
|
1408
|
+
const { executeTriageResolutions } = await import("./triage-resolution.js");
|
|
1409
|
+
const state = await deriveState(basePath);
|
|
1410
|
+
const mid = state.activeMilestone?.id;
|
|
1411
|
+
const sid = state.activeSlice?.id;
|
|
1412
|
+
|
|
1413
|
+
if (mid && sid) {
|
|
1414
|
+
const triageResult = executeTriageResolutions(basePath, mid, sid);
|
|
1415
|
+
|
|
1416
|
+
if (triageResult.injected > 0) {
|
|
1417
|
+
ctx.ui.notify(
|
|
1418
|
+
`Triage: injected ${triageResult.injected} task${triageResult.injected === 1 ? "" : "s"} into ${sid} plan.`,
|
|
1419
|
+
"info",
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (triageResult.replanned > 0) {
|
|
1423
|
+
ctx.ui.notify(
|
|
1424
|
+
`Triage: replan trigger written for ${sid} — next dispatch will enter replanning.`,
|
|
1425
|
+
"info",
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
if (triageResult.quickTasks.length > 0) {
|
|
1429
|
+
// Queue quick-tasks for dispatch. They'll be picked up by the
|
|
1430
|
+
// quick-task dispatch block below the triage check.
|
|
1431
|
+
for (const qt of triageResult.quickTasks) {
|
|
1432
|
+
pendingQuickTasks.push(qt);
|
|
1433
|
+
}
|
|
1434
|
+
ctx.ui.notify(
|
|
1435
|
+
`Triage: ${triageResult.quickTasks.length} quick-task${triageResult.quickTasks.length === 1 ? "" : "s"} queued for execution.`,
|
|
1436
|
+
"info",
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
for (const action of triageResult.actions) {
|
|
1440
|
+
process.stderr.write(`gsd-triage: ${action}\n`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
// Non-fatal — triage resolution failure shouldn't block dispatch
|
|
1445
|
+
process.stderr.write(`gsd-triage: resolution execution failed: ${(err as Error).message}\n`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1300
1449
|
// ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
|
|
1301
1450
|
// After doctor + rebuildState, check whether the just-completed unit actually
|
|
1302
1451
|
// produced its expected artifact. If so, persist the completion key now so the
|
|
@@ -1391,7 +1540,7 @@ export async function handleAgentEnd(
|
|
|
1391
1540
|
const result = await cmdCtx!.newSession();
|
|
1392
1541
|
if (result.cancelled) {
|
|
1393
1542
|
resetHookState();
|
|
1394
|
-
await stopAuto(ctx, pi);
|
|
1543
|
+
await stopAuto(ctx, pi, "Hook session cancelled");
|
|
1395
1544
|
return;
|
|
1396
1545
|
}
|
|
1397
1546
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
@@ -1521,7 +1670,7 @@ export async function handleAgentEnd(
|
|
|
1521
1670
|
return;
|
|
1522
1671
|
}
|
|
1523
1672
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1524
|
-
writeLock(
|
|
1673
|
+
writeLock(lockBase(), triageUnitType, triageUnitId, completedUnits.length, sessionFile);
|
|
1525
1674
|
|
|
1526
1675
|
// Start unit timeout for triage (use same supervisor config as hooks)
|
|
1527
1676
|
clearUnitTimeout();
|
|
@@ -1551,6 +1700,85 @@ export async function handleAgentEnd(
|
|
|
1551
1700
|
}
|
|
1552
1701
|
}
|
|
1553
1702
|
|
|
1703
|
+
// ── Quick-task dispatch: execute queued quick-tasks from triage resolution ──
|
|
1704
|
+
// Quick-tasks are self-contained one-off tasks that don't modify the plan.
|
|
1705
|
+
// They're queued during post-triage resolution and dispatched here one at a time.
|
|
1706
|
+
if (
|
|
1707
|
+
!stepMode &&
|
|
1708
|
+
pendingQuickTasks.length > 0 &&
|
|
1709
|
+
currentUnit &&
|
|
1710
|
+
currentUnit.type !== "quick-task"
|
|
1711
|
+
) {
|
|
1712
|
+
try {
|
|
1713
|
+
const capture = pendingQuickTasks.shift()!;
|
|
1714
|
+
const { buildQuickTaskPrompt } = await import("./triage-resolution.js");
|
|
1715
|
+
const { markCaptureExecuted } = await import("./captures.js");
|
|
1716
|
+
const prompt = buildQuickTaskPrompt(capture);
|
|
1717
|
+
|
|
1718
|
+
ctx.ui.notify(
|
|
1719
|
+
`Executing quick-task: ${capture.id} — "${capture.text}"`,
|
|
1720
|
+
"info",
|
|
1721
|
+
);
|
|
1722
|
+
|
|
1723
|
+
// Close out previous unit metrics
|
|
1724
|
+
if (currentUnit) {
|
|
1725
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1726
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1727
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// Dispatch quick-task as a new unit
|
|
1731
|
+
const qtUnitType = "quick-task";
|
|
1732
|
+
const qtUnitId = `${currentMilestoneId}/${capture.id}`;
|
|
1733
|
+
const qtStartedAt = Date.now();
|
|
1734
|
+
currentUnit = { type: qtUnitType, id: qtUnitId, startedAt: qtStartedAt };
|
|
1735
|
+
writeUnitRuntimeRecord(basePath, qtUnitType, qtUnitId, qtStartedAt, {
|
|
1736
|
+
phase: "dispatched",
|
|
1737
|
+
wrapupWarningSent: false,
|
|
1738
|
+
timeoutAt: null,
|
|
1739
|
+
lastProgressAt: qtStartedAt,
|
|
1740
|
+
progressCount: 0,
|
|
1741
|
+
lastProgressKind: "dispatch",
|
|
1742
|
+
});
|
|
1743
|
+
const state = await deriveState(basePath);
|
|
1744
|
+
updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
|
|
1745
|
+
|
|
1746
|
+
const result = await cmdCtx!.newSession();
|
|
1747
|
+
if (result.cancelled) {
|
|
1748
|
+
await stopAuto(ctx, pi);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1752
|
+
writeLock(lockBase(), qtUnitType, qtUnitId, completedUnits.length, sessionFile);
|
|
1753
|
+
|
|
1754
|
+
// Mark capture as executed now that the unit is dispatched
|
|
1755
|
+
markCaptureExecuted(basePath, capture.id);
|
|
1756
|
+
|
|
1757
|
+
// Start unit timeout for quick-task
|
|
1758
|
+
clearUnitTimeout();
|
|
1759
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
1760
|
+
const qtTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
1761
|
+
unitTimeoutHandle = setTimeout(async () => {
|
|
1762
|
+
unitTimeoutHandle = null;
|
|
1763
|
+
if (!active) return;
|
|
1764
|
+
ctx.ui.notify(
|
|
1765
|
+
`Quick-task ${capture.id} exceeded timeout. Pausing auto-mode.`,
|
|
1766
|
+
"warning",
|
|
1767
|
+
);
|
|
1768
|
+
await pauseAuto(ctx, pi);
|
|
1769
|
+
}, qtTimeoutMs);
|
|
1770
|
+
|
|
1771
|
+
if (!active) return;
|
|
1772
|
+
pi.sendMessage(
|
|
1773
|
+
{ customType: "gsd-auto", content: prompt, display: verbose },
|
|
1774
|
+
{ triggerTurn: true },
|
|
1775
|
+
);
|
|
1776
|
+
return; // handleAgentEnd will fire again when quick-task session completes
|
|
1777
|
+
} catch {
|
|
1778
|
+
// Non-fatal — proceed to normal dispatch
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1554
1782
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
1555
1783
|
if (stepMode) {
|
|
1556
1784
|
await showStepWizard(ctx, pi);
|
|
@@ -1610,7 +1838,15 @@ async function showStepWizard(
|
|
|
1610
1838
|
|
|
1611
1839
|
// If no active milestone or everything is complete, stop
|
|
1612
1840
|
if (!mid || state.phase === "complete") {
|
|
1613
|
-
|
|
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
|
+
}
|
|
1614
1850
|
return;
|
|
1615
1851
|
}
|
|
1616
1852
|
|
|
@@ -1733,8 +1969,7 @@ async function dispatchNextUnit(
|
|
|
1733
1969
|
// doesn't provide. Stop gracefully instead of crashing.
|
|
1734
1970
|
const staleMsg = checkResourcesStale();
|
|
1735
1971
|
if (staleMsg) {
|
|
1736
|
-
await stopAuto(ctx, pi);
|
|
1737
|
-
ctx.ui.notify(staleMsg, "error");
|
|
1972
|
+
await stopAuto(ctx, pi, staleMsg);
|
|
1738
1973
|
return;
|
|
1739
1974
|
}
|
|
1740
1975
|
|
|
@@ -1788,6 +2023,7 @@ async function dispatchNextUnit(
|
|
|
1788
2023
|
// Reset stuck detection for new milestone
|
|
1789
2024
|
unitDispatchCount.clear();
|
|
1790
2025
|
unitRecoveryCount.clear();
|
|
2026
|
+
unitConsecutiveSkips.clear();
|
|
1791
2027
|
unitLifetimeDispatches.clear();
|
|
1792
2028
|
// Clear completed-units.json for the finished milestone
|
|
1793
2029
|
try {
|
|
@@ -1880,8 +2116,25 @@ async function dispatchNextUnit(
|
|
|
1880
2116
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1881
2117
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1882
2118
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
+
}
|
|
1885
2138
|
return;
|
|
1886
2139
|
}
|
|
1887
2140
|
|
|
@@ -1889,8 +2142,8 @@ async function dispatchNextUnit(
|
|
|
1889
2142
|
// The !mid check above returns early if mid is falsy; midTitle comes from
|
|
1890
2143
|
// the same object so it should always be present when mid is.
|
|
1891
2144
|
if (!midTitle) {
|
|
1892
|
-
|
|
1893
|
-
|
|
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");
|
|
1894
2147
|
}
|
|
1895
2148
|
|
|
1896
2149
|
// ── Mid-merge safety check: detect leftover merge state from a prior session ──
|
|
@@ -1908,7 +2161,10 @@ async function dispatchNextUnit(
|
|
|
1908
2161
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1909
2162
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1910
2163
|
}
|
|
1911
|
-
|
|
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);
|
|
1912
2168
|
return;
|
|
1913
2169
|
}
|
|
1914
2170
|
|
|
@@ -1983,7 +2239,7 @@ async function dispatchNextUnit(
|
|
|
1983
2239
|
}
|
|
1984
2240
|
}
|
|
1985
2241
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
1986
|
-
await stopAuto(ctx, pi);
|
|
2242
|
+
await stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
1987
2243
|
return;
|
|
1988
2244
|
}
|
|
1989
2245
|
|
|
@@ -1993,8 +2249,8 @@ async function dispatchNextUnit(
|
|
|
1993
2249
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1994
2250
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1995
2251
|
}
|
|
1996
|
-
await stopAuto(ctx, pi);
|
|
1997
2252
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2253
|
+
await stopAuto(ctx, pi, blockerMsg);
|
|
1998
2254
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
1999
2255
|
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2000
2256
|
return;
|
|
@@ -2020,9 +2276,8 @@ async function dispatchNextUnit(
|
|
|
2020
2276
|
const msg = `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}).`;
|
|
2021
2277
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2022
2278
|
if (budgetEnforcementAction === "halt") {
|
|
2023
|
-
ctx.ui.notify(`${msg} Stopping auto-mode.`, "error");
|
|
2024
2279
|
sendDesktopNotification("GSD", msg, "error", "budget");
|
|
2025
|
-
await stopAuto(ctx, pi);
|
|
2280
|
+
await stopAuto(ctx, pi, "Budget ceiling reached");
|
|
2026
2281
|
return;
|
|
2027
2282
|
}
|
|
2028
2283
|
if (budgetEnforcementAction === "pause") {
|
|
@@ -2037,6 +2292,10 @@ async function dispatchNextUnit(
|
|
|
2037
2292
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2038
2293
|
ctx.ui.notify(`Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2039
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");
|
|
2040
2299
|
} else if (newBudgetAlertLevel === 75) {
|
|
2041
2300
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2042
2301
|
ctx.ui.notify(`Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info");
|
|
@@ -2101,8 +2360,7 @@ async function dispatchNextUnit(
|
|
|
2101
2360
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2102
2361
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2103
2362
|
}
|
|
2104
|
-
await stopAuto(ctx, pi);
|
|
2105
|
-
ctx.ui.notify(dispatchResult.reason, dispatchResult.level);
|
|
2363
|
+
await stopAuto(ctx, pi, dispatchResult.reason);
|
|
2106
2364
|
return;
|
|
2107
2365
|
}
|
|
2108
2366
|
|
|
@@ -2142,8 +2400,7 @@ async function dispatchNextUnit(
|
|
|
2142
2400
|
|
|
2143
2401
|
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
2144
2402
|
if (priorSliceBlocker) {
|
|
2145
|
-
await stopAuto(ctx, pi);
|
|
2146
|
-
ctx.ui.notify(priorSliceBlocker, "error");
|
|
2403
|
+
await stopAuto(ctx, pi, priorSliceBlocker);
|
|
2147
2404
|
return;
|
|
2148
2405
|
}
|
|
2149
2406
|
|
|
@@ -2155,6 +2412,26 @@ async function dispatchNextUnit(
|
|
|
2155
2412
|
// Cross-validate: does the expected artifact actually exist?
|
|
2156
2413
|
const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
|
|
2157
2414
|
if (artifactExists) {
|
|
2415
|
+
// Guard against infinite skip loops: if deriveState keeps returning the
|
|
2416
|
+
// same completed unit, consecutive skips will trip this breaker. Evict the
|
|
2417
|
+
// key so the next dispatch forces full reconciliation instead of looping.
|
|
2418
|
+
const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2419
|
+
unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
2420
|
+
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
2421
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2422
|
+
completedKeySet.delete(idempotencyKey);
|
|
2423
|
+
removePersistedKey(basePath, idempotencyKey);
|
|
2424
|
+
invalidateStateCache();
|
|
2425
|
+
ctx.ui.notify(
|
|
2426
|
+
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2427
|
+
"warning",
|
|
2428
|
+
);
|
|
2429
|
+
_skipDepth++;
|
|
2430
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2431
|
+
await dispatchNextUnit(ctx, pi);
|
|
2432
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2158
2435
|
ctx.ui.notify(
|
|
2159
2436
|
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
2160
2437
|
"info",
|
|
@@ -2184,6 +2461,24 @@ async function dispatchNextUnit(
|
|
|
2184
2461
|
persistCompletedKey(basePath, idempotencyKey);
|
|
2185
2462
|
completedKeySet.add(idempotencyKey);
|
|
2186
2463
|
invalidateStateCache();
|
|
2464
|
+
// Same consecutive-skip guard as the idempotency path above.
|
|
2465
|
+
const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2466
|
+
unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
2467
|
+
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
2468
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2469
|
+
completedKeySet.delete(idempotencyKey);
|
|
2470
|
+
removePersistedKey(basePath, idempotencyKey);
|
|
2471
|
+
invalidateStateCache();
|
|
2472
|
+
ctx.ui.notify(
|
|
2473
|
+
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2474
|
+
"warning",
|
|
2475
|
+
);
|
|
2476
|
+
_skipDepth++;
|
|
2477
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2478
|
+
await dispatchNextUnit(ctx, pi);
|
|
2479
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2187
2482
|
ctx.ui.notify(
|
|
2188
2483
|
`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
|
|
2189
2484
|
"info",
|
|
@@ -2199,6 +2494,8 @@ async function dispatchNextUnit(
|
|
|
2199
2494
|
// Pattern A→B→A→B would reset retryCount every time; this map catches it.
|
|
2200
2495
|
const dispatchKey = `${unitType}/${unitId}`;
|
|
2201
2496
|
const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
|
|
2497
|
+
// Real dispatch reached — clear the consecutive-skip counter for this unit.
|
|
2498
|
+
unitConsecutiveSkips.delete(dispatchKey);
|
|
2202
2499
|
|
|
2203
2500
|
debugLog("dispatch-unit", {
|
|
2204
2501
|
type: unitType,
|
|
@@ -2220,9 +2517,9 @@ async function dispatchNextUnit(
|
|
|
2220
2517
|
}
|
|
2221
2518
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2222
2519
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2223
|
-
await stopAuto(ctx, pi);
|
|
2520
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId}`);
|
|
2224
2521
|
ctx.ui.notify(
|
|
2225
|
-
`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.`,
|
|
2226
2523
|
"error",
|
|
2227
2524
|
);
|
|
2228
2525
|
return;
|
|
@@ -2315,7 +2612,7 @@ async function dispatchNextUnit(
|
|
|
2315
2612
|
|
|
2316
2613
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2317
2614
|
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
2318
|
-
await stopAuto(ctx, pi);
|
|
2615
|
+
await stopAuto(ctx, pi, `Loop: ${unitType} ${unitId}`);
|
|
2319
2616
|
sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
|
|
2320
2617
|
ctx.ui.notify(
|
|
2321
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."}`,
|
|
@@ -2456,8 +2753,7 @@ async function dispatchNextUnit(
|
|
|
2456
2753
|
// Fresh session
|
|
2457
2754
|
const result = await cmdCtx!.newSession();
|
|
2458
2755
|
if (result.cancelled) {
|
|
2459
|
-
await stopAuto(ctx, pi);
|
|
2460
|
-
ctx.ui.notify("Auto-mode stopped.", "info");
|
|
2756
|
+
await stopAuto(ctx, pi, "Session cancelled");
|
|
2461
2757
|
return;
|
|
2462
2758
|
}
|
|
2463
2759
|
|
|
@@ -2713,13 +3009,27 @@ async function dispatchNextUnit(
|
|
|
2713
3009
|
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
2714
3010
|
|
|
2715
3011
|
// Agent has tool calls currently executing (await_job, long bash, etc.) —
|
|
2716
|
-
// not idle, just waiting for tool completion.
|
|
3012
|
+
// not idle, just waiting for tool completion. But only suppress recovery
|
|
3013
|
+
// if the tool started recently. A tool in-flight for longer than the idle
|
|
3014
|
+
// timeout is likely stuck — e.g., `python -m http.server 8080 &` keeps the
|
|
3015
|
+
// shell's stdout/stderr open, causing the Bash tool to hang indefinitely.
|
|
2717
3016
|
if (inFlightTools.size > 0) {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3017
|
+
const oldestStart = Math.min(...inFlightTools.values());
|
|
3018
|
+
const toolAgeMs = Date.now() - oldestStart;
|
|
3019
|
+
if (toolAgeMs < idleTimeoutMs) {
|
|
3020
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
3021
|
+
lastProgressAt: Date.now(),
|
|
3022
|
+
lastProgressKind: "tool-in-flight",
|
|
3023
|
+
});
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
// Oldest tool has been running >= idleTimeoutMs — treat as a stuck/hung
|
|
3027
|
+
// tool (e.g., background process holding stdout open). Fall through to
|
|
3028
|
+
// idle recovery without resetting the progress clock.
|
|
3029
|
+
ctx.ui.notify(
|
|
3030
|
+
`Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`,
|
|
3031
|
+
"warning",
|
|
3032
|
+
);
|
|
2723
3033
|
}
|
|
2724
3034
|
|
|
2725
3035
|
// Before triggering recovery, check if the agent is actually producing
|
|
@@ -3144,6 +3454,14 @@ export {
|
|
|
3144
3454
|
buildLoopRemediationSteps,
|
|
3145
3455
|
} from "./auto-recovery.js";
|
|
3146
3456
|
|
|
3457
|
+
/**
|
|
3458
|
+
* Test-only: expose skip-loop state for unit tests.
|
|
3459
|
+
* Not part of the public API.
|
|
3460
|
+
*/
|
|
3461
|
+
export function _getUnitConsecutiveSkips(): Map<string, number> { return unitConsecutiveSkips; }
|
|
3462
|
+
export function _resetUnitConsecutiveSkips(): void { unitConsecutiveSkips.clear(); }
|
|
3463
|
+
export { MAX_CONSECUTIVE_SKIPS };
|
|
3464
|
+
|
|
3147
3465
|
/**
|
|
3148
3466
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
3149
3467
|
* Used for manual hook triggers via /gsd run-hook.
|
|
@@ -3168,6 +3486,7 @@ export async function dispatchHookUnit(
|
|
|
3168
3486
|
autoStartTime = Date.now();
|
|
3169
3487
|
currentUnit = null;
|
|
3170
3488
|
completedUnits = [];
|
|
3489
|
+
pendingQuickTasks = [];
|
|
3171
3490
|
}
|
|
3172
3491
|
|
|
3173
3492
|
const hookUnitType = `hook/${hookName}`;
|
|
@@ -3248,3 +3567,192 @@ export async function dispatchHookUnit(
|
|
|
3248
3567
|
|
|
3249
3568
|
return true;
|
|
3250
3569
|
}
|
|
3570
|
+
|
|
3571
|
+
|
|
3572
|
+
// ─── Direct Phase Dispatch ────────────────────────────────────────────────────
|
|
3573
|
+
|
|
3574
|
+
export async function dispatchDirectPhase(
|
|
3575
|
+
ctx: ExtensionCommandContext,
|
|
3576
|
+
pi: ExtensionAPI,
|
|
3577
|
+
phase: string,
|
|
3578
|
+
base: string,
|
|
3579
|
+
): Promise<void> {
|
|
3580
|
+
const state = await deriveState(base);
|
|
3581
|
+
const mid = state.activeMilestone?.id;
|
|
3582
|
+
const midTitle = state.activeMilestone?.title ?? "";
|
|
3583
|
+
|
|
3584
|
+
if (!mid) {
|
|
3585
|
+
ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
const normalized = phase.toLowerCase();
|
|
3590
|
+
let unitType: string;
|
|
3591
|
+
let unitId: string;
|
|
3592
|
+
let prompt: string;
|
|
3593
|
+
|
|
3594
|
+
switch (normalized) {
|
|
3595
|
+
case "research":
|
|
3596
|
+
case "research-milestone":
|
|
3597
|
+
case "research-slice": {
|
|
3598
|
+
const isSlice = normalized === "research-slice" || (normalized === "research" && state.phase !== "pre-planning");
|
|
3599
|
+
if (isSlice) {
|
|
3600
|
+
const sid = state.activeSlice?.id;
|
|
3601
|
+
const sTitle = state.activeSlice?.title ?? "";
|
|
3602
|
+
if (!sid) {
|
|
3603
|
+
ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
unitType = "research-slice";
|
|
3607
|
+
unitId = `${mid}/${sid}`;
|
|
3608
|
+
prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
3609
|
+
} else {
|
|
3610
|
+
unitType = "research-milestone";
|
|
3611
|
+
unitId = mid;
|
|
3612
|
+
prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
|
|
3613
|
+
}
|
|
3614
|
+
break;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
case "plan":
|
|
3618
|
+
case "plan-milestone":
|
|
3619
|
+
case "plan-slice": {
|
|
3620
|
+
const isSlice = normalized === "plan-slice" || (normalized === "plan" && state.phase !== "pre-planning");
|
|
3621
|
+
if (isSlice) {
|
|
3622
|
+
const sid = state.activeSlice?.id;
|
|
3623
|
+
const sTitle = state.activeSlice?.title ?? "";
|
|
3624
|
+
if (!sid) {
|
|
3625
|
+
ctx.ui.notify("Cannot dispatch plan-slice: no active slice.", "warning");
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
unitType = "plan-slice";
|
|
3629
|
+
unitId = `${mid}/${sid}`;
|
|
3630
|
+
prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
3631
|
+
} else {
|
|
3632
|
+
unitType = "plan-milestone";
|
|
3633
|
+
unitId = mid;
|
|
3634
|
+
prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
|
|
3635
|
+
}
|
|
3636
|
+
break;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
case "execute":
|
|
3640
|
+
case "execute-task": {
|
|
3641
|
+
const sid = state.activeSlice?.id;
|
|
3642
|
+
const sTitle = state.activeSlice?.title ?? "";
|
|
3643
|
+
const tid = state.activeTask?.id;
|
|
3644
|
+
const tTitle = state.activeTask?.title ?? "";
|
|
3645
|
+
if (!sid) {
|
|
3646
|
+
ctx.ui.notify("Cannot dispatch execute-task: no active slice.", "warning");
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
if (!tid) {
|
|
3650
|
+
ctx.ui.notify("Cannot dispatch execute-task: no active task.", "warning");
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
unitType = "execute-task";
|
|
3654
|
+
unitId = `${mid}/${sid}/${tid}`;
|
|
3655
|
+
prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
|
|
3656
|
+
break;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
case "complete":
|
|
3660
|
+
case "complete-slice":
|
|
3661
|
+
case "complete-milestone": {
|
|
3662
|
+
const isSlice = normalized === "complete-slice" || (normalized === "complete" && state.phase === "summarizing");
|
|
3663
|
+
if (isSlice) {
|
|
3664
|
+
const sid = state.activeSlice?.id;
|
|
3665
|
+
const sTitle = state.activeSlice?.title ?? "";
|
|
3666
|
+
if (!sid) {
|
|
3667
|
+
ctx.ui.notify("Cannot dispatch complete-slice: no active slice.", "warning");
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
unitType = "complete-slice";
|
|
3671
|
+
unitId = `${mid}/${sid}`;
|
|
3672
|
+
prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
3673
|
+
} else {
|
|
3674
|
+
unitType = "complete-milestone";
|
|
3675
|
+
unitId = mid;
|
|
3676
|
+
prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
|
|
3677
|
+
}
|
|
3678
|
+
break;
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
case "reassess":
|
|
3682
|
+
case "reassess-roadmap": {
|
|
3683
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
3684
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
3685
|
+
if (!roadmapContent) {
|
|
3686
|
+
ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
3690
|
+
const completedSlices = roadmap.slices.filter(s => s.done);
|
|
3691
|
+
if (completedSlices.length === 0) {
|
|
3692
|
+
ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
const completedSliceId = completedSlices[completedSlices.length - 1].id;
|
|
3696
|
+
unitType = "reassess-roadmap";
|
|
3697
|
+
unitId = `${mid}/${completedSliceId}`;
|
|
3698
|
+
prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
|
|
3699
|
+
break;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
case "uat":
|
|
3703
|
+
case "run-uat": {
|
|
3704
|
+
const sid = state.activeSlice?.id;
|
|
3705
|
+
if (!sid) {
|
|
3706
|
+
ctx.ui.notify("Cannot dispatch run-uat: no active slice.", "warning");
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
const uatFile = resolveSliceFile(base, mid, sid, "UAT");
|
|
3710
|
+
if (!uatFile) {
|
|
3711
|
+
ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
const uatContent = await loadFile(uatFile);
|
|
3715
|
+
if (!uatContent) {
|
|
3716
|
+
ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
const uatPath = relSliceFile(base, mid, sid, "UAT");
|
|
3720
|
+
unitType = "run-uat";
|
|
3721
|
+
unitId = `${mid}/${sid}`;
|
|
3722
|
+
prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
|
|
3723
|
+
break;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
case "replan":
|
|
3727
|
+
case "replan-slice": {
|
|
3728
|
+
const sid = state.activeSlice?.id;
|
|
3729
|
+
const sTitle = state.activeSlice?.title ?? "";
|
|
3730
|
+
if (!sid) {
|
|
3731
|
+
ctx.ui.notify("Cannot dispatch replan-slice: no active slice.", "warning");
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
unitType = "replan-slice";
|
|
3735
|
+
unitId = `${mid}/${sid}`;
|
|
3736
|
+
prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
3737
|
+
break;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
default:
|
|
3741
|
+
ctx.ui.notify(
|
|
3742
|
+
`Unknown phase "${phase}". Valid phases: research, plan, execute, complete, reassess, uat, replan.`,
|
|
3743
|
+
"warning",
|
|
3744
|
+
);
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
|
|
3749
|
+
const result = await ctx.newSession();
|
|
3750
|
+
if (result.cancelled) {
|
|
3751
|
+
ctx.ui.notify("Session creation cancelled.", "warning");
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
pi.sendMessage(
|
|
3755
|
+
{ customType: "gsd-dispatch", content: prompt, display: false },
|
|
3756
|
+
{ triggerTurn: true },
|
|
3757
|
+
);
|
|
3758
|
+
}
|