@chllming/wave-orchestration 0.7.3 → 0.8.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +14 -13
  3. package/docs/README.md +3 -1
  4. package/docs/agents/wave-cont-qa-role.md +1 -0
  5. package/docs/agents/wave-integration-role.md +1 -0
  6. package/docs/agents/wave-launcher-role.md +4 -1
  7. package/docs/agents/wave-orchestrator-role.md +5 -3
  8. package/docs/concepts/operating-modes.md +1 -1
  9. package/docs/concepts/runtime-agnostic-orchestration.md +5 -4
  10. package/docs/concepts/what-is-a-wave.md +12 -10
  11. package/docs/guides/author-and-run-waves.md +3 -3
  12. package/docs/plans/architecture-hardening-migration.md +206 -0
  13. package/docs/plans/current-state.md +5 -3
  14. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  15. package/docs/plans/master-plan.md +2 -2
  16. package/docs/plans/migration.md +12 -2
  17. package/docs/plans/wave-orchestrator.md +10 -8
  18. package/docs/reference/coordination-and-closure.md +8 -4
  19. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  20. package/docs/reference/sample-waves.md +4 -4
  21. package/docs/reference/skills.md +3 -0
  22. package/docs/reference/wave-control.md +4 -2
  23. package/docs/research/coordination-failure-review.md +4 -4
  24. package/docs/roadmap.md +1 -1
  25. package/package.json +1 -1
  26. package/releases/manifest.json +19 -0
  27. package/scripts/wave-orchestrator/agent-state.mjs +405 -89
  28. package/scripts/wave-orchestrator/contradiction-entity.mjs +487 -0
  29. package/scripts/wave-orchestrator/launcher-gates.mjs +79 -11
  30. package/scripts/wave-orchestrator/launcher-retry.mjs +36 -6
  31. package/scripts/wave-orchestrator/launcher.mjs +163 -2
  32. package/scripts/wave-orchestrator/task-entity.mjs +425 -51
  33. package/scripts/wave-orchestrator/wave-control-schema.mjs +2 -0
  34. package/scripts/wave-orchestrator/wave-state-reducer.mjs +260 -111
  35. package/skills/README.md +3 -0
  36. package/skills/repo-coding-rules/SKILL.md +1 -1
  37. package/skills/role-cont-qa/SKILL.md +2 -2
  38. package/skills/role-deploy/SKILL.md +1 -1
  39. package/skills/role-documentation/SKILL.md +1 -1
  40. package/skills/role-implementation/SKILL.md +1 -1
  41. package/skills/role-infra/SKILL.md +1 -1
  42. package/skills/role-integration/SKILL.md +2 -2
  43. package/skills/role-security/SKILL.md +1 -1
  44. package/skills/runtime-claude/SKILL.md +1 -1
  45. package/skills/runtime-codex/SKILL.md +1 -1
  46. package/skills/runtime-opencode/SKILL.md +1 -1
  47. package/skills/wave-core/SKILL.md +14 -6
  48. package/skills/wave-core/references/marker-syntax.md +1 -1
@@ -49,11 +49,19 @@ export const LEASE_STATES = new Set([
49
49
  "expired",
50
50
  ]);
51
51
 
52
+ export const TASK_STATUSES = new Set([
53
+ "pending",
54
+ "in_progress",
55
+ "proven",
56
+ "blocked",
57
+ "completed",
58
+ ]);
59
+
52
60
  const VALID_PRIORITIES = new Set(["low", "normal", "high", "urgent"]);
53
61
 
