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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Merge — Worktree reconciliation for parallel milestones.
|
|
3
|
+
*
|
|
4
|
+
* Handles merging completed milestone worktrees back to main branch
|
|
5
|
+
* with safety checks for parallel execution context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadFile } from "./files.js";
|
|
9
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
10
|
+
import { mergeMilestoneToMain } from "./auto-worktree.js";
|
|
11
|
+
import { MergeConflictError } from "./git-service.js";
|
|
12
|
+
import { removeSessionStatus } from "./session-status-io.js";
|
|
13
|
+
import type { WorkerInfo } from "./parallel-orchestrator.js";
|
|
14
|
+
|
|
15
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface MergeResult {
|
|
18
|
+
milestoneId: string;
|
|
19
|
+
success: boolean;
|
|
20
|
+
commitMessage?: string;
|
|
21
|
+
pushed?: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
conflictFiles?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type MergeOrder = "sequential" | "by-completion";
|
|
27
|
+
|
|
28
|
+
// ─── Merge Queue ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Determine safe merge order for completed milestones.
|
|
32
|
+
* Sequential: merge in milestone ID order (M001 before M002).
|
|
33
|
+
* By-completion: merge in the order milestones finished.
|
|
34
|
+
*/
|
|
35
|
+
export function determineMergeOrder(
|
|
36
|
+
workers: WorkerInfo[],
|
|
37
|
+
order: MergeOrder = "sequential",
|
|
38
|
+
): string[] {
|
|
39
|
+
const completed = workers.filter(w => w.state === "stopped" && w.completedUnits > 0);
|
|
40
|
+
if (order === "by-completion") {
|
|
41
|
+
return completed
|
|
42
|
+
.sort((a, b) => a.startedAt - b.startedAt) // earliest first
|
|
43
|
+
.map(w => w.milestoneId);
|
|
44
|
+
}
|
|
45
|
+
return completed
|
|
46
|
+
.sort((a, b) => a.milestoneId.localeCompare(b.milestoneId))
|
|
47
|
+
.map(w => w.milestoneId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attempt to merge a single milestone's worktree back to main.
|
|
52
|
+
* Wraps mergeMilestoneToMain with error handling for parallel context.
|
|
53
|
+
*/
|
|
54
|
+
export async function mergeCompletedMilestone(
|
|
55
|
+
basePath: string,
|
|
56
|
+
milestoneId: string,
|
|
57
|
+
): Promise<MergeResult> {
|
|
58
|
+
try {
|
|
59
|
+
// Load the roadmap content (needed by mergeMilestoneToMain)
|
|
60
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
61
|
+
if (!roadmapPath) {
|
|
62
|
+
return {
|
|
63
|
+
milestoneId,
|
|
64
|
+
success: false,
|
|
65
|
+
error: `No roadmap found for ${milestoneId}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const roadmapContent = await loadFile(roadmapPath);
|
|
70
|
+
if (!roadmapContent) {
|
|
71
|
+
return {
|
|
72
|
+
milestoneId,
|
|
73
|
+
success: false,
|
|
74
|
+
error: `Could not read roadmap for ${milestoneId}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Attempt the merge
|
|
79
|
+
const result = mergeMilestoneToMain(basePath, milestoneId, roadmapContent);
|
|
80
|
+
|
|
81
|
+
// Clean up parallel session status
|
|
82
|
+
removeSessionStatus(basePath, milestoneId);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
milestoneId,
|
|
86
|
+
success: true,
|
|
87
|
+
commitMessage: result.commitMessage,
|
|
88
|
+
pushed: result.pushed,
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof MergeConflictError) {
|
|
92
|
+
return {
|
|
93
|
+
milestoneId,
|
|
94
|
+
success: false,
|
|
95
|
+
error: `Merge conflict: ${err.conflictedFiles.length} conflicting file(s)`,
|
|
96
|
+
conflictFiles: err.conflictedFiles,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
milestoneId,
|
|
101
|
+
success: false,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merge all completed milestones in sequence.
|
|
109
|
+
* Stops on first conflict and returns results so far.
|
|
110
|
+
*/
|
|
111
|
+
export async function mergeAllCompleted(
|
|
112
|
+
basePath: string,
|
|
113
|
+
workers: WorkerInfo[],
|
|
114
|
+
order: MergeOrder = "sequential",
|
|
115
|
+
): Promise<MergeResult[]> {
|
|
116
|
+
const mergeOrder = determineMergeOrder(workers, order);
|
|
117
|
+
const results: MergeResult[] = [];
|
|
118
|
+
|
|
119
|
+
for (const mid of mergeOrder) {
|
|
120
|
+
const result = await mergeCompletedMilestone(basePath, mid);
|
|
121
|
+
results.push(result);
|
|
122
|
+
|
|
123
|
+
// Stop on first conflict — later merges may depend on this one
|
|
124
|
+
if (!result.success && result.conflictFiles) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format merge results for display.
|
|
134
|
+
*/
|
|
135
|
+
export function formatMergeResults(results: MergeResult[]): string {
|
|
136
|
+
if (results.length === 0) return "No completed milestones to merge.";
|
|
137
|
+
|
|
138
|
+
const lines: string[] = ["# Merge Results\n"];
|
|
139
|
+
|
|
140
|
+
for (const r of results) {
|
|
141
|
+
if (r.success) {
|
|
142
|
+
const pushStatus = r.pushed ? " (pushed)" : "";
|
|
143
|
+
lines.push(`- **${r.milestoneId}** — merged successfully${pushStatus}`);
|
|
144
|
+
} else if (r.conflictFiles) {
|
|
145
|
+
lines.push(`- **${r.milestoneId}** — CONFLICT (${r.conflictFiles.length} file(s)):`);
|
|
146
|
+
for (const f of r.conflictFiles) {
|
|
147
|
+
lines.push(` - \`${f}\``);
|
|
148
|
+
}
|
|
149
|
+
lines.push(` Resolve conflicts manually and run \`/gsd parallel merge ${r.milestoneId}\` to retry.`);
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(`- **${r.milestoneId}** — failed: ${r.error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Parallel Orchestrator — Core engine for parallel milestone orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Manages worker lifecycle, budget tracking, and coordination. Workers are
|
|
5
|
+
* separate processes spawned via child_process, each running in its own git
|
|
6
|
+
* worktree with GSD_MILESTONE_LOCK env var set. The coordinator monitors
|
|
7
|
+
* workers via session status files (see session-status-io.ts).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { gsdRoot } from "./paths.js";
|
|
15
|
+
import { createWorktree, worktreePath } from "./worktree-manager.js";
|
|
16
|
+
import { autoWorktreeBranch, runWorktreePostCreateHook } from "./auto-worktree.js";
|
|
17
|
+
import { nativeBranchExists } from "./native-git-bridge.js";
|
|
18
|
+
import { readIntegrationBranch } from "./git-service.js";
|
|
19
|
+
import { resolveParallelConfig } from "./preferences.js";
|
|
20
|
+
import type { GSDPreferences } from "./preferences.js";
|
|
21
|
+
import type { ParallelConfig } from "./types.js";
|
|
22
|
+
import {
|
|
23
|
+
writeSessionStatus,
|
|
24
|
+
readAllSessionStatuses,
|
|
25
|
+
removeSessionStatus,
|
|
26
|
+
sendSignal,
|
|
27
|
+
cleanupStaleSessions,
|
|
28
|
+
type SessionStatus,
|
|
29
|
+
} from "./session-status-io.js";
|
|
30
|
+
import {
|
|
31
|
+
analyzeParallelEligibility,
|
|
32
|
+
type ParallelCandidates,
|
|
33
|
+
} from "./parallel-eligibility.js";
|
|
34
|
+
|
|
35
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface WorkerInfo {
|
|
38
|
+
milestoneId: string;
|
|
39
|
+
title: string;
|
|
40
|
+
pid: number;
|
|
41
|
+
process: ChildProcess | null; // null after process exits
|
|
42
|
+
worktreePath: string;
|
|
43
|
+
startedAt: number;
|
|
44
|
+
state: "running" | "paused" | "stopped" | "error";
|
|
45
|
+
completedUnits: number;
|
|
46
|
+
cost: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface OrchestratorState {
|
|
50
|
+
active: boolean;
|
|
51
|
+
workers: Map<string, WorkerInfo>;
|
|
52
|
+
config: ParallelConfig;
|
|
53
|
+
totalCost: number;
|
|
54
|
+
startedAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Module State ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
let state: OrchestratorState | null = null;
|
|
60
|
+
|
|
61
|
+
// ─── Accessors ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** Returns true if the orchestrator is active and has been initialized. */
|
|
64
|
+
export function isParallelActive(): boolean {
|
|
65
|
+
return state?.active ?? false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns the current orchestrator state, or null if not initialized. */
|
|
69
|
+
export function getOrchestratorState(): OrchestratorState | null {
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Returns a snapshot of all tracked workers as an array. */
|
|
74
|
+
export function getWorkerStatuses(): WorkerInfo[] {
|
|
75
|
+
if (!state) return [];
|
|
76
|
+
return [...state.workers.values()];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Preparation ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Analyze eligibility and prepare for parallel start.
|
|
83
|
+
* Returns the candidates report without actually starting workers.
|
|
84
|
+
*/
|
|
85
|
+
export async function prepareParallelStart(
|
|
86
|
+
basePath: string,
|
|
87
|
+
_prefs: GSDPreferences | undefined,
|
|
88
|
+
): Promise<ParallelCandidates> {
|
|
89
|
+
return analyzeParallelEligibility(basePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Start ─────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start parallel execution with the given eligible milestones.
|
|
96
|
+
* Creates worktrees, spawns worker processes, and begins monitoring.
|
|
97
|
+
*/
|
|
98
|
+
export async function startParallel(
|
|
99
|
+
basePath: string,
|
|
100
|
+
milestoneIds: string[],
|
|
101
|
+
prefs: GSDPreferences | undefined,
|
|
102
|
+
): Promise<{ started: string[]; errors: Array<{ mid: string; error: string }> }> {
|
|
103
|
+
// Prevent workers from spawning nested parallel sessions
|
|
104
|
+
if (process.env.GSD_PARALLEL_WORKER) {
|
|
105
|
+
return { started: [], errors: [{ mid: "all", error: "Cannot start parallel from within a parallel worker" }] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const config = resolveParallelConfig(prefs);
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
|
|
111
|
+
// Initialize orchestrator state
|
|
112
|
+
state = {
|
|
113
|
+
active: true,
|
|
114
|
+
workers: new Map(),
|
|
115
|
+
config,
|
|
116
|
+
totalCost: 0,
|
|
117
|
+
startedAt: now,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const started: string[] = [];
|
|
121
|
+
const errors: Array<{ mid: string; error: string }> = [];
|
|
122
|
+
|
|
123
|
+
// Cap to max_workers
|
|
124
|
+
const toStart = milestoneIds.slice(0, config.max_workers);
|
|
125
|
+
|
|
126
|
+
for (const mid of toStart) {
|
|
127
|
+
try {
|
|
128
|
+
// Create the worktree (without chdir — coordinator stays in project root)
|
|
129
|
+
let wtPath: string;
|
|
130
|
+
try {
|
|
131
|
+
wtPath = createMilestoneWorktree(basePath, mid);
|
|
132
|
+
} catch {
|
|
133
|
+
// Worktree creation may fail in test environments or when git
|
|
134
|
+
// is not available. Fall back to a placeholder path.
|
|
135
|
+
wtPath = worktreePath(basePath, mid);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const worker: WorkerInfo = {
|
|
139
|
+
milestoneId: mid,
|
|
140
|
+
title: mid,
|
|
141
|
+
pid: process.pid,
|
|
142
|
+
process: null,
|
|
143
|
+
worktreePath: wtPath,
|
|
144
|
+
startedAt: now,
|
|
145
|
+
state: "running",
|
|
146
|
+
completedUnits: 0,
|
|
147
|
+
cost: 0,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
state.workers.set(mid, worker);
|
|
151
|
+
|
|
152
|
+
// Write initial session status
|
|
153
|
+
const sessionStatus: SessionStatus = {
|
|
154
|
+
milestoneId: mid,
|
|
155
|
+
pid: worker.pid,
|
|
156
|
+
state: "running",
|
|
157
|
+
currentUnit: null,
|
|
158
|
+
completedUnits: 0,
|
|
159
|
+
cost: 0,
|
|
160
|
+
lastHeartbeat: now,
|
|
161
|
+
startedAt: now,
|
|
162
|
+
worktreePath: wtPath,
|
|
163
|
+
};
|
|
164
|
+
writeSessionStatus(basePath, sessionStatus);
|
|
165
|
+
|
|
166
|
+
// Attempt to spawn the worker process.
|
|
167
|
+
// Spawning may fail if the CLI binary is not available (e.g., in tests).
|
|
168
|
+
// The worker is still tracked and can be spawned later via spawnWorker().
|
|
169
|
+
const spawned = spawnWorker(basePath, mid);
|
|
170
|
+
if (!spawned) {
|
|
171
|
+
// Worker tracked but not yet running a process.
|
|
172
|
+
// State stays "running" so coordinator can retry or user can investigate.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
started.push(mid);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
errors.push({ mid, error: message });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If nothing started successfully, deactivate
|
|
183
|
+
if (started.length === 0) {
|
|
184
|
+
state.active = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { started, errors };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Worktree Creation ────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a git worktree for a milestone without changing the coordinator's cwd.
|
|
194
|
+
* Uses milestone/<MID> branch naming (same as auto-worktree.ts).
|
|
195
|
+
*/
|
|
196
|
+
function createMilestoneWorktree(basePath: string, milestoneId: string): string {
|
|
197
|
+
const branch = autoWorktreeBranch(milestoneId);
|
|
198
|
+
const branchExists = nativeBranchExists(basePath, branch);
|
|
199
|
+
|
|
200
|
+
let info: { name: string; path: string; branch: string; exists: boolean };
|
|
201
|
+
if (branchExists) {
|
|
202
|
+
info = createWorktree(basePath, milestoneId, { branch, reuseExistingBranch: true });
|
|
203
|
+
} else {
|
|
204
|
+
const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
|
|
205
|
+
info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Run post-create hook if configured
|
|
209
|
+
runWorktreePostCreateHook(basePath, info.path);
|
|
210
|
+
|
|
211
|
+
return info.path;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Worker Spawning ───────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Spawn a worker process for a milestone.
|
|
218
|
+
* The worker runs `gsd --print "/gsd auto"` in the milestone's worktree
|
|
219
|
+
* with GSD_MILESTONE_LOCK set to isolate state derivation.
|
|
220
|
+
*/
|
|
221
|
+
export function spawnWorker(
|
|
222
|
+
basePath: string,
|
|
223
|
+
milestoneId: string,
|
|
224
|
+
): boolean {
|
|
225
|
+
if (!state) return false;
|
|
226
|
+
const worker = state.workers.get(milestoneId);
|
|
227
|
+
if (!worker) return false;
|
|
228
|
+
if (worker.process) return true; // already spawned
|
|
229
|
+
|
|
230
|
+
// Resolve the GSD CLI binary path
|
|
231
|
+
const binPath = resolveGsdBin();
|
|
232
|
+
if (!binPath) return false;
|
|
233
|
+
|
|
234
|
+
let child: ChildProcess;
|
|
235
|
+
try {
|
|
236
|
+
child = spawn(process.execPath, [binPath, "--print", "/gsd auto"], {
|
|
237
|
+
cwd: worker.worktreePath,
|
|
238
|
+
env: {
|
|
239
|
+
...process.env,
|
|
240
|
+
GSD_MILESTONE_LOCK: milestoneId,
|
|
241
|
+
// Prevent workers from spawning their own parallel sessions
|
|
242
|
+
GSD_PARALLEL_WORKER: "1",
|
|
243
|
+
},
|
|
244
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
245
|
+
detached: false,
|
|
246
|
+
});
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle spawn errors (e.g., ENOENT when binary doesn't exist)
|
|
252
|
+
child.on("error", () => {
|
|
253
|
+
if (!state) return;
|
|
254
|
+
const w = state.workers.get(milestoneId);
|
|
255
|
+
if (w) {
|
|
256
|
+
w.process = null;
|
|
257
|
+
// Don't change state — spawn failure is non-fatal, coordinator can retry
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
worker.process = child;
|
|
262
|
+
worker.pid = child.pid ?? 0;
|
|
263
|
+
|
|
264
|
+
if (!child.pid) {
|
|
265
|
+
// Spawn returned but no PID — process failed to start
|
|
266
|
+
worker.process = null;
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Update session status with real PID
|
|
271
|
+
writeSessionStatus(basePath, {
|
|
272
|
+
milestoneId,
|
|
273
|
+
pid: worker.pid,
|
|
274
|
+
state: "running",
|
|
275
|
+
currentUnit: null,
|
|
276
|
+
completedUnits: worker.completedUnits,
|
|
277
|
+
cost: worker.cost,
|
|
278
|
+
lastHeartbeat: Date.now(),
|
|
279
|
+
startedAt: worker.startedAt,
|
|
280
|
+
worktreePath: worker.worktreePath,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Handle worker exit
|
|
284
|
+
child.on("exit", (code) => {
|
|
285
|
+
if (!state) return;
|
|
286
|
+
const w = state.workers.get(milestoneId);
|
|
287
|
+
if (!w) return;
|
|
288
|
+
|
|
289
|
+
w.process = null;
|
|
290
|
+
if (w.state === "stopped") return; // graceful stop, already handled
|
|
291
|
+
|
|
292
|
+
if (code === 0) {
|
|
293
|
+
w.state = "stopped";
|
|
294
|
+
} else {
|
|
295
|
+
w.state = "error";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update session status
|
|
299
|
+
writeSessionStatus(basePath, {
|
|
300
|
+
milestoneId,
|
|
301
|
+
pid: w.pid,
|
|
302
|
+
state: w.state,
|
|
303
|
+
currentUnit: null,
|
|
304
|
+
completedUnits: w.completedUnits,
|
|
305
|
+
cost: w.cost,
|
|
306
|
+
lastHeartbeat: Date.now(),
|
|
307
|
+
startedAt: w.startedAt,
|
|
308
|
+
worktreePath: w.worktreePath,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the GSD CLI binary path.
|
|
317
|
+
* Uses GSD_BIN_PATH env var (set by loader.ts) or falls back to
|
|
318
|
+
* finding the binary relative to the current module.
|
|
319
|
+
*/
|
|
320
|
+
function resolveGsdBin(): string | null {
|
|
321
|
+
// GSD_BIN_PATH is set by loader.ts to the absolute path of dist/loader.js
|
|
322
|
+
if (process.env.GSD_BIN_PATH && existsSync(process.env.GSD_BIN_PATH)) {
|
|
323
|
+
return process.env.GSD_BIN_PATH;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Fallback: try to find loader.js relative to this file
|
|
327
|
+
// This file is at dist/resources/extensions/gsd/parallel-orchestrator.js
|
|
328
|
+
// loader.js is at dist/loader.js
|
|
329
|
+
let thisDir: string;
|
|
330
|
+
try {
|
|
331
|
+
thisDir = dirname(fileURLToPath(import.meta.url));
|
|
332
|
+
} catch {
|
|
333
|
+
thisDir = process.cwd();
|
|
334
|
+
}
|
|
335
|
+
const candidates = [
|
|
336
|
+
join(thisDir, "..", "..", "..", "loader.js"),
|
|
337
|
+
join(thisDir, "..", "..", "..", "..", "dist", "loader.js"),
|
|
338
|
+
];
|
|
339
|
+
for (const candidate of candidates) {
|
|
340
|
+
if (existsSync(candidate)) return candidate;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Stop ──────────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Stop all workers or a specific milestone's worker.
|
|
350
|
+
* Sends stop signals and updates tracking state.
|
|
351
|
+
*/
|
|
352
|
+
export async function stopParallel(
|
|
353
|
+
basePath: string,
|
|
354
|
+
milestoneId?: string,
|
|
355
|
+
): Promise<void> {
|
|
356
|
+
if (!state) return;
|
|
357
|
+
|
|
358
|
+
const targets = milestoneId
|
|
359
|
+
? [milestoneId]
|
|
360
|
+
: [...state.workers.keys()];
|
|
361
|
+
|
|
362
|
+
for (const mid of targets) {
|
|
363
|
+
const worker = state.workers.get(mid);
|
|
364
|
+
if (!worker) continue;
|
|
365
|
+
|
|
366
|
+
// Send stop signal via file-based IPC (worker checks on next dispatch)
|
|
367
|
+
sendSignal(basePath, mid, "stop");
|
|
368
|
+
|
|
369
|
+
// Also send SIGTERM to the process for immediate response
|
|
370
|
+
if (worker.process && worker.pid > 0) {
|
|
371
|
+
try {
|
|
372
|
+
worker.process.kill("SIGTERM");
|
|
373
|
+
} catch { /* process may already be dead */ }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update in-memory state
|
|
377
|
+
worker.state = "stopped";
|
|
378
|
+
worker.process = null;
|
|
379
|
+
|
|
380
|
+
// Clean up session status file
|
|
381
|
+
removeSessionStatus(basePath, mid);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// If stopping all workers, deactivate the orchestrator
|
|
385
|
+
if (!milestoneId) {
|
|
386
|
+
state.active = false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Pause / Resume ────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/** Pause a specific worker or all workers. */
|
|
393
|
+
export function pauseWorker(
|
|
394
|
+
basePath: string,
|
|
395
|
+
milestoneId?: string,
|
|
396
|
+
): void {
|
|
397
|
+
if (!state) return;
|
|
398
|
+
|
|
399
|
+
const targets = milestoneId
|
|
400
|
+
? [milestoneId]
|
|
401
|
+
: [...state.workers.keys()];
|
|
402
|
+
|
|
403
|
+
for (const mid of targets) {
|
|
404
|
+
const worker = state.workers.get(mid);
|
|
405
|
+
if (!worker || worker.state !== "running") continue;
|
|
406
|
+
|
|
407
|
+
sendSignal(basePath, mid, "pause");
|
|
408
|
+
worker.state = "paused";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Resume a specific worker or all workers. */
|
|
413
|
+
export function resumeWorker(
|
|
414
|
+
basePath: string,
|
|
415
|
+
milestoneId?: string,
|
|
416
|
+
): void {
|
|
417
|
+
if (!state) return;
|
|
418
|
+
|
|
419
|
+
const targets = milestoneId
|
|
420
|
+
? [milestoneId]
|
|
421
|
+
: [...state.workers.keys()];
|
|
422
|
+
|
|
423
|
+
for (const mid of targets) {
|
|
424
|
+
const worker = state.workers.get(mid);
|
|
425
|
+
if (!worker || worker.state !== "paused") continue;
|
|
426
|
+
|
|
427
|
+
sendSignal(basePath, mid, "resume");
|
|
428
|
+
worker.state = "running";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Status Refresh ────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Poll worker statuses from disk and update orchestrator state.
|
|
436
|
+
* Call this periodically from the dashboard refresh cycle.
|
|
437
|
+
*/
|
|
438
|
+
export function refreshWorkerStatuses(basePath: string): void {
|
|
439
|
+
if (!state) return;
|
|
440
|
+
|
|
441
|
+
// Clean up stale sessions first
|
|
442
|
+
const staleIds = cleanupStaleSessions(basePath);
|
|
443
|
+
for (const mid of staleIds) {
|
|
444
|
+
const worker = state.workers.get(mid);
|
|
445
|
+
if (worker) {
|
|
446
|
+
worker.state = "error";
|
|
447
|
+
worker.process = null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Read all live session statuses from disk
|
|
452
|
+
const statuses = readAllSessionStatuses(basePath);
|
|
453
|
+
const statusMap = new Map<string, SessionStatus>();
|
|
454
|
+
for (const s of statuses) {
|
|
455
|
+
statusMap.set(s.milestoneId, s);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Update in-memory worker state from disk data
|
|
459
|
+
for (const [mid, worker] of state.workers) {
|
|
460
|
+
const diskStatus = statusMap.get(mid);
|
|
461
|
+
if (!diskStatus) continue;
|
|
462
|
+
|
|
463
|
+
worker.state = diskStatus.state;
|
|
464
|
+
worker.completedUnits = diskStatus.completedUnits;
|
|
465
|
+
worker.cost = diskStatus.cost;
|
|
466
|
+
worker.pid = diskStatus.pid;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Recalculate aggregate cost
|
|
470
|
+
state.totalCost = 0;
|
|
471
|
+
for (const worker of state.workers.values()) {
|
|
472
|
+
state.totalCost += worker.cost;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ─── Budget ────────────────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
/** Get aggregate cost across all workers. */
|
|
479
|
+
export function getAggregateCost(): number {
|
|
480
|
+
if (!state) return 0;
|
|
481
|
+
return state.totalCost;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Check if budget ceiling has been reached. */
|
|
485
|
+
export function isBudgetExceeded(): boolean {
|
|
486
|
+
if (!state) return false;
|
|
487
|
+
if (state.config.budget_ceiling == null) return false;
|
|
488
|
+
return state.totalCost >= state.config.budget_ceiling;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─── Reset ─────────────────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
/** Reset orchestrator state. Called on clean shutdown. */
|
|
494
|
+
export function resetOrchestrator(): void {
|
|
495
|
+
state = null;
|
|
496
|
+
}
|