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.
@@ -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
+ }