gsd-pi 2.23.0 → 2.25.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 +2 -1
- 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/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +64 -18
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/dist/resources/extensions/gsd/auto.ts +307 -77
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- 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 +58 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
- package/dist/resources/extensions/gsd/index.ts +48 -2
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- 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/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- 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 +256 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -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/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -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/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- 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-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +55 -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/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.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/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +59 -9
- 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/mistral.ts +3 -0
- 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-ai/src/types.ts +19 -1
- 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/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.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/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
- package/src/resources/extensions/gsd/auto.ts +307 -77
- package/src/resources/extensions/gsd/cache.ts +3 -1
- 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 +58 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +109 -12
- package/src/resources/extensions/gsd/index.ts +48 -2
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- 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/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- 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 +256 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -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/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -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/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +15 -5
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -16,9 +16,9 @@ import type {
|
|
|
16
16
|
ExtensionCommandContext,
|
|
17
17
|
} from "@gsd/pi-coding-agent";
|
|
18
18
|
|
|
19
|
-
import { deriveState
|
|
19
|
+
import { deriveState } from "./state.js";
|
|
20
20
|
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
|
|
21
|
+
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides, parseSummary } from "./files.js";
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
24
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
parseSliceBranch,
|
|
95
95
|
setActiveMilestoneId,
|
|
96
96
|
} from "./worktree.js";
|
|
97
|
-
import { GitServiceImpl } from "./git-service.js";
|
|
97
|
+
import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
|
|
98
98
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
99
99
|
import { formatGitError } from "./git-self-heal.js";
|
|
100
100
|
import {
|
|
@@ -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
|
|
@@ -865,23 +889,37 @@ export async function startAuto(
|
|
|
865
889
|
);
|
|
866
890
|
return;
|
|
867
891
|
}
|
|
868
|
-
// Stale lock from a dead process —
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
892
|
+
// Stale lock from a dead process — validate before synthesizing recovery context.
|
|
893
|
+
// If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
|
|
894
|
+
// discard recovery context to prevent phantom skip loops (#790).
|
|
895
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
896
|
+
const milestoneAlreadyComplete = recoveredMid
|
|
897
|
+
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
898
|
+
: false;
|
|
899
|
+
|
|
900
|
+
if (milestoneAlreadyComplete) {
|
|
876
901
|
ctx.ui.notify(
|
|
877
|
-
|
|
878
|
-
"
|
|
902
|
+
`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
|
|
903
|
+
"info",
|
|
879
904
|
);
|
|
880
905
|
} else {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
906
|
+
const activityDir = join(gsdRoot(base), "activity");
|
|
907
|
+
const recovery = synthesizeCrashRecovery(
|
|
908
|
+
base, crashLock.unitType, crashLock.unitId,
|
|
909
|
+
crashLock.sessionFile, activityDir,
|
|
884
910
|
);
|
|
911
|
+
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
912
|
+
pendingCrashRecovery = recovery.prompt;
|
|
913
|
+
ctx.ui.notify(
|
|
914
|
+
`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
|
|
915
|
+
"warning",
|
|
916
|
+
);
|
|
917
|
+
} else {
|
|
918
|
+
ctx.ui.notify(
|
|
919
|
+
`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
|
|
920
|
+
"warning",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
885
923
|
}
|
|
886
924
|
clearLock(base);
|
|
887
925
|
}
|
|
@@ -1094,6 +1132,13 @@ export async function startAuto(
|
|
|
1094
1132
|
}
|
|
1095
1133
|
// Re-register SIGTERM handler with the original basePath (lock lives there)
|
|
1096
1134
|
registerSigtermHandler(originalBasePath);
|
|
1135
|
+
|
|
1136
|
+
// After worktree entry, load completed keys from BOTH locations (project root
|
|
1137
|
+
// + worktree) so the in-memory set is the union. Prevents re-dispatch of units
|
|
1138
|
+
// completed in either location after crash/restart (#769).
|
|
1139
|
+
if (basePath !== originalBasePath) {
|
|
1140
|
+
loadPersistedKeys(basePath, completedKeySet);
|
|
1141
|
+
}
|
|
1097
1142
|
} catch (err) {
|
|
1098
1143
|
// Worktree creation is non-fatal — continue in the project root.
|
|
1099
1144
|
ctx.ui.notify(
|
|
@@ -1130,11 +1175,12 @@ export async function startAuto(
|
|
|
1130
1175
|
}
|
|
1131
1176
|
}
|
|
1132
1177
|
|
|
1133
|
-
// Initialize metrics — loads existing ledger from disk
|
|
1134
|
-
|
|
1178
|
+
// Initialize metrics — loads existing ledger from disk.
|
|
1179
|
+
// Use basePath (not base) so worktree-mode reads the worktree ledger (#769).
|
|
1180
|
+
initMetrics(basePath);
|
|
1135
1181
|
|
|
1136
1182
|
// Initialize routing history for adaptive learning
|
|
1137
|
-
initRoutingHistory(
|
|
1183
|
+
initRoutingHistory(basePath);
|
|
1138
1184
|
|
|
1139
1185
|
// Capture the session's current model at auto-mode start (#650).
|
|
1140
1186
|
// This prevents model bleed when multiple GSD instances share the
|
|
@@ -1185,8 +1231,10 @@ export async function startAuto(
|
|
|
1185
1231
|
);
|
|
1186
1232
|
}
|
|
1187
1233
|
|
|
1188
|
-
// Self-heal: clear stale runtime records where artifacts already exist
|
|
1189
|
-
|
|
1234
|
+
// Self-heal: clear stale runtime records where artifacts already exist.
|
|
1235
|
+
// Use basePath (not base) — in worktree mode, basePath points to the worktree
|
|
1236
|
+
// where runtime records and artifacts actually live (#769).
|
|
1237
|
+
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
1190
1238
|
|
|
1191
1239
|
// Self-heal: remove stale .git/index.lock from prior crash.
|
|
1192
1240
|
// A stale lock file blocks all git operations (commit, merge, checkout).
|
|
@@ -1252,6 +1300,27 @@ export async function handleAgentEnd(
|
|
|
1252
1300
|
// Unit completed — clear its timeout
|
|
1253
1301
|
clearUnitTimeout();
|
|
1254
1302
|
|
|
1303
|
+
// ── Parallel worker signal check ─────────────────────────────────────
|
|
1304
|
+
// When running as a parallel worker (GSD_MILESTONE_LOCK set), check for
|
|
1305
|
+
// coordinator signals before dispatching the next unit.
|
|
1306
|
+
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
1307
|
+
if (milestoneLock) {
|
|
1308
|
+
const signal = consumeSignal(basePath, milestoneLock);
|
|
1309
|
+
if (signal) {
|
|
1310
|
+
if (signal.signal === "stop") {
|
|
1311
|
+
_handlingAgentEnd = false;
|
|
1312
|
+
await stopAuto(ctx, pi);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (signal.signal === "pause") {
|
|
1316
|
+
_handlingAgentEnd = false;
|
|
1317
|
+
await pauseAuto(ctx, pi);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
// "resume" and "rebase" signals are handled elsewhere or no-op here
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1255
1324
|
// Invalidate all caches — the unit just completed and may have
|
|
1256
1325
|
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
1257
1326
|
invalidateAllCaches();
|
|
@@ -1259,12 +1328,41 @@ export async function handleAgentEnd(
|
|
|
1259
1328
|
// Small delay to let files settle (git commits, file writes)
|
|
1260
1329
|
await new Promise(r => setTimeout(r, 500));
|
|
1261
1330
|
|
|
1262
|
-
//
|
|
1331
|
+
// Commit any dirty files the LLM left behind on the current branch.
|
|
1332
|
+
// For execute-task units, build a meaningful commit message from the
|
|
1333
|
+
// task summary (one-liner, key_files, inferred type). For other unit
|
|
1334
|
+
// types, fall back to the generic chore() message.
|
|
1263
1335
|
if (currentUnit) {
|
|
1264
1336
|
try {
|
|
1265
|
-
|
|
1337
|
+
let taskContext: TaskCommitContext | undefined;
|
|
1338
|
+
|
|
1339
|
+
if (currentUnit.type === "execute-task") {
|
|
1340
|
+
const parts = currentUnit.id.split("/");
|
|
1341
|
+
const [mid, sid, tid] = parts;
|
|
1342
|
+
if (mid && sid && tid) {
|
|
1343
|
+
const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
1344
|
+
if (summaryPath) {
|
|
1345
|
+
try {
|
|
1346
|
+
const summaryContent = await loadFile(summaryPath);
|
|
1347
|
+
if (summaryContent) {
|
|
1348
|
+
const summary = parseSummary(summaryContent);
|
|
1349
|
+
taskContext = {
|
|
1350
|
+
taskId: `${sid}/${tid}`,
|
|
1351
|
+
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
1352
|
+
oneLiner: summary.oneLiner || undefined,
|
|
1353
|
+
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
// Non-fatal — fall back to generic message
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
|
|
1266
1364
|
if (commitMsg) {
|
|
1267
|
-
ctx.ui.notify(`
|
|
1365
|
+
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
1268
1366
|
}
|
|
1269
1367
|
} catch {
|
|
1270
1368
|
// Non-fatal
|
|
@@ -1317,7 +1415,8 @@ export async function handleAgentEnd(
|
|
|
1317
1415
|
}
|
|
1318
1416
|
try {
|
|
1319
1417
|
await rebuildState(basePath);
|
|
1320
|
-
|
|
1418
|
+
// State rebuild commit is bookkeeping — generic message is appropriate
|
|
1419
|
+
autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
|
|
1321
1420
|
} catch {
|
|
1322
1421
|
// Non-fatal
|
|
1323
1422
|
}
|
|
@@ -1410,7 +1509,7 @@ export async function handleAgentEnd(
|
|
|
1410
1509
|
persistCompletedKey(basePath, completionKey);
|
|
1411
1510
|
completedKeySet.add(completionKey);
|
|
1412
1511
|
}
|
|
1413
|
-
|
|
1512
|
+
invalidateAllCaches();
|
|
1414
1513
|
}
|
|
1415
1514
|
} catch {
|
|
1416
1515
|
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
|
|
@@ -1449,7 +1548,16 @@ export async function handleAgentEnd(
|
|
|
1449
1548
|
if (currentUnit) {
|
|
1450
1549
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1451
1550
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1452
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1551
|
+
const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1552
|
+
if (hookActivityFile) {
|
|
1553
|
+
try {
|
|
1554
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1555
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1556
|
+
if (llmCallFn) {
|
|
1557
|
+
extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1558
|
+
}
|
|
1559
|
+
} catch { /* non-fatal */ }
|
|
1560
|
+
}
|
|
1453
1561
|
}
|
|
1454
1562
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
1455
1563
|
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
@@ -1485,7 +1593,7 @@ export async function handleAgentEnd(
|
|
|
1485
1593
|
const result = await cmdCtx!.newSession();
|
|
1486
1594
|
if (result.cancelled) {
|
|
1487
1595
|
resetHookState();
|
|
1488
|
-
await stopAuto(ctx, pi);
|
|
1596
|
+
await stopAuto(ctx, pi, "Hook session cancelled");
|
|
1489
1597
|
return;
|
|
1490
1598
|
}
|
|
1491
1599
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
@@ -1591,7 +1699,16 @@ export async function handleAgentEnd(
|
|
|
1591
1699
|
if (currentUnit) {
|
|
1592
1700
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1593
1701
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1594
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1702
|
+
const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1703
|
+
if (triageActivityFile) {
|
|
1704
|
+
try {
|
|
1705
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1706
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1707
|
+
if (llmCallFn) {
|
|
1708
|
+
extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1709
|
+
}
|
|
1710
|
+
} catch { /* non-fatal */ }
|
|
1711
|
+
}
|
|
1595
1712
|
}
|
|
1596
1713
|
|
|
1597
1714
|
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
@@ -1669,7 +1786,16 @@ export async function handleAgentEnd(
|
|
|
1669
1786
|
if (currentUnit) {
|
|
1670
1787
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1671
1788
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1672
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1789
|
+
const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1790
|
+
if (qtActivityFile) {
|
|
1791
|
+
try {
|
|
1792
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1793
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1794
|
+
if (llmCallFn) {
|
|
1795
|
+
extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1796
|
+
}
|
|
1797
|
+
} catch { /* non-fatal */ }
|
|
1798
|
+
}
|
|
1673
1799
|
}
|
|
1674
1800
|
|
|
1675
1801
|
// Dispatch quick-task as a new unit
|
|
@@ -1783,7 +1909,15 @@ async function showStepWizard(
|
|
|
1783
1909
|
|
|
1784
1910
|
// If no active milestone or everything is complete, stop
|
|
1785
1911
|
if (!mid || state.phase === "complete") {
|
|
1786
|
-
|
|
1912
|
+
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
1913
|
+
if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked") {
|
|
1914
|
+
const ids = incomplete.map(m => m.id).join(", ");
|
|
1915
|
+
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1916
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
1917
|
+
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
|
|
1918
|
+
} else {
|
|
1919
|
+
await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
|
|
1920
|
+
}
|
|
1787
1921
|
return;
|
|
1788
1922
|
}
|
|
1789
1923
|
|
|
@@ -1906,8 +2040,7 @@ async function dispatchNextUnit(
|
|
|
1906
2040
|
// doesn't provide. Stop gracefully instead of crashing.
|
|
1907
2041
|
const staleMsg = checkResourcesStale();
|
|
1908
2042
|
if (staleMsg) {
|
|
1909
|
-
await stopAuto(ctx, pi);
|
|
1910
|
-
ctx.ui.notify(staleMsg, "error");
|
|
2043
|
+
await stopAuto(ctx, pi, staleMsg);
|
|
1911
2044
|
return;
|
|
1912
2045
|
}
|
|
1913
2046
|
|
|
@@ -2054,8 +2187,25 @@ async function dispatchNextUnit(
|
|
|
2054
2187
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2055
2188
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2056
2189
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2190
|
+
|
|
2191
|
+
const incomplete = state.registry.filter(m => m.status !== "complete");
|
|
2192
|
+
if (incomplete.length === 0) {
|
|
2193
|
+
// Genuinely all complete
|
|
2194
|
+
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
2195
|
+
await stopAuto(ctx, pi, "All milestones complete");
|
|
2196
|
+
} else if (state.phase === "blocked") {
|
|
2197
|
+
// Milestones exist but are dependency-blocked
|
|
2198
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2199
|
+
await stopAuto(ctx, pi, blockerMsg);
|
|
2200
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
2201
|
+
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2202
|
+
} else {
|
|
2203
|
+
// Milestones with remaining work exist but none became active — unexpected
|
|
2204
|
+
const ids = incomplete.map(m => m.id).join(", ");
|
|
2205
|
+
const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
2206
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
2207
|
+
await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
2208
|
+
}
|
|
2059
2209
|
return;
|
|
2060
2210
|
}
|
|
2061
2211
|
|
|
@@ -2063,8 +2213,8 @@ async function dispatchNextUnit(
|
|
|
2063
2213
|
// The !mid check above returns early if mid is falsy; midTitle comes from
|
|
2064
2214
|
// the same object so it should always be present when mid is.
|
|
2065
2215
|
if (!midTitle) {
|
|
2066
|
-
|
|
2067
|
-
|
|
2216
|
+
midTitle = mid; // Defensive fallback: use milestone ID as title
|
|
2217
|
+
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
2068
2218
|
}
|
|
2069
2219
|
|
|
2070
2220
|
// ── Mid-merge safety check: detect leftover merge state from a prior session ──
|
|
@@ -2082,7 +2232,10 @@ async function dispatchNextUnit(
|
|
|
2082
2232
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2083
2233
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2084
2234
|
}
|
|
2085
|
-
|
|
2235
|
+
const noMilestoneReason = !mid
|
|
2236
|
+
? "No active milestone after merge reconciliation"
|
|
2237
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
2238
|
+
await stopAuto(ctx, pi, noMilestoneReason);
|
|
2086
2239
|
return;
|
|
2087
2240
|
}
|
|
2088
2241
|
|
|
@@ -2157,7 +2310,7 @@ async function dispatchNextUnit(
|
|
|
2157
2310
|
}
|
|
2158
2311
|
}
|
|
2159
2312
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
2160
|
-
await stopAuto(ctx, pi);
|
|
2313
|
+
await stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
2161
2314
|
return;
|
|
2162
2315
|
}
|
|
2163
2316
|
|
|
@@ -2167,8 +2320,8 @@ async function dispatchNextUnit(
|
|
|
2167
2320
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2168
2321
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2169
2322
|
}
|
|
2170
|
-
await stopAuto(ctx, pi);
|
|
2171
2323
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
2324
|
+
await stopAuto(ctx, pi, blockerMsg);
|
|
2172
2325
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
2173
2326
|
sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
2174
2327
|
return;
|
|
@@ -2194,9 +2347,8 @@ async function dispatchNextUnit(
|
|
|
2194
2347
|
const msg = `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}).`;
|
|
2195
2348
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2196
2349
|
if (budgetEnforcementAction === "halt") {
|
|
2197
|
-
ctx.ui.notify(`${msg} Stopping auto-mode.`, "error");
|
|
2198
2350
|
sendDesktopNotification("GSD", msg, "error", "budget");
|
|
2199
|
-
await stopAuto(ctx, pi);
|
|
2351
|
+
await stopAuto(ctx, pi, "Budget ceiling reached");
|
|
2200
2352
|
return;
|
|
2201
2353
|
}
|
|
2202
2354
|
if (budgetEnforcementAction === "pause") {
|
|
@@ -2211,6 +2363,10 @@ async function dispatchNextUnit(
|
|
|
2211
2363
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2212
2364
|
ctx.ui.notify(`Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2213
2365
|
sendDesktopNotification("GSD", `Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2366
|
+
} else if (newBudgetAlertLevel === 80) {
|
|
2367
|
+
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2368
|
+
ctx.ui.notify(`Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
|
|
2369
|
+
sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
|
|
2214
2370
|
} else if (newBudgetAlertLevel === 75) {
|
|
2215
2371
|
lastBudgetAlertLevel = newBudgetAlertLevel;
|
|
2216
2372
|
ctx.ui.notify(`Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info");
|
|
@@ -2275,8 +2431,7 @@ async function dispatchNextUnit(
|
|
|
2275
2431
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2276
2432
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2277
2433
|
}
|
|
2278
|
-
await stopAuto(ctx, pi);
|
|
2279
|
-
ctx.ui.notify(dispatchResult.reason, dispatchResult.level);
|
|
2434
|
+
await stopAuto(ctx, pi, dispatchResult.reason);
|
|
2280
2435
|
return;
|
|
2281
2436
|
}
|
|
2282
2437
|
|
|
@@ -2316,8 +2471,7 @@ async function dispatchNextUnit(
|
|
|
2316
2471
|
|
|
2317
2472
|
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
2318
2473
|
if (priorSliceBlocker) {
|
|
2319
|
-
await stopAuto(ctx, pi);
|
|
2320
|
-
ctx.ui.notify(priorSliceBlocker, "error");
|
|
2474
|
+
await stopAuto(ctx, pi, priorSliceBlocker);
|
|
2321
2475
|
return;
|
|
2322
2476
|
}
|
|
2323
2477
|
|
|
@@ -2335,26 +2489,61 @@ async function dispatchNextUnit(
|
|
|
2335
2489
|
const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2336
2490
|
unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
2337
2491
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
2492
|
+
// Cross-check: verify deriveState actually returns this unit (#790).
|
|
2493
|
+
// If the unit's milestone is already complete, this is a phantom skip
|
|
2494
|
+
// loop from stale crash recovery context — don't evict.
|
|
2495
|
+
const skippedMid = unitId.split("/")[0];
|
|
2496
|
+
const skippedMilestoneComplete = skippedMid
|
|
2497
|
+
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
2498
|
+
: false;
|
|
2499
|
+
if (skippedMilestoneComplete) {
|
|
2500
|
+
// Milestone is complete — evicting this key would fight self-heal.
|
|
2501
|
+
// Clear skip counter and re-dispatch from fresh state.
|
|
2502
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2503
|
+
invalidateAllCaches();
|
|
2504
|
+
ctx.ui.notify(
|
|
2505
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
|
|
2506
|
+
"info",
|
|
2507
|
+
);
|
|
2508
|
+
_skipDepth++;
|
|
2509
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2510
|
+
await dispatchNextUnit(ctx, pi);
|
|
2511
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2338
2514
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2339
2515
|
completedKeySet.delete(idempotencyKey);
|
|
2340
2516
|
removePersistedKey(basePath, idempotencyKey);
|
|
2341
|
-
|
|
2517
|
+
invalidateAllCaches();
|
|
2342
2518
|
ctx.ui.notify(
|
|
2343
2519
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2344
2520
|
"warning",
|
|
2345
2521
|
);
|
|
2522
|
+
if (!active) return;
|
|
2346
2523
|
_skipDepth++;
|
|
2347
|
-
await new Promise(r => setTimeout(r,
|
|
2524
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2348
2525
|
await dispatchNextUnit(ctx, pi);
|
|
2349
2526
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2350
2527
|
return;
|
|
2351
2528
|
}
|
|
2529
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2530
|
+
const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2531
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
|
|
2532
|
+
if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
|
|
2533
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2534
|
+
ctx.ui.notify(
|
|
2535
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
|
|
2536
|
+
"error",
|
|
2537
|
+
);
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2352
2540
|
ctx.ui.notify(
|
|
2353
2541
|
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
2354
2542
|
"info",
|
|
2355
2543
|
);
|
|
2544
|
+
if (!active) return;
|
|
2356
2545
|
_skipDepth++;
|
|
2357
|
-
await new Promise(r => setTimeout(r,
|
|
2546
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2358
2547
|
await dispatchNextUnit(ctx, pi);
|
|
2359
2548
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2360
2549
|
return;
|
|
@@ -2377,31 +2566,62 @@ async function dispatchNextUnit(
|
|
|
2377
2566
|
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2378
2567
|
persistCompletedKey(basePath, idempotencyKey);
|
|
2379
2568
|
completedKeySet.add(idempotencyKey);
|
|
2380
|
-
|
|
2569
|
+
invalidateAllCaches();
|
|
2381
2570
|
// Same consecutive-skip guard as the idempotency path above.
|
|
2382
2571
|
const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2383
2572
|
unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
2384
2573
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
2574
|
+
// Cross-check: verify the unit's milestone is still active (#790).
|
|
2575
|
+
const skippedMid2 = unitId.split("/")[0];
|
|
2576
|
+
const skippedMilestoneComplete2 = skippedMid2
|
|
2577
|
+
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
2578
|
+
: false;
|
|
2579
|
+
if (skippedMilestoneComplete2) {
|
|
2580
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2581
|
+
invalidateAllCaches();
|
|
2582
|
+
ctx.ui.notify(
|
|
2583
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
|
|
2584
|
+
"info",
|
|
2585
|
+
);
|
|
2586
|
+
_skipDepth++;
|
|
2587
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2588
|
+
await dispatchNextUnit(ctx, pi);
|
|
2589
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2385
2592
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2386
2593
|
completedKeySet.delete(idempotencyKey);
|
|
2387
2594
|
removePersistedKey(basePath, idempotencyKey);
|
|
2388
|
-
|
|
2595
|
+
invalidateAllCaches();
|
|
2389
2596
|
ctx.ui.notify(
|
|
2390
2597
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2391
2598
|
"warning",
|
|
2392
2599
|
);
|
|
2600
|
+
if (!active) return;
|
|
2393
2601
|
_skipDepth++;
|
|
2394
|
-
await new Promise(r => setTimeout(r,
|
|
2602
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2395
2603
|
await dispatchNextUnit(ctx, pi);
|
|
2396
2604
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2397
2605
|
return;
|
|
2398
2606
|
}
|
|
2607
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2608
|
+
const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2609
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
|
|
2610
|
+
if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
|
|
2611
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2612
|
+
ctx.ui.notify(
|
|
2613
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
|
|
2614
|
+
"error",
|
|
2615
|
+
);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2399
2618
|
ctx.ui.notify(
|
|
2400
2619
|
`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
|
|
2401
2620
|
"info",
|
|
2402
2621
|
);
|
|
2622
|
+
if (!active) return;
|
|
2403
2623
|
_skipDepth++;
|
|
2404
|
-
await new Promise(r => setTimeout(r,
|
|
2624
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2405
2625
|
await dispatchNextUnit(ctx, pi);
|
|
2406
2626
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2407
2627
|
return;
|
|
@@ -2434,9 +2654,9 @@ async function dispatchNextUnit(
|
|
|
2434
2654
|
}
|
|
2435
2655
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2436
2656
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2437
|
-
await stopAuto(ctx, pi);
|
|
2657
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId}`);
|
|
2438
2658
|
ctx.ui.notify(
|
|
2439
|
-
`Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles)
|
|
2659
|
+
`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
2660
|
"error",
|
|
2441
2661
|
);
|
|
2442
2662
|
return;
|
|
@@ -2471,7 +2691,7 @@ async function dispatchNextUnit(
|
|
|
2471
2691
|
persistCompletedKey(basePath, reconciledKey);
|
|
2472
2692
|
completedKeySet.add(reconciledKey);
|
|
2473
2693
|
unitDispatchCount.delete(dispatchKey);
|
|
2474
|
-
|
|
2694
|
+
invalidateAllCaches();
|
|
2475
2695
|
await new Promise(r => setImmediate(r));
|
|
2476
2696
|
await dispatchNextUnit(ctx, pi);
|
|
2477
2697
|
return;
|
|
@@ -2498,7 +2718,7 @@ async function dispatchNextUnit(
|
|
|
2498
2718
|
persistCompletedKey(basePath, dispatchKey);
|
|
2499
2719
|
completedKeySet.add(dispatchKey);
|
|
2500
2720
|
unitDispatchCount.delete(dispatchKey);
|
|
2501
|
-
|
|
2721
|
+
invalidateAllCaches();
|
|
2502
2722
|
await new Promise(r => setImmediate(r));
|
|
2503
2723
|
await dispatchNextUnit(ctx, pi);
|
|
2504
2724
|
return;
|
|
@@ -2518,7 +2738,7 @@ async function dispatchNextUnit(
|
|
|
2518
2738
|
persistCompletedKey(basePath, dispatchKey);
|
|
2519
2739
|
completedKeySet.add(dispatchKey);
|
|
2520
2740
|
unitDispatchCount.delete(dispatchKey);
|
|
2521
|
-
|
|
2741
|
+
invalidateAllCaches();
|
|
2522
2742
|
await new Promise(r => setImmediate(r));
|
|
2523
2743
|
await dispatchNextUnit(ctx, pi);
|
|
2524
2744
|
return;
|
|
@@ -2529,7 +2749,7 @@ async function dispatchNextUnit(
|
|
|
2529
2749
|
|
|
2530
2750
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2531
2751
|
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
2532
|
-
await stopAuto(ctx, pi);
|
|
2752
|
+
await stopAuto(ctx, pi, `Loop: ${unitType} ${unitId}`);
|
|
2533
2753
|
sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
|
|
2534
2754
|
ctx.ui.notify(
|
|
2535
2755
|
`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."}`,
|
|
@@ -2559,7 +2779,7 @@ async function dispatchNextUnit(
|
|
|
2559
2779
|
persistCompletedKey(basePath, repairedKey);
|
|
2560
2780
|
completedKeySet.add(repairedKey);
|
|
2561
2781
|
unitDispatchCount.delete(dispatchKey);
|
|
2562
|
-
|
|
2782
|
+
invalidateAllCaches();
|
|
2563
2783
|
await new Promise(r => setImmediate(r));
|
|
2564
2784
|
await dispatchNextUnit(ctx, pi);
|
|
2565
2785
|
return;
|
|
@@ -2603,7 +2823,18 @@ async function dispatchNextUnit(
|
|
|
2603
2823
|
if (currentUnit) {
|
|
2604
2824
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2605
2825
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2606
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2826
|
+
const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2827
|
+
|
|
2828
|
+
// Fire-and-forget memory extraction from completed unit
|
|
2829
|
+
if (activityFile) {
|
|
2830
|
+
try {
|
|
2831
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
2832
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
2833
|
+
if (llmCallFn) {
|
|
2834
|
+
extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
2835
|
+
}
|
|
2836
|
+
} catch { /* non-fatal */ }
|
|
2837
|
+
}
|
|
2607
2838
|
|
|
2608
2839
|
// Record routing outcome for adaptive learning
|
|
2609
2840
|
if (currentUnitRouting) {
|
|
@@ -2670,8 +2901,7 @@ async function dispatchNextUnit(
|
|
|
2670
2901
|
// Fresh session
|
|
2671
2902
|
const result = await cmdCtx!.newSession();
|
|
2672
2903
|
if (result.cancelled) {
|
|
2673
|
-
await stopAuto(ctx, pi);
|
|
2674
|
-
ctx.ui.notify("Auto-mode stopped.", "info");
|
|
2904
|
+
await stopAuto(ctx, pi, "Session cancelled");
|
|
2675
2905
|
return;
|
|
2676
2906
|
}
|
|
2677
2907
|
|