@prometheus-ai/swarm-extension 0.5.1

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Swarm agent execution via prometheus's subagent infrastructure.
3
+ *
4
+ * Wraps `runSubprocess` to spawn individual swarm agents with full tool access.
5
+ * Each agent runs in the swarm workspace with its task instructions as the user prompt.
6
+ */
7
+ import * as path from "node:path";
8
+ import type {
9
+ AgentDefinition,
10
+ AgentProgress,
11
+ AgentSource,
12
+ ModelRegistry,
13
+ Settings,
14
+ SingleResult,
15
+ } from "@prometheus-ai/agent";
16
+ import { runSubprocess } from "@prometheus-ai/agent";
17
+ import type { SwarmAgent } from "./schema";
18
+ import type { StateTracker } from "./state";
19
+
20
+ export interface SwarmExecutorOptions {
21
+ workspace: string;
22
+ swarmName: string;
23
+ iteration: number;
24
+ modelOverride?: string;
25
+ signal?: AbortSignal;
26
+ onProgress?: (agentName: string, progress: AgentProgress) => void;
27
+ modelRegistry?: ModelRegistry;
28
+ settings?: Settings;
29
+ stateTracker: StateTracker;
30
+ }
31
+
32
+ /**
33
+ * Execute a single swarm agent as an prometheus subagent.
34
+ *
35
+ * The agent receives:
36
+ * - System prompt: built from role + extra_context
37
+ * - User prompt (task): the full task instructions from the YAML
38
+ * - Working directory: the swarm workspace
39
+ * - Full tool access (bash, python, read, write, edit, grep, find, fetch, web_search, browser)
40
+ */
41
+ export async function executeSwarmAgent(
42
+ agent: SwarmAgent,
43
+ index: number,
44
+ options: SwarmExecutorOptions,
45
+ ): Promise<SingleResult> {
46
+ const { workspace, swarmName, iteration, modelOverride, signal, onProgress, modelRegistry, settings, stateTracker } =
47
+ options;
48
+
49
+ const agentId = `swarm-${swarmName}-${agent.name}-${iteration}`;
50
+
51
+ const agentDef: AgentDefinition = {
52
+ name: agent.name,
53
+ description: `Swarm agent: ${agent.role}`,
54
+ systemPrompt: buildSystemPrompt(agent),
55
+ source: "project" as AgentSource,
56
+ };
57
+
58
+ await stateTracker.updateAgent(agent.name, {
59
+ status: "running",
60
+ iteration,
61
+ startedAt: Date.now(),
62
+ });
63
+ await stateTracker.appendLog(agent.name, `Starting iteration ${iteration}`);
64
+
65
+ try {
66
+ const result = await runSubprocess({
67
+ cwd: workspace,
68
+ agent: agentDef,
69
+ task: agent.task,
70
+ index,
71
+ id: agentId,
72
+ modelOverride,
73
+ signal,
74
+ onProgress: progress => onProgress?.(agent.name, progress),
75
+ modelRegistry,
76
+ settings,
77
+ enableLsp: false,
78
+ artifactsDir: path.join(stateTracker.swarmDir, "context"),
79
+ });
80
+
81
+ const status = result.exitCode === 0 ? ("completed" as const) : ("failed" as const);
82
+ await stateTracker.updateAgent(agent.name, {
83
+ status,
84
+ completedAt: Date.now(),
85
+ error: result.error,
86
+ });
87
+ await stateTracker.appendLog(
88
+ agent.name,
89
+ `Iteration ${iteration} ${status}${result.error ? `: ${result.error}` : ""}`,
90
+ );
91
+
92
+ return result;
93
+ } catch (err) {
94
+ const error = err instanceof Error ? err.message : String(err);
95
+ await stateTracker.updateAgent(agent.name, {
96
+ status: "failed",
97
+ completedAt: Date.now(),
98
+ error,
99
+ });
100
+ await stateTracker.appendLog(agent.name, `Iteration ${iteration} error: ${error}`);
101
+ throw err;
102
+ }
103
+ }
104
+
105
+ function buildSystemPrompt(agent: SwarmAgent): string {
106
+ const parts = [`You are a ${agent.role}.`];
107
+ if (agent.extraContext) {
108
+ parts.push(agent.extraContext);
109
+ }
110
+ return parts.join("\n\n");
111
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Pipeline controller for swarm execution.
3
+ *
4
+ * Orchestrates execution waves within each iteration:
5
+ * - Agents in the same wave execute in parallel
6
+ * - Waves execute sequentially (wave N+1 starts after wave N completes)
7
+ * - For pipeline mode, iterations repeat the full DAG execution
8
+ */
9
+ import type { AgentSource, ModelRegistry, Settings, SingleResult } from "@prometheus-ai/agent";
10
+ import { executeSwarmAgent } from "./executor";
11
+ import type { SwarmDefinition } from "./schema";
12
+ import type { StateTracker } from "./state";
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ export interface PipelineOptions {
19
+ workspace: string;
20
+ signal?: AbortSignal;
21
+ onProgress?: (state: PipelineProgress) => void;
22
+ modelRegistry?: ModelRegistry;
23
+ settings?: Settings;
24
+ }
25
+
26
+ export interface PipelineProgress {
27
+ iteration: number;
28
+ targetCount: number;
29
+ currentWave: number;
30
+ totalWaves: number;
31
+ agents: Record<string, { status: string; iteration: number }>;
32
+ }
33
+
34
+ export interface PipelineResult {
35
+ status: "completed" | "failed" | "aborted";
36
+ iterations: number;
37
+ agentResults: Map<string, SingleResult[]>;
38
+ errors: string[];
39
+ }
40
+
41
+ // ============================================================================
42
+ // Controller
43
+ // ============================================================================
44
+
45
+ export class PipelineController {
46
+ #def: SwarmDefinition;
47
+ #waves: string[][];
48
+ #stateTracker: StateTracker;
49
+
50
+ constructor(def: SwarmDefinition, waves: string[][], stateTracker: StateTracker) {
51
+ this.#def = def;
52
+ this.#waves = waves;
53
+ this.#stateTracker = stateTracker;
54
+ }
55
+
56
+ async run(options: PipelineOptions): Promise<PipelineResult> {
57
+ const { workspace, signal, onProgress, modelRegistry, settings } = options;
58
+ const allResults = new Map<string, SingleResult[]>();
59
+ const errors: string[] = [];
60
+
61
+ for (const name of this.#def.agents.keys()) {
62
+ allResults.set(name, []);
63
+ }
64
+
65
+ const targetCount = this.#def.targetCount;
66
+
67
+ await this.#stateTracker.appendOrchestratorLog(
68
+ `Pipeline '${this.#def.name}' starting: mode=${this.#def.mode} iterations=${targetCount} waves=${this.#waves.length} agents=${this.#def.agents.size}`,
69
+ );
70
+
71
+ try {
72
+ for (let iteration = 0; iteration < targetCount; iteration++) {
73
+ if (signal?.aborted) {
74
+ await this.#stateTracker.updatePipeline({ status: "aborted" });
75
+ return { status: "aborted", iterations: iteration, agentResults: allResults, errors };
76
+ }
77
+
78
+ await this.#stateTracker.updatePipeline({ iteration });
79
+ await this.#stateTracker.appendOrchestratorLog(`--- Iteration ${iteration + 1}/${targetCount} ---`);
80
+
81
+ const emitProgress = (currentWave: number) => {
82
+ onProgress?.({
83
+ iteration,
84
+ targetCount,
85
+ currentWave,
86
+ totalWaves: this.#waves.length,
87
+ agents: this.#buildProgressSnapshot(),
88
+ });
89
+ };
90
+
91
+ const iterationResults = await this.#runIteration(iteration, {
92
+ workspace,
93
+ signal,
94
+ emitProgress,
95
+ modelRegistry,
96
+ settings,
97
+ });
98
+
99
+ for (const [agentName, result] of iterationResults) {
100
+ allResults.get(agentName)!.push(result);
101
+ if (result.exitCode !== 0) {
102
+ errors.push(
103
+ `${agentName} (iteration ${iteration + 1}): ${result.error || `exit code ${result.exitCode}`}`,
104
+ );
105
+ }
106
+ }
107
+ }
108
+
109
+ const status = errors.length > 0 ? ("failed" as const) : ("completed" as const);
110
+ await this.#stateTracker.updatePipeline({ status, completedAt: Date.now() });
111
+ await this.#stateTracker.appendOrchestratorLog(`Pipeline ${status} (${errors.length} errors)`);
112
+ return { status, iterations: targetCount, agentResults: allResults, errors };
113
+ } catch (err) {
114
+ const error = err instanceof Error ? err.message : String(err);
115
+ await this.#stateTracker.updatePipeline({ status: "failed", completedAt: Date.now() });
116
+ await this.#stateTracker.appendOrchestratorLog(`Pipeline fatal error: ${error}`);
117
+ errors.push(error);
118
+ return { status: "failed", iterations: 0, agentResults: allResults, errors };
119
+ }
120
+ }
121
+
122
+ async #runIteration(
123
+ iteration: number,
124
+ options: {
125
+ workspace: string;
126
+ signal?: AbortSignal;
127
+ emitProgress: (currentWave: number) => void;
128
+ modelRegistry?: ModelRegistry;
129
+ settings?: Settings;
130
+ },
131
+ ): Promise<Map<string, SingleResult>> {
132
+ const results = new Map<string, SingleResult>();
133
+ let agentIndex = 0;
134
+
135
+ for (let waveIdx = 0; waveIdx < this.#waves.length; waveIdx++) {
136
+ const wave = this.#waves[waveIdx];
137
+
138
+ if (options.signal?.aborted) break;
139
+
140
+ await this.#stateTracker.appendOrchestratorLog(
141
+ `Wave ${waveIdx + 1}/${this.#waves.length}: [${wave.join(", ")}]`,
142
+ );
143
+
144
+ // Mark agents in this wave as waiting
145
+ for (const agentName of wave) {
146
+ await this.#stateTracker.updateAgent(agentName, {
147
+ status: "waiting",
148
+ iteration,
149
+ wave: waveIdx,
150
+ });
151
+ }
152
+ options.emitProgress(waveIdx);
153
+
154
+ // Execute all agents in wave in parallel, catching per-agent errors
155
+ const waveResults = await Promise.all(
156
+ wave.map(async agentName => {
157
+ const agent = this.#def.agents.get(agentName)!;
158
+ const currentIndex = agentIndex++;
159
+ try {
160
+ const result = await executeSwarmAgent(agent, currentIndex, {
161
+ workspace: options.workspace,
162
+ swarmName: this.#def.name,
163
+ iteration,
164
+ modelOverride: agent.model ?? this.#def.model,
165
+ signal: options.signal,
166
+ onProgress: (_name, _progress) => {
167
+ options.emitProgress(waveIdx);
168
+ },
169
+ modelRegistry: options.modelRegistry,
170
+ settings: options.settings,
171
+ stateTracker: this.#stateTracker,
172
+ });
173
+ return { agentName, result };
174
+ } catch (err) {
175
+ const error = err instanceof Error ? err.message : String(err);
176
+ const failResult: SingleResult = {
177
+ index: currentIndex,
178
+ id: `swarm-${this.#def.name}-${agentName}-${iteration}`,
179
+ agent: agentName,
180
+ agentSource: "project" as AgentSource,
181
+ task: agent.task,
182
+ exitCode: 1,
183
+ output: "",
184
+ stderr: error,
185
+ truncated: false,
186
+ durationMs: 0,
187
+ tokens: 0,
188
+ error,
189
+ };
190
+ return { agentName, result: failResult };
191
+ }
192
+ }),
193
+ );
194
+
195
+ for (const { agentName, result } of waveResults) {
196
+ results.set(agentName, result);
197
+ }
198
+
199
+ options.emitProgress(waveIdx);
200
+ }
201
+
202
+ return results;
203
+ }
204
+
205
+ #buildProgressSnapshot(): Record<string, { status: string; iteration: number }> {
206
+ const snapshot: Record<string, { status: string; iteration: number }> = {};
207
+ for (const [name, agent] of Object.entries(this.#stateTracker.state.agents)) {
208
+ snapshot[name] = { status: agent.status, iteration: agent.iteration };
209
+ }
210
+ return snapshot;
211
+ }
212
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * TUI progress rendering for swarm pipeline status.
3
+ */
4
+ import { formatDuration, truncate } from "@prometheus-ai/utils";
5
+ import type { AgentState, SwarmState } from "./state";
6
+
7
+ const STATUS_LABELS: Record<string, string> = {
8
+ completed: "[done]",
9
+ running: "[....]",
10
+ failed: "[FAIL]",
11
+ pending: "[ ]",
12
+ waiting: "[wait]",
13
+ idle: "[idle]",
14
+ aborted: "[stop]",
15
+ };
16
+
17
+ export function renderSwarmProgress(state: SwarmState): string[] {
18
+ const lines: string[] = [];
19
+
20
+ const statusLabel = state.status.toUpperCase();
21
+ lines.push(`Swarm: ${state.name} [${statusLabel}]`);
22
+ lines.push(`Mode: ${state.mode} | Iteration: ${state.iteration + 1}/${state.targetCount}`);
23
+ lines.push("");
24
+
25
+ const agents: AgentState[] = Object.values(state.agents);
26
+ if (agents.length === 0) {
27
+ lines.push(" (no agents)");
28
+ return lines;
29
+ }
30
+
31
+ for (const agent of agents) {
32
+ const icon = STATUS_LABELS[agent.status] ?? "[????]";
33
+ const duration = formatAgentDuration(agent);
34
+ const errorSuffix = agent.error ? ` - ${truncate(agent.error, 60)}` : "";
35
+ lines.push(` ${icon} ${agent.name}: ${agent.status}${duration}${errorSuffix}`);
36
+ }
37
+
38
+ // Summary line
39
+ const completed = agents.filter(a => a.status === "completed").length;
40
+ const failed = agents.filter(a => a.status === "failed").length;
41
+ const running = agents.filter(a => a.status === "running").length;
42
+
43
+ lines.push("");
44
+ const parts = [`${completed}/${agents.length} done`];
45
+ if (running > 0) parts.push(`${running} running`);
46
+ if (failed > 0) parts.push(`${failed} failed`);
47
+ if (state.startedAt) {
48
+ parts.push(`elapsed: ${formatDuration(Date.now() - state.startedAt)}`);
49
+ }
50
+ lines.push(` ${parts.join(" | ")}`);
51
+
52
+ return lines;
53
+ }
54
+
55
+ function formatAgentDuration(agent: { startedAt?: number; completedAt?: number; status: string }): string {
56
+ if (agent.startedAt && agent.completedAt) {
57
+ return ` (${formatDuration(agent.completedAt - agent.startedAt)})`;
58
+ }
59
+ if (agent.startedAt && (agent.status === "running" || agent.status === "waiting")) {
60
+ return ` (${formatDuration(Date.now() - agent.startedAt)}...)`;
61
+ }
62
+ return "";
63
+ }
@@ -0,0 +1,157 @@
1
+ // ============================================================================
2
+ // Raw YAML shape (snake_case, optional fields)
3
+ // ============================================================================
4
+
5
+ interface RawSwarmAgentConfig {
6
+ role: string;
7
+ task: string;
8
+ extra_context?: string;
9
+ reports_to?: string[];
10
+ waits_for?: string[];
11
+ model?: string;
12
+ }
13
+
14
+ interface RawSwarmConfig {
15
+ name: string;
16
+ workspace: string;
17
+ mode?: string;
18
+ target_count?: number;
19
+ model?: string;
20
+ agents: Record<string, RawSwarmAgentConfig>;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Normalized types (camelCase, defaults applied)
25
+ // ============================================================================
26
+
27
+ export type SwarmMode = "pipeline" | "parallel" | "sequential";
28
+
29
+ export interface SwarmAgent {
30
+ name: string;
31
+ role: string;
32
+ task: string;
33
+ extraContext?: string;
34
+ reportsTo: string[];
35
+ waitsFor: string[];
36
+ model?: string;
37
+ }
38
+
39
+ export interface SwarmDefinition {
40
+ name: string;
41
+ workspace: string;
42
+ mode: SwarmMode;
43
+ targetCount: number;
44
+ model?: string;
45
+ agents: Map<string, SwarmAgent>;
46
+ /** Preserves YAML declaration order for implicit pipeline sequencing. */
47
+ agentOrder: string[];
48
+ }
49
+
50
+ // ============================================================================
51
+ // Parsing
52
+ // ============================================================================
53
+
54
+ const VALID_MODES = new Set<string>(["pipeline", "parallel", "sequential"]);
55
+ const VALID_SWARM_NAME = /^[a-zA-Z0-9._-]+$/;
56
+
57
+ export function parseSwarmYaml(content: string): SwarmDefinition {
58
+ const raw = Bun.YAML.parse(content) as { swarm?: RawSwarmConfig } | null;
59
+ if (!raw?.swarm) {
60
+ throw new Error("YAML must have a top-level 'swarm' key");
61
+ }
62
+ const swarm = raw.swarm;
63
+
64
+ if (!swarm.name || typeof swarm.name !== "string") {
65
+ throw new Error("swarm.name is required and must be a string");
66
+ }
67
+ if (!VALID_SWARM_NAME.test(swarm.name)) {
68
+ throw new Error("swarm.name may only contain letters, numbers, dot, underscore, and dash");
69
+ }
70
+ if (!swarm.workspace || typeof swarm.workspace !== "string") {
71
+ throw new Error("swarm.workspace is required and must be a string");
72
+ }
73
+ if (!swarm.agents || typeof swarm.agents !== "object" || Object.keys(swarm.agents).length === 0) {
74
+ throw new Error("swarm.agents must contain at least one agent");
75
+ }
76
+
77
+ const mode = swarm.mode ?? "sequential";
78
+ if (!VALID_MODES.has(mode)) {
79
+ throw new Error(`Invalid mode '${mode}'. Must be one of: ${[...VALID_MODES].join(", ")}`);
80
+ }
81
+
82
+ const agentOrder: string[] = [];
83
+ const agents = new Map<string, SwarmAgent>();
84
+
85
+ for (const [name, config] of Object.entries(swarm.agents)) {
86
+ if (!config.role || typeof config.role !== "string") {
87
+ throw new Error(`Agent '${name}': 'role' is required`);
88
+ }
89
+ if (!config.task || typeof config.task !== "string") {
90
+ throw new Error(`Agent '${name}': 'task' is required`);
91
+ }
92
+
93
+ agentOrder.push(name);
94
+ agents.set(name, {
95
+ name,
96
+ role: config.role,
97
+ task: config.task.trim(),
98
+ extraContext: config.extra_context?.trim(),
99
+ reportsTo: Array.isArray(config.reports_to) ? config.reports_to : [],
100
+ model: typeof config.model === "string" ? config.model.trim() : undefined,
101
+ waitsFor: Array.isArray(config.waits_for) ? config.waits_for : [],
102
+ });
103
+ }
104
+
105
+ return {
106
+ name: swarm.name,
107
+ workspace: swarm.workspace,
108
+ mode: mode as SwarmMode,
109
+ targetCount: swarm.target_count ?? 1,
110
+ model: typeof swarm.model === "string" ? swarm.model.trim() : undefined,
111
+ agents,
112
+ agentOrder,
113
+ };
114
+ }
115
+
116
+ // ============================================================================
117
+ // Validation (semantic — references, constraints)
118
+ // ============================================================================
119
+
120
+ export function validateSwarmDefinition(def: SwarmDefinition): string[] {
121
+ const errors: string[] = [];
122
+ const agentNames = new Set(def.agents.keys());
123
+
124
+ if (def.model !== undefined && def.model.length === 0) {
125
+ errors.push("swarm.model must not be empty when provided");
126
+ }
127
+ for (const [name, agent] of def.agents) {
128
+ for (const dep of agent.waitsFor) {
129
+ if (!agentNames.has(dep)) {
130
+ errors.push(`Agent '${name}' waits_for unknown agent '${dep}'`);
131
+ }
132
+ if (dep === name) {
133
+ errors.push(`Agent '${name}' cannot wait for itself`);
134
+ }
135
+ }
136
+ for (const target of agent.reportsTo) {
137
+ if (!agentNames.has(target)) {
138
+ errors.push(`Agent '${name}' reports_to unknown agent '${target}'`);
139
+ }
140
+ if (target === name) {
141
+ errors.push(`Agent '${name}' cannot report to itself`);
142
+ }
143
+ }
144
+ if (agent.model !== undefined && agent.model.length === 0) {
145
+ errors.push(`Agent '${name}' model must not be empty when provided`);
146
+ }
147
+ }
148
+
149
+ if (def.targetCount < 1) {
150
+ errors.push("target_count must be at least 1");
151
+ }
152
+ if (def.mode !== "pipeline" && def.targetCount !== 1) {
153
+ errors.push("target_count is only supported in pipeline mode");
154
+ }
155
+
156
+ return errors;
157
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Filesystem state tracker for swarm pipeline execution.
3
+ *
4
+ * Persists pipeline and per-agent state to `.swarm_<name>/` in the workspace.
5
+ * Supports resumability by loading state from disk.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+
10
+ // ============================================================================
11
+ // State types
12
+ // ============================================================================
13
+
14
+ export type PipelineStatus = "idle" | "running" | "completed" | "failed" | "aborted";
15
+ export type AgentStatus = "pending" | "waiting" | "running" | "completed" | "failed";
16
+
17
+ export interface AgentState {
18
+ name: string;
19
+ status: AgentStatus;
20
+ iteration: number;
21
+ wave: number;
22
+ startedAt?: number;
23
+ completedAt?: number;
24
+ error?: string;
25
+ }
26
+
27
+ export interface SwarmState {
28
+ name: string;
29
+ status: PipelineStatus;
30
+ mode: string;
31
+ iteration: number;
32
+ targetCount: number;
33
+ agents: Record<string, AgentState>;
34
+ startedAt: number;
35
+ completedAt?: number;
36
+ }
37
+
38
+ // ============================================================================
39
+ // State tracker
40
+ // ============================================================================
41
+
42
+ export class StateTracker {
43
+ #swarmDir: string;
44
+ #state: SwarmState;
45
+
46
+ constructor(workspaceDir: string, name: string) {
47
+ this.#swarmDir = path.join(workspaceDir, `.swarm_${name}`);
48
+ this.#state = {
49
+ name,
50
+ status: "idle",
51
+ mode: "sequential",
52
+ iteration: 0,
53
+ targetCount: 1,
54
+ agents: {},
55
+ startedAt: Date.now(),
56
+ };
57
+ }
58
+
59
+ get swarmDir(): string {
60
+ return this.#swarmDir;
61
+ }
62
+
63
+ get state(): Readonly<SwarmState> {
64
+ return this.#state;
65
+ }
66
+
67
+ async init(agentNames: string[], targetCount: number, mode: string): Promise<void> {
68
+ await fs.mkdir(path.join(this.#swarmDir, "state"), { recursive: true });
69
+ await fs.mkdir(path.join(this.#swarmDir, "logs"), { recursive: true });
70
+ await fs.mkdir(path.join(this.#swarmDir, "context"), { recursive: true });
71
+
72
+ this.#state.targetCount = targetCount;
73
+ this.#state.mode = mode;
74
+ this.#state.status = "running";
75
+ this.#state.startedAt = Date.now();
76
+
77
+ for (const name of agentNames) {
78
+ this.#state.agents[name] = {
79
+ name,
80
+ status: "pending",
81
+ iteration: 0,
82
+ wave: 0,
83
+ };
84
+ }
85
+
86
+ await this.#persist();
87
+ }
88
+
89
+ async updateAgent(name: string, update: Partial<AgentState>): Promise<void> {
90
+ const agent = this.#state.agents[name];
91
+ if (!agent) return;
92
+ Object.assign(agent, update);
93
+ await this.#persist();
94
+ }
95
+
96
+ async updatePipeline(update: Partial<SwarmState>): Promise<void> {
97
+ Object.assign(this.#state, update);
98
+ await this.#persist();
99
+ }
100
+
101
+ async appendLog(agentName: string, message: string): Promise<void> {
102
+ const logPath = path.join(this.#swarmDir, "logs", `${agentName}.log`);
103
+ const timestamp = new Date().toISOString();
104
+ await fs.appendFile(logPath, `[${timestamp}] ${message}\n`);
105
+ }
106
+
107
+ async appendOrchestratorLog(message: string): Promise<void> {
108
+ const logPath = path.join(this.#swarmDir, "logs", "orchestrator.log");
109
+ const timestamp = new Date().toISOString();
110
+ await fs.appendFile(logPath, `[${timestamp}] ${message}\n`);
111
+ }
112
+
113
+ async load(): Promise<SwarmState | null> {
114
+ const statePath = path.join(this.#swarmDir, "state", "pipeline.json");
115
+ try {
116
+ const content = await Bun.file(statePath).text();
117
+ this.#state = JSON.parse(content) as SwarmState;
118
+ return this.#state;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ async #persist(): Promise<void> {
125
+ await Bun.write(path.join(this.#swarmDir, "state", "pipeline.json"), JSON.stringify(this.#state, null, 2));
126
+ }
127
+ }