role-os 1.2.0 → 1.4.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 +58 -0
- package/README.md +9 -2
- package/bin/roleos.mjs +23 -1
- package/package.json +1 -1
- package/src/calibration.mjs +292 -0
- package/src/composite.mjs +454 -0
- package/src/decompose.mjs +311 -0
- package/src/packs.mjs +33 -5
- package/src/replan.mjs +404 -0
- package/src/session.mjs +337 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite Execution — Phase O
|
|
3
|
+
*
|
|
4
|
+
* Takes a decomposed mixed task and runs child packets as one coherent
|
|
5
|
+
* operation: dependency-driven execution, artifact passing between
|
|
6
|
+
* children, branch recovery, and final synthesis.
|
|
7
|
+
*
|
|
8
|
+
* O1 — Dependency-driven child execution
|
|
9
|
+
* O2 — Branch recovery
|
|
10
|
+
* O3 — Final handoff synthesis
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Artifact contracts ────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Defines what each category produces and what it expects as input.
|
|
17
|
+
* This is the structure that makes cross-packet handoffs real instead
|
|
18
|
+
* of hoping the operator connects the dots.
|
|
19
|
+
*/
|
|
20
|
+
export const ARTIFACT_CONTRACTS = {
|
|
21
|
+
research: {
|
|
22
|
+
produces: ["research-brief", "recommendation", "evidence-summary"],
|
|
23
|
+
expects: [],
|
|
24
|
+
description: "Produces research findings and recommendations that downstream decisions consume.",
|
|
25
|
+
},
|
|
26
|
+
build: {
|
|
27
|
+
produces: ["implementation", "code-changes", "test-results"],
|
|
28
|
+
expects: ["scope-doc", "spec"],
|
|
29
|
+
description: "Produces working code with tests. Expects scope or spec from upstream.",
|
|
30
|
+
},
|
|
31
|
+
bugfix: {
|
|
32
|
+
produces: ["fix-implementation", "regression-tests", "root-cause-analysis"],
|
|
33
|
+
expects: ["bug-report", "reproduction-steps"],
|
|
34
|
+
description: "Produces a fix with regression tests. Expects a clear bug report.",
|
|
35
|
+
},
|
|
36
|
+
security: {
|
|
37
|
+
produces: ["security-findings", "threat-model", "remediation-plan"],
|
|
38
|
+
expects: ["implementation", "code-changes"],
|
|
39
|
+
description: "Produces security findings. Best when run after implementation exists.",
|
|
40
|
+
},
|
|
41
|
+
docs: {
|
|
42
|
+
produces: ["documentation", "handbook-pages", "changelog-entry"],
|
|
43
|
+
expects: ["implementation", "code-changes", "security-findings"],
|
|
44
|
+
description: "Produces docs from shipped work. Expects something to document.",
|
|
45
|
+
},
|
|
46
|
+
launch: {
|
|
47
|
+
produces: ["release-notes", "announcement-copy", "social-posts"],
|
|
48
|
+
expects: ["implementation", "documentation", "changelog-entry"],
|
|
49
|
+
description: "Produces launch copy. Expects shipped + documented work.",
|
|
50
|
+
},
|
|
51
|
+
treatment: {
|
|
52
|
+
produces: ["audit-report", "remediation-list", "release-readiness"],
|
|
53
|
+
expects: ["implementation"],
|
|
54
|
+
description: "Produces a ship-readiness assessment. Expects a repo with real content.",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ── Execution engine ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} ChildExecution
|
|
62
|
+
* @property {string} category
|
|
63
|
+
* @property {string} pack
|
|
64
|
+
* @property {"pending"|"ready"|"running"|"completed"|"blocked"|"failed"|"recovery"} status
|
|
65
|
+
* @property {string[]} dependsOn
|
|
66
|
+
* @property {string[]} producedArtifacts - Artifact types this child has produced
|
|
67
|
+
* @property {string[]} receivedArtifacts - Artifact types received from upstream
|
|
68
|
+
* @property {object|null} result - Outcome data when completed
|
|
69
|
+
* @property {object|null} blockInfo - Recovery info when blocked
|
|
70
|
+
* @property {string|null} startedAt
|
|
71
|
+
* @property {string|null} completedAt
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} CompositeExecution
|
|
76
|
+
* @property {string} parentId
|
|
77
|
+
* @property {"pending"|"running"|"completed"|"blocked"|"failed"} status
|
|
78
|
+
* @property {ChildExecution[]} children
|
|
79
|
+
* @property {string[]} dependencyOrder
|
|
80
|
+
* @property {object[]} artifactLedger - All artifacts passed between children
|
|
81
|
+
* @property {object[]} recoveryLog - All recovery events
|
|
82
|
+
* @property {string|null} startedAt
|
|
83
|
+
* @property {string|null} completedAt
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Initialize a composite execution from a run plan.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} runPlan - From decompose.createRunPlan()
|
|
90
|
+
* @returns {CompositeExecution}
|
|
91
|
+
*/
|
|
92
|
+
export function initExecution(runPlan) {
|
|
93
|
+
return {
|
|
94
|
+
parentId: runPlan.parentId,
|
|
95
|
+
status: "pending",
|
|
96
|
+
children: runPlan.children.map(child => ({
|
|
97
|
+
category: child.category,
|
|
98
|
+
pack: child.pack,
|
|
99
|
+
status: "pending",
|
|
100
|
+
dependsOn: child.dependsOn,
|
|
101
|
+
producedArtifacts: [],
|
|
102
|
+
receivedArtifacts: [],
|
|
103
|
+
result: null,
|
|
104
|
+
blockInfo: null,
|
|
105
|
+
startedAt: null,
|
|
106
|
+
completedAt: null,
|
|
107
|
+
})),
|
|
108
|
+
dependencyOrder: runPlan.dependencyOrder,
|
|
109
|
+
artifactLedger: [],
|
|
110
|
+
recoveryLog: [],
|
|
111
|
+
startedAt: null,
|
|
112
|
+
completedAt: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Advance the execution: find and return the next ready child.
|
|
118
|
+
* A child is ready when all its dependencies are completed and
|
|
119
|
+
* their required artifacts are available.
|
|
120
|
+
*
|
|
121
|
+
* @param {CompositeExecution} exec
|
|
122
|
+
* @returns {{ child: ChildExecution, artifacts: object[], reason: string } | null}
|
|
123
|
+
*/
|
|
124
|
+
export function advance(exec) {
|
|
125
|
+
if (exec.status === "completed" || exec.status === "failed") return null;
|
|
126
|
+
|
|
127
|
+
if (!exec.startedAt) exec.startedAt = new Date().toISOString();
|
|
128
|
+
exec.status = "running";
|
|
129
|
+
|
|
130
|
+
for (const child of exec.children) {
|
|
131
|
+
if (child.status !== "pending") continue;
|
|
132
|
+
|
|
133
|
+
// Check all dependencies are completed
|
|
134
|
+
const depsComplete = child.dependsOn.every(dep => {
|
|
135
|
+
const depChild = exec.children.find(c => c.category === dep);
|
|
136
|
+
return depChild && depChild.status === "completed";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!depsComplete) continue;
|
|
140
|
+
|
|
141
|
+
// Gather artifacts from upstream dependencies
|
|
142
|
+
const availableArtifacts = exec.artifactLedger.filter(a =>
|
|
143
|
+
child.dependsOn.includes(a.fromCategory)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Check artifact expectations
|
|
147
|
+
const contract = ARTIFACT_CONTRACTS[child.category];
|
|
148
|
+
const received = availableArtifacts.map(a => a.type);
|
|
149
|
+
child.receivedArtifacts = received;
|
|
150
|
+
|
|
151
|
+
// Mark as ready
|
|
152
|
+
child.status = "ready";
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
child,
|
|
156
|
+
artifacts: availableArtifacts,
|
|
157
|
+
reason: child.dependsOn.length === 0
|
|
158
|
+
? `${child.category} has no dependencies — ready to start.`
|
|
159
|
+
: `Dependencies complete (${child.dependsOn.join(", ")}). ${received.length} artifacts available.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mark a child as started (running).
|
|
168
|
+
*
|
|
169
|
+
* @param {CompositeExecution} exec
|
|
170
|
+
* @param {string} category
|
|
171
|
+
*/
|
|
172
|
+
export function startChild(exec, category) {
|
|
173
|
+
const child = exec.children.find(c => c.category === category);
|
|
174
|
+
if (!child) return;
|
|
175
|
+
child.status = "running";
|
|
176
|
+
child.startedAt = new Date().toISOString();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Complete a child with artifacts.
|
|
181
|
+
*
|
|
182
|
+
* @param {CompositeExecution} exec
|
|
183
|
+
* @param {string} category
|
|
184
|
+
* @param {{ artifacts: string[], result: object }} outcome
|
|
185
|
+
*/
|
|
186
|
+
export function completeChild(exec, category, outcome) {
|
|
187
|
+
const child = exec.children.find(c => c.category === category);
|
|
188
|
+
if (!child) return;
|
|
189
|
+
|
|
190
|
+
child.status = "completed";
|
|
191
|
+
child.completedAt = new Date().toISOString();
|
|
192
|
+
child.producedArtifacts = outcome.artifacts || [];
|
|
193
|
+
child.result = outcome.result || {};
|
|
194
|
+
|
|
195
|
+
// Record artifacts in the ledger
|
|
196
|
+
for (const artifactType of child.producedArtifacts) {
|
|
197
|
+
exec.artifactLedger.push({
|
|
198
|
+
type: artifactType,
|
|
199
|
+
fromCategory: category,
|
|
200
|
+
fromPack: child.pack,
|
|
201
|
+
producedAt: child.completedAt,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if all children complete
|
|
206
|
+
if (exec.children.every(c => c.status === "completed")) {
|
|
207
|
+
exec.status = "completed";
|
|
208
|
+
exec.completedAt = new Date().toISOString();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Branch recovery (O2) ──────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Block a child and create a recovery path.
|
|
216
|
+
* Downstream children stay paused. The blocked child gets a recovery plan.
|
|
217
|
+
*
|
|
218
|
+
* @param {CompositeExecution} exec
|
|
219
|
+
* @param {string} category
|
|
220
|
+
* @param {{ reason: string, recoveryType: "retry"|"reroute"|"escalate", targetRole?: string }} blockInfo
|
|
221
|
+
*/
|
|
222
|
+
export function blockChild(exec, category, blockInfo) {
|
|
223
|
+
const child = exec.children.find(c => c.category === category);
|
|
224
|
+
if (!child) return;
|
|
225
|
+
|
|
226
|
+
child.status = "blocked";
|
|
227
|
+
child.blockInfo = {
|
|
228
|
+
...blockInfo,
|
|
229
|
+
blockedAt: new Date().toISOString(),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Log the recovery event
|
|
233
|
+
exec.recoveryLog.push({
|
|
234
|
+
event: "child-blocked",
|
|
235
|
+
category,
|
|
236
|
+
reason: blockInfo.reason,
|
|
237
|
+
recoveryType: blockInfo.recoveryType,
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Mark parent as blocked
|
|
242
|
+
exec.status = "blocked";
|
|
243
|
+
|
|
244
|
+
// Downstream children that depend on this one stay pending (they can't advance)
|
|
245
|
+
// This is already handled by advance() checking dependency completion
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Recover a blocked child (retry it).
|
|
250
|
+
*
|
|
251
|
+
* @param {CompositeExecution} exec
|
|
252
|
+
* @param {string} category
|
|
253
|
+
*/
|
|
254
|
+
export function recoverChild(exec, category) {
|
|
255
|
+
const child = exec.children.find(c => c.category === category);
|
|
256
|
+
if (!child || child.status !== "blocked") return;
|
|
257
|
+
|
|
258
|
+
child.status = "pending";
|
|
259
|
+
child.blockInfo = null;
|
|
260
|
+
|
|
261
|
+
exec.recoveryLog.push({
|
|
262
|
+
event: "child-recovered",
|
|
263
|
+
category,
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// If no other children are blocked, parent returns to running
|
|
268
|
+
const anyBlocked = exec.children.some(c => c.status === "blocked");
|
|
269
|
+
if (!anyBlocked) {
|
|
270
|
+
exec.status = "running";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Fail a child permanently. Downstream children become unreachable.
|
|
276
|
+
*
|
|
277
|
+
* @param {CompositeExecution} exec
|
|
278
|
+
* @param {string} category
|
|
279
|
+
* @param {string} reason
|
|
280
|
+
*/
|
|
281
|
+
export function failChild(exec, category, reason) {
|
|
282
|
+
const child = exec.children.find(c => c.category === category);
|
|
283
|
+
if (!child) return;
|
|
284
|
+
|
|
285
|
+
child.status = "failed";
|
|
286
|
+
child.blockInfo = { reason, permanent: true };
|
|
287
|
+
|
|
288
|
+
exec.recoveryLog.push({
|
|
289
|
+
event: "child-failed",
|
|
290
|
+
category,
|
|
291
|
+
reason,
|
|
292
|
+
timestamp: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Check if any remaining children can still complete
|
|
296
|
+
// A child is unreachable if it directly or transitively depends on the failed one
|
|
297
|
+
const unreachable = findUnreachable(exec, category);
|
|
298
|
+
for (const cat of unreachable) {
|
|
299
|
+
const c = exec.children.find(ch => ch.category === cat);
|
|
300
|
+
if (c && c.status === "pending") {
|
|
301
|
+
c.status = "failed";
|
|
302
|
+
c.blockInfo = { reason: `Upstream dependency "${category}" failed.`, permanent: true };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
exec.status = "failed";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Find all categories that transitively depend on a given category.
|
|
311
|
+
*/
|
|
312
|
+
function findUnreachable(exec, failedCategory) {
|
|
313
|
+
const unreachable = new Set();
|
|
314
|
+
let changed = true;
|
|
315
|
+
while (changed) {
|
|
316
|
+
changed = false;
|
|
317
|
+
for (const child of exec.children) {
|
|
318
|
+
if (unreachable.has(child.category)) continue;
|
|
319
|
+
if (child.dependsOn.includes(failedCategory) || child.dependsOn.some(d => unreachable.has(d))) {
|
|
320
|
+
unreachable.add(child.category);
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return [...unreachable];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Invalidation ──────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Invalidate downstream children when an upstream change makes their
|
|
332
|
+
* artifacts stale. Resets completed children back to pending.
|
|
333
|
+
*
|
|
334
|
+
* @param {CompositeExecution} exec
|
|
335
|
+
* @param {string} category - The category whose output changed
|
|
336
|
+
*/
|
|
337
|
+
export function invalidateDownstream(exec, category) {
|
|
338
|
+
const downstream = findUnreachable(exec, category);
|
|
339
|
+
for (const cat of downstream) {
|
|
340
|
+
const child = exec.children.find(c => c.category === cat);
|
|
341
|
+
if (child && child.status === "completed") {
|
|
342
|
+
child.status = "pending";
|
|
343
|
+
child.result = null;
|
|
344
|
+
child.producedArtifacts = [];
|
|
345
|
+
child.completedAt = null;
|
|
346
|
+
|
|
347
|
+
// Remove stale artifacts from ledger
|
|
348
|
+
exec.artifactLedger = exec.artifactLedger.filter(a => a.fromCategory !== cat);
|
|
349
|
+
|
|
350
|
+
exec.recoveryLog.push({
|
|
351
|
+
event: "child-invalidated",
|
|
352
|
+
category: cat,
|
|
353
|
+
reason: `Upstream "${category}" changed — artifacts are stale.`,
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Parent goes back to running
|
|
360
|
+
if (exec.status === "completed") {
|
|
361
|
+
exec.status = "running";
|
|
362
|
+
exec.completedAt = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Final synthesis (O3) ──────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Synthesize child outcomes into a parent-level completion report.
|
|
370
|
+
*
|
|
371
|
+
* @param {CompositeExecution} exec
|
|
372
|
+
* @returns {object}
|
|
373
|
+
*/
|
|
374
|
+
export function synthesize(exec) {
|
|
375
|
+
const completed = exec.children.filter(c => c.status === "completed");
|
|
376
|
+
const blocked = exec.children.filter(c => c.status === "blocked");
|
|
377
|
+
const failed = exec.children.filter(c => c.status === "failed");
|
|
378
|
+
const pending = exec.children.filter(c => c.status === "pending" || c.status === "ready");
|
|
379
|
+
|
|
380
|
+
const allArtifacts = exec.artifactLedger.map(a => `${a.type} (from ${a.fromCategory})`);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
parentId: exec.parentId,
|
|
384
|
+
status: exec.status,
|
|
385
|
+
summary: {
|
|
386
|
+
total: exec.children.length,
|
|
387
|
+
completed: completed.length,
|
|
388
|
+
blocked: blocked.length,
|
|
389
|
+
failed: failed.length,
|
|
390
|
+
pending: pending.length,
|
|
391
|
+
},
|
|
392
|
+
completedChildren: completed.map(c => ({
|
|
393
|
+
category: c.category,
|
|
394
|
+
pack: c.pack,
|
|
395
|
+
artifacts: c.producedArtifacts,
|
|
396
|
+
})),
|
|
397
|
+
unresolvedBranches: [...blocked, ...failed].map(c => ({
|
|
398
|
+
category: c.category,
|
|
399
|
+
status: c.status,
|
|
400
|
+
reason: c.blockInfo?.reason || "unknown",
|
|
401
|
+
})),
|
|
402
|
+
allArtifacts,
|
|
403
|
+
recoveryEvents: exec.recoveryLog.length,
|
|
404
|
+
truthful: exec.status === "completed"
|
|
405
|
+
? "All child packets completed in dependency order."
|
|
406
|
+
: blocked.length > 0
|
|
407
|
+
? `${blocked.length} child packet(s) blocked. Parent cannot be marked complete.`
|
|
408
|
+
: failed.length > 0
|
|
409
|
+
? `${failed.length} child packet(s) failed. Downstream work was halted.`
|
|
410
|
+
: `${pending.length} child packet(s) still pending.`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Format the synthesis report for display.
|
|
416
|
+
*
|
|
417
|
+
* @param {object} synthesis
|
|
418
|
+
* @returns {string}
|
|
419
|
+
*/
|
|
420
|
+
export function formatSynthesis(synthesis) {
|
|
421
|
+
const lines = [
|
|
422
|
+
`\nComposite Execution Report`,
|
|
423
|
+
`──────────────────────────`,
|
|
424
|
+
`Parent: ${synthesis.parentId}`,
|
|
425
|
+
`Status: ${synthesis.status}`,
|
|
426
|
+
`Children: ${synthesis.summary.completed}/${synthesis.summary.total} completed`,
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
if (synthesis.completedChildren.length > 0) {
|
|
430
|
+
lines.push(`\nCompleted:`);
|
|
431
|
+
for (const c of synthesis.completedChildren) {
|
|
432
|
+
lines.push(` ✓ ${c.category} (${c.pack} pack) → ${c.artifacts.join(", ") || "no artifacts"}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (synthesis.unresolvedBranches.length > 0) {
|
|
437
|
+
lines.push(`\nUnresolved:`);
|
|
438
|
+
for (const b of synthesis.unresolvedBranches) {
|
|
439
|
+
lines.push(` ✗ ${b.category} — ${b.status}: ${b.reason}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (synthesis.allArtifacts.length > 0) {
|
|
444
|
+
lines.push(`\nArtifact ledger: ${synthesis.allArtifacts.join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (synthesis.recoveryEvents > 0) {
|
|
448
|
+
lines.push(`\nRecovery events: ${synthesis.recoveryEvents}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
lines.push(`\n${synthesis.truthful}`);
|
|
452
|
+
|
|
453
|
+
return lines.join("\n");
|
|
454
|
+
}
|