gsd-pi 2.26.0 → 2.27.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 +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- package/src/resources/extensions/subagent/isolation.ts +9 -6
|
@@ -93,9 +93,57 @@ export function writeExportFile(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Export session/milestone data to JSON or
|
|
96
|
+
* Export session/milestone data to JSON, markdown, or HTML.
|
|
97
97
|
*/
|
|
98
98
|
export async function handleExport(args: string, ctx: ExtensionCommandContext, basePath: string): Promise<void> {
|
|
99
|
+
// HTML report — delegates to the full visualizer-data pipeline
|
|
100
|
+
if (args.includes("--html")) {
|
|
101
|
+
try {
|
|
102
|
+
const { loadVisualizerData } = await import("./visualizer-data.js");
|
|
103
|
+
const { generateHtmlReport } = await import("./export-html.js");
|
|
104
|
+
const { writeReportSnapshot, reportsDir } = await import("./reports.js");
|
|
105
|
+
const { basename: bn } = await import("node:path");
|
|
106
|
+
const data = await loadVisualizerData(basePath);
|
|
107
|
+
const projName = basename(basePath);
|
|
108
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
109
|
+
const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
110
|
+
const totalSlices = data.milestones.reduce((s, m) => s + m.slices.length, 0);
|
|
111
|
+
const outPath = writeReportSnapshot({
|
|
112
|
+
basePath,
|
|
113
|
+
html: generateHtmlReport(data, {
|
|
114
|
+
projectName: projName,
|
|
115
|
+
projectPath: basePath,
|
|
116
|
+
gsdVersion,
|
|
117
|
+
indexRelPath: "index.html",
|
|
118
|
+
}),
|
|
119
|
+
milestoneId: data.milestones.find(m => m.status === "active")?.id ?? "manual",
|
|
120
|
+
milestoneTitle: data.milestones.find(m => m.status === "active")?.title ?? "",
|
|
121
|
+
kind: "manual",
|
|
122
|
+
projectName: projName,
|
|
123
|
+
projectPath: basePath,
|
|
124
|
+
gsdVersion,
|
|
125
|
+
totalCost: data.totals?.cost ?? 0,
|
|
126
|
+
totalTokens: data.totals?.tokens.total ?? 0,
|
|
127
|
+
totalDuration: data.totals?.duration ?? 0,
|
|
128
|
+
doneSlices,
|
|
129
|
+
totalSlices,
|
|
130
|
+
doneMilestones: data.milestones.filter(m => m.status === "complete").length,
|
|
131
|
+
totalMilestones: data.milestones.length,
|
|
132
|
+
phase: data.phase,
|
|
133
|
+
});
|
|
134
|
+
ctx.ui.notify(
|
|
135
|
+
`HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
|
|
136
|
+
"success",
|
|
137
|
+
);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
`HTML export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
141
|
+
"error",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
99
147
|
const format = args.includes("--json") ? "json" : "markdown";
|
|
100
148
|
|
|
101
149
|
const ledger = getLedger();
|
|
@@ -53,6 +53,12 @@ export interface GitPreferences {
|
|
|
53
53
|
* Default: true (planning docs are tracked in git).
|
|
54
54
|
*/
|
|
55
55
|
commit_docs?: boolean;
|
|
56
|
+
/** When false, GSD will not modify .gitignore at all — no baseline patterns
|
|
57
|
+
* are added and no self-healing occurs. Use this if you manage your own
|
|
58
|
+
* .gitignore and don't want GSD touching it.
|
|
59
|
+
* Default: true (GSD ensures baseline patterns are present).
|
|
60
|
+
*/
|
|
61
|
+
manage_gitignore?: boolean;
|
|
56
62
|
/** Script to run after a worktree is created (#597).
|
|
57
63
|
* Receives SOURCE_DIR and WORKTREE_DIR as environment variables.
|
|
58
64
|
* Can be an absolute path or relative to the project root.
|
|
@@ -85,7 +85,10 @@ const BASELINE_PATTERNS = [
|
|
|
85
85
|
* .gitignore instead of individual runtime patterns, keeping all GSD
|
|
86
86
|
* artifacts local-only.
|
|
87
87
|
*/
|
|
88
|
-
export function ensureGitignore(basePath: string, options?: { commitDocs?: boolean }): boolean {
|
|
88
|
+
export function ensureGitignore(basePath: string, options?: { commitDocs?: boolean; manageGitignore?: boolean }): boolean {
|
|
89
|
+
// If manage_gitignore is explicitly false, do not touch .gitignore at all
|
|
90
|
+
if (options?.manageGitignore === false) return false;
|
|
91
|
+
|
|
89
92
|
const gitignorePath = join(basePath, ".gitignore");
|
|
90
93
|
const commitDocs = options?.commitDocs !== false; // default true
|
|
91
94
|
|
|
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@g
|
|
|
10
10
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
11
11
|
import { loadFile, parseRoadmap } from "./files.js";
|
|
12
12
|
import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
13
|
-
import { deriveState } from "./state.js";
|
|
13
|
+
import { deriveState, invalidateStateCache } from "./state.js";
|
|
14
14
|
import { startAuto } from "./auto.js";
|
|
15
15
|
import { readCrashLock, clearLock, formatCrashInfo } from "./crash-recovery.js";
|
|
16
16
|
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
@@ -959,10 +959,28 @@ export async function showDiscuss(
|
|
|
959
959
|
|
|
960
960
|
// Loop: show picker, dispatch discuss, repeat until "not_yet"
|
|
961
961
|
while (true) {
|
|
962
|
-
|
|
963
|
-
|
|
962
|
+
// Build discussion-state map: which slices have CONTEXT files already?
|
|
963
|
+
const discussedMap = new Map<string, boolean>();
|
|
964
|
+
for (const s of pendingSlices) {
|
|
964
965
|
const contextFile = resolveSliceFile(basePath, mid, s.id, "CONTEXT");
|
|
965
|
-
|
|
966
|
+
discussedMap.set(s.id, !!contextFile);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// If all pending slices are discussed, notify and exit instead of looping
|
|
970
|
+
const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id));
|
|
971
|
+
if (allDiscussed) {
|
|
972
|
+
ctx.ui.notify(
|
|
973
|
+
`All ${pendingSlices.length} slices discussed. Run /gsd to start planning.`,
|
|
974
|
+
"info",
|
|
975
|
+
);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Find the first undiscussed slice to recommend
|
|
980
|
+
const firstUndiscussedId = pendingSlices.find(s => !discussedMap.get(s.id))?.id;
|
|
981
|
+
|
|
982
|
+
const actions = pendingSlices.map((s) => {
|
|
983
|
+
const discussed = discussedMap.get(s.id) ?? false;
|
|
966
984
|
const statusParts: string[] = [];
|
|
967
985
|
if (state.activeSlice?.id === s.id) statusParts.push("active");
|
|
968
986
|
else statusParts.push("upcoming");
|
|
@@ -972,7 +990,7 @@ export async function showDiscuss(
|
|
|
972
990
|
id: s.id,
|
|
973
991
|
label: `${s.id}: ${s.title}`,
|
|
974
992
|
description: statusParts.join(" · "),
|
|
975
|
-
recommended:
|
|
993
|
+
recommended: s.id === firstUndiscussedId,
|
|
976
994
|
};
|
|
977
995
|
});
|
|
978
996
|
|
|
@@ -996,6 +1014,7 @@ export async function showDiscuss(
|
|
|
996
1014
|
|
|
997
1015
|
// Wait for the discuss session to finish, then loop back to the picker
|
|
998
1016
|
await ctx.waitForIdle();
|
|
1017
|
+
invalidateStateCache();
|
|
999
1018
|
}
|
|
1000
1019
|
}
|
|
1001
1020
|
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
resolveAllSkillReferences,
|
|
45
45
|
resolveModelWithFallbacksForUnit,
|
|
46
46
|
getNextFallbackModel,
|
|
47
|
+
isTransientNetworkError,
|
|
47
48
|
} from "./preferences.js";
|
|
48
49
|
import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-discovery.js";
|
|
49
50
|
import {
|
|
@@ -60,6 +61,7 @@ import { shortcutDesc } from "../shared/terminal.js";
|
|
|
60
61
|
import { Text } from "@gsd/pi-tui";
|
|
61
62
|
import { pauseAutoForProviderError } from "./provider-error-pause.js";
|
|
62
63
|
import { toPosixPath } from "../shared/path-display.js";
|
|
64
|
+
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
63
65
|
|
|
64
66
|
// ── Agent Instructions ────────────────────────────────────────────────────
|
|
65
67
|
// Lightweight "always follow" files injected into every GSD agent session.
|
|
@@ -92,6 +94,11 @@ function loadAgentInstructions(): string | null {
|
|
|
92
94
|
// ── Depth verification state ──────────────────────────────────────────────
|
|
93
95
|
let depthVerificationDone = false;
|
|
94
96
|
|
|
97
|
+
// ── Network error retry counters ──────────────────────────────────────────
|
|
98
|
+
// Tracks per-model retry attempts for transient network errors.
|
|
99
|
+
// Cleared when a model switch occurs or retries are exhausted.
|
|
100
|
+
const networkRetryCounters = new Map<string, number>();
|
|
101
|
+
|
|
95
102
|
export function isDepthVerified(): boolean {
|
|
96
103
|
return depthVerificationDone;
|
|
97
104
|
}
|
|
@@ -727,6 +734,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
727
734
|
? `: ${lastMsg.errorMessage}`
|
|
728
735
|
: "";
|
|
729
736
|
|
|
737
|
+
const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
738
|
+
|
|
739
|
+
// ── Transient network error retry ──────────────────────────────────
|
|
740
|
+
// Before falling back to a different model, retry the current model
|
|
741
|
+
// for transient network errors (connection reset, timeout, DNS, etc.).
|
|
742
|
+
// This prevents providers with occasional network flakiness from being
|
|
743
|
+
// immediately abandoned in favor of fallback models (#941).
|
|
744
|
+
if (isTransientNetworkError(errorMsg)) {
|
|
745
|
+
const currentModelId = ctx.model?.id ?? "unknown";
|
|
746
|
+
const retryKey = `network-retry:${currentModelId}`;
|
|
747
|
+
const maxRetries = 2;
|
|
748
|
+
const currentRetries = networkRetryCounters.get(retryKey) ?? 0;
|
|
749
|
+
|
|
750
|
+
if (currentRetries < maxRetries) {
|
|
751
|
+
networkRetryCounters.set(retryKey, currentRetries + 1);
|
|
752
|
+
const attempt = currentRetries + 1;
|
|
753
|
+
const delayMs = attempt * 3000; // 3s, 6s backoff
|
|
754
|
+
ctx.ui.notify(
|
|
755
|
+
`Network error on ${currentModelId}${errorDetail}. Retry ${attempt}/${maxRetries} in ${delayMs / 1000}s...`,
|
|
756
|
+
"warning",
|
|
757
|
+
);
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
pi.sendMessage(
|
|
760
|
+
{ customType: "gsd-auto-timeout-recovery", content: "Continue execution — retrying after transient network error.", display: false },
|
|
761
|
+
{ triggerTurn: true },
|
|
762
|
+
);
|
|
763
|
+
}, delayMs);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Retries exhausted — clear counter and fall through to fallback logic
|
|
767
|
+
networkRetryCounters.delete(retryKey);
|
|
768
|
+
ctx.ui.notify(
|
|
769
|
+
`Network retries exhausted for ${currentModelId}. Attempting model fallback.`,
|
|
770
|
+
"warning",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
730
774
|
const dash = getAutoDashboardData();
|
|
731
775
|
if (dash.currentUnit) {
|
|
732
776
|
const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
|
|
@@ -737,6 +781,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
737
781
|
const nextModelId = getNextFallbackModel(currentModelId, modelConfig);
|
|
738
782
|
|
|
739
783
|
if (nextModelId) {
|
|
784
|
+
// Clear any network retry counters when switching models
|
|
785
|
+
networkRetryCounters.clear();
|
|
786
|
+
|
|
740
787
|
let modelToSet;
|
|
741
788
|
const slashIdx = nextModelId.indexOf("/");
|
|
742
789
|
if (slashIdx !== -1) {
|
|
@@ -771,7 +818,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
771
818
|
}
|
|
772
819
|
|
|
773
820
|
// Detect rate-limit errors and extract retry delay for auto-resume
|
|
774
|
-
const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
775
821
|
const isRateLimit = /rate.?limit|too many requests|429/i.test(errorMsg);
|
|
776
822
|
const retryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number")
|
|
777
823
|
? lastMsg.retryAfterMs
|
|
@@ -791,6 +837,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
791
837
|
}
|
|
792
838
|
|
|
793
839
|
try {
|
|
840
|
+
networkRetryCounters.clear(); // Clear network retry state on successful unit completion
|
|
794
841
|
await handleAgentEnd(ctx, pi);
|
|
795
842
|
} catch (err) {
|
|
796
843
|
// Safety net: if handleAgentEnd throws despite its internal try-catch,
|
|
@@ -856,6 +903,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
856
903
|
|
|
857
904
|
// ── session_shutdown: save activity log on Ctrl+C / SIGTERM ─────────────
|
|
858
905
|
pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
|
|
906
|
+
if (isParallelActive()) {
|
|
907
|
+
try {
|
|
908
|
+
await shutdownParallel(process.cwd());
|
|
909
|
+
} catch { /* best-effort */ }
|
|
910
|
+
}
|
|
911
|
+
|
|
859
912
|
if (!isAutoActive() && !isAutoPaused()) return;
|
|
860
913
|
|
|
861
914
|
// Save the current session — the lock file stays on disk
|
|
@@ -249,18 +249,46 @@ export function nativeWorkingTreeStatus(basePath: string): string {
|
|
|
249
249
|
return gitExec(basePath, ["status", "--porcelain"], true);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// ─── nativeHasChanges fallback cache (10s TTL) ─────────────────────────
|
|
253
|
+
let _hasChangesCachedResult: boolean = false;
|
|
254
|
+
let _hasChangesCachedAt: number = 0;
|
|
255
|
+
let _hasChangesCachedPath: string = "";
|
|
256
|
+
const HAS_CHANGES_CACHE_TTL_MS = 10_000; // 10 seconds
|
|
257
|
+
|
|
252
258
|
/**
|
|
253
259
|
* Quick check: any staged or unstaged changes?
|
|
254
260
|
* Native: libgit2 status check (single syscall).
|
|
255
|
-
* Fallback: `git status --short
|
|
261
|
+
* Fallback: `git status --short` (cached for 10s per basePath).
|
|
256
262
|
*/
|
|
257
263
|
export function nativeHasChanges(basePath: string): boolean {
|
|
258
264
|
const native = loadNative();
|
|
259
265
|
if (native) {
|
|
260
266
|
return native.gitHasChanges(basePath);
|
|
261
267
|
}
|
|
268
|
+
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
if (
|
|
271
|
+
basePath === _hasChangesCachedPath &&
|
|
272
|
+
now - _hasChangesCachedAt < HAS_CHANGES_CACHE_TTL_MS
|
|
273
|
+
) {
|
|
274
|
+
return _hasChangesCachedResult;
|
|
275
|
+
}
|
|
276
|
+
|
|
262
277
|
const result = gitExec(basePath, ["status", "--short"], true);
|
|
263
|
-
|
|
278
|
+
const hasChanges = result !== "";
|
|
279
|
+
|
|
280
|
+
_hasChangesCachedResult = hasChanges;
|
|
281
|
+
_hasChangesCachedAt = now;
|
|
282
|
+
_hasChangesCachedPath = basePath;
|
|
283
|
+
|
|
284
|
+
return hasChanges;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Reset the nativeHasChanges fallback cache (exported for testing). */
|
|
288
|
+
export function _resetHasChangesCache(): void {
|
|
289
|
+
_hasChangesCachedResult = false;
|
|
290
|
+
_hasChangesCachedAt = 0;
|
|
291
|
+
_hasChangesCachedPath = "";
|
|
264
292
|
}
|
|
265
293
|
|
|
266
294
|
/**
|
|
@@ -298,6 +298,27 @@ export function validateTaskSummaryContent(file: string, content: string): Valid
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
const evidence = getSection(content, "Verification Evidence", 2);
|
|
302
|
+
if (!evidence) {
|
|
303
|
+
issues.push({
|
|
304
|
+
severity: "warning",
|
|
305
|
+
scope: "task-summary",
|
|
306
|
+
file,
|
|
307
|
+
ruleId: "evidence_block_missing",
|
|
308
|
+
message: "Task summary is missing `## Verification Evidence`.",
|
|
309
|
+
suggestion: "Add a verification evidence table showing gate check results (command, exit code, verdict, duration).",
|
|
310
|
+
});
|
|
311
|
+
} else if (sectionLooksPlaceholderOnly(evidence)) {
|
|
312
|
+
issues.push({
|
|
313
|
+
severity: "warning",
|
|
314
|
+
scope: "task-summary",
|
|
315
|
+
file,
|
|
316
|
+
ruleId: "evidence_block_placeholder",
|
|
317
|
+
message: "Task summary verification evidence section still looks like placeholder text.",
|
|
318
|
+
suggestion: "Replace placeholders with actual gate results or note that no verification commands were discovered.",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
301
322
|
return issues;
|
|
302
323
|
}
|
|
303
324
|
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
} from "node:fs";
|
|
12
19
|
import { join, dirname } from "node:path";
|
|
13
20
|
import { fileURLToPath } from "node:url";
|
|
14
21
|
import { gsdRoot } from "./paths.js";
|
|
@@ -58,6 +65,142 @@ export interface OrchestratorState {
|
|
|
58
65
|
|
|
59
66
|
let state: OrchestratorState | null = null;
|
|
60
67
|
|
|
68
|
+
// ─── Persistence ──────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const ORCHESTRATOR_STATE_FILE = "orchestrator.json";
|
|
71
|
+
const TMP_SUFFIX = ".tmp";
|
|
72
|
+
|
|
73
|
+
export interface PersistedState {
|
|
74
|
+
active: boolean;
|
|
75
|
+
workers: Array<{
|
|
76
|
+
milestoneId: string;
|
|
77
|
+
title: string;
|
|
78
|
+
pid: number;
|
|
79
|
+
worktreePath: string;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
state: "running" | "paused" | "stopped" | "error";
|
|
82
|
+
completedUnits: number;
|
|
83
|
+
cost: number;
|
|
84
|
+
}>;
|
|
85
|
+
totalCost: number;
|
|
86
|
+
startedAt: number;
|
|
87
|
+
configSnapshot: { max_workers: number; budget_ceiling?: number };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stateFilePath(basePath: string): string {
|
|
91
|
+
return join(gsdRoot(basePath), ORCHESTRATOR_STATE_FILE);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Persist the current orchestrator state to .gsd/orchestrator.json.
|
|
96
|
+
* Uses atomic write (tmp + rename) to prevent partial reads.
|
|
97
|
+
*/
|
|
98
|
+
export function persistState(basePath: string): void {
|
|
99
|
+
if (!state) return;
|
|
100
|
+
try {
|
|
101
|
+
const dir = gsdRoot(basePath);
|
|
102
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const persisted: PersistedState = {
|
|
105
|
+
active: state.active,
|
|
106
|
+
workers: [...state.workers.values()].map((w) => ({
|
|
107
|
+
milestoneId: w.milestoneId,
|
|
108
|
+
title: w.title,
|
|
109
|
+
pid: w.pid,
|
|
110
|
+
worktreePath: w.worktreePath,
|
|
111
|
+
startedAt: w.startedAt,
|
|
112
|
+
state: w.state,
|
|
113
|
+
completedUnits: w.completedUnits,
|
|
114
|
+
cost: w.cost,
|
|
115
|
+
})),
|
|
116
|
+
totalCost: state.totalCost,
|
|
117
|
+
startedAt: state.startedAt,
|
|
118
|
+
configSnapshot: {
|
|
119
|
+
max_workers: state.config.max_workers,
|
|
120
|
+
budget_ceiling: state.config.budget_ceiling,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const dest = stateFilePath(basePath);
|
|
125
|
+
const tmp = dest + TMP_SUFFIX;
|
|
126
|
+
writeFileSync(tmp, JSON.stringify(persisted, null, 2), "utf-8");
|
|
127
|
+
renameSync(tmp, dest);
|
|
128
|
+
} catch { /* non-fatal */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove the persisted state file.
|
|
133
|
+
*/
|
|
134
|
+
function removeStateFile(basePath: string): void {
|
|
135
|
+
try {
|
|
136
|
+
const p = stateFilePath(basePath);
|
|
137
|
+
if (existsSync(p)) unlinkSync(p);
|
|
138
|
+
} catch { /* non-fatal */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isPidAlive(pid: number): boolean {
|
|
142
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
143
|
+
try {
|
|
144
|
+
process.kill(pid, 0);
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Restore orchestrator state from .gsd/orchestrator.json.
|
|
153
|
+
* Checks PID liveness for each worker:
|
|
154
|
+
* - Living PID → state "running", process stays null (no handle)
|
|
155
|
+
* - Dead PID → removed from restored state
|
|
156
|
+
* Returns null if no state file exists or no workers survive.
|
|
157
|
+
*/
|
|
158
|
+
export function restoreState(basePath: string): PersistedState | null {
|
|
159
|
+
try {
|
|
160
|
+
const p = stateFilePath(basePath);
|
|
161
|
+
if (!existsSync(p)) return null;
|
|
162
|
+
const raw = readFileSync(p, "utf-8");
|
|
163
|
+
const persisted = JSON.parse(raw) as PersistedState;
|
|
164
|
+
|
|
165
|
+
// Filter to only workers with living PIDs
|
|
166
|
+
persisted.workers = persisted.workers.filter((w) => {
|
|
167
|
+
if (w.state === "stopped" || w.state === "error") return false;
|
|
168
|
+
return isPidAlive(w.pid);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (persisted.workers.length === 0) {
|
|
172
|
+
// No surviving workers — clean up and return null
|
|
173
|
+
removeStateFile(basePath);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return persisted;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function waitForWorkerExit(worker: WorkerInfo, timeoutMs: number): Promise<boolean> {
|
|
184
|
+
if (worker.process) {
|
|
185
|
+
await new Promise<void>((resolve) => {
|
|
186
|
+
const done = () => resolve();
|
|
187
|
+
const timer = setTimeout(done, timeoutMs);
|
|
188
|
+
worker.process!.once("exit", () => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
return worker.process === null || !isPidAlive(worker.pid);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const startedAt = Date.now();
|
|
197
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
198
|
+
if (!isPidAlive(worker.pid)) return true;
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
200
|
+
}
|
|
201
|
+
return !isPidAlive(worker.pid);
|
|
202
|
+
}
|
|
203
|
+
|
|
61
204
|
// ─── Accessors ─────────────────────────────────────────────────────────────
|
|
62
205
|
|
|
63
206
|
/** Returns true if the orchestrator is active and has been initialized. */
|
|
@@ -81,12 +224,26 @@ export function getWorkerStatuses(): WorkerInfo[] {
|
|
|
81
224
|
/**
|
|
82
225
|
* Analyze eligibility and prepare for parallel start.
|
|
83
226
|
* Returns the candidates report without actually starting workers.
|
|
227
|
+
* Also detects orphaned sessions from prior crashes.
|
|
84
228
|
*/
|
|
85
229
|
export async function prepareParallelStart(
|
|
86
230
|
basePath: string,
|
|
87
231
|
_prefs: GSDPreferences | undefined,
|
|
88
|
-
): Promise<ParallelCandidates> {
|
|
89
|
-
|
|
232
|
+
): Promise<ParallelCandidates & { orphans?: Array<{ milestoneId: string; pid: number; alive: boolean }> }> {
|
|
233
|
+
// Detect orphaned sessions before eligibility analysis
|
|
234
|
+
const sessions = readAllSessionStatuses(basePath);
|
|
235
|
+
const orphans: Array<{ milestoneId: string; pid: number; alive: boolean }> = [];
|
|
236
|
+
for (const session of sessions) {
|
|
237
|
+
const alive = isPidAlive(session.pid);
|
|
238
|
+
orphans.push({ milestoneId: session.milestoneId, pid: session.pid, alive });
|
|
239
|
+
if (!alive) {
|
|
240
|
+
// Clean up dead session
|
|
241
|
+
removeSessionStatus(basePath, session.milestoneId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const candidates = await analyzeParallelEligibility(basePath);
|
|
246
|
+
return orphans.length > 0 ? { ...candidates, orphans } : candidates;
|
|
90
247
|
}
|
|
91
248
|
|
|
92
249
|
// ─── Start ─────────────────────────────────────────────────────────────────
|
|
@@ -106,6 +263,36 @@ export async function startParallel(
|
|
|
106
263
|
}
|
|
107
264
|
|
|
108
265
|
const config = resolveParallelConfig(prefs);
|
|
266
|
+
|
|
267
|
+
// Try to restore from a previous crash
|
|
268
|
+
const restored = restoreState(basePath);
|
|
269
|
+
if (restored && restored.workers.length > 0) {
|
|
270
|
+
// Adopt surviving workers instead of starting new ones
|
|
271
|
+
state = {
|
|
272
|
+
active: true,
|
|
273
|
+
workers: new Map(),
|
|
274
|
+
config,
|
|
275
|
+
totalCost: restored.totalCost,
|
|
276
|
+
startedAt: restored.startedAt,
|
|
277
|
+
};
|
|
278
|
+
const adopted: string[] = [];
|
|
279
|
+
for (const w of restored.workers) {
|
|
280
|
+
state.workers.set(w.milestoneId, {
|
|
281
|
+
milestoneId: w.milestoneId,
|
|
282
|
+
title: w.title,
|
|
283
|
+
pid: w.pid,
|
|
284
|
+
process: null, // no handle for adopted workers
|
|
285
|
+
worktreePath: w.worktreePath,
|
|
286
|
+
startedAt: w.startedAt,
|
|
287
|
+
state: "running",
|
|
288
|
+
completedUnits: w.completedUnits,
|
|
289
|
+
cost: w.cost,
|
|
290
|
+
});
|
|
291
|
+
adopted.push(w.milestoneId);
|
|
292
|
+
}
|
|
293
|
+
return { started: adopted, errors: [] };
|
|
294
|
+
}
|
|
295
|
+
|
|
109
296
|
const now = Date.now();
|
|
110
297
|
|
|
111
298
|
// Initialize orchestrator state
|
|
@@ -144,7 +331,7 @@ export async function startParallel(
|
|
|
144
331
|
const worker: WorkerInfo = {
|
|
145
332
|
milestoneId: mid,
|
|
146
333
|
title: mid,
|
|
147
|
-
pid:
|
|
334
|
+
pid: 0, // placeholder — real PID set by spawnWorker()
|
|
148
335
|
process: null,
|
|
149
336
|
worktreePath: wtPath,
|
|
150
337
|
startedAt: now,
|
|
@@ -155,28 +342,24 @@ export async function startParallel(
|
|
|
155
342
|
|
|
156
343
|
state.workers.set(mid, worker);
|
|
157
344
|
|
|
158
|
-
//
|
|
159
|
-
const
|
|
345
|
+
// Spawn BEFORE writing session status so the file gets the real worker PID.
|
|
346
|
+
const spawned = spawnWorker(basePath, mid);
|
|
347
|
+
if (!spawned) {
|
|
348
|
+
worker.state = "error";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Write session status with real PID (or 0 if spawn failed)
|
|
352
|
+
writeSessionStatus(basePath, {
|
|
160
353
|
milestoneId: mid,
|
|
161
354
|
pid: worker.pid,
|
|
162
|
-
state:
|
|
355
|
+
state: worker.state,
|
|
163
356
|
currentUnit: null,
|
|
164
357
|
completedUnits: 0,
|
|
165
358
|
cost: 0,
|
|
166
359
|
lastHeartbeat: now,
|
|
167
360
|
startedAt: now,
|
|
168
361
|
worktreePath: wtPath,
|
|
169
|
-
};
|
|
170
|
-
writeSessionStatus(basePath, sessionStatus);
|
|
171
|
-
|
|
172
|
-
// Attempt to spawn the worker process.
|
|
173
|
-
// Spawning may fail if the CLI binary is not available (e.g., in tests).
|
|
174
|
-
// The worker is still tracked and can be spawned later via spawnWorker().
|
|
175
|
-
const spawned = spawnWorker(basePath, mid);
|
|
176
|
-
if (!spawned) {
|
|
177
|
-
// Worker tracked but not yet running a process.
|
|
178
|
-
// State stays "running" so coordinator can retry or user can investigate.
|
|
179
|
-
}
|
|
362
|
+
});
|
|
180
363
|
|
|
181
364
|
started.push(mid);
|
|
182
365
|
} catch (err) {
|
|
@@ -190,6 +373,9 @@ export async function startParallel(
|
|
|
190
373
|
state.active = false;
|
|
191
374
|
}
|
|
192
375
|
|
|
376
|
+
// Persist state for crash recovery
|
|
377
|
+
persistState(basePath);
|
|
378
|
+
|
|
193
379
|
return { started, errors };
|
|
194
380
|
}
|
|
195
381
|
|
|
@@ -323,7 +509,7 @@ export function spawnWorker(
|
|
|
323
509
|
w.state = "error";
|
|
324
510
|
}
|
|
325
511
|
|
|
326
|
-
// Update session status
|
|
512
|
+
// Update session status and persist orchestrator state for crash recovery
|
|
327
513
|
writeSessionStatus(basePath, {
|
|
328
514
|
milestoneId,
|
|
329
515
|
pid: w.pid,
|
|
@@ -335,6 +521,7 @@ export function spawnWorker(
|
|
|
335
521
|
startedAt: w.startedAt,
|
|
336
522
|
worktreePath: w.worktreePath,
|
|
337
523
|
});
|
|
524
|
+
persistState(basePath);
|
|
338
525
|
});
|
|
339
526
|
|
|
340
527
|
return true;
|
|
@@ -485,12 +672,24 @@ export async function stopParallel(
|
|
|
485
672
|
try {
|
|
486
673
|
if (worker.process) {
|
|
487
674
|
worker.process.kill("SIGTERM");
|
|
488
|
-
} else {
|
|
675
|
+
} else if (worker.pid !== process.pid) {
|
|
489
676
|
process.kill(worker.pid, "SIGTERM");
|
|
490
677
|
}
|
|
491
678
|
} catch { /* process may already be dead */ }
|
|
492
679
|
}
|
|
493
680
|
|
|
681
|
+
const exitedAfterTerm = await waitForWorkerExit(worker, 750);
|
|
682
|
+
if (!exitedAfterTerm && worker.pid > 0) {
|
|
683
|
+
try {
|
|
684
|
+
if (worker.process) {
|
|
685
|
+
worker.process.kill("SIGKILL");
|
|
686
|
+
} else if (worker.pid !== process.pid) {
|
|
687
|
+
process.kill(worker.pid, "SIGKILL");
|
|
688
|
+
}
|
|
689
|
+
} catch { /* process may already be dead */ }
|
|
690
|
+
await waitForWorkerExit(worker, 250);
|
|
691
|
+
}
|
|
692
|
+
|
|
494
693
|
// Update in-memory state
|
|
495
694
|
worker.state = "stopped";
|
|
496
695
|
worker.process = null;
|
|
@@ -503,6 +702,15 @@ export async function stopParallel(
|
|
|
503
702
|
if (!milestoneId) {
|
|
504
703
|
state.active = false;
|
|
505
704
|
}
|
|
705
|
+
|
|
706
|
+
// Persist final state and clean up state file
|
|
707
|
+
removeStateFile(basePath);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export async function shutdownParallel(basePath: string): Promise<void> {
|
|
711
|
+
if (!state) return;
|
|
712
|
+
await stopParallel(basePath);
|
|
713
|
+
resetOrchestrator();
|
|
506
714
|
}
|
|
507
715
|
|
|
508
716
|
// ─── Pause / Resume ────────────────────────────────────────────────────────
|
|
@@ -589,6 +797,9 @@ export function refreshWorkerStatuses(basePath: string): void {
|
|
|
589
797
|
for (const worker of state.workers.values()) {
|
|
590
798
|
state.totalCost += worker.cost;
|
|
591
799
|
}
|
|
800
|
+
|
|
801
|
+
// Persist updated state for crash recovery
|
|
802
|
+
persistState(basePath);
|
|
592
803
|
}
|
|
593
804
|
|
|
594
805
|
// ─── Budget ────────────────────────────────────────────────────────────────
|