54
62
  const CLOSURE_TRANSITIONS = {
55
63
  open: new Set(["owned_slice_proven", "cancelled", "superseded"]),
56
- owned_slice_proven: new Set(["wave_closure_ready", "cancelled", "superseded"]),
64
+ owned_slice_proven: new Set(["open", "wave_closure_ready", "cancelled", "superseded"]),
57
65
  wave_closure_ready: new Set(["closed", "cancelled", "superseded"]),
58
66
  closed: new Set(),
59
67
  cancelled: new Set(),
@@ -88,6 +96,153 @@ function normalizePlainObject(value) {
88
96
  : null;
89
97
  }
90
98
 
99
+ function slugTaskScopeSegment(value, fallback = "item") {
100
+ const normalized = normalizeText(value, fallback)
101
+ .toLowerCase()
102
+ .replace(/[^a-z0-9._:-]+/g, "-")
103
+ .replace(/-+/g, "-")
104
+ .replace(/^-+|-+$/g, "");
105
+ return normalized || fallback;
106
+ }
107
+
108
+ /**
109
+ * Build a stable semantic task ID.
110
+ * Format: "wave-{waveNumber}:{agentId}:{scope}"
111
+ */
112
+ export function buildSemanticTaskId(waveNumber, agentId, scope) {
113
+ const safeWave = Number.isFinite(waveNumber) ? waveNumber : 0;
114
+ const safeAgent = normalizeText(agentId, "unassigned");
115
+ const safeScope = normalizeText(scope, "primary");
116
+ return `wave-${safeWave}:${safeAgent}:${safeScope}`;
117
+ }
118
+
119
+ function buildCoordinationTaskId({
120
+ waveNumber,
121
+ ownerAgentId,
122
+ taskType,
123
+ sourceRecordId,
124
+ title,
125
+ detail,
126
+ }) {
127
+ const sourceScope = normalizeText(sourceRecordId);
128
+ const fingerprint = sourceScope || crypto
129
+ .createHash("sha1")
130
+ .update(JSON.stringify({
131
+ taskType: normalizeText(taskType, "task"),
132
+ title: normalizeText(title),
133
+ detail: normalizeText(detail),
134
+ ownerAgentId: normalizeText(ownerAgentId, "system"),
135
+ waveNumber: Number.isFinite(waveNumber) ? waveNumber : 0,
136
+ }))
137
+ .digest("hex")
138
+ .slice(0, 12);
139
+ return buildSemanticTaskId(
140
+ waveNumber,
141
+ normalizeText(ownerAgentId, "system"),
142
+ `${slugTaskScopeSegment(taskType, "task")}-${slugTaskScopeSegment(fingerprint)}`,
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Compute a content hash (SHA256) for change detection over a task definition subset.
148
+ */
149
+ export function computeContentHash(definitionSubset) {
150
+ const payload = JSON.stringify(definitionSubset ?? {});
151
+ return crypto.createHash("sha256").update(payload).digest("hex");
152
+ }
153
+
154
+ /**
155
+ * Normalize a deliverable entry to the end-state schema: { path, exists, sha256 }.
156
+ */
157
+ function normalizeDeliverable(entry) {
158
+ if (typeof entry === "string") {
159
+ return { path: entry, exists: false, sha256: null };
160
+ }
161
+ if (entry && typeof entry === "object") {
162
+ return {
163
+ path: normalizeText(entry.path),
164
+ exists: entry.exists === true,
165
+ sha256: normalizeText(entry.sha256, null),
166
+ };
167
+ }
168
+ return { path: "", exists: false, sha256: null };
169
+ }
170
+
171
+ /**
172
+ * Normalize proofRequirements to end-state object shape.
173
+ * Accepts both legacy string[] and new object shape.
174
+ */
175
+ function normalizeProofRequirements(raw, defaults) {
176
+ // Already in new object shape
177
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
178
+ return {
179
+ proofLevel: normalizeText(raw.proofLevel, "unit"),
180
+ proofCentric: raw.proofCentric === true,
181
+ maturityTarget: normalizeText(raw.maturityTarget, null),
182
+ };
183
+ }
184
+ // Legacy string array: infer from contents
185
+ if (Array.isArray(raw) && raw.length > 0) {
186
+ return {
187
+ proofLevel: "unit",
188
+ proofCentric: raw.includes("proof-artifacts-present"),
189
+ maturityTarget: raw.includes("component-level-met") ? "component" : null,
190
+ };
191
+ }
192
+ // Default from defaults arg
193
+ if (defaults && typeof defaults === "object" && !Array.isArray(defaults)) {
194
+ return {
195
+ proofLevel: normalizeText(defaults.proofLevel, "unit"),
196
+ proofCentric: defaults.proofCentric === true,
197
+ maturityTarget: normalizeText(defaults.maturityTarget, null),
198
+ };
199
+ }
200
+ return { proofLevel: "unit", proofCentric: false, maturityTarget: null };
201
+ }
202
+
203
+ /**
204
+ * Normalize dependencyEdges to end-state shape: [{ taskId, kind, status }].
205
+ * Accepts both legacy { targetTaskId, kind } and new { taskId, kind, status } shapes.
206
+ */
207
+ function normalizeDependencyEdges(raw) {
208
+ if (!Array.isArray(raw)) {
209
+ return [];
210
+ }
211
+ return raw.map((edge) => ({
212
+ taskId: normalizeText(edge.taskId || edge.targetTaskId),
213
+ kind: normalizeText(edge.kind, "blocks"),
214
+ status: normalizeText(edge.status, "pending"),
215
+ }));
216
+ }
217
+
218
+ /**
219
+ * Normalize components array: [{ componentId, targetLevel }].
220
+ */
221
+ function normalizeComponents(raw) {
222
+ if (!Array.isArray(raw)) {
223
+ return [];
224
+ }
225
+ return raw
226
+ .filter((c) => c && typeof c === "object")
227
+ .map((c) => ({
228
+ componentId: normalizeText(c.componentId),
229
+ targetLevel: normalizeText(c.targetLevel, null),
230
+ }));
231
+ }
232
+
233
+ /**
234
+ * Derive components from componentTargets for backward compatibility.
235
+ */
236
+ function deriveComponentsFromTargets(componentTargets) {
237
+ if (!componentTargets || typeof componentTargets !== "object" || Array.isArray(componentTargets)) {
238
+ return [];
239
+ }
240
+ return Object.entries(componentTargets).map(([componentId, targetLevel]) => ({
241
+ componentId,
242
+ targetLevel: normalizeText(targetLevel, null),
243
+ }));
244
+ }
245
+
91
246
  export function normalizeTask(rawTask, defaults = {}) {
92
247
  if (!rawTask || typeof rawTask !== "object" || Array.isArray(rawTask)) {
93
248
  throw new Error("Task must be an object");
@@ -109,28 +264,82 @@ export function normalizeTask(rawTask, defaults = {}) {
109
264
  if (!VALID_PRIORITIES.has(priority)) {
110
265
  throw new Error(`priority must be one of ${[...VALID_PRIORITIES].join(", ")} (got: ${priority})`);
111
266
  }
267
+
268
+ // Status field (end-state P0-4)
269
+ const rawStatus = normalizeText(rawTask.status) || defaults.status || "pending";
270
+ const status = TASK_STATUSES.has(rawStatus) ? rawStatus : "pending";
271
+
112
272
  const now = toIsoTimestamp();
113
- const artifactContract = normalizePlainObject(rawTask.artifactContract) || {
114
- requiredPaths: [],
115
- proofArtifacts: [],
116
- exitContract: null,
117
- componentTargets: {},
273
+ const createdAt = normalizeText(rawTask.createdAt) || defaults.createdAt || now;
274
+ const updatedAt = normalizeText(rawTask.updatedAt) || defaults.updatedAt || createdAt;
275
+
276
+ // Normalize artifactContract with end-state deliverables shape
277
+ const rawContract = normalizePlainObject(rawTask.artifactContract) || {};
278
+ const deliverables = Array.isArray(rawContract.deliverables)
279
+ ? rawContract.deliverables.map(normalizeDeliverable)
280
+ : [];
281
+ const proofArtifacts = Array.isArray(rawContract.proofArtifacts)
282
+ ? rawContract.proofArtifacts
283
+ : (Array.isArray(rawContract.requiredPaths) ? [] : []);
284
+ const exitContract =
285
+ rawContract.exitContract && typeof rawContract.exitContract === "object"
286
+ ? { ...rawContract.exitContract }
287
+ : null;
288
+
289
+ // Also maintain backward-compat: if old shape had requiredPaths + proofArtifacts,
290
+ // keep requiredPaths in the contract for backward-compat readers
291
+ const requiredPaths = Array.isArray(rawContract.requiredPaths) ? rawContract.requiredPaths : [];
292
+ const componentTargets =
293
+ rawContract.componentTargets && typeof rawContract.componentTargets === "object"
294
+ && !Array.isArray(rawContract.componentTargets)
295
+ ? { ...rawContract.componentTargets }
296
+ : {};
297
+
298
+ const artifactContract = {
299
+ deliverables,
300
+ proofArtifacts,
301
+ exitContract,
302
+ requiredPaths,
303
+ componentTargets,
118
304
  };
119
- if (!Array.isArray(artifactContract.requiredPaths)) {
120
- artifactContract.requiredPaths = [];
121
- }
122
- if (!Array.isArray(artifactContract.proofArtifacts)) {
123
- artifactContract.proofArtifacts = [];
124
- }
125
- if (!artifactContract.exitContract || typeof artifactContract.exitContract !== "object") {
126
- artifactContract.exitContract = null;
127
- }
128
- if (!artifactContract.componentTargets || typeof artifactContract.componentTargets !== "object" || Array.isArray(artifactContract.componentTargets)) {
129
- artifactContract.componentTargets = {};
130
- }
305
+
306
+ // Version field
307
+ const version = typeof rawTask.version === "number" ? rawTask.version : 1;
308
+
309
+ // Wave number and lane
310
+ const waveNumber = typeof rawTask.waveNumber === "number" ? rawTask.waveNumber
311
+ : (typeof defaults.waveNumber === "number" ? defaults.waveNumber : null);
312
+ const lane = normalizeText(rawTask.lane) || defaults.lane || null;
313
+
314
+ // Content hash for change detection
315
+ const definitionSubset = {
316
+ taskType,
317
+ title: normalizeText(rawTask.title, defaults.title || ""),
318
+ ownerAgentId: normalizeText(rawTask.ownerAgentId) || defaults.ownerAgentId || null,
319
+ artifactContract,
320
+ };
321
+ const contentHash = normalizeText(rawTask.contentHash) || computeContentHash(definitionSubset);
322
+
323
+ // Components (end-state top-level field)
324
+ const components = Array.isArray(rawTask.components)
325
+ ? normalizeComponents(rawTask.components)
326
+ : deriveComponentsFromTargets(componentTargets);
327
+
328
+ // ProofRequirements as end-state object
329
+ const proofRequirements = normalizeProofRequirements(
330
+ rawTask.proofRequirements,
331
+ defaults.proofRequirements,
332
+ );
333
+
334
+ // Dependency edges with status
335
+ const dependencyEdges = normalizeDependencyEdges(rawTask.dependencyEdges);
131
336
 
132
337
  return {
133
338
  taskId,
339
+ version,
340
+ contentHash,
341
+ waveNumber,
342
+ lane,
134
343
  taskType,
135
344
  title: normalizeText(rawTask.title, defaults.title || ""),
136
345
  detail: normalizeText(rawTask.detail, defaults.detail || ""),
@@ -142,18 +351,15 @@ export function normalizeTask(rawTask, defaults = {}) {
142
351
  leaseExpiresAt: normalizeText(rawTask.leaseExpiresAt) || null,
143
352
  leaseHeartbeatAt: normalizeText(rawTask.leaseHeartbeatAt) || null,
144
353
  artifactContract,
145
- proofRequirements: normalizeStringArray(rawTask.proofRequirements || defaults.proofRequirements || []),
146
- dependencyEdges: Array.isArray(rawTask.dependencyEdges)
147
- ? rawTask.dependencyEdges.map((edge) => ({
148
- targetTaskId: normalizeText(edge.targetTaskId),
149
- kind: normalizeText(edge.kind, "blocks"),
150
- }))
151
- : [],
354
+ proofRequirements,
355
+ dependencyEdges,
356
+ components,
357
+ status,
152
358
  closureState,
153
359
  sourceRecordId: normalizeText(rawTask.sourceRecordId) || defaults.sourceRecordId || null,
154
360
  priority,
155
- createdAt: normalizeText(rawTask.createdAt) || defaults.createdAt || now,
156
- updatedAt: normalizeText(rawTask.updatedAt) || now,
361
+ createdAt,
362
+ updatedAt,
157
363
  };
158
364
  }
159
365
 
@@ -211,6 +417,25 @@ export function releaseLease(task) {
211
417
  };
212
418
  }
213
419
 
420
+ /**
421
+ * Expire a lease: transition from leased to expired.
422
+ */
423
+ export function expireLease(task) {
424
+ if (!task || typeof task !== "object") {
425
+ throw new Error("task must be an object");
426
+ }
427
+ if (task.leaseState !== "leased") {
428
+ throw new Error(`Cannot expire task ${task.taskId} in leaseState ${task.leaseState}`);
429
+ }
430
+ const now = toIsoTimestamp();
431
+ return {
432
+ ...task,
433
+ leaseState: "expired",
434
+ leaseExpiresAt: now,
435
+ updatedAt: now,
436
+ };
437
+ }
438
+
214
439
  export function heartbeatLease(task) {
215
440
  if (!task || typeof task !== "object") {
216
441
  throw new Error("task must be an object");
@@ -249,7 +474,12 @@ export function buildTasksFromWaveDefinition(waveDefinition, laneConfig = {}) {
249
474
  const contEvalAgentId = laneConfig.contEvalAgentId || "E0";
250
475
  const integrationAgentId = laneConfig.integrationAgentId || "A8";
251
476
  const documentationAgentId = laneConfig.documentationAgentId || "A9";
252
- const now = toIsoTimestamp();
477
+ const waveNumber = typeof waveDefinition.wave === "number" ? waveDefinition.wave : 0;
478
+ const lane = laneConfig.lane || "main";
479
+ const seededAt = normalizeText(
480
+ laneConfig.seededAt,
481
+ normalizeText(waveDefinition.generatedAt, "1970-01-01T00:00:00.000Z"),
482
+ );
253
483
  const tasks = [];
254
484
 
255
485
  for (const agent of agents) {
@@ -274,67 +504,121 @@ export function buildTasksFromWaveDefinition(waveDefinition, laneConfig = {}) {
274
504
  agent.componentTargets && typeof agent.componentTargets === "object"
275
505
  ? { ...agent.componentTargets }
276
506
  : {};
277
- const proofRequirements = [];
507
+
508
+ // Build end-state proofRequirements object
509
+ const proofLevel = exitContract?.proof || "unit";
510
+ let proofCentric = false;
511
+ let maturityTarget = null;
278
512
  if (taskType === "implementation") {
279
- proofRequirements.push("implementation-exit-met");
280
- if (Object.keys(componentTargets).length > 0) {
281
- proofRequirements.push("component-level-met");
282
- }
283
513
  if (Array.isArray(agent.deliverables) && agent.deliverables.length > 0) {
284
- proofRequirements.push("proof-artifacts-present");
514
+ proofCentric = true;
515
+ }
516
+ if (Object.keys(componentTargets).length > 0) {
517
+ maturityTarget = "component";
285
518
  }
286
519
  }
520
+
521
+ // Build deliverables in end-state shape: [{ path, exists, sha256 }]
522
+ const deliverables = Array.isArray(agent.deliverables)
523
+ ? agent.deliverables.map(normalizeDeliverable)
524
+ : [];
525
+
526
+ // Build proofArtifacts in existing shape
527
+ const proofArtifacts = Array.isArray(agent.deliverables)
528
+ ? agent.deliverables.map((deliverable) => ({
529
+ path: typeof deliverable === "string" ? deliverable : deliverable?.path || "",
530
+ kind: typeof deliverable === "object" ? deliverable?.kind || "file" : "file",
531
+ requiredFor: typeof deliverable === "object" ? deliverable?.requiredFor || null : null,
532
+ }))
533
+ : [];
534
+
535
+ // Components from componentTargets
536
+ const components = deriveComponentsFromTargets(componentTargets);
537
+
538
+ // Semantic task ID
539
+ const scope = agent.title
540
+ ? normalizeText(agent.title).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")
541
+ : "primary";
542
+ const semanticId = buildSemanticTaskId(waveNumber, agentId, scope);
543
+
287
544
  tasks.push(
288
545
  normalizeTask(
289
546
  {
547
+ taskId: semanticId,
290
548
  taskType,
291
549
  title: `${agentId}: ${agent.title || ""}`.trim(),
292
550
  detail: agent.detail || "",
293
551
  ownerAgentId: agentId,
294
552
  assigneeAgentId: agentId,
553
+ waveNumber,
554
+ lane,
295
555
  artifactContract: {
556
+ deliverables,
557
+ proofArtifacts,
296
558
  requiredPaths: normalizeStringArray(agent.ownedPaths || []),
297
- proofArtifacts: Array.isArray(agent.deliverables)
298
- ? agent.deliverables.map((deliverable) => ({
299
- path: typeof deliverable === "string" ? deliverable : deliverable?.path || "",
300
- kind: typeof deliverable === "object" ? deliverable?.kind || "file" : "file",
301
- requiredFor: typeof deliverable === "object" ? deliverable?.requiredFor || null : null,
302
- }))
303
- : [],
304
559
  exitContract,
305
560
  componentTargets,
306
561
  },
307
- proofRequirements,
562
+ proofRequirements: {
563
+ proofLevel,
564
+ proofCentric,
565
+ maturityTarget,
566
+ },
567
+ dependencyEdges: [],
568
+ components,
569
+ status: "pending",
308
570
  closureState: "open",
309
571
  priority:
310
572
  taskType === "implementation"
311
573
  ? "normal"
312
574
  : "high",
313
575
  },
314
- { createdAt: now },
576
+ {
577
+ createdAt: seededAt,
578
+ updatedAt: seededAt,
579
+ waveNumber,
580
+ lane,
581
+ },
315
582
  ),
316
583
  );
317
584
  }
318
585
 
319
586
  for (const promotion of waveDefinition.componentPromotions || []) {
587
+ const semanticId = buildSemanticTaskId(waveNumber, "system", `promote-${promotion.componentId}`);
320
588
  tasks.push(
321
589
  normalizeTask(
322
590
  {
591
+ taskId: semanticId,
323
592
  taskType: "component",
324
593
  title: `Promote ${promotion.componentId} to ${promotion.targetLevel}`,
325
594
  detail: "",
326
595
  ownerAgentId: null,
596
+ waveNumber,
597
+ lane,
327
598
  artifactContract: {
328
- requiredPaths: [promotion.componentId],
599
+ deliverables: [],
329
600
  proofArtifacts: [],
601
+ requiredPaths: [promotion.componentId],
330
602
  exitContract: null,
331
603
  componentTargets: { [promotion.componentId]: promotion.targetLevel },
332
604
  },
333
- proofRequirements: ["component-level-met"],
605
+ proofRequirements: {
606
+ proofLevel: "unit",
607
+ proofCentric: false,
608
+ maturityTarget: promotion.targetLevel,
609
+ },
610
+ dependencyEdges: [],
611
+ components: [{ componentId: promotion.componentId, targetLevel: promotion.targetLevel }],
612
+ status: "pending",
334
613
  closureState: "open",
335
614
  priority: "high",
336
615
  },
337
- { createdAt: now },
616
+ {
617
+ createdAt: seededAt,
618
+ updatedAt: seededAt,
619
+ waveNumber,
620
+ lane,
621
+ },
338
622
  ),
339
623
  );
340
624
  }
@@ -346,25 +630,41 @@ export function buildTasksFromCoordinationState(coordinationState, feedbackReque
346
630
  if (!coordinationState || typeof coordinationState !== "object") {
347
631
  return [];
348
632
  }
349
- const now = toIsoTimestamp();
633
+ const fallbackTimestamp = "1970-01-01T00:00:00.000Z";
350
634
  const tasks = [];
351
635
 
352
636
  for (const record of coordinationState.clarifications || []) {
353
637
  if (!isOpenCoordinationStatus(record.status)) {
354
638
  continue;
355
639
  }
640
+ const waveNumber = Number.isFinite(record.wave) ? record.wave : 0;
641
+ const lane = normalizeText(record.lane) || null;
642
+ const createdAt = normalizeText(record.createdAt, fallbackTimestamp);
643
+ const updatedAt = normalizeText(record.updatedAt, createdAt);
356
644
  tasks.push(
357
645
  normalizeTask(
358
646
  {
647
+ taskId: buildCoordinationTaskId({
648
+ waveNumber,
649
+ ownerAgentId: record.agentId,
650
+ taskType: "clarification",
651
+ sourceRecordId: record.id,
652
+ title: record.summary || record.id,
653
+ detail: record.detail || "",
654
+ }),
359
655
  taskType: "clarification",
360
656
  title: `Clarification: ${record.summary || record.id}`,
361
657
  detail: record.detail || "",
362
658
  ownerAgentId: record.agentId || null,
363
659
  sourceRecordId: record.id || null,
660
+ waveNumber,
661
+ lane,
364
662
  closureState: "open",
365
663
  priority: record.priority || "normal",
664
+ createdAt,
665
+ updatedAt,
366
666
  },
367
- { createdAt: now },
667
+ { createdAt, updatedAt, waveNumber, lane },
368
668
  ),
369
669
  );
370
670
  }
@@ -373,18 +673,34 @@ export function buildTasksFromCoordinationState(coordinationState, feedbackReque
373
673
  if (!isOpenCoordinationStatus(record.status)) {
374
674
  continue;
375
675
  }
676
+ const waveNumber = Number.isFinite(record.wave) ? record.wave : 0;
677
+ const lane = normalizeText(record.lane) || null;
678
+ const createdAt = normalizeText(record.createdAt, fallbackTimestamp);
679
+ const updatedAt = normalizeText(record.updatedAt, createdAt);
376
680
  tasks.push(
377
681
  normalizeTask(
378
682
  {
683
+ taskId: buildCoordinationTaskId({
684
+ waveNumber,
685
+ ownerAgentId: record.agentId,
686
+ taskType: "human-input",
687
+ sourceRecordId: record.id,
688
+ title: record.summary || record.id,
689
+ detail: record.detail || "",
690
+ }),
379
691
  taskType: "human-input",
380
692
  title: `Human feedback: ${record.summary || record.id}`,
381
693
  detail: record.detail || "",
382
694
  ownerAgentId: record.agentId || null,
383
695
  sourceRecordId: record.id || null,
696
+ waveNumber,
697
+ lane,
384
698
  closureState: "open",
385
699
  priority: record.priority || "high",
700
+ createdAt,
701
+ updatedAt,
386
702
  },
387
- { createdAt: now },
703
+ { createdAt, updatedAt, waveNumber, lane },
388
704
  ),
389
705
  );
390
706
  }
@@ -393,18 +709,34 @@ export function buildTasksFromCoordinationState(coordinationState, feedbackReque
393
709
  if (!isOpenCoordinationStatus(record.status)) {
394
710
  continue;
395
711
  }
712
+ const waveNumber = Number.isFinite(record.wave) ? record.wave : 0;
713
+ const lane = normalizeText(record.lane) || null;
714
+ const createdAt = normalizeText(record.createdAt, fallbackTimestamp);
715
+ const updatedAt = normalizeText(record.updatedAt, createdAt);
396
716
  tasks.push(
397
717
  normalizeTask(
398
718
  {
719
+ taskId: buildCoordinationTaskId({
720
+ waveNumber,
721
+ ownerAgentId: record.agentId,
722
+ taskType: "escalation",
723
+ sourceRecordId: record.id,
724
+ title: record.summary || record.id,
725
+ detail: record.detail || "",
726
+ }),
399
727
  taskType: "escalation",
400
728
  title: `Escalation: ${record.summary || record.id}`,
401
729
  detail: record.detail || "",
402
730
  ownerAgentId: record.agentId || null,
403
731
  sourceRecordId: record.id || null,
732
+ waveNumber,
733
+ lane,
404
734
  closureState: "open",
405
735
  priority: "urgent",
736
+ createdAt,
737
+ updatedAt,
406
738
  },
407
- { createdAt: now },
739
+ { createdAt, updatedAt, waveNumber, lane },
408
740
  ),
409
741
  );
410
742
  }
@@ -413,18 +745,34 @@ export function buildTasksFromCoordinationState(coordinationState, feedbackReque
413
745
  if (!isOpenCoordinationStatus(request.status || "open")) {
414
746
  continue;
415
747
  }
748
+ const waveNumber = Number.isFinite(request.wave) ? request.wave : 0;
749
+ const lane = normalizeText(request.lane) || null;
750
+ const createdAt = normalizeText(request.createdAt, fallbackTimestamp);
751
+ const updatedAt = normalizeText(request.updatedAt, createdAt);
416
752
  tasks.push(
417
753
  normalizeTask(
418
754
  {
755
+ taskId: buildCoordinationTaskId({
756
+ waveNumber,
757
+ ownerAgentId: request.agentId,
758
+ taskType: "human-input",
759
+ sourceRecordId: request.id,
760
+ title: request.summary || request.id || "",
761
+ detail: request.detail || "",
762
+ }),
419
763
  taskType: "human-input",
420
764
  title: `Feedback request: ${request.summary || request.id || ""}`,
421
765
  detail: request.detail || "",
422
766
  ownerAgentId: request.agentId || null,
423
767
  sourceRecordId: request.id || null,
768
+ waveNumber,
769
+ lane,
424
770
  closureState: "open",
425
771
  priority: request.priority || "high",
772
+ createdAt,
773
+ updatedAt,
426
774
  },
427
- { createdAt: now },
775
+ { createdAt, updatedAt, waveNumber, lane },
428
776
  ),
429
777
  );
430
778
  }
@@ -489,6 +837,28 @@ export function evaluateOwnedSliceProven(task, agentResult, proofBundles = []) {
489
837
  return { proven: true, reason: "Exit contract satisfied" };
490
838
  }
491
839
 
840
+ if (task.taskType === "component") {
841
+ // Component promotion task: validate that all relevant owners have promoted
842
+ const componentTargets = task.artifactContract?.componentTargets || {};
843
+ const componentIds = Object.keys(componentTargets);
844
+ if (componentIds.length === 0) {
845
+ return { proven: false, reason: "No component targets declared" };
846
+ }
847
+ const componentMarkers = new Map(
848
+ Array.isArray(agentResult?.components)
849
+ ? agentResult.components.map((component) => [component.componentId, component])
850
+ : [],
851
+ );
852
+ for (const componentId of componentIds) {
853
+ const expectedLevel = componentTargets[componentId];
854
+ const marker = componentMarkers.get(componentId);
855
+ if (!marker || marker.state !== "met" || (expectedLevel && marker.level !== expectedLevel)) {
856
+ return { proven: false, reason: `Component ${componentId} not promoted to ${expectedLevel || "target level"}` };
857
+ }
858
+ }
859
+ return { proven: true, reason: "Component promotion validated" };
860
+ }
861
+
492
862
  if (task.taskType === "cont-qa") {
493
863
  const validation = validateContQaSummary(agent, agentResult, { mode: "live" });
494
864
  return validation.ok
@@ -501,6 +871,10 @@ export function evaluateOwnedSliceProven(task, agentResult, proofBundles = []) {
501
871
  if (!evalValidation.ok) {
502
872
  return { proven: false, reason: evalValidation.detail || evalValidation.statusCode };
503
873
  }
874
+ // Differentiate: report-only vs implementation-owning cont-eval
875
+ if (isContEvalReportOnlyAgent(agent, { contEvalAgentId: agent.agentId })) {
876
+ return { proven: true, reason: "Cont-EVAL report-only satisfied" };
877
+ }
504
878
  if (isContEvalImplementationOwningAgent(agent, { contEvalAgentId: agent.agentId })) {
505
879
  const implValidation = validateImplementationSummary(agent, agentResult);
506
880
  if (!implValidation.ok) {
@@ -9,6 +9,8 @@ export const WAVE_CONTROL_ENTITY_TYPES = new Set([
9
9
  "rerun_request",
10
10
  "attempt",
11
11
  "human_input",
12
+ "contradiction",
13
+ "fact",
12
14
  "wave_run",
13
15
  "agent_run",
14
16
  "coordination_record",