role-os 1.0.2 → 1.2.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 +74 -0
- package/README.md +55 -23
- package/bin/roleos.mjs +8 -1
- package/package.json +13 -3
- package/src/conflicts.mjs +217 -0
- package/src/dispatch.mjs +310 -0
- package/src/escalation.mjs +288 -0
- package/src/evidence.mjs +288 -0
- package/src/packs-cmd.mjs +143 -0
- package/src/packs.mjs +331 -0
- package/src/review.mjs +12 -0
- package/src/route.mjs +477 -82
- package/src/trial.mjs +252 -0
package/src/dispatch.mjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
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 };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalation auto-routing.
|
|
3
|
+
*
|
|
4
|
+
* When a packet hits a stop condition (blocked, rejected, conflict, split-needed),
|
|
5
|
+
* this module routes it to the right resolver with a reason, required artifact,
|
|
6
|
+
* and recovery type.
|
|
7
|
+
*
|
|
8
|
+
* Every escalation produces a clean next-step — not just a destination.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── Recovery types ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {'retry' | 'revise' | 'review' | 'split' | 'reroute' | 'add-role'} RecoveryType
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} EscalationResult
|
|
19
|
+
* @property {string} targetRole - Who owns the recovery
|
|
20
|
+
* @property {RecoveryType} recovery - What kind of recovery this is
|
|
21
|
+
* @property {string} reason - Why this role was chosen
|
|
22
|
+
* @property {string} requiredArtifact - What they must produce to unblock
|
|
23
|
+
* @property {string} handoffContext - What the target needs to know
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ── Blocked work routing ──────────────────────────────────────────────────────
|
|
27
|
+
// Each blocked reason maps to a resolver role + recovery path.
|
|
28
|
+
|
|
29
|
+
const BLOCKED_ROUTES = {
|
|
30
|
+
// Missing information / unclear requirements
|
|
31
|
+
"missing info": {
|
|
32
|
+
targetRole: "Product Strategist",
|
|
33
|
+
recovery: "revise",
|
|
34
|
+
reason: "Blocked on missing information — Product Strategist owns requirement clarification",
|
|
35
|
+
requiredArtifact: "Updated scope doc or clarified requirements",
|
|
36
|
+
handoffContext: "Identify exactly what information is missing and whether it requires user input or can be derived from existing context",
|
|
37
|
+
},
|
|
38
|
+
"unclear requirements": {
|
|
39
|
+
targetRole: "Product Strategist",
|
|
40
|
+
recovery: "revise",
|
|
41
|
+
reason: "Requirements are ambiguous — Product Strategist owns scope clarification",
|
|
42
|
+
requiredArtifact: "Clarified requirements with acceptance criteria",
|
|
43
|
+
handoffContext: "Flag which requirements are ambiguous and what interpretations exist",
|
|
44
|
+
},
|
|
45
|
+
"scope contradiction": {
|
|
46
|
+
targetRole: "Product Strategist",
|
|
47
|
+
recovery: "revise",
|
|
48
|
+
reason: "Scope contains contradictions — Product Strategist must resolve",
|
|
49
|
+
requiredArtifact: "Revised scope with contradictions resolved and tradeoffs documented",
|
|
50
|
+
handoffContext: "List the specific contradictions and what each interpretation implies for delivery",
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Dependency unavailable
|
|
54
|
+
"dependency unavailable": {
|
|
55
|
+
targetRole: "Orchestrator",
|
|
56
|
+
recovery: "reroute",
|
|
57
|
+
reason: "Upstream dependency is unavailable — Orchestrator must re-sequence or find alternative",
|
|
58
|
+
requiredArtifact: "Revised execution plan with dependency resolved or worked around",
|
|
59
|
+
handoffContext: "Identify which dependency is blocked, what it provides, and whether alternatives exist",
|
|
60
|
+
},
|
|
61
|
+
"blocked upstream": {
|
|
62
|
+
targetRole: "Orchestrator",
|
|
63
|
+
recovery: "reroute",
|
|
64
|
+
reason: "Upstream work is incomplete — Orchestrator must re-sequence",
|
|
65
|
+
requiredArtifact: "Updated chain with dependency ordering corrected",
|
|
66
|
+
handoffContext: "Identify which upstream deliverable is missing and which role was supposed to produce it",
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Technical infeasibility
|
|
70
|
+
"technical infeasibility": {
|
|
71
|
+
targetRole: "Backend Engineer",
|
|
72
|
+
recovery: "revise",
|
|
73
|
+
reason: "Proposed approach is technically infeasible — needs engineering assessment",
|
|
74
|
+
requiredArtifact: "Feasibility analysis with alternative approaches",
|
|
75
|
+
handoffContext: "Document what was attempted, why it failed, and what constraints make the original approach infeasible",
|
|
76
|
+
},
|
|
77
|
+
"architecture mismatch": {
|
|
78
|
+
targetRole: "Orchestrator",
|
|
79
|
+
recovery: "split",
|
|
80
|
+
reason: "Work doesn't fit the current architecture — may need decomposition",
|
|
81
|
+
requiredArtifact: "Architectural assessment and revised packet(s)",
|
|
82
|
+
handoffContext: "Explain the mismatch between the work and the architecture, and what would need to change",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Owner ambiguity
|
|
86
|
+
"owner ambiguity": {
|
|
87
|
+
targetRole: "Orchestrator",
|
|
88
|
+
recovery: "reroute",
|
|
89
|
+
reason: "Unclear which role owns this work — Orchestrator must assign",
|
|
90
|
+
requiredArtifact: "Clear role assignment with justification",
|
|
91
|
+
handoffContext: "Describe what the work is and which roles could plausibly own it",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ── Rejected work routing ─────────────────────────────────────────────────────
|
|
96
|
+
// Rejected work routes backward with intent.
|
|
97
|
+
|
|
98
|
+
const REJECTED_ROUTES = {
|
|
99
|
+
"quality bar miss": {
|
|
100
|
+
recovery: "retry",
|
|
101
|
+
reason: "Output did not meet the role's quality bar — send back to the producing role",
|
|
102
|
+
requiredArtifact: "Revised output addressing quality bar gaps",
|
|
103
|
+
handoffContext: "Cite the specific quality bar items that were not met",
|
|
104
|
+
targetRoleRule: "previous", // route to the role that produced the rejected output
|
|
105
|
+
},
|
|
106
|
+
"wrong artifact type": {
|
|
107
|
+
recovery: "revise",
|
|
108
|
+
reason: "Wrong deliverable type — role may have misunderstood the packet",
|
|
109
|
+
requiredArtifact: "Correct artifact type as specified in the packet",
|
|
110
|
+
handoffContext: "Clarify what artifact was expected vs what was produced",
|
|
111
|
+
targetRoleRule: "previous",
|
|
112
|
+
},
|
|
113
|
+
"incomplete evidence": {
|
|
114
|
+
recovery: "retry",
|
|
115
|
+
reason: "Evidence is incomplete — role must provide missing proof",
|
|
116
|
+
requiredArtifact: "Complete evidence set as required by the review contract",
|
|
117
|
+
handoffContext: "List exactly which evidence items are missing or insufficient",
|
|
118
|
+
targetRoleRule: "previous",
|
|
119
|
+
},
|
|
120
|
+
"role mismatch": {
|
|
121
|
+
recovery: "reroute",
|
|
122
|
+
reason: "This work was assigned to the wrong role — needs re-routing",
|
|
123
|
+
requiredArtifact: "Re-routed packet with correct role assignment",
|
|
124
|
+
handoffContext: "Explain why the current role is wrong and which role should own this",
|
|
125
|
+
targetRoleRule: "orchestrator",
|
|
126
|
+
},
|
|
127
|
+
"chain problem": {
|
|
128
|
+
recovery: "reroute",
|
|
129
|
+
reason: "The chain/ordering is wrong — Orchestrator must restructure",
|
|
130
|
+
requiredArtifact: "Revised chain with corrected ordering or role selection",
|
|
131
|
+
handoffContext: "Describe the chain problem and what the correct structure should be",
|
|
132
|
+
targetRoleRule: "orchestrator",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ── Conflict escalation routing ───────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const CONFLICT_ROUTES = {
|
|
139
|
+
blocking: {
|
|
140
|
+
targetRole: "Orchestrator",
|
|
141
|
+
recovery: "reroute",
|
|
142
|
+
reason: "Hard conflict in chain — cannot execute without restructuring",
|
|
143
|
+
requiredArtifact: "Revised chain with conflict resolved",
|
|
144
|
+
},
|
|
145
|
+
sequence: {
|
|
146
|
+
targetRole: "Orchestrator",
|
|
147
|
+
recovery: "reroute",
|
|
148
|
+
reason: "Sequence conflict — roles are in the wrong order",
|
|
149
|
+
requiredArtifact: "Reordered chain with correct role sequencing",
|
|
150
|
+
},
|
|
151
|
+
redundancy: {
|
|
152
|
+
targetRole: "Orchestrator",
|
|
153
|
+
recovery: "revise",
|
|
154
|
+
reason: "Redundant roles in chain — consider trimming or splitting",
|
|
155
|
+
requiredArtifact: "Trimmed chain or split into sub-packets",
|
|
156
|
+
},
|
|
157
|
+
coverage: {
|
|
158
|
+
targetRole: "Orchestrator",
|
|
159
|
+
recovery: "add-role",
|
|
160
|
+
reason: "Coverage gap — chain is missing a critical role",
|
|
161
|
+
requiredArtifact: "Updated chain with missing role added",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ── Split routing ─────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const SPLIT_ROUTE = {
|
|
168
|
+
targetRole: "Orchestrator",
|
|
169
|
+
recovery: "split",
|
|
170
|
+
reason: "Chain is too large (>7 roles) — needs decomposition into sub-packets",
|
|
171
|
+
requiredArtifact: "Decomposed packets, each with its own chain and scope",
|
|
172
|
+
handoffContext: "Identify natural seams in the work where splitting reduces complexity without creating integration debt",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ── Resolution engine ─────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a blocked verdict to an escalation target.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} blockedReason - Why the work is blocked (free text, matched against known patterns)
|
|
181
|
+
* @returns {EscalationResult}
|
|
182
|
+
*/
|
|
183
|
+
export function resolveBlocked(blockedReason) {
|
|
184
|
+
const lower = (blockedReason || "").toLowerCase();
|
|
185
|
+
|
|
186
|
+
// Try exact-ish matches first
|
|
187
|
+
for (const [pattern, route] of Object.entries(BLOCKED_ROUTES)) {
|
|
188
|
+
if (lower.includes(pattern)) {
|
|
189
|
+
return { ...route };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Default: Orchestrator owns unknown blocks
|
|
194
|
+
return {
|
|
195
|
+
targetRole: "Orchestrator",
|
|
196
|
+
recovery: "reroute",
|
|
197
|
+
reason: "Blocked for unrecognized reason — Orchestrator must assess and route",
|
|
198
|
+
requiredArtifact: "Assessment of block cause and recovery plan",
|
|
199
|
+
handoffContext: `Original block reason: "${blockedReason}"`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a rejected verdict to an escalation target.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} rejectedReason - Why the work was rejected
|
|
207
|
+
* @param {string} previousRole - The role that produced the rejected output
|
|
208
|
+
* @returns {EscalationResult}
|
|
209
|
+
*/
|
|
210
|
+
export function resolveRejected(rejectedReason, previousRole) {
|
|
211
|
+
const lower = (rejectedReason || "").toLowerCase();
|
|
212
|
+
|
|
213
|
+
for (const [pattern, route] of Object.entries(REJECTED_ROUTES)) {
|
|
214
|
+
if (lower.includes(pattern)) {
|
|
215
|
+
const target = route.targetRoleRule === "previous"
|
|
216
|
+
? (previousRole || "Orchestrator")
|
|
217
|
+
: route.targetRoleRule === "orchestrator"
|
|
218
|
+
? "Orchestrator"
|
|
219
|
+
: route.targetRoleRule;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
targetRole: target,
|
|
223
|
+
recovery: route.recovery,
|
|
224
|
+
reason: route.reason,
|
|
225
|
+
requiredArtifact: route.requiredArtifact,
|
|
226
|
+
handoffContext: route.handoffContext,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Default: route back to previous role for retry
|
|
232
|
+
return {
|
|
233
|
+
targetRole: previousRole || "Orchestrator",
|
|
234
|
+
recovery: "retry",
|
|
235
|
+
reason: "Rejected — route back to producing role for revision",
|
|
236
|
+
requiredArtifact: "Revised output addressing rejection reason",
|
|
237
|
+
handoffContext: `Original rejection reason: "${rejectedReason}"`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a conflict finding to an escalation target.
|
|
243
|
+
*
|
|
244
|
+
* @param {{type: string, severity: string, roles: string[], message: string, repair: string}} conflict
|
|
245
|
+
* @returns {EscalationResult}
|
|
246
|
+
*/
|
|
247
|
+
export function resolveConflict(conflict) {
|
|
248
|
+
const route = CONFLICT_ROUTES[conflict.type] || CONFLICT_ROUTES.blocking;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
targetRole: route.targetRole,
|
|
252
|
+
recovery: route.recovery,
|
|
253
|
+
reason: `${route.reason}: ${conflict.message}`,
|
|
254
|
+
requiredArtifact: route.requiredArtifact,
|
|
255
|
+
handoffContext: `Repair suggestion: ${conflict.repair}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve a split-needed chain to a decomposition owner.
|
|
261
|
+
*
|
|
262
|
+
* @param {number} chainSize
|
|
263
|
+
* @returns {EscalationResult}
|
|
264
|
+
*/
|
|
265
|
+
export function resolveSplit(chainSize) {
|
|
266
|
+
return {
|
|
267
|
+
...SPLIT_ROUTE,
|
|
268
|
+
handoffContext: `Current chain has ${chainSize} roles. ${SPLIT_ROUTE.handoffContext}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Format an escalation result for operator display.
|
|
274
|
+
*
|
|
275
|
+
* @param {EscalationResult} result
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
export function formatEscalation(result) {
|
|
279
|
+
const lines = [
|
|
280
|
+
` → ${result.targetRole} (${result.recovery})`,
|
|
281
|
+
` why: ${result.reason}`,
|
|
282
|
+
` must produce: ${result.requiredArtifact}`,
|
|
283
|
+
];
|
|
284
|
+
if (result.handoffContext) {
|
|
285
|
+
lines.push(` context: ${result.handoffContext}`);
|
|
286
|
+
}
|
|
287
|
+
return lines.join("\n");
|
|
288
|
+
}
|