role-os 2.0.0 → 2.1.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/src/dispatch.mjs CHANGED
@@ -1,310 +1,333 @@
1
- /**
2
- * Runtime dispatch engine.
3
- *
4
- * Turns a staffed, validated chain into an executable dispatch manifest
5
- * that multi-claude can consume. Role-OS owns staffing/routing/evidence;
6
- * multi-claude owns execution.
7
- *
8
- * This module:
9
- * 1. Maps roles → role configs (tool profiles, system prompts, budgets)
10
- * 2. Tracks execution state (queued → running → completed/failed/blocked)
11
- * 3. Collects outputs back into the packet system
12
- * 4. Generates escalation packets for blocked/rejected work
13
- * 5. Closes chains to final completion
14
- */
15
-
16
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
17
- import { join, resolve } from "node:path";
18
- import { resolveBlocked, resolveRejected } from "./escalation.mjs";
19
-
20
- // ── Tool profiles per role ────────────────────────────────────────────────────
21
- // What tools each role is allowed to use. Sandboxing by contract.
22
-
23
- const TOOL_PROFILES = {
24
- // Core
25
- "Orchestrator": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
26
- "Product Strategist": ["Read", "Glob", "Grep", "Write"],
27
- "Critic Reviewer": ["Read", "Glob", "Grep", "Bash"],
28
-
29
- // Design
30
- "UI Designer": ["Read", "Glob", "Grep", "Write", "Edit"],
31
- "Brand Guardian": ["Read", "Glob", "Grep"],
32
-
33
- // Engineering
34
- "Backend Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
35
- "Frontend Developer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
36
- "Test Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
37
- "Performance Engineer": ["Read", "Glob", "Grep", "Bash"],
38
- "Refactor Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
39
- "Security Reviewer": ["Read", "Glob", "Grep", "Bash"],
40
- "Dependency Auditor": ["Read", "Glob", "Grep", "Bash"],
41
-
42
- // Treatment
43
- "Repo Researcher": ["Read", "Glob", "Grep", "Bash"],
44
- "Repo Translator": ["Read", "Glob", "Grep", "Write", "Edit"],
45
- "Docs Architect": ["Read", "Glob", "Grep", "Write", "Edit"],
46
- "Metadata Curator": ["Read", "Glob", "Grep", "Write", "Edit"],
47
- "Coverage Auditor": ["Read", "Glob", "Grep", "Bash"],
48
- "Deployment Verifier": ["Read", "Glob", "Grep", "Bash"],
49
- "Release Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
50
-
51
- // Growth / Marketing
52
- "Launch Strategist": ["Read", "Glob", "Grep", "Write"],
53
- "Content Strategist": ["Read", "Glob", "Grep", "Write"],
54
- "Community Manager": ["Read", "Glob", "Grep", "Write"],
55
- "Support Triage Lead": ["Read", "Glob", "Grep", "Write"],
56
- "Launch Copywriter": ["Read", "Glob", "Grep", "Write", "Edit"],
57
-
58
- // Product
59
- "Feedback Synthesizer": ["Read", "Glob", "Grep"],
60
- "Roadmap Prioritizer": ["Read", "Glob", "Grep", "Write"],
61
- "Spec Writer": ["Read", "Glob", "Grep", "Write", "Edit"],
62
-
63
- // Research
64
- "UX Researcher": ["Read", "Glob", "Grep"],
65
- "Competitive Analyst": ["Read", "Glob", "Grep"],
66
- "Trend Researcher": ["Read", "Glob", "Grep"],
67
- "User Interview Synthesizer": ["Read", "Glob", "Grep"],
68
- };
69
-
70
- // ── Default role config ─────────────────────────────────────────────────────
71
-
72
- const DEFAULTS = {
73
- model: "sonnet",
74
- maxTurns: 30,
75
- maxBudgetUsd: 5.0,
76
- timeoutMs: 10 * 60 * 1000, // 10 minutes
77
- };
78
-
79
- // ── Execution states ──────────────────────────────────────────────────────────
80
-
81
- export const EXEC_STATES = [
82
- "queued", // in chain, not yet launched
83
- "running", // role session active
84
- "waiting", // waiting for upstream handoff
85
- "blocked", // hit a block condition
86
- "needs-review", // completed work, awaiting verdict
87
- "completed", // approved and done
88
- "failed", // session error or timeout
89
- "interrupted", // manually stopped
90
- ];
91
-
92
- // ── System prompt builder ─────────────────────────────────────────────────────
93
-
94
- function buildRolePrompt(roleName, packetContent, chainContext) {
95
- return `You are operating as ${roleName} in a Role-OS managed chain.
96
-
97
- ## Your Role Contract
98
- Follow the ${roleName} contract exactly. Your quality bar, inputs, outputs, and escalation rules are defined in .claude/agents/.
99
-
100
- ## Current Packet
101
- ${packetContent}
102
-
103
- ## Chain Context
104
- You are step ${chainContext.stepNumber} of ${chainContext.totalSteps} in this chain.
105
- ${chainContext.previousRole ? `Previous role: ${chainContext.previousRole} (${chainContext.previousStatus})` : "You are the first role in this chain."}
106
- ${chainContext.nextRole ? `Next role: ${chainContext.nextRole}` : "You are the last role before Critic review."}
107
-
108
- ## Handoff Requirements
109
- When you finish, produce a structured handoff:
110
- 1. Summary of what you did
111
- 2. Artifacts produced (files, changes, docs)
112
- 3. Open questions or risks for the next role
113
- 4. Evidence items for your verdict (kind, reference, claim, status)
114
-
115
- ## Stop Conditions
116
- - If you cannot proceed: output a BLOCKED status with a clear reason
117
- - If the upstream handoff is insufficient: output a BLOCKED status explaining what's missing
118
- - Do not silently skip work surface every gap explicitly
119
- `;
120
- }
121
-
122
- // ── Dispatch manifest builder ─────────────────────────────────────────────────
123
-
124
- /**
125
- * Build a dispatch manifest from a routed chain.
126
- * This is the contract between Role-OS (planning) and multi-claude (execution).
127
- *
128
- * @param {Object} options
129
- * @param {string} options.packetFile - Path to the packet markdown
130
- * @param {string} options.packetContent - Packet markdown content
131
- * @param {Array<{role: {name: string, pack: string}}>} options.chainRoles - Assembled chain
132
- * @param {string} options.cwd - Working directory for execution
133
- * @param {Object} [options.overrides] - Per-role config overrides
134
- * @returns {DispatchManifest}
135
- */
136
- export function buildDispatchManifest({ packetFile, packetContent, chainRoles, cwd, overrides = {} }) {
137
- const runId = `run-${Date.now()}`;
138
- const steps = [];
139
-
140
- for (let i = 0; i < chainRoles.length; i++) {
141
- const { role } = chainRoles[i];
142
- const roleOverrides = overrides[role.name] || {};
143
-
144
- const chainContext = {
145
- stepNumber: i + 1,
146
- totalSteps: chainRoles.length,
147
- previousRole: i > 0 ? chainRoles[i - 1].role.name : null,
148
- previousStatus: i > 0 ? "pending" : null,
149
- nextRole: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
150
- };
151
-
152
- steps.push({
153
- stepIndex: i,
154
- packetId: `${runId}-step-${i}`,
155
- role: role.name,
156
- pack: role.pack,
157
- tools: TOOL_PROFILES[role.name] || ["Read", "Glob", "Grep"],
158
- systemPrompt: buildRolePrompt(role.name, packetContent, chainContext),
159
- model: roleOverrides.model || DEFAULTS.model,
160
- maxTurns: roleOverrides.maxTurns || DEFAULTS.maxTurns,
161
- maxBudgetUsd: roleOverrides.maxBudgetUsd || DEFAULTS.maxBudgetUsd,
162
- timeoutMs: roleOverrides.timeoutMs || DEFAULTS.timeoutMs,
163
- state: "queued",
164
- dependsOn: i > 0 ? [`${runId}-step-${i - 1}`] : [],
165
- handoff: {
166
- from: i > 0 ? chainRoles[i - 1].role.name : null,
167
- to: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
168
- },
169
- });
170
- }
171
-
172
- return {
173
- version: 1,
174
- runId,
175
- createdAt: new Date().toISOString(),
176
- packetFile: resolve(packetFile),
177
- cwd: resolve(cwd),
178
- totalSteps: steps.length,
179
- steps,
180
- };
181
- }
182
-
183
- // ── Execution state tracker ───────────────────────────────────────────────────
184
-
185
- /**
186
- * Update a step's execution state in the manifest.
187
- *
188
- * @param {DispatchManifest} manifest
189
- * @param {number} stepIndex
190
- * @param {string} newState - One of EXEC_STATES
191
- * @param {Object} [result] - Execution result data
192
- * @returns {DispatchManifest} Updated manifest (mutated in place)
193
- */
194
- export function updateStepState(manifest, stepIndex, newState, result = {}) {
195
- const step = manifest.steps[stepIndex];
196
- if (!step) throw new Error(`Invalid step index: ${stepIndex}`);
197
- if (!EXEC_STATES.includes(newState)) throw new Error(`Invalid state: ${newState}`);
198
-
199
- step.state = newState;
200
- step.result = {
201
- ...step.result,
202
- ...result,
203
- updatedAt: new Date().toISOString(),
204
- };
205
-
206
- // Auto-advance: if this step completed, mark next step as ready
207
- if (newState === "completed" && stepIndex < manifest.steps.length - 1) {
208
- const nextStep = manifest.steps[stepIndex + 1];
209
- if (nextStep.state === "queued") {
210
- nextStep.state = "waiting"; // ready to launch
211
- }
212
- }
213
-
214
- return manifest;
215
- }
216
-
217
- /**
218
- * Get the chain's overall status from individual step states.
219
- *
220
- * @param {DispatchManifest} manifest
221
- * @returns {{status: string, completedSteps: number, totalSteps: number, currentStep: Object|null, blockedSteps: Object[]}}
222
- */
223
- export function getChainStatus(manifest) {
224
- const completed = manifest.steps.filter(s => s.state === "completed").length;
225
- const blocked = manifest.steps.filter(s => s.state === "blocked");
226
- const failed = manifest.steps.filter(s => s.state === "failed");
227
- const running = manifest.steps.find(s => s.state === "running");
228
- const waiting = manifest.steps.find(s => s.state === "waiting");
229
-
230
- let status;
231
- if (failed.length > 0) status = "failed";
232
- else if (blocked.length > 0) status = "blocked";
233
- else if (completed === manifest.steps.length) status = "completed";
234
- else if (running) status = "running";
235
- else if (waiting) status = "ready";
236
- else status = "queued";
237
-
238
- return {
239
- status,
240
- completedSteps: completed,
241
- totalSteps: manifest.steps.length,
242
- currentStep: running || waiting || null,
243
- blockedSteps: blocked,
244
- failedSteps: failed,
245
- };
246
- }
247
-
248
- // ── Escalation packet generator ───────────────────────────────────────────────
249
-
250
- /**
251
- * Generate an escalation packet when a step is blocked or rejected.
252
- *
253
- * @param {Object} step - The blocked/rejected step
254
- * @param {string} reason - Block/rejection reason
255
- * @param {string} verdict - "blocked" or "reject"
256
- * @returns {Object} Escalation packet ready for routing
257
- */
258
- export function generateEscalationPacket(step, reason, verdict) {
259
- const escalation = verdict === "blocked"
260
- ? resolveBlocked(reason)
261
- : resolveRejected(reason, step.role);
262
-
263
- return {
264
- type: "escalation",
265
- sourceStep: step.packetId,
266
- sourceRole: step.role,
267
- verdict,
268
- reason,
269
- escalation,
270
- packet: {
271
- title: `Escalation: ${step.role} ${verdict} — ${reason.slice(0, 80)}`,
272
- assignedRole: escalation.targetRole,
273
- recovery: escalation.recovery,
274
- requiredArtifact: escalation.requiredArtifact,
275
- context: escalation.handoffContext || escalation.reason,
276
- sourcePacketFile: step.packetId,
277
- },
278
- generatedAt: new Date().toISOString(),
279
- };
280
- }
281
-
282
- // ── Manifest persistence ──────────────────────────────────────────────────────
283
-
284
- /**
285
- * Save a dispatch manifest to disk.
286
- *
287
- * @param {DispatchManifest} manifest
288
- * @param {string} outputDir - Directory to write to
289
- * @returns {string} Path to the saved manifest
290
- */
291
- export function saveManifest(manifest, outputDir) {
292
- mkdirSync(outputDir, { recursive: true });
293
- const path = join(outputDir, `${manifest.runId}.dispatch.json`);
294
- writeFileSync(path, JSON.stringify(manifest, null, 2));
295
- return path;
296
- }
297
-
298
- /**
299
- * Load a dispatch manifest from disk.
300
- *
301
- * @param {string} path
302
- * @returns {DispatchManifest}
303
- */
304
- export function loadManifest(path) {
305
- return JSON.parse(readFileSync(path, "utf-8"));
306
- }
307
-
308
- // ── Exports for integration ───────────────────────────────────────────────────
309
-
310
- export { TOOL_PROFILES, DEFAULTS };
1
+ /**
2
+ * Runtime dispatch engine.
3
+ *
4
+ * Turns a staffed, validated chain into an executable dispatch manifest
5
+ * that multi-claude can consume. Role-OS owns staffing/routing/evidence;
6
+ * multi-claude owns execution.
7
+ *
8
+ * This module:
9
+ * 1. Maps roles → role configs (tool profiles, system prompts, budgets)
10
+ * 2. Tracks execution state (queued → running → completed/failed/blocked)
11
+ * 3. Collects outputs back into the packet system
12
+ * 4. Generates escalation packets for blocked/rejected work
13
+ * 5. Closes chains to final completion
14
+ */
15
+
16
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
17
+ import { join, resolve } from "node:path";
18
+ import { resolveBlocked, resolveRejected } from "./escalation.mjs";
19
+
20
+ // ── Tool profiles per role ────────────────────────────────────────────────────
21
+ // What tools each role is allowed to use. Sandboxing by contract.
22
+
23
+ const TOOL_PROFILES = {
24
+ // Core
25
+ "Orchestrator": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
26
+ "Product Strategist": ["Read", "Glob", "Grep", "Write"],
27
+ "Critic Reviewer": ["Read", "Glob", "Grep", "Bash"],
28
+
29
+ // Design
30
+ "UI Designer": ["Read", "Glob", "Grep", "Write", "Edit"],
31
+ "Brand Guardian": ["Read", "Glob", "Grep"],
32
+
33
+ // Engineering
34
+ "Backend Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
35
+ "Frontend Developer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
36
+ "Test Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
37
+ "Performance Engineer": ["Read", "Glob", "Grep", "Bash"],
38
+ "Refactor Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
39
+ "Security Reviewer": ["Read", "Glob", "Grep", "Bash"],
40
+ "Dependency Auditor": ["Read", "Glob", "Grep", "Bash"],
41
+
42
+ // Treatment
43
+ "Repo Researcher": ["Read", "Glob", "Grep", "Bash"],
44
+ "Repo Translator": ["Read", "Glob", "Grep", "Write", "Edit"],
45
+ "Docs Architect": ["Read", "Glob", "Grep", "Write", "Edit"],
46
+ "Metadata Curator": ["Read", "Glob", "Grep", "Write", "Edit"],
47
+ "Coverage Auditor": ["Read", "Glob", "Grep", "Bash"],
48
+ "Deployment Verifier": ["Read", "Glob", "Grep", "Bash"],
49
+ "Release Engineer": ["Read", "Glob", "Grep", "Bash", "Write", "Edit"],
50
+
51
+ // Growth / Marketing
52
+ "Launch Strategist": ["Read", "Glob", "Grep", "Write"],
53
+ "Content Strategist": ["Read", "Glob", "Grep", "Write"],
54
+ "Community Manager": ["Read", "Glob", "Grep", "Write"],
55
+ "Support Triage Lead": ["Read", "Glob", "Grep", "Write"],
56
+ "Launch Copywriter": ["Read", "Glob", "Grep", "Write", "Edit"],
57
+
58
+ // Product
59
+ "Feedback Synthesizer": ["Read", "Glob", "Grep"],
60
+ "Roadmap Prioritizer": ["Read", "Glob", "Grep", "Write"],
61
+ "Spec Writer": ["Read", "Glob", "Grep", "Write", "Edit"],
62
+
63
+ // Research
64
+ "UX Researcher": ["Read", "Glob", "Grep"],
65
+ "Competitive Analyst": ["Read", "Glob", "Grep"],
66
+ "Trend Researcher": ["Read", "Glob", "Grep"],
67
+ "User Interview Synthesizer": ["Read", "Glob", "Grep"],
68
+
69
+ // Brainstorm
70
+ "Context Scout": ["Read", "Glob", "Grep"],
71
+ "User Value Scout": ["Read", "Glob", "Grep"],
72
+ "Creative Leap Scout": ["Read", "Glob", "Grep"],
73
+ "Normalizer": ["Read", "Glob", "Grep"],
74
+ "Synthesizer": ["Read", "Glob", "Grep", "Write"],
75
+ "Product Expander": ["Read", "Glob", "Grep", "Write"],
76
+ "Judge": ["Read", "Glob", "Grep"],
77
+ "Mechanics Scout": ["Read", "Glob", "Grep"],
78
+ "Market Scout": ["Read", "Glob", "Grep"],
79
+ "Contrarian Scout": ["Read", "Glob", "Grep"],
80
+ "Feasibility Scout": ["Read", "Glob", "Grep"],
81
+ "Quality Bar Scout": ["Read", "Glob", "Grep"],
82
+ "Scenario Expander": ["Read", "Glob", "Grep", "Write"],
83
+ "Moat Expander": ["Read", "Glob", "Grep", "Write"],
84
+
85
+ // Brainstorm v0.3 analysts
86
+ "Context Analyst": ["Read", "Glob", "Grep"],
87
+ "User Value Analyst": ["Read", "Glob", "Grep"],
88
+ "Mechanics Analyst": ["Read", "Glob", "Grep"],
89
+ "Positioning Analyst": ["Read", "Glob", "Grep"],
90
+ "Contrarian Analyst": ["Read", "Glob", "Grep"],
91
+ };
92
+
93
+ // ── Default role config ─────────────────────────────────────────────────────
94
+
95
+ const DEFAULTS = {
96
+ model: "sonnet",
97
+ maxTurns: 30,
98
+ maxBudgetUsd: 5.0,
99
+ timeoutMs: 10 * 60 * 1000, // 10 minutes
100
+ };
101
+
102
+ // ── Execution states ──────────────────────────────────────────────────────────
103
+
104
+ export const EXEC_STATES = [
105
+ "queued", // in chain, not yet launched
106
+ "running", // role session active
107
+ "waiting", // waiting for upstream handoff
108
+ "blocked", // hit a block condition
109
+ "needs-review", // completed work, awaiting verdict
110
+ "completed", // approved and done
111
+ "failed", // session error or timeout
112
+ "interrupted", // manually stopped
113
+ ];
114
+
115
+ // ── System prompt builder ─────────────────────────────────────────────────────
116
+
117
+ function buildRolePrompt(roleName, packetContent, chainContext) {
118
+ return `You are operating as ${roleName} in a Role-OS managed chain.
119
+
120
+ ## Your Role Contract
121
+ Follow the ${roleName} contract exactly. Your quality bar, inputs, outputs, and escalation rules are defined in .claude/agents/.
122
+
123
+ ## Current Packet
124
+ ${packetContent}
125
+
126
+ ## Chain Context
127
+ You are step ${chainContext.stepNumber} of ${chainContext.totalSteps} in this chain.
128
+ ${chainContext.previousRole ? `Previous role: ${chainContext.previousRole} (${chainContext.previousStatus})` : "You are the first role in this chain."}
129
+ ${chainContext.nextRole ? `Next role: ${chainContext.nextRole}` : "You are the last role before Critic review."}
130
+
131
+ ## Handoff Requirements
132
+ When you finish, produce a structured handoff:
133
+ 1. Summary of what you did
134
+ 2. Artifacts produced (files, changes, docs)
135
+ 3. Open questions or risks for the next role
136
+ 4. Evidence items for your verdict (kind, reference, claim, status)
137
+
138
+ ## Stop Conditions
139
+ - If you cannot proceed: output a BLOCKED status with a clear reason
140
+ - If the upstream handoff is insufficient: output a BLOCKED status explaining what's missing
141
+ - Do not silently skip work — surface every gap explicitly
142
+ `;
143
+ }
144
+
145
+ // ── Dispatch manifest builder ─────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Build a dispatch manifest from a routed chain.
149
+ * This is the contract between Role-OS (planning) and multi-claude (execution).
150
+ *
151
+ * @param {Object} options
152
+ * @param {string} options.packetFile - Path to the packet markdown
153
+ * @param {string} options.packetContent - Packet markdown content
154
+ * @param {Array<{role: {name: string, pack: string}}>} options.chainRoles - Assembled chain
155
+ * @param {string} options.cwd - Working directory for execution
156
+ * @param {Object} [options.overrides] - Per-role config overrides
157
+ * @returns {DispatchManifest}
158
+ */
159
+ export function buildDispatchManifest({ packetFile, packetContent, chainRoles, cwd, overrides = {} }) {
160
+ const runId = `run-${Date.now()}`;
161
+ const steps = [];
162
+
163
+ for (let i = 0; i < chainRoles.length; i++) {
164
+ const { role } = chainRoles[i];
165
+ const roleOverrides = overrides[role.name] || {};
166
+
167
+ const chainContext = {
168
+ stepNumber: i + 1,
169
+ totalSteps: chainRoles.length,
170
+ previousRole: i > 0 ? chainRoles[i - 1].role.name : null,
171
+ previousStatus: i > 0 ? "pending" : null,
172
+ nextRole: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
173
+ };
174
+
175
+ steps.push({
176
+ stepIndex: i,
177
+ packetId: `${runId}-step-${i}`,
178
+ role: role.name,
179
+ pack: role.pack,
180
+ tools: TOOL_PROFILES[role.name] || ["Read", "Glob", "Grep"],
181
+ systemPrompt: buildRolePrompt(role.name, packetContent, chainContext),
182
+ model: roleOverrides.model || DEFAULTS.model,
183
+ maxTurns: roleOverrides.maxTurns || DEFAULTS.maxTurns,
184
+ maxBudgetUsd: roleOverrides.maxBudgetUsd || DEFAULTS.maxBudgetUsd,
185
+ timeoutMs: roleOverrides.timeoutMs || DEFAULTS.timeoutMs,
186
+ state: "queued",
187
+ dependsOn: i > 0 ? [`${runId}-step-${i - 1}`] : [],
188
+ handoff: {
189
+ from: i > 0 ? chainRoles[i - 1].role.name : null,
190
+ to: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
191
+ },
192
+ });
193
+ }
194
+
195
+ return {
196
+ version: 1,
197
+ runId,
198
+ createdAt: new Date().toISOString(),
199
+ packetFile: resolve(packetFile),
200
+ cwd: resolve(cwd),
201
+ totalSteps: steps.length,
202
+ steps,
203
+ };
204
+ }
205
+
206
+ // ── Execution state tracker ───────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Update a step's execution state in the manifest.
210
+ *
211
+ * @param {DispatchManifest} manifest
212
+ * @param {number} stepIndex
213
+ * @param {string} newState - One of EXEC_STATES
214
+ * @param {Object} [result] - Execution result data
215
+ * @returns {DispatchManifest} Updated manifest (mutated in place)
216
+ */
217
+ export function updateStepState(manifest, stepIndex, newState, result = {}) {
218
+ const step = manifest.steps[stepIndex];
219
+ if (!step) throw new Error(`Invalid step index: ${stepIndex}`);
220
+ if (!EXEC_STATES.includes(newState)) throw new Error(`Invalid state: ${newState}`);
221
+
222
+ step.state = newState;
223
+ step.result = {
224
+ ...step.result,
225
+ ...result,
226
+ updatedAt: new Date().toISOString(),
227
+ };
228
+
229
+ // Auto-advance: if this step completed, mark next step as ready
230
+ if (newState === "completed" && stepIndex < manifest.steps.length - 1) {
231
+ const nextStep = manifest.steps[stepIndex + 1];
232
+ if (nextStep.state === "queued") {
233
+ nextStep.state = "waiting"; // ready to launch
234
+ }
235
+ }
236
+
237
+ return manifest;
238
+ }
239
+
240
+ /**
241
+ * Get the chain's overall status from individual step states.
242
+ *
243
+ * @param {DispatchManifest} manifest
244
+ * @returns {{status: string, completedSteps: number, totalSteps: number, currentStep: Object|null, blockedSteps: Object[]}}
245
+ */
246
+ export function getChainStatus(manifest) {
247
+ const completed = manifest.steps.filter(s => s.state === "completed").length;
248
+ const blocked = manifest.steps.filter(s => s.state === "blocked");
249
+ const failed = manifest.steps.filter(s => s.state === "failed");
250
+ const running = manifest.steps.find(s => s.state === "running");
251
+ const waiting = manifest.steps.find(s => s.state === "waiting");
252
+
253
+ let status;
254
+ if (failed.length > 0) status = "failed";
255
+ else if (blocked.length > 0) status = "blocked";
256
+ else if (completed === manifest.steps.length) status = "completed";
257
+ else if (running) status = "running";
258
+ else if (waiting) status = "ready";
259
+ else status = "queued";
260
+
261
+ return {
262
+ status,
263
+ completedSteps: completed,
264
+ totalSteps: manifest.steps.length,
265
+ currentStep: running || waiting || null,
266
+ blockedSteps: blocked,
267
+ failedSteps: failed,
268
+ };
269
+ }
270
+
271
+ // ── Escalation packet generator ───────────────────────────────────────────────
272
+
273
+ /**
274
+ * Generate an escalation packet when a step is blocked or rejected.
275
+ *
276
+ * @param {Object} step - The blocked/rejected step
277
+ * @param {string} reason - Block/rejection reason
278
+ * @param {string} verdict - "blocked" or "reject"
279
+ * @returns {Object} Escalation packet ready for routing
280
+ */
281
+ export function generateEscalationPacket(step, reason, verdict) {
282
+ const escalation = verdict === "blocked"
283
+ ? resolveBlocked(reason)
284
+ : resolveRejected(reason, step.role);
285
+
286
+ return {
287
+ type: "escalation",
288
+ sourceStep: step.packetId,
289
+ sourceRole: step.role,
290
+ verdict,
291
+ reason,
292
+ escalation,
293
+ packet: {
294
+ title: `Escalation: ${step.role} ${verdict} — ${reason.slice(0, 80)}`,
295
+ assignedRole: escalation.targetRole,
296
+ recovery: escalation.recovery,
297
+ requiredArtifact: escalation.requiredArtifact,
298
+ context: escalation.handoffContext || escalation.reason,
299
+ sourcePacketFile: step.packetId,
300
+ },
301
+ generatedAt: new Date().toISOString(),
302
+ };
303
+ }
304
+
305
+ // ── Manifest persistence ──────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * Save a dispatch manifest to disk.
309
+ *
310
+ * @param {DispatchManifest} manifest
311
+ * @param {string} outputDir - Directory to write to
312
+ * @returns {string} Path to the saved manifest
313
+ */
314
+ export function saveManifest(manifest, outputDir) {
315
+ mkdirSync(outputDir, { recursive: true });
316
+ const path = join(outputDir, `${manifest.runId}.dispatch.json`);
317
+ writeFileSync(path, JSON.stringify(manifest, null, 2));
318
+ return path;
319
+ }
320
+
321
+ /**
322
+ * Load a dispatch manifest from disk.
323
+ *
324
+ * @param {string} path
325
+ * @returns {DispatchManifest}
326
+ */
327
+ export function loadManifest(path) {
328
+ return JSON.parse(readFileSync(path, "utf-8"));
329
+ }
330
+
331
+ // ── Exports for integration ───────────────────────────────────────────────────
332
+
333
+ export { TOOL_PROFILES, DEFAULTS };