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
package/src/replan.mjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Replanning — Phase P
|
|
3
|
+
*
|
|
4
|
+
* When reality changes mid-run, revise the plan without collapsing
|
|
5
|
+
* into manual rescue or restarting everything. Preserve valid work,
|
|
6
|
+
* reopen stale work, insert missing branches, keep the run truthful.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ARTIFACT_CONTRACTS, invalidateDownstream } from "./composite.mjs";
|
|
10
|
+
|
|
11
|
+
// ── Change events ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {"scope-change"|"artifact-changed"|"new-requirement"|"review-finding"|"dependency-discovered"|"priority-change"} ChangeType
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a structured change event.
|
|
19
|
+
*
|
|
20
|
+
* @param {ChangeType} type
|
|
21
|
+
* @param {object} detail
|
|
22
|
+
* @returns {object}
|
|
23
|
+
*/
|
|
24
|
+
export function createChangeEvent(type, detail) {
|
|
25
|
+
const VALID_TYPES = [
|
|
26
|
+
"scope-change",
|
|
27
|
+
"artifact-changed",
|
|
28
|
+
"new-requirement",
|
|
29
|
+
"review-finding",
|
|
30
|
+
"dependency-discovered",
|
|
31
|
+
"priority-change",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
if (!VALID_TYPES.includes(type)) {
|
|
35
|
+
throw new Error(`Invalid change type: "${type}". Valid types: ${VALID_TYPES.join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
type,
|
|
40
|
+
detail: {
|
|
41
|
+
description: detail.description || "",
|
|
42
|
+
affectedCategory: detail.affectedCategory || null,
|
|
43
|
+
newCategory: detail.newCategory || null,
|
|
44
|
+
newPack: detail.newPack || null,
|
|
45
|
+
insertAfter: detail.insertAfter || null,
|
|
46
|
+
invalidatesArtifacts: detail.invalidatesArtifacts || [],
|
|
47
|
+
...detail,
|
|
48
|
+
},
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Impact analysis ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Analyze the impact of a change event on a composite execution.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} exec - CompositeExecution from composite.mjs
|
|
59
|
+
* @param {object} changeEvent
|
|
60
|
+
* @returns {{
|
|
61
|
+
* validChildren: string[],
|
|
62
|
+
* staleChildren: string[],
|
|
63
|
+
* staleArtifacts: string[],
|
|
64
|
+
* needsNewChild: boolean,
|
|
65
|
+
* needsReorder: boolean,
|
|
66
|
+
* needsPackChange: boolean,
|
|
67
|
+
* summary: string,
|
|
68
|
+
* }}
|
|
69
|
+
*/
|
|
70
|
+
export function analyzeImpact(exec, changeEvent) {
|
|
71
|
+
const { type, detail } = changeEvent;
|
|
72
|
+
const validChildren = [];
|
|
73
|
+
const staleChildren = [];
|
|
74
|
+
const staleArtifacts = [];
|
|
75
|
+
let needsNewChild = false;
|
|
76
|
+
let needsReorder = false;
|
|
77
|
+
let needsPackChange = false;
|
|
78
|
+
|
|
79
|
+
switch (type) {
|
|
80
|
+
case "scope-change": {
|
|
81
|
+
// Scope change affects everything downstream of the affected category
|
|
82
|
+
const affected = detail.affectedCategory;
|
|
83
|
+
if (affected) {
|
|
84
|
+
for (const child of exec.children) {
|
|
85
|
+
if (child.category === affected || isDependentOn(exec, child.category, affected)) {
|
|
86
|
+
if (child.status === "completed") {
|
|
87
|
+
staleChildren.push(child.category);
|
|
88
|
+
staleArtifacts.push(...child.producedArtifacts);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (child.status === "completed") validChildren.push(child.category);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "artifact-changed": {
|
|
99
|
+
// Specific artifacts are invalidated
|
|
100
|
+
for (const artifactType of detail.invalidatesArtifacts) {
|
|
101
|
+
const producer = exec.artifactLedger.find(a => a.type === artifactType);
|
|
102
|
+
if (producer) {
|
|
103
|
+
staleArtifacts.push(artifactType);
|
|
104
|
+
// Find consumers of this artifact
|
|
105
|
+
for (const child of exec.children) {
|
|
106
|
+
if (child.receivedArtifacts?.includes(artifactType) && child.status === "completed") {
|
|
107
|
+
staleChildren.push(child.category);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Everything not stale is valid
|
|
113
|
+
for (const child of exec.children) {
|
|
114
|
+
if (child.status === "completed" && !staleChildren.includes(child.category)) {
|
|
115
|
+
validChildren.push(child.category);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "new-requirement": {
|
|
122
|
+
needsNewChild = !!detail.newCategory;
|
|
123
|
+
// Existing completed work stays valid unless the new requirement
|
|
124
|
+
// changes what they depend on
|
|
125
|
+
for (const child of exec.children) {
|
|
126
|
+
if (child.status === "completed") validChildren.push(child.category);
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case "review-finding": {
|
|
132
|
+
// A review finding typically requires inserting a new branch
|
|
133
|
+
needsNewChild = !!detail.newCategory;
|
|
134
|
+
// The category that produced the finding is valid (the finding came FROM it)
|
|
135
|
+
// Downstream might need revalidation
|
|
136
|
+
if (detail.affectedCategory) {
|
|
137
|
+
for (const child of exec.children) {
|
|
138
|
+
if (isDependentOn(exec, child.category, detail.affectedCategory) && child.status === "completed") {
|
|
139
|
+
staleChildren.push(child.category);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const child of exec.children) {
|
|
144
|
+
if (child.status === "completed" && !staleChildren.includes(child.category)) {
|
|
145
|
+
validChildren.push(child.category);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "dependency-discovered": {
|
|
152
|
+
// A missing prerequisite was found — may need reorder or new child
|
|
153
|
+
needsNewChild = !!detail.newCategory;
|
|
154
|
+
needsReorder = true;
|
|
155
|
+
for (const child of exec.children) {
|
|
156
|
+
if (child.status === "completed") validChildren.push(child.category);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "priority-change": {
|
|
162
|
+
needsReorder = true;
|
|
163
|
+
// Priority change doesn't invalidate completed work
|
|
164
|
+
for (const child of exec.children) {
|
|
165
|
+
if (child.status === "completed") validChildren.push(child.category);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const summaryParts = [];
|
|
172
|
+
if (validChildren.length > 0) summaryParts.push(`${validChildren.length} children still valid`);
|
|
173
|
+
if (staleChildren.length > 0) summaryParts.push(`${staleChildren.length} children stale`);
|
|
174
|
+
if (staleArtifacts.length > 0) summaryParts.push(`${staleArtifacts.length} artifacts invalidated`);
|
|
175
|
+
if (needsNewChild) summaryParts.push("new child packet needed");
|
|
176
|
+
if (needsReorder) summaryParts.push("reorder needed");
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
validChildren,
|
|
180
|
+
staleChildren,
|
|
181
|
+
staleArtifacts,
|
|
182
|
+
needsNewChild,
|
|
183
|
+
needsReorder,
|
|
184
|
+
needsPackChange,
|
|
185
|
+
summary: summaryParts.join("; ") || "No impact detected.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Selective replanning ──────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Apply a replan to a composite execution. Returns the plan diff.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} exec - CompositeExecution
|
|
195
|
+
* @param {object} impact - From analyzeImpact()
|
|
196
|
+
* @param {object} changeEvent
|
|
197
|
+
* @returns {{ actions: object[], diff: object }}
|
|
198
|
+
*/
|
|
199
|
+
export function replan(exec, impact, changeEvent) {
|
|
200
|
+
const actions = [];
|
|
201
|
+
const before = snapshotState(exec);
|
|
202
|
+
|
|
203
|
+
// 1. Invalidate stale children
|
|
204
|
+
if (impact.staleChildren.length > 0) {
|
|
205
|
+
for (const cat of impact.staleChildren) {
|
|
206
|
+
const child = exec.children.find(c => c.category === cat);
|
|
207
|
+
if (child && child.status === "completed") {
|
|
208
|
+
child.status = "pending";
|
|
209
|
+
child.result = null;
|
|
210
|
+
child.producedArtifacts = [];
|
|
211
|
+
child.completedAt = null;
|
|
212
|
+
|
|
213
|
+
// Remove stale artifacts
|
|
214
|
+
exec.artifactLedger = exec.artifactLedger.filter(a => a.fromCategory !== cat);
|
|
215
|
+
|
|
216
|
+
actions.push({
|
|
217
|
+
action: "invalidate",
|
|
218
|
+
category: cat,
|
|
219
|
+
reason: `Stale due to ${changeEvent.type}: ${changeEvent.detail.description}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 2. Insert new child if needed
|
|
226
|
+
if (impact.needsNewChild && changeEvent.detail.newCategory) {
|
|
227
|
+
const newCat = changeEvent.detail.newCategory;
|
|
228
|
+
const existing = exec.children.find(c => c.category === newCat);
|
|
229
|
+
|
|
230
|
+
if (!existing) {
|
|
231
|
+
const insertAfter = changeEvent.detail.insertAfter;
|
|
232
|
+
const contract = ARTIFACT_CONTRACTS[newCat];
|
|
233
|
+
|
|
234
|
+
const newChild = {
|
|
235
|
+
category: newCat,
|
|
236
|
+
pack: changeEvent.detail.newPack || newCat,
|
|
237
|
+
status: "pending",
|
|
238
|
+
dependsOn: insertAfter ? [insertAfter] : [],
|
|
239
|
+
producedArtifacts: [],
|
|
240
|
+
receivedArtifacts: [],
|
|
241
|
+
result: null,
|
|
242
|
+
blockInfo: null,
|
|
243
|
+
startedAt: null,
|
|
244
|
+
completedAt: null,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Insert after the specified position
|
|
248
|
+
if (insertAfter) {
|
|
249
|
+
const idx = exec.children.findIndex(c => c.category === insertAfter);
|
|
250
|
+
exec.children.splice(idx + 1, 0, newChild);
|
|
251
|
+
|
|
252
|
+
// Update dependencies: children that depended on insertAfter now depend on newCat
|
|
253
|
+
for (const child of exec.children) {
|
|
254
|
+
if (child.category !== newCat && child.dependsOn.includes(insertAfter)) {
|
|
255
|
+
child.dependsOn = child.dependsOn.map(d => d === insertAfter ? newCat : d);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// The new child depends on insertAfter
|
|
259
|
+
newChild.dependsOn = [insertAfter];
|
|
260
|
+
} else {
|
|
261
|
+
exec.children.push(newChild);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
exec.dependencyOrder = exec.children.map(c => c.category);
|
|
265
|
+
|
|
266
|
+
actions.push({
|
|
267
|
+
action: "insert",
|
|
268
|
+
category: newCat,
|
|
269
|
+
pack: newChild.pack,
|
|
270
|
+
insertAfter: insertAfter || "end",
|
|
271
|
+
reason: changeEvent.detail.description,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 3. Reorder if needed
|
|
277
|
+
if (impact.needsReorder && !impact.needsNewChild) {
|
|
278
|
+
// Simple reorder: move priority-changed category earlier
|
|
279
|
+
// For now, this is recorded as an action but the actual reorder
|
|
280
|
+
// requires operator confirmation since it may change semantics
|
|
281
|
+
actions.push({
|
|
282
|
+
action: "reorder-suggested",
|
|
283
|
+
reason: changeEvent.detail.description,
|
|
284
|
+
note: "Reorder requires operator confirmation.",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Reset parent status
|
|
289
|
+
if (exec.status === "completed") {
|
|
290
|
+
exec.status = "running";
|
|
291
|
+
exec.completedAt = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const after = snapshotState(exec);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
actions,
|
|
298
|
+
diff: buildDiff(before, after, changeEvent),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Plan diff ─────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
function snapshotState(exec) {
|
|
305
|
+
return {
|
|
306
|
+
childCount: exec.children.length,
|
|
307
|
+
categories: exec.children.map(c => c.category),
|
|
308
|
+
statuses: Object.fromEntries(exec.children.map(c => [c.category, c.status])),
|
|
309
|
+
artifactCount: exec.artifactLedger.length,
|
|
310
|
+
parentStatus: exec.status,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildDiff(before, after, changeEvent) {
|
|
315
|
+
const changes = [];
|
|
316
|
+
|
|
317
|
+
// New children
|
|
318
|
+
const added = after.categories.filter(c => !before.categories.includes(c));
|
|
319
|
+
if (added.length > 0) changes.push(`Added: ${added.join(", ")}`);
|
|
320
|
+
|
|
321
|
+
// Status changes
|
|
322
|
+
for (const cat of before.categories) {
|
|
323
|
+
if (before.statuses[cat] !== after.statuses[cat]) {
|
|
324
|
+
changes.push(`${cat}: ${before.statuses[cat]} → ${after.statuses[cat]}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Artifact changes
|
|
329
|
+
if (before.artifactCount !== after.artifactCount) {
|
|
330
|
+
changes.push(`Artifacts: ${before.artifactCount} → ${after.artifactCount}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
trigger: changeEvent.type,
|
|
335
|
+
description: changeEvent.detail.description,
|
|
336
|
+
changes,
|
|
337
|
+
preserved: before.categories.filter(c => before.statuses[c] === "completed" && after.statuses[c] === "completed"),
|
|
338
|
+
reopened: before.categories.filter(c => before.statuses[c] === "completed" && after.statuses[c] === "pending"),
|
|
339
|
+
inserted: added,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Format a plan diff for display.
|
|
345
|
+
*
|
|
346
|
+
* @param {object} result - From replan()
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
export function formatReplan(result) {
|
|
350
|
+
const lines = [
|
|
351
|
+
`\nAdaptive Replan`,
|
|
352
|
+
`───────────────`,
|
|
353
|
+
`Trigger: ${result.diff.trigger}`,
|
|
354
|
+
`Reason: ${result.diff.description}`,
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
if (result.actions.length === 0) {
|
|
358
|
+
lines.push(`\nNo changes needed.`);
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
lines.push(`\nActions taken:`);
|
|
363
|
+
for (const a of result.actions) {
|
|
364
|
+
if (a.action === "invalidate") {
|
|
365
|
+
lines.push(` ↻ ${a.category} — reopened (${a.reason})`);
|
|
366
|
+
} else if (a.action === "insert") {
|
|
367
|
+
lines.push(` + ${a.category} (${a.pack} pack) — inserted after ${a.insertAfter}`);
|
|
368
|
+
} else if (a.action === "reorder-suggested") {
|
|
369
|
+
lines.push(` ⚠ Reorder suggested — ${a.reason}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (result.diff.preserved.length > 0) {
|
|
374
|
+
lines.push(`\nPreserved (still valid):`);
|
|
375
|
+
for (const c of result.diff.preserved) {
|
|
376
|
+
lines.push(` ✓ ${c}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (result.diff.reopened.length > 0) {
|
|
381
|
+
lines.push(`\nReopened (stale):`);
|
|
382
|
+
for (const c of result.diff.reopened) {
|
|
383
|
+
lines.push(` ↻ ${c}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (result.diff.inserted.length > 0) {
|
|
388
|
+
lines.push(`\nInserted:`);
|
|
389
|
+
for (const c of result.diff.inserted) {
|
|
390
|
+
lines.push(` + ${c}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return lines.join("\n");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Helper ────────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
function isDependentOn(exec, category, upstream) {
|
|
400
|
+
const child = exec.children.find(c => c.category === category);
|
|
401
|
+
if (!child) return false;
|
|
402
|
+
if (child.dependsOn.includes(upstream)) return true;
|
|
403
|
+
return child.dependsOn.some(dep => isDependentOn(exec, dep, upstream));
|
|
404
|
+
}
|