role-os 2.3.1 → 2.5.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/CHANGELOG.md +472 -437
- package/README.es.md +319 -319
- package/README.fr.md +319 -319
- package/README.hi.md +319 -319
- package/README.it.md +319 -319
- package/README.ja.md +319 -319
- package/README.md +387 -387
- package/README.pt-BR.md +319 -319
- package/README.zh.md +322 -322
- package/bin/roleos.mjs +230 -225
- package/package.json +51 -51
- package/src/artifacts.mjs +693 -647
- package/src/brainstorm-render.mjs +462 -462
- package/src/brainstorm-roles.mjs +817 -817
- package/src/brainstorm.mjs +778 -778
- package/src/citation-panel.mjs +249 -0
- package/src/dispatch.mjs +265 -265
- package/src/mission.mjs +655 -655
- package/src/packs.mjs +467 -467
- package/src/route.mjs +766 -766
- package/src/run-cmd.mjs +408 -408
- package/src/run.mjs +1000 -1000
- package/src/swarm/persist-bridge.mjs +4 -4
- package/src/verify-citations-cmd.mjs +138 -0
- package/src/verify-citations.mjs +522 -0
- package/starter-pack/agents/engineering/caption-auditor.md +61 -0
- package/starter-pack/agents/engineering/monster-taxonomy-verifier.md +62 -0
- package/starter-pack/agents/engineering/red-teamer.md +75 -0
- package/starter-pack/policy/tool-permissions.md +19 -0
package/src/dispatch.mjs
CHANGED
|
@@ -1,265 +1,265 @@
|
|
|
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
|
-
import { TOOL_PROFILES } from "./tool-profiles.mjs";
|
|
20
|
-
import { renderKnowledgeBlock, knowledgeManifestSummary } from "./knowledge/render-knowledge-block.mjs";
|
|
21
|
-
|
|
22
|
-
// ── Default role config ─────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const DEFAULTS = {
|
|
25
|
-
model: "sonnet",
|
|
26
|
-
maxTurns: 30,
|
|
27
|
-
maxBudgetUsd: 5.0,
|
|
28
|
-
timeoutMs: 10 * 60 * 1000, // 10 minutes
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ── Execution states ──────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export const EXEC_STATES = [
|
|
34
|
-
"queued", // in chain, not yet launched
|
|
35
|
-
"running", // role session active
|
|
36
|
-
"waiting", // waiting for upstream handoff
|
|
37
|
-
"blocked", // hit a block condition
|
|
38
|
-
"needs-review", // completed work, awaiting verdict
|
|
39
|
-
"completed", // approved and done
|
|
40
|
-
"failed", // session error or timeout
|
|
41
|
-
"interrupted", // manually stopped
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
// ── System prompt builder ─────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
function buildRolePrompt(roleName, packetContent, chainContext, packetKnowledge) {
|
|
47
|
-
const knowledgeBlock = renderKnowledgeBlock(packetKnowledge);
|
|
48
|
-
|
|
49
|
-
return `You are operating as ${roleName} in a Role-OS managed chain.
|
|
50
|
-
|
|
51
|
-
## Your Role Contract
|
|
52
|
-
Follow the ${roleName} contract exactly. Your quality bar, inputs, outputs, and escalation rules are defined in .claude/agents/.
|
|
53
|
-
|
|
54
|
-
## Current Packet
|
|
55
|
-
${packetContent}
|
|
56
|
-
|
|
57
|
-
## Chain Context
|
|
58
|
-
You are step ${chainContext.stepNumber} of ${chainContext.totalSteps} in this chain.
|
|
59
|
-
${chainContext.previousRole ? `Previous role: ${chainContext.previousRole} (${chainContext.previousStatus})` : "You are the first role in this chain."}
|
|
60
|
-
${chainContext.nextRole ? `Next role: ${chainContext.nextRole}` : "You are the last role before Critic review."}
|
|
61
|
-
${knowledgeBlock ? `\n${knowledgeBlock}\n` : ""}
|
|
62
|
-
## Handoff Requirements
|
|
63
|
-
When you finish, produce a structured handoff:
|
|
64
|
-
1. Summary of what you did
|
|
65
|
-
2. Artifacts produced (files, changes, docs)
|
|
66
|
-
3. Open questions or risks for the next role
|
|
67
|
-
4. Evidence items for your verdict (kind, reference, claim, status)
|
|
68
|
-
|
|
69
|
-
## Stop Conditions
|
|
70
|
-
- If you cannot proceed: output a BLOCKED status with a clear reason
|
|
71
|
-
- If the upstream handoff is insufficient: output a BLOCKED status explaining what's missing
|
|
72
|
-
- Do not silently skip work — surface every gap explicitly
|
|
73
|
-
`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── Dispatch manifest builder ─────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Build a dispatch manifest from a routed chain.
|
|
80
|
-
* This is the contract between Role-OS (planning) and multi-claude (execution).
|
|
81
|
-
*
|
|
82
|
-
* @param {Object} options
|
|
83
|
-
* @param {string} options.packetFile - Path to the packet markdown
|
|
84
|
-
* @param {string} options.packetContent - Packet markdown content
|
|
85
|
-
* @param {Array<{role: {name: string, pack: string}}>} options.chainRoles - Assembled chain
|
|
86
|
-
* @param {string} options.cwd - Working directory for execution
|
|
87
|
-
* @param {Object} [options.overrides] - Per-role config overrides
|
|
88
|
-
* @returns {DispatchManifest}
|
|
89
|
-
*/
|
|
90
|
-
export function buildDispatchManifest({ packetFile, packetContent, chainRoles, cwd, overrides = {}, packetKnowledge = null }) {
|
|
91
|
-
const runId = `run-${Date.now()}`;
|
|
92
|
-
const steps = [];
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < chainRoles.length; i++) {
|
|
95
|
-
const { role } = chainRoles[i];
|
|
96
|
-
const roleOverrides = overrides[role.name] || {};
|
|
97
|
-
|
|
98
|
-
const chainContext = {
|
|
99
|
-
stepNumber: i + 1,
|
|
100
|
-
totalSteps: chainRoles.length,
|
|
101
|
-
previousRole: i > 0 ? chainRoles[i - 1].role.name : null,
|
|
102
|
-
previousStatus: i > 0 ? "pending" : null,
|
|
103
|
-
nextRole: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
steps.push({
|
|
107
|
-
stepIndex: i,
|
|
108
|
-
packetId: `${runId}-step-${i}`,
|
|
109
|
-
role: role.name,
|
|
110
|
-
pack: role.pack,
|
|
111
|
-
tools: TOOL_PROFILES[role.name] || ["Read", "Glob", "Grep"],
|
|
112
|
-
systemPrompt: buildRolePrompt(role.name, packetContent, chainContext, packetKnowledge),
|
|
113
|
-
model: roleOverrides.model || DEFAULTS.model,
|
|
114
|
-
maxTurns: roleOverrides.maxTurns || DEFAULTS.maxTurns,
|
|
115
|
-
maxBudgetUsd: roleOverrides.maxBudgetUsd || DEFAULTS.maxBudgetUsd,
|
|
116
|
-
timeoutMs: roleOverrides.timeoutMs || DEFAULTS.timeoutMs,
|
|
117
|
-
state: "queued",
|
|
118
|
-
dependsOn: i > 0 ? [`${runId}-step-${i - 1}`] : [],
|
|
119
|
-
handoff: {
|
|
120
|
-
from: i > 0 ? chainRoles[i - 1].role.name : null,
|
|
121
|
-
to: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
|
|
122
|
-
},
|
|
123
|
-
knowledge: knowledgeManifestSummary(packetKnowledge),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
version: 1,
|
|
129
|
-
runId,
|
|
130
|
-
createdAt: new Date().toISOString(),
|
|
131
|
-
packetFile: resolve(packetFile),
|
|
132
|
-
cwd: resolve(cwd),
|
|
133
|
-
totalSteps: steps.length,
|
|
134
|
-
steps,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Execution state tracker ───────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Update a step's execution state in the manifest.
|
|
142
|
-
*
|
|
143
|
-
* @param {DispatchManifest} manifest
|
|
144
|
-
* @param {number} stepIndex
|
|
145
|
-
* @param {string} newState - One of EXEC_STATES
|
|
146
|
-
* @param {Object} [result] - Execution result data
|
|
147
|
-
* @returns {DispatchManifest} Updated manifest (mutated in place)
|
|
148
|
-
*/
|
|
149
|
-
export function updateStepState(manifest, stepIndex, newState, result = {}) {
|
|
150
|
-
const step = manifest.steps[stepIndex];
|
|
151
|
-
if (!step) throw new Error(`Invalid step index: ${stepIndex}`);
|
|
152
|
-
if (!EXEC_STATES.includes(newState)) throw new Error(`Invalid state: ${newState}`);
|
|
153
|
-
|
|
154
|
-
step.state = newState;
|
|
155
|
-
step.result = {
|
|
156
|
-
...step.result,
|
|
157
|
-
...result,
|
|
158
|
-
updatedAt: new Date().toISOString(),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Auto-advance: if this step completed, mark next step as ready
|
|
162
|
-
if (newState === "completed" && stepIndex < manifest.steps.length - 1) {
|
|
163
|
-
const nextStep = manifest.steps[stepIndex + 1];
|
|
164
|
-
if (nextStep.state === "queued") {
|
|
165
|
-
nextStep.state = "waiting"; // ready to launch
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return manifest;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get the chain's overall status from individual step states.
|
|
174
|
-
*
|
|
175
|
-
* @param {DispatchManifest} manifest
|
|
176
|
-
* @returns {{status: string, completedSteps: number, totalSteps: number, currentStep: Object|null, blockedSteps: Object[]}}
|
|
177
|
-
*/
|
|
178
|
-
export function getChainStatus(manifest) {
|
|
179
|
-
const completed = manifest.steps.filter(s => s.state === "completed").length;
|
|
180
|
-
const blocked = manifest.steps.filter(s => s.state === "blocked");
|
|
181
|
-
const failed = manifest.steps.filter(s => s.state === "failed");
|
|
182
|
-
const running = manifest.steps.find(s => s.state === "running");
|
|
183
|
-
const waiting = manifest.steps.find(s => s.state === "waiting");
|
|
184
|
-
|
|
185
|
-
let status;
|
|
186
|
-
if (failed.length > 0) status = "failed";
|
|
187
|
-
else if (blocked.length > 0) status = "blocked";
|
|
188
|
-
else if (completed === manifest.steps.length) status = "completed";
|
|
189
|
-
else if (running) status = "running";
|
|
190
|
-
else if (waiting) status = "ready";
|
|
191
|
-
else status = "queued";
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
status,
|
|
195
|
-
completedSteps: completed,
|
|
196
|
-
totalSteps: manifest.steps.length,
|
|
197
|
-
currentStep: running || waiting || null,
|
|
198
|
-
blockedSteps: blocked,
|
|
199
|
-
failedSteps: failed,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ── Escalation packet generator ───────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Generate an escalation packet when a step is blocked or rejected.
|
|
207
|
-
*
|
|
208
|
-
* @param {Object} step - The blocked/rejected step
|
|
209
|
-
* @param {string} reason - Block/rejection reason
|
|
210
|
-
* @param {string} verdict - "blocked" or "reject"
|
|
211
|
-
* @returns {Object} Escalation packet ready for routing
|
|
212
|
-
*/
|
|
213
|
-
export function generateEscalationPacket(step, reason, verdict) {
|
|
214
|
-
const escalation = verdict === "blocked"
|
|
215
|
-
? resolveBlocked(reason)
|
|
216
|
-
: resolveRejected(reason, step.role);
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
type: "escalation",
|
|
220
|
-
sourceStep: step.packetId,
|
|
221
|
-
sourceRole: step.role,
|
|
222
|
-
verdict,
|
|
223
|
-
reason,
|
|
224
|
-
escalation,
|
|
225
|
-
packet: {
|
|
226
|
-
title: `Escalation: ${step.role} ${verdict} — ${reason.slice(0, 80)}`,
|
|
227
|
-
assignedRole: escalation.targetRole,
|
|
228
|
-
recovery: escalation.recovery,
|
|
229
|
-
requiredArtifact: escalation.requiredArtifact,
|
|
230
|
-
context: escalation.handoffContext || escalation.reason,
|
|
231
|
-
sourcePacketFile: step.packetId,
|
|
232
|
-
},
|
|
233
|
-
generatedAt: new Date().toISOString(),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Manifest persistence ──────────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Save a dispatch manifest to disk.
|
|
241
|
-
*
|
|
242
|
-
* @param {DispatchManifest} manifest
|
|
243
|
-
* @param {string} outputDir - Directory to write to
|
|
244
|
-
* @returns {string} Path to the saved manifest
|
|
245
|
-
*/
|
|
246
|
-
export function saveManifest(manifest, outputDir) {
|
|
247
|
-
mkdirSync(outputDir, { recursive: true });
|
|
248
|
-
const path = join(outputDir, `${manifest.runId}.dispatch.json`);
|
|
249
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2));
|
|
250
|
-
return path;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Load a dispatch manifest from disk.
|
|
255
|
-
*
|
|
256
|
-
* @param {string} path
|
|
257
|
-
* @returns {DispatchManifest}
|
|
258
|
-
*/
|
|
259
|
-
export function loadManifest(path) {
|
|
260
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── Exports for integration ───────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
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
|
+
import { TOOL_PROFILES } from "./tool-profiles.mjs";
|
|
20
|
+
import { renderKnowledgeBlock, knowledgeManifestSummary } from "./knowledge/render-knowledge-block.mjs";
|
|
21
|
+
|
|
22
|
+
// ── Default role config ─────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const DEFAULTS = {
|
|
25
|
+
model: "sonnet",
|
|
26
|
+
maxTurns: 30,
|
|
27
|
+
maxBudgetUsd: 5.0,
|
|
28
|
+
timeoutMs: 10 * 60 * 1000, // 10 minutes
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Execution states ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export const EXEC_STATES = [
|
|
34
|
+
"queued", // in chain, not yet launched
|
|
35
|
+
"running", // role session active
|
|
36
|
+
"waiting", // waiting for upstream handoff
|
|
37
|
+
"blocked", // hit a block condition
|
|
38
|
+
"needs-review", // completed work, awaiting verdict
|
|
39
|
+
"completed", // approved and done
|
|
40
|
+
"failed", // session error or timeout
|
|
41
|
+
"interrupted", // manually stopped
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// ── System prompt builder ─────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function buildRolePrompt(roleName, packetContent, chainContext, packetKnowledge) {
|
|
47
|
+
const knowledgeBlock = renderKnowledgeBlock(packetKnowledge);
|
|
48
|
+
|
|
49
|
+
return `You are operating as ${roleName} in a Role-OS managed chain.
|
|
50
|
+
|
|
51
|
+
## Your Role Contract
|
|
52
|
+
Follow the ${roleName} contract exactly. Your quality bar, inputs, outputs, and escalation rules are defined in .claude/agents/.
|
|
53
|
+
|
|
54
|
+
## Current Packet
|
|
55
|
+
${packetContent}
|
|
56
|
+
|
|
57
|
+
## Chain Context
|
|
58
|
+
You are step ${chainContext.stepNumber} of ${chainContext.totalSteps} in this chain.
|
|
59
|
+
${chainContext.previousRole ? `Previous role: ${chainContext.previousRole} (${chainContext.previousStatus})` : "You are the first role in this chain."}
|
|
60
|
+
${chainContext.nextRole ? `Next role: ${chainContext.nextRole}` : "You are the last role before Critic review."}
|
|
61
|
+
${knowledgeBlock ? `\n${knowledgeBlock}\n` : ""}
|
|
62
|
+
## Handoff Requirements
|
|
63
|
+
When you finish, produce a structured handoff:
|
|
64
|
+
1. Summary of what you did
|
|
65
|
+
2. Artifacts produced (files, changes, docs)
|
|
66
|
+
3. Open questions or risks for the next role
|
|
67
|
+
4. Evidence items for your verdict (kind, reference, claim, status)
|
|
68
|
+
|
|
69
|
+
## Stop Conditions
|
|
70
|
+
- If you cannot proceed: output a BLOCKED status with a clear reason
|
|
71
|
+
- If the upstream handoff is insufficient: output a BLOCKED status explaining what's missing
|
|
72
|
+
- Do not silently skip work — surface every gap explicitly
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Dispatch manifest builder ─────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a dispatch manifest from a routed chain.
|
|
80
|
+
* This is the contract between Role-OS (planning) and multi-claude (execution).
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {string} options.packetFile - Path to the packet markdown
|
|
84
|
+
* @param {string} options.packetContent - Packet markdown content
|
|
85
|
+
* @param {Array<{role: {name: string, pack: string}}>} options.chainRoles - Assembled chain
|
|
86
|
+
* @param {string} options.cwd - Working directory for execution
|
|
87
|
+
* @param {Object} [options.overrides] - Per-role config overrides
|
|
88
|
+
* @returns {DispatchManifest}
|
|
89
|
+
*/
|
|
90
|
+
export function buildDispatchManifest({ packetFile, packetContent, chainRoles, cwd, overrides = {}, packetKnowledge = null }) {
|
|
91
|
+
const runId = `run-${Date.now()}`;
|
|
92
|
+
const steps = [];
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < chainRoles.length; i++) {
|
|
95
|
+
const { role } = chainRoles[i];
|
|
96
|
+
const roleOverrides = overrides[role.name] || {};
|
|
97
|
+
|
|
98
|
+
const chainContext = {
|
|
99
|
+
stepNumber: i + 1,
|
|
100
|
+
totalSteps: chainRoles.length,
|
|
101
|
+
previousRole: i > 0 ? chainRoles[i - 1].role.name : null,
|
|
102
|
+
previousStatus: i > 0 ? "pending" : null,
|
|
103
|
+
nextRole: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
steps.push({
|
|
107
|
+
stepIndex: i,
|
|
108
|
+
packetId: `${runId}-step-${i}`,
|
|
109
|
+
role: role.name,
|
|
110
|
+
pack: role.pack,
|
|
111
|
+
tools: TOOL_PROFILES[role.name] || ["Read", "Glob", "Grep"],
|
|
112
|
+
systemPrompt: buildRolePrompt(role.name, packetContent, chainContext, packetKnowledge),
|
|
113
|
+
model: roleOverrides.model || DEFAULTS.model,
|
|
114
|
+
maxTurns: roleOverrides.maxTurns || DEFAULTS.maxTurns,
|
|
115
|
+
maxBudgetUsd: roleOverrides.maxBudgetUsd || DEFAULTS.maxBudgetUsd,
|
|
116
|
+
timeoutMs: roleOverrides.timeoutMs || DEFAULTS.timeoutMs,
|
|
117
|
+
state: "queued",
|
|
118
|
+
dependsOn: i > 0 ? [`${runId}-step-${i - 1}`] : [],
|
|
119
|
+
handoff: {
|
|
120
|
+
from: i > 0 ? chainRoles[i - 1].role.name : null,
|
|
121
|
+
to: i < chainRoles.length - 1 ? chainRoles[i + 1].role.name : null,
|
|
122
|
+
},
|
|
123
|
+
knowledge: knowledgeManifestSummary(packetKnowledge),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
version: 1,
|
|
129
|
+
runId,
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
packetFile: resolve(packetFile),
|
|
132
|
+
cwd: resolve(cwd),
|
|
133
|
+
totalSteps: steps.length,
|
|
134
|
+
steps,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Execution state tracker ───────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update a step's execution state in the manifest.
|
|
142
|
+
*
|
|
143
|
+
* @param {DispatchManifest} manifest
|
|
144
|
+
* @param {number} stepIndex
|
|
145
|
+
* @param {string} newState - One of EXEC_STATES
|
|
146
|
+
* @param {Object} [result] - Execution result data
|
|
147
|
+
* @returns {DispatchManifest} Updated manifest (mutated in place)
|
|
148
|
+
*/
|
|
149
|
+
export function updateStepState(manifest, stepIndex, newState, result = {}) {
|
|
150
|
+
const step = manifest.steps[stepIndex];
|
|
151
|
+
if (!step) throw new Error(`Invalid step index: ${stepIndex}`);
|
|
152
|
+
if (!EXEC_STATES.includes(newState)) throw new Error(`Invalid state: ${newState}`);
|
|
153
|
+
|
|
154
|
+
step.state = newState;
|
|
155
|
+
step.result = {
|
|
156
|
+
...step.result,
|
|
157
|
+
...result,
|
|
158
|
+
updatedAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Auto-advance: if this step completed, mark next step as ready
|
|
162
|
+
if (newState === "completed" && stepIndex < manifest.steps.length - 1) {
|
|
163
|
+
const nextStep = manifest.steps[stepIndex + 1];
|
|
164
|
+
if (nextStep.state === "queued") {
|
|
165
|
+
nextStep.state = "waiting"; // ready to launch
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return manifest;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the chain's overall status from individual step states.
|
|
174
|
+
*
|
|
175
|
+
* @param {DispatchManifest} manifest
|
|
176
|
+
* @returns {{status: string, completedSteps: number, totalSteps: number, currentStep: Object|null, blockedSteps: Object[]}}
|
|
177
|
+
*/
|
|
178
|
+
export function getChainStatus(manifest) {
|
|
179
|
+
const completed = manifest.steps.filter(s => s.state === "completed").length;
|
|
180
|
+
const blocked = manifest.steps.filter(s => s.state === "blocked");
|
|
181
|
+
const failed = manifest.steps.filter(s => s.state === "failed");
|
|
182
|
+
const running = manifest.steps.find(s => s.state === "running");
|
|
183
|
+
const waiting = manifest.steps.find(s => s.state === "waiting");
|
|
184
|
+
|
|
185
|
+
let status;
|
|
186
|
+
if (failed.length > 0) status = "failed";
|
|
187
|
+
else if (blocked.length > 0) status = "blocked";
|
|
188
|
+
else if (completed === manifest.steps.length) status = "completed";
|
|
189
|
+
else if (running) status = "running";
|
|
190
|
+
else if (waiting) status = "ready";
|
|
191
|
+
else status = "queued";
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
status,
|
|
195
|
+
completedSteps: completed,
|
|
196
|
+
totalSteps: manifest.steps.length,
|
|
197
|
+
currentStep: running || waiting || null,
|
|
198
|
+
blockedSteps: blocked,
|
|
199
|
+
failedSteps: failed,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Escalation packet generator ───────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate an escalation packet when a step is blocked or rejected.
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} step - The blocked/rejected step
|
|
209
|
+
* @param {string} reason - Block/rejection reason
|
|
210
|
+
* @param {string} verdict - "blocked" or "reject"
|
|
211
|
+
* @returns {Object} Escalation packet ready for routing
|
|
212
|
+
*/
|
|
213
|
+
export function generateEscalationPacket(step, reason, verdict) {
|
|
214
|
+
const escalation = verdict === "blocked"
|
|
215
|
+
? resolveBlocked(reason)
|
|
216
|
+
: resolveRejected(reason, step.role);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
type: "escalation",
|
|
220
|
+
sourceStep: step.packetId,
|
|
221
|
+
sourceRole: step.role,
|
|
222
|
+
verdict,
|
|
223
|
+
reason,
|
|
224
|
+
escalation,
|
|
225
|
+
packet: {
|
|
226
|
+
title: `Escalation: ${step.role} ${verdict} — ${reason.slice(0, 80)}`,
|
|
227
|
+
assignedRole: escalation.targetRole,
|
|
228
|
+
recovery: escalation.recovery,
|
|
229
|
+
requiredArtifact: escalation.requiredArtifact,
|
|
230
|
+
context: escalation.handoffContext || escalation.reason,
|
|
231
|
+
sourcePacketFile: step.packetId,
|
|
232
|
+
},
|
|
233
|
+
generatedAt: new Date().toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Manifest persistence ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Save a dispatch manifest to disk.
|
|
241
|
+
*
|
|
242
|
+
* @param {DispatchManifest} manifest
|
|
243
|
+
* @param {string} outputDir - Directory to write to
|
|
244
|
+
* @returns {string} Path to the saved manifest
|
|
245
|
+
*/
|
|
246
|
+
export function saveManifest(manifest, outputDir) {
|
|
247
|
+
mkdirSync(outputDir, { recursive: true });
|
|
248
|
+
const path = join(outputDir, `${manifest.runId}.dispatch.json`);
|
|
249
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2));
|
|
250
|
+
return path;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Load a dispatch manifest from disk.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} path
|
|
257
|
+
* @returns {DispatchManifest}
|
|
258
|
+
*/
|
|
259
|
+
export function loadManifest(path) {
|
|
260
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Exports for integration ───────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export { TOOL_PROFILES, DEFAULTS };
|