@chllming/wave-orchestration 0.5.4 → 0.6.1

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 (126) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/README.md +33 -5
  3. package/docs/README.md +18 -4
  4. package/docs/agents/wave-cont-eval-role.md +36 -0
  5. package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
  6. package/docs/agents/wave-documentation-role.md +1 -1
  7. package/docs/agents/wave-infra-role.md +1 -1
  8. package/docs/agents/wave-integration-role.md +3 -3
  9. package/docs/agents/wave-launcher-role.md +4 -3
  10. package/docs/agents/wave-security-role.md +40 -0
  11. package/docs/concepts/context7-vs-skills.md +1 -1
  12. package/docs/concepts/what-is-a-wave.md +56 -6
  13. package/docs/evals/README.md +166 -0
  14. package/docs/evals/benchmark-catalog.json +663 -0
  15. package/docs/guides/author-and-run-waves.md +135 -0
  16. package/docs/guides/planner.md +5 -0
  17. package/docs/guides/terminal-surfaces.md +2 -0
  18. package/docs/plans/component-cutover-matrix.json +1 -1
  19. package/docs/plans/component-cutover-matrix.md +1 -1
  20. package/docs/plans/current-state.md +19 -1
  21. package/docs/plans/examples/wave-example-live-proof.md +435 -0
  22. package/docs/plans/migration.md +42 -0
  23. package/docs/plans/wave-orchestrator.md +46 -7
  24. package/docs/plans/waves/wave-0.md +4 -4
  25. package/docs/reference/live-proof-waves.md +177 -0
  26. package/docs/reference/migration-0.2-to-0.5.md +26 -19
  27. package/docs/reference/npmjs-trusted-publishing.md +6 -5
  28. package/docs/reference/runtime-config/README.md +14 -4
  29. package/docs/reference/sample-waves.md +87 -0
  30. package/docs/reference/skills.md +110 -42
  31. package/docs/research/agent-context-sources.md +130 -11
  32. package/docs/research/coordination-failure-review.md +266 -0
  33. package/docs/roadmap.md +6 -2
  34. package/package.json +2 -2
  35. package/releases/manifest.json +35 -2
  36. package/scripts/research/agent-context-archive.mjs +83 -1
  37. package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
  38. package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
  39. package/scripts/wave-orchestrator/agent-state.mjs +358 -6
  40. package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
  41. package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
  42. package/scripts/wave-orchestrator/config.mjs +48 -12
  43. package/scripts/wave-orchestrator/context7.mjs +2 -0
  44. package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
  45. package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
  46. package/scripts/wave-orchestrator/coordination.mjs +83 -9
  47. package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
  48. package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
  49. package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
  50. package/scripts/wave-orchestrator/evals.mjs +451 -0
  51. package/scripts/wave-orchestrator/feedback.mjs +15 -1
  52. package/scripts/wave-orchestrator/install.mjs +32 -9
  53. package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
  54. package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
  55. package/scripts/wave-orchestrator/launcher.mjs +709 -601
  56. package/scripts/wave-orchestrator/ledger.mjs +123 -20
  57. package/scripts/wave-orchestrator/local-executor.mjs +99 -12
  58. package/scripts/wave-orchestrator/planner.mjs +177 -42
  59. package/scripts/wave-orchestrator/replay.mjs +6 -3
  60. package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
  61. package/scripts/wave-orchestrator/shared.mjs +75 -11
  62. package/scripts/wave-orchestrator/skills.mjs +637 -106
  63. package/scripts/wave-orchestrator/traces.mjs +71 -48
  64. package/scripts/wave-orchestrator/wave-files.mjs +947 -101
  65. package/scripts/wave.mjs +9 -0
  66. package/skills/README.md +202 -0
  67. package/skills/provider-aws/SKILL.md +111 -0
  68. package/skills/provider-aws/adapters/claude.md +1 -0
  69. package/skills/provider-aws/adapters/codex.md +1 -0
  70. package/skills/provider-aws/references/service-verification.md +39 -0
  71. package/skills/provider-aws/skill.json +50 -1
  72. package/skills/provider-custom-deploy/SKILL.md +59 -0
  73. package/skills/provider-custom-deploy/skill.json +46 -1
  74. package/skills/provider-docker-compose/SKILL.md +90 -0
  75. package/skills/provider-docker-compose/adapters/local.md +1 -0
  76. package/skills/provider-docker-compose/skill.json +49 -1
  77. package/skills/provider-github-release/SKILL.md +116 -1
  78. package/skills/provider-github-release/adapters/claude.md +1 -0
  79. package/skills/provider-github-release/adapters/codex.md +1 -0
  80. package/skills/provider-github-release/skill.json +51 -1
  81. package/skills/provider-kubernetes/SKILL.md +137 -0
  82. package/skills/provider-kubernetes/adapters/claude.md +1 -0
  83. package/skills/provider-kubernetes/adapters/codex.md +1 -0
  84. package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
  85. package/skills/provider-kubernetes/skill.json +48 -1
  86. package/skills/provider-railway/SKILL.md +118 -1
  87. package/skills/provider-railway/references/verification-commands.md +39 -0
  88. package/skills/provider-railway/skill.json +67 -1
  89. package/skills/provider-ssh-manual/SKILL.md +91 -0
  90. package/skills/provider-ssh-manual/skill.json +50 -1
  91. package/skills/repo-coding-rules/SKILL.md +84 -0
  92. package/skills/repo-coding-rules/skill.json +30 -1
  93. package/skills/role-cont-eval/SKILL.md +90 -0
  94. package/skills/role-cont-eval/adapters/codex.md +1 -0
  95. package/skills/role-cont-eval/skill.json +36 -0
  96. package/skills/role-cont-qa/SKILL.md +93 -0
  97. package/skills/role-cont-qa/adapters/claude.md +1 -0
  98. package/skills/role-cont-qa/skill.json +36 -0
  99. package/skills/role-deploy/SKILL.md +90 -0
  100. package/skills/role-deploy/skill.json +32 -1
  101. package/skills/role-documentation/SKILL.md +66 -0
  102. package/skills/role-documentation/skill.json +32 -1
  103. package/skills/role-implementation/SKILL.md +62 -0
  104. package/skills/role-implementation/skill.json +32 -1
  105. package/skills/role-infra/SKILL.md +74 -0
  106. package/skills/role-infra/skill.json +32 -1
  107. package/skills/role-integration/SKILL.md +79 -1
  108. package/skills/role-integration/skill.json +32 -1
  109. package/skills/role-research/SKILL.md +58 -0
  110. package/skills/role-research/skill.json +32 -1
  111. package/skills/role-security/SKILL.md +60 -0
  112. package/skills/role-security/skill.json +36 -0
  113. package/skills/runtime-claude/SKILL.md +60 -1
  114. package/skills/runtime-claude/skill.json +32 -1
  115. package/skills/runtime-codex/SKILL.md +52 -1
  116. package/skills/runtime-codex/skill.json +32 -1
  117. package/skills/runtime-local/SKILL.md +39 -0
  118. package/skills/runtime-local/skill.json +32 -1
  119. package/skills/runtime-opencode/SKILL.md +51 -0
  120. package/skills/runtime-opencode/skill.json +32 -1
  121. package/skills/wave-core/SKILL.md +107 -0
  122. package/skills/wave-core/references/marker-syntax.md +62 -0
  123. package/skills/wave-core/skill.json +31 -1
  124. package/wave.config.json +35 -6
  125. package/skills/role-evaluator/SKILL.md +0 -6
  126. package/skills/role-evaluator/skill.json +0 -5
@@ -3,12 +3,15 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
5
5
  DEFAULT_CODEX_SANDBOX_MODE,
6
+ DEFAULT_CONT_EVAL_AGENT_ID,
7
+ DEFAULT_CONT_EVAL_ROLE_PROMPT_PATH,
8
+ DEFAULT_CONT_QA_AGENT_ID,
9
+ DEFAULT_CONT_QA_ROLE_PROMPT_PATH,
6
10
  DEFAULT_DOCUMENTATION_AGENT_ID,
7
11
  DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH,
8
- DEFAULT_EVALUATOR_AGENT_ID,
9
- DEFAULT_EVALUATOR_ROLE_PROMPT_PATH,
10
12
  DEFAULT_INTEGRATION_AGENT_ID,
11
13
  DEFAULT_INTEGRATION_ROLE_PROMPT_PATH,
14
+ DEFAULT_SECURITY_ROLE_PROMPT_PATH,
12
15
  DEFAULT_WAVE_LANE,
13
16
  loadWaveConfig,
14
17
  normalizeCodexSandboxMode,
@@ -26,6 +29,7 @@ import {
26
29
  REPORT_VERDICT_REGEX,
27
30
  WAVE_VERDICT_REGEX,
28
31
  walkFiles,
32
+ toIsoTimestamp,
29
33
  writeJsonAtomic,
30
34
  } from "./shared.mjs";
31
35
  import { normalizeContext7Config, hashAgentPromptFingerprint } from "./context7.mjs";
@@ -35,19 +39,43 @@ import {
35
39
  readMaterializedCoordinationState,
36
40
  } from "./coordination-store.mjs";
37
41
  import {
42
+ agentSummaryPathFromStatusPath,
43
+ buildAgentExecutionSummary,
38
44
  normalizeExitContract,
39
45
  readAgentExecutionSummary,
46
+ validateContEvalSummary,
47
+ validateContQaSummary,
40
48
  validateDocumentationClosureSummary,
41
- validateEvaluatorSummary,
42
49
  validateExitContractShape,
43
50
  validateIntegrationSummary,
44
51
  validateImplementationSummary,
52
+ validateSecuritySummary,
53
+ writeAgentExecutionSummary,
45
54
  } from "./agent-state.mjs";
55
+ import { parseEvalTargets, validateEvalTargets } from "./evals.mjs";
46
56
  import { normalizeSkillId, resolveAgentSkills } from "./skills.mjs";
47
-
48
- export const WAVE_EVALUATOR_ROLE_PROMPT_PATH = DEFAULT_EVALUATOR_ROLE_PROMPT_PATH;
57
+ import {
58
+ isContEvalImplementationOwningAgent,
59
+ isContEvalReportOnlyAgent,
60
+ isContEvalReportPath,
61
+ isContQaReportPath,
62
+ isSecurityRolePromptPath,
63
+ isSecurityReviewAgent,
64
+ resolveSecurityReviewReportPath,
65
+ } from "./role-helpers.mjs";
66
+ import {
67
+ RUN_STATE_KIND,
68
+ RUN_STATE_SCHEMA_VERSION,
69
+ normalizeManifest,
70
+ readAssignmentSnapshot,
71
+ readDependencySnapshot,
72
+ } from "./artifact-schemas.mjs";
73
+
74
+ export const WAVE_CONT_QA_ROLE_PROMPT_PATH = DEFAULT_CONT_QA_ROLE_PROMPT_PATH;
75
+ export const WAVE_CONT_EVAL_ROLE_PROMPT_PATH = DEFAULT_CONT_EVAL_ROLE_PROMPT_PATH;
49
76
  export const WAVE_INTEGRATION_ROLE_PROMPT_PATH = DEFAULT_INTEGRATION_ROLE_PROMPT_PATH;
50
77
  export const WAVE_DOCUMENTATION_ROLE_PROMPT_PATH = DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH;
78
+ export const WAVE_SECURITY_ROLE_PROMPT_PATH = DEFAULT_SECURITY_ROLE_PROMPT_PATH;
51
79
  export const SHARED_PLAN_DOC_PATHS = [
52
80
  "docs/plans/current-state.md",
53
81
  "docs/plans/master-plan.md",
@@ -55,6 +83,22 @@ export const SHARED_PLAN_DOC_PATHS = [
55
83
  ];
56
84
 
57
85
  const COMPONENT_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
86
+ const COMPONENT_MATURITY_LEVELS = [
87
+ "inventoried",
88
+ "contract-frozen",
89
+ "repo-landed",
90
+ "baseline-proved",
91
+ "pilot-live",
92
+ "qa-proved",
93
+ "fleet-ready",
94
+ "cutover-ready",
95
+ "deprecation-ready",
96
+ ];
97
+ const COMPONENT_MATURITY_ORDER = Object.fromEntries(
98
+ COMPONENT_MATURITY_LEVELS.map((level, index) => [level, index]),
99
+ );
100
+ const PROOF_CENTRIC_COMPONENT_LEVEL = "pilot-live";
101
+ const RETRY_POLICY_VALUES = new Set(["sticky", "fallback-allowed"]);
58
102
 
59
103
  function resolveLaneProfileForOptions(options = {}) {
60
104
  if (options.laneProfile) {
@@ -64,6 +108,26 @@ function resolveLaneProfileForOptions(options = {}) {
64
108
  return resolveLaneProfile(config, options.lane || config.defaultLane || DEFAULT_WAVE_LANE);
65
109
  }
66
110
 
111
+ function resolveSecurityRolePromptPath(laneProfile) {
112
+ return laneProfile?.roles?.securityRolePromptPath || DEFAULT_SECURITY_ROLE_PROMPT_PATH;
113
+ }
114
+
115
+ function normalizeSecurityCapabilities(capabilities, rolePromptPaths, securityRolePromptPath) {
116
+ const normalized = Array.isArray(capabilities) ? [...capabilities] : [];
117
+ const hasSecurityRolePrompt = Array.isArray(rolePromptPaths)
118
+ ? rolePromptPaths.some((rolePromptPath) =>
119
+ isSecurityRolePromptPath(rolePromptPath, securityRolePromptPath),
120
+ )
121
+ : false;
122
+ if (
123
+ hasSecurityRolePrompt &&
124
+ !normalized.some((capability) => String(capability || "").trim().toLowerCase() === "security-review")
125
+ ) {
126
+ normalized.push("security-review");
127
+ }
128
+ return normalized;
129
+ }
130
+
67
131
  export function waveNumberFromFileName(fileName) {
68
132
  const match = fileName.match(/^wave-(\d+)\.md$/);
69
133
  if (!match) {
@@ -316,6 +380,147 @@ function validateAgentDeliverables(deliverables, ownedPaths, filePath, agentId)
316
380
  }
317
381
  }
318
382
 
383
+ function normalizeMaturityLevel(value, label, filePath) {
384
+ const normalized = String(value || "").trim();
385
+ if (!COMPONENT_MATURITY_ORDER.hasOwnProperty(normalized)) {
386
+ throw new Error(`Invalid maturity level "${value}" in ${label} (${filePath})`);
387
+ }
388
+ return normalized;
389
+ }
390
+
391
+ function proofCentricLevelReached(level) {
392
+ return (
393
+ COMPONENT_MATURITY_ORDER[String(level || "").trim()] >=
394
+ COMPONENT_MATURITY_ORDER[PROOF_CENTRIC_COMPONENT_LEVEL]
395
+ );
396
+ }
397
+
398
+ export function waveRequiresProofCentricValidation(wave) {
399
+ return Array.isArray(wave?.componentPromotions)
400
+ ? wave.componentPromotions.some((promotion) => proofCentricLevelReached(promotion?.targetLevel))
401
+ : false;
402
+ }
403
+
404
+ function agentHighestComponentTargetLevel(agent) {
405
+ const levels = Array.isArray(agent?.components)
406
+ ? agent.components
407
+ .map((componentId) => agent?.componentTargets?.[componentId] || null)
408
+ .filter(Boolean)
409
+ : [];
410
+ if (levels.length === 0) {
411
+ return null;
412
+ }
413
+ return levels.sort((left, right) => COMPONENT_MATURITY_ORDER[right] - COMPONENT_MATURITY_ORDER[left])[0];
414
+ }
415
+
416
+ export function agentRequiresProofCentricValidation(agent) {
417
+ const highestTarget = agentHighestComponentTargetLevel(agent);
418
+ if (highestTarget && proofCentricLevelReached(highestTarget)) {
419
+ return true;
420
+ }
421
+ return Array.isArray(agent?.proofArtifacts) && agent.proofArtifacts.some((artifact) => {
422
+ if (!Array.isArray(artifact?.requiredFor) || artifact.requiredFor.length === 0) {
423
+ return true;
424
+ }
425
+ return artifact.requiredFor.some((level) => proofCentricLevelReached(level));
426
+ });
427
+ }
428
+
429
+ function parseProofArtifacts(blockText, filePath, label) {
430
+ if (!blockText) {
431
+ return [];
432
+ }
433
+ const artifacts = [];
434
+ const seenPaths = new Set();
435
+ for (const line of String(blockText || "").split(/\r?\n/)) {
436
+ const trimmed = line.trim();
437
+ if (!trimmed) {
438
+ continue;
439
+ }
440
+ const bulletMatch = trimmed.match(/^-\s+(.+?)\s*$/);
441
+ if (!bulletMatch) {
442
+ throw new Error(`Malformed proof artifact entry "${trimmed}" in ${label} (${filePath})`);
443
+ }
444
+ const rawEntry = bulletMatch[1].trim();
445
+ let artifact = null;
446
+ if (!rawEntry.includes("|") && !/^path\s*:/i.test(rawEntry)) {
447
+ const relPath = rawEntry.replace(/[`"']/g, "").trim();
448
+ if (!isRepoContainedPath(relPath)) {
449
+ throw new Error(`Path "${relPath}" in ${label} (${filePath}) must stay within the repo root`);
450
+ }
451
+ artifact = {
452
+ path: relPath,
453
+ kind: null,
454
+ requiredFor: [],
455
+ };
456
+ } else {
457
+ const fields = {};
458
+ for (const segment of rawEntry.split("|")) {
459
+ const pair = segment.trim();
460
+ if (!pair) {
461
+ continue;
462
+ }
463
+ const separatorIndex = pair.indexOf(":");
464
+ if (separatorIndex <= 0) {
465
+ throw new Error(`Malformed proof artifact field "${pair}" in ${label} (${filePath})`);
466
+ }
467
+ const key = pair.slice(0, separatorIndex).trim().toLowerCase();
468
+ const value = pair
469
+ .slice(separatorIndex + 1)
470
+ .trim()
471
+ .replace(/^["'`]|["'`]$/g, "");
472
+ if (!key || !value) {
473
+ throw new Error(`Malformed proof artifact field "${pair}" in ${label} (${filePath})`);
474
+ }
475
+ fields[key] = value;
476
+ }
477
+ const relPath = String(fields.path || "").trim();
478
+ if (!relPath) {
479
+ throw new Error(`Proof artifact entry in ${label} (${filePath}) must include path`);
480
+ }
481
+ if (!isRepoContainedPath(relPath)) {
482
+ throw new Error(`Path "${relPath}" in ${label} (${filePath}) must stay within the repo root`);
483
+ }
484
+ const requiredFor = String(fields["required-for"] || "")
485
+ .split(",")
486
+ .map((entry) => entry.trim())
487
+ .filter(Boolean)
488
+ .map((entry) => normalizeMaturityLevel(entry, label, filePath));
489
+ artifact = {
490
+ path: relPath,
491
+ kind: String(fields.kind || "").trim() || null,
492
+ requiredFor,
493
+ };
494
+ }
495
+ if (normalizeRepoRelativePath(artifact.path).endsWith("/")) {
496
+ throw new Error(
497
+ `Proof artifact "${artifact.path}" in ${label} (${filePath}) must be a file path, not a directory path`,
498
+ );
499
+ }
500
+ if (seenPaths.has(artifact.path)) {
501
+ throw new Error(`Duplicate proof artifact "${artifact.path}" in ${label} (${filePath})`);
502
+ }
503
+ seenPaths.add(artifact.path);
504
+ artifacts.push(artifact);
505
+ }
506
+ return artifacts;
507
+ }
508
+
509
+ function validateAgentProofArtifacts(proofArtifacts, ownedPaths, filePath, agentId) {
510
+ if (!Array.isArray(proofArtifacts) || proofArtifacts.length === 0) {
511
+ return;
512
+ }
513
+ const owned = Array.isArray(ownedPaths) ? ownedPaths : [];
514
+ for (const artifact of proofArtifacts) {
515
+ const normalized = normalizeRepoRelativePath(artifact?.path);
516
+ if (!owned.some((ownedPath) => deliverableIsOwned(normalized, ownedPath))) {
517
+ throw new Error(
518
+ `Proof artifact "${artifact?.path}" for agent ${agentId} in ${filePath} must stay within the agent's declared file ownership`,
519
+ );
520
+ }
521
+ }
522
+ }
523
+
319
524
  function extractFencedBlock(blockText, messagePrefix) {
320
525
  const fencedBlockMatch = String(blockText || "").match(
321
526
  /```(?:[a-zA-Z0-9_-]+)?\r?\n([\s\S]*?)\r?\n```/,
@@ -466,6 +671,8 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
466
671
  fallbacks: [],
467
672
  tags: [],
468
673
  budget: null,
674
+ retryPolicy: null,
675
+ allowFallbackOnRetry: null,
469
676
  codex: null,
470
677
  claude: null,
471
678
  opencode: null,
@@ -476,6 +683,8 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
476
683
  "model",
477
684
  "fallbacks",
478
685
  "tags",
686
+ "retry-policy",
687
+ "allow-fallback-on-retry",
479
688
  "budget.turns",
480
689
  "budget.minutes",
481
690
  "codex.command",
@@ -530,6 +739,20 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
530
739
  );
531
740
  } else if (key === "tags") {
532
741
  executorConfig.tags = parseExecutorStringList(value);
742
+ } else if (key === "retry-policy") {
743
+ const normalizedPolicy = value.toLowerCase();
744
+ if (!RETRY_POLICY_VALUES.has(normalizedPolicy)) {
745
+ throw new Error(
746
+ `Invalid ${label}.retry-policy "${value}" in ${filePath}; expected sticky or fallback-allowed`,
747
+ );
748
+ }
749
+ executorConfig.retryPolicy = normalizedPolicy;
750
+ } else if (key === "allow-fallback-on-retry") {
751
+ executorConfig.allowFallbackOnRetry = parseExecutorBoolean(
752
+ value,
753
+ `${label}.allow-fallback-on-retry`,
754
+ filePath,
755
+ );
533
756
  } else if (key === "budget.turns" || key === "budget.minutes") {
534
757
  executorConfig.budget = {
535
758
  ...(executorConfig.budget || { turns: null, minutes: null }),
@@ -793,6 +1016,13 @@ export function extractAgentDeliverablesFromSection(sectionText, filePath, agent
793
1016
  return parsePathList(block, filePath, `agent ${agentId} deliverables`);
794
1017
  }
795
1018
 
1019
+ export function extractAgentProofArtifactsFromSection(sectionText, filePath, agentId) {
1020
+ const block = extractSectionBody(sectionText, "Proof artifacts", filePath, agentId, {
1021
+ required: false,
1022
+ });
1023
+ return parseProofArtifacts(block, filePath, `agent ${agentId} proof artifacts`);
1024
+ }
1025
+
796
1026
  export function slugify(value) {
797
1027
  return value
798
1028
  .toLowerCase()
@@ -890,21 +1120,102 @@ export function composeResolvedPrompt(rolePromptPaths, localPrompt, filePath, ag
890
1120
  .join("\n\n");
891
1121
  }
892
1122
 
893
- export function resolveEvaluatorReportPath(wave, options = {}) {
894
- const evaluatorAgentId = options.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
895
- const evaluator = wave?.agents?.find((agent) => agent.agentId === evaluatorAgentId);
896
- if (!evaluator) {
1123
+ function resolveAgentReportPath(wave, agentId, pattern) {
1124
+ const agent = wave?.agents?.find((entry) => entry.agentId === agentId);
1125
+ if (!agent) {
897
1126
  return null;
898
1127
  }
899
1128
  return (
900
- evaluator.ownedPaths.find((ownedPath) =>
901
- /(?:^|\/)(?:reviews?|.*evaluator).*\.(?:md|txt)$/i.test(ownedPath),
1129
+ agent.ownedPaths.find((ownedPath) =>
1130
+ pattern.test(ownedPath),
902
1131
  ) ??
903
- evaluator.ownedPaths[0] ??
1132
+ agent.ownedPaths[0] ??
904
1133
  null
905
1134
  );
906
1135
  }
907
1136
 
1137
+ export function resolveContQaReportPath(wave, options = {}) {
1138
+ const contQaAgentId = options.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
1139
+ return resolveAgentReportPath(
1140
+ wave,
1141
+ contQaAgentId,
1142
+ { test: isContQaReportPath },
1143
+ );
1144
+ }
1145
+
1146
+ export function resolveContEvalReportPath(wave, options = {}) {
1147
+ const contEvalAgentId = options.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
1148
+ return resolveAgentReportPath(
1149
+ wave,
1150
+ contEvalAgentId,
1151
+ { test: isContEvalReportPath },
1152
+ );
1153
+ }
1154
+
1155
+ function isImplementationOwningWaveAgent(
1156
+ agent,
1157
+ {
1158
+ contQaAgentId,
1159
+ contEvalAgentId,
1160
+ integrationAgentId,
1161
+ documentationAgentId,
1162
+ securityRolePromptPath,
1163
+ },
1164
+ ) {
1165
+ return (
1166
+ ![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
1167
+ !isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
1168
+ !isSecurityReviewAgent(agent, { securityRolePromptPath })
1169
+ );
1170
+ }
1171
+
1172
+ function resolveAgentSummaryReportPath(wave, agentId, { contQaAgentId, contEvalAgentId } = {}) {
1173
+ if (agentId === contQaAgentId && wave.contQaReportPath) {
1174
+ return path.resolve(REPO_ROOT, wave.contQaReportPath);
1175
+ }
1176
+ if (agentId === contEvalAgentId && wave.contEvalReportPath) {
1177
+ return path.resolve(REPO_ROOT, wave.contEvalReportPath);
1178
+ }
1179
+ const agent = wave?.agents?.find((entry) => entry.agentId === agentId);
1180
+ if (isSecurityReviewAgent(agent)) {
1181
+ const securityReportPath = resolveSecurityReviewReportPath(agent);
1182
+ if (securityReportPath) {
1183
+ return path.resolve(REPO_ROOT, securityReportPath);
1184
+ }
1185
+ }
1186
+ return null;
1187
+ }
1188
+
1189
+ function materializeLiveExecutionSummaryIfMissing({
1190
+ wave,
1191
+ agent,
1192
+ statusPath,
1193
+ statusRecord,
1194
+ logsDir,
1195
+ contQaAgentId,
1196
+ contEvalAgentId,
1197
+ }) {
1198
+ const existing = readAgentExecutionSummary(statusPath);
1199
+ if (existing) {
1200
+ return existing;
1201
+ }
1202
+ const logPath = logsDir ? path.join(logsDir, `wave-${wave.wave}-${agent.slug}.log`) : null;
1203
+ if (!statusRecord || !logPath || !fs.existsSync(logPath)) {
1204
+ return null;
1205
+ }
1206
+ const summary = buildAgentExecutionSummary({
1207
+ agent,
1208
+ statusRecord,
1209
+ logPath,
1210
+ reportPath: resolveAgentSummaryReportPath(wave, agent.agentId, {
1211
+ contQaAgentId,
1212
+ contEvalAgentId,
1213
+ }),
1214
+ });
1215
+ writeAgentExecutionSummary(statusPath, summary);
1216
+ return summary;
1217
+ }
1218
+
908
1219
  function normalizeMatrixStringArray(values, label, filePath) {
909
1220
  if (!Array.isArray(values)) {
910
1221
  return [];
@@ -1049,11 +1360,13 @@ export function requiredDocumentationStewardPathsForWave(waveNumber, options = {
1049
1360
  export function validateWaveDefinition(wave, options = {}) {
1050
1361
  const laneProfile = resolveLaneProfileForOptions(options);
1051
1362
  const lane = laneProfile.lane;
1052
- const evaluatorAgentId = laneProfile.roles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1363
+ const contQaAgentId = laneProfile.roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
1364
+ const contEvalAgentId = laneProfile.roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
1053
1365
  const integrationAgentId =
1054
1366
  laneProfile.roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
1055
1367
  const documentationAgentId =
1056
1368
  laneProfile.roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
1369
+ const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
1057
1370
  const documentationThreshold = laneProfile.validation.requireDocumentationStewardFromWave;
1058
1371
  const context7Threshold = laneProfile.validation.requireContext7DeclarationsFromWave;
1059
1372
  const exitContractThreshold = laneProfile.validation.requireExitContractsFromWave;
@@ -1094,10 +1407,27 @@ export function validateWaveDefinition(wave, options = {}) {
1094
1407
  if (duplicateAgentIds.length > 0) {
1095
1408
  errors.push(`must not repeat agent ids (${Array.from(new Set(duplicateAgentIds)).join(", ")})`);
1096
1409
  }
1097
- if (!wave.agents.some((agent) => agent.agentId === evaluatorAgentId)) {
1098
- errors.push(`must include Agent ${evaluatorAgentId} as the running evaluator`);
1410
+ const contEvalAgent = wave.agents.find((agent) => agent.agentId === contEvalAgentId) || null;
1411
+ const contEvalImplementationOwning = contEvalAgent
1412
+ ? isContEvalImplementationOwningAgent(contEvalAgent, { contEvalAgentId })
1413
+ : false;
1414
+ const implementationOwningAgents = wave.agents.filter((agent) =>
1415
+ isImplementationOwningWaveAgent(agent, {
1416
+ contQaAgentId,
1417
+ contEvalAgentId,
1418
+ integrationAgentId,
1419
+ documentationAgentId,
1420
+ securityRolePromptPath,
1421
+ }),
1422
+ );
1423
+ if (!wave.agents.some((agent) => agent.agentId === contQaAgentId)) {
1424
+ errors.push(`must include Agent ${contQaAgentId} as the cont-QA closure role`);
1099
1425
  }
1100
- if (componentPromotionRuleActive && promotedComponents.size === 0) {
1426
+ if (
1427
+ componentPromotionRuleActive &&
1428
+ promotedComponents.size === 0 &&
1429
+ implementationOwningAgents.length > 0
1430
+ ) {
1101
1431
  errors.push(
1102
1432
  `Wave ${wave.wave} must declare a ## Component promotions section in waves ${componentPromotionThreshold} and later`,
1103
1433
  );
@@ -1203,7 +1533,11 @@ export function validateWaveDefinition(wave, options = {}) {
1203
1533
  );
1204
1534
  }
1205
1535
  }
1206
- if ([evaluatorAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId)) {
1536
+ if (
1537
+ [contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) ||
1538
+ isContEvalReportOnlyAgent(agent, { contEvalAgentId }) ||
1539
+ isSecurityReviewAgent(agent, { securityRolePromptPath })
1540
+ ) {
1207
1541
  if (Array.isArray(agent.components) && agent.components.length > 0) {
1208
1542
  errors.push(`Agent ${agent.agentId} must not declare a ### Components section`);
1209
1543
  }
@@ -1230,7 +1564,11 @@ export function validateWaveDefinition(wave, options = {}) {
1230
1564
  }
1231
1565
  }
1232
1566
  if (exitContractThreshold !== null && wave.wave >= exitContractThreshold) {
1233
- if (![evaluatorAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId)) {
1567
+ if (
1568
+ ![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
1569
+ !isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
1570
+ !isSecurityReviewAgent(agent, { securityRolePromptPath })
1571
+ ) {
1234
1572
  if (!agent.exitContract) {
1235
1573
  errors.push(
1236
1574
  `Agent ${agent.agentId} must declare a ### Exit contract section in waves ${exitContractThreshold} and later`,
@@ -1245,22 +1583,77 @@ export function validateWaveDefinition(wave, options = {}) {
1245
1583
  }
1246
1584
  }
1247
1585
  }
1586
+ if (
1587
+ agentRequiresProofCentricValidation(agent) &&
1588
+ (!Array.isArray(agent.proofArtifacts) || agent.proofArtifacts.length === 0) &&
1589
+ ![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
1590
+ !isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
1591
+ !isSecurityReviewAgent(agent, { securityRolePromptPath })
1592
+ ) {
1593
+ errors.push(
1594
+ `Agent ${agent.agentId} must declare a ### Proof artifacts section when it targets ${PROOF_CENTRIC_COMPONENT_LEVEL} or above`,
1595
+ );
1596
+ }
1597
+ if (
1598
+ agentRequiresProofCentricValidation(agent) &&
1599
+ (agent.executorConfig?.id === "local" || agent.executorResolved?.id === "local")
1600
+ ) {
1601
+ errors.push(
1602
+ `Agent ${agent.agentId} must not use executor=local when it carries proof-centric validation artifacts`,
1603
+ );
1604
+ }
1248
1605
  }
1249
1606
  for (const agent of wave.agents) {
1250
- for (const requiredRef of laneProfile.validation.requiredPromptReferences) {
1607
+ for (const requiredRef of laneProfile.validation.requiredPromptReferences || []) {
1251
1608
  if (!agent.prompt.includes(requiredRef)) {
1252
1609
  errors.push(`Agent ${agent.agentId} must reference ${requiredRef}`);
1253
1610
  }
1254
1611
  }
1255
1612
  }
1256
- const evaluator = wave.agents.find((agent) => agent.agentId === evaluatorAgentId);
1257
- if (!evaluator?.rolePromptPaths?.includes(laneProfile.roles.evaluatorRolePromptPath)) {
1613
+ const contQaAgent = wave.agents.find((agent) => agent.agentId === contQaAgentId);
1614
+ if (!contQaAgent?.rolePromptPaths?.includes(laneProfile.roles.contQaRolePromptPath)) {
1258
1615
  errors.push(
1259
- `Agent ${evaluatorAgentId} must import ${laneProfile.roles.evaluatorRolePromptPath}`,
1616
+ `Agent ${contQaAgentId} must import ${laneProfile.roles.contQaRolePromptPath}`,
1260
1617
  );
1261
1618
  }
1262
- if (!resolveEvaluatorReportPath(wave, { evaluatorAgentId })) {
1263
- errors.push(`Agent ${evaluatorAgentId} must own an evaluator report path`);
1619
+ if (!resolveContQaReportPath(wave, { contQaAgentId })) {
1620
+ errors.push(`Agent ${contQaAgentId} must own a cont-QA report path`);
1621
+ }
1622
+ if (contEvalAgent) {
1623
+ if (!contEvalAgent.rolePromptPaths?.includes(laneProfile.roles.contEvalRolePromptPath)) {
1624
+ errors.push(
1625
+ `Agent ${contEvalAgentId} must import ${laneProfile.roles.contEvalRolePromptPath}`,
1626
+ );
1627
+ }
1628
+ if (!resolveContEvalReportPath(wave, { contEvalAgentId })) {
1629
+ errors.push(`Agent ${contEvalAgentId} must own a cont-EVAL report path`);
1630
+ }
1631
+ if (!Array.isArray(wave.evalTargets) || wave.evalTargets.length === 0) {
1632
+ errors.push(`Wave ${wave.wave} must declare a ## Eval targets section when ${contEvalAgentId} is present`);
1633
+ } else {
1634
+ try {
1635
+ validateEvalTargets(wave.evalTargets, {
1636
+ benchmarkCatalogPath: laneProfile.paths.benchmarkCatalogPath,
1637
+ });
1638
+ } catch (error) {
1639
+ errors.push(error.message);
1640
+ }
1641
+ }
1642
+ } else if (Array.isArray(wave.evalTargets) && wave.evalTargets.length > 0) {
1643
+ errors.push(`Wave ${wave.wave} declares ## Eval targets but does not include Agent ${contEvalAgentId}`);
1644
+ }
1645
+ const securityReviewers = wave.agents.filter((agent) =>
1646
+ isSecurityReviewAgent(agent, { securityRolePromptPath }),
1647
+ );
1648
+ for (const securityReviewer of securityReviewers) {
1649
+ if (!securityReviewer.rolePromptPaths?.includes(securityRolePromptPath)) {
1650
+ errors.push(
1651
+ `Security reviewer ${securityReviewer.agentId} must import ${securityRolePromptPath}`,
1652
+ );
1653
+ }
1654
+ if (!resolveSecurityReviewReportPath(securityReviewer)) {
1655
+ errors.push(`Security reviewer ${securityReviewer.agentId} must own a security review report path`);
1656
+ }
1264
1657
  }
1265
1658
  if (integrationRuleActive) {
1266
1659
  const integrationStewards = wave.agents.filter((agent) =>
@@ -1317,8 +1710,13 @@ export function validateWaveDefinition(wave, options = {}) {
1317
1710
  }
1318
1711
  for (const [componentId, owners] of componentOwners.entries()) {
1319
1712
  if (owners.size === 0) {
1713
+ const requiredOwnerIds = [
1714
+ contQaAgentId,
1715
+ ...(contEvalImplementationOwning ? [] : [contEvalAgentId]),
1716
+ documentationAgentId,
1717
+ ];
1320
1718
  errors.push(
1321
- `Wave ${wave.wave} must assign promoted component "${componentId}" to at least one non-${evaluatorAgentId}/${documentationAgentId} agent`,
1719
+ `Wave ${wave.wave} must assign promoted component "${componentId}" to at least one non-${requiredOwnerIds.join("/")} agent`,
1322
1720
  );
1323
1721
  }
1324
1722
  }
@@ -1330,6 +1728,7 @@ export function validateWaveDefinition(wave, options = {}) {
1330
1728
 
1331
1729
  export function parseWaveContent(content, filePath, options = {}) {
1332
1730
  const laneProfile = resolveLaneProfileForOptions(options);
1731
+ const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
1333
1732
  const fileName = path.basename(filePath);
1334
1733
  const waveNumber = waveNumberFromFileName(fileName);
1335
1734
  const commitMessageMatch = content.match(/\*\*Commit message\*\*:\s*`([^`]+)`/);
@@ -1358,13 +1757,22 @@ export function parseWaveContent(content, filePath, options = {}) {
1358
1757
  const exitContract = extractExitContractFromSection(sectionText, filePath, current.agentId);
1359
1758
  const executorConfig = extractExecutorConfigFromSection(sectionText, filePath, current.agentId);
1360
1759
  const components = extractAgentComponentsFromSection(sectionText, filePath, current.agentId);
1361
- const capabilities = extractAgentCapabilitiesFromSection(sectionText, filePath, current.agentId);
1760
+ const capabilities = normalizeSecurityCapabilities(
1761
+ extractAgentCapabilitiesFromSection(sectionText, filePath, current.agentId),
1762
+ rolePromptPaths,
1763
+ securityRolePromptPath,
1764
+ );
1362
1765
  const skills = extractAgentSkillsFromSection(sectionText, filePath, current.agentId);
1363
1766
  const deliverables = extractAgentDeliverablesFromSection(
1364
1767
  sectionText,
1365
1768
  filePath,
1366
1769
  current.agentId,
1367
1770
  );
1771
+ const proofArtifacts = extractAgentProofArtifactsFromSection(
1772
+ sectionText,
1773
+ filePath,
1774
+ current.agentId,
1775
+ );
1368
1776
  const promptOverlay = extractPromptFromSection(sectionText, filePath, current.agentId);
1369
1777
  const prompt = composeResolvedPrompt(
1370
1778
  rolePromptPaths,
@@ -1377,6 +1785,7 @@ export function parseWaveContent(content, filePath, options = {}) {
1377
1785
  );
1378
1786
  const ownedPaths = extractOwnedPaths(promptOverlay);
1379
1787
  validateAgentDeliverables(deliverables, ownedPaths, filePath, current.agentId);
1788
+ validateAgentProofArtifacts(proofArtifacts, ownedPaths, filePath, current.agentId);
1380
1789
  agents.push({
1381
1790
  agentId: current.agentId,
1382
1791
  title: current.title,
@@ -1391,6 +1800,7 @@ export function parseWaveContent(content, filePath, options = {}) {
1391
1800
  capabilities,
1392
1801
  skills,
1393
1802
  deliverables,
1803
+ proofArtifacts,
1394
1804
  ownedPaths,
1395
1805
  });
1396
1806
  }
@@ -1416,12 +1826,22 @@ export function parseWaveContent(content, filePath, options = {}) {
1416
1826
  }),
1417
1827
  filePath,
1418
1828
  ),
1829
+ evalTargets: parseEvalTargets(
1830
+ extractTopLevelSectionBody(content, "Eval targets", filePath, {
1831
+ required: false,
1832
+ }),
1833
+ filePath,
1834
+ ),
1419
1835
  context7Defaults: extractWaveContext7Defaults(content, filePath),
1420
1836
  componentPromotions,
1421
1837
  agents: agentsWithComponentTargets,
1422
- evaluatorReportPath: resolveEvaluatorReportPath(
1838
+ contQaReportPath: resolveContQaReportPath(
1839
+ { agents: agentsWithComponentTargets },
1840
+ { contQaAgentId: laneProfile.roles.contQaAgentId },
1841
+ ),
1842
+ contEvalReportPath: resolveContEvalReportPath(
1423
1843
  { agents: agentsWithComponentTargets },
1424
- { evaluatorAgentId: laneProfile.roles.evaluatorAgentId },
1844
+ { contEvalAgentId: laneProfile.roles.contEvalAgentId },
1425
1845
  ),
1426
1846
  };
1427
1847
  }
@@ -1459,8 +1879,11 @@ function mergeExecutorSections(baseSection, profileSection, inlineSection, array
1459
1879
  }
1460
1880
 
1461
1881
  function inferAgentRuntimeRole(agent, laneProfile) {
1462
- if (agent?.agentId === laneProfile.roles.evaluatorAgentId) {
1463
- return "evaluator";
1882
+ if (agent?.agentId === laneProfile.roles.contQaAgentId) {
1883
+ return "cont-qa";
1884
+ }
1885
+ if (agent?.agentId === laneProfile.roles.contEvalAgentId) {
1886
+ return "cont-eval";
1464
1887
  }
1465
1888
  if (agent?.agentId === laneProfile.roles.integrationAgentId) {
1466
1889
  return "integration";
@@ -1468,6 +1891,9 @@ function inferAgentRuntimeRole(agent, laneProfile) {
1468
1891
  if (agent?.agentId === laneProfile.roles.documentationAgentId) {
1469
1892
  return "documentation";
1470
1893
  }
1894
+ if (isSecurityReviewAgent(agent)) {
1895
+ return "security";
1896
+ }
1471
1897
  const capabilities = Array.isArray(agent?.capabilities)
1472
1898
  ? agent.capabilities.map((entry) => String(entry || "").trim().toLowerCase())
1473
1899
  : [];
@@ -1535,6 +1961,8 @@ export function resolveAgentExecutor(agent, options = {}) {
1535
1961
  const laneProfile = resolveLaneProfileForOptions(options);
1536
1962
  const executorConfig = agent?.executorConfig || null;
1537
1963
  const role = inferAgentRuntimeRole(agent, laneProfile);
1964
+ const proofCentricAgent =
1965
+ agentRequiresProofCentricValidation(agent) || waveRequiresProofCentricValidation(options.wave);
1538
1966
  const profileName = executorConfig?.profile || null;
1539
1967
  if (profileName && !laneProfile.executors.profiles?.[profileName]) {
1540
1968
  throw new Error(
@@ -1574,12 +2002,31 @@ export function resolveAgentExecutor(agent, options = {}) {
1574
2002
  profile?.fallbacks,
1575
2003
  executorConfig?.fallbacks,
1576
2004
  );
2005
+ const explicitAllowFallback =
2006
+ executorConfig?.allowFallbackOnRetry ??
2007
+ profile?.allowFallbackOnRetry ??
2008
+ null;
2009
+ const explicitRetryPolicy =
2010
+ executorConfig?.retryPolicy ||
2011
+ profile?.retryPolicy ||
2012
+ null;
2013
+ const allowFallbackOnRetry =
2014
+ explicitAllowFallback !== null
2015
+ ? explicitAllowFallback
2016
+ : explicitRetryPolicy
2017
+ ? explicitRetryPolicy !== "sticky"
2018
+ : !proofCentricAgent;
2019
+ const retryPolicy =
2020
+ explicitRetryPolicy ||
2021
+ (allowFallbackOnRetry ? "fallback-allowed" : "sticky");
1577
2022
  const runtimeFallbacks =
1578
- fallbacks.length > 0
2023
+ allowFallbackOnRetry && fallbacks.length > 0
1579
2024
  ? fallbacks
1580
- : (laneProfile.runtimePolicy?.fallbackExecutorOrder || []).filter(
2025
+ : allowFallbackOnRetry
2026
+ ? (laneProfile.runtimePolicy?.fallbackExecutorOrder || []).filter(
1581
2027
  (candidate) => candidate !== executorId,
1582
- );
2028
+ )
2029
+ : [];
1583
2030
  const runtimeTags = mergeUniqueStringArrays(profile?.tags, executorConfig?.tags);
1584
2031
  const runtimeBudget = {
1585
2032
  turns:
@@ -1600,6 +2047,8 @@ export function resolveAgentExecutor(agent, options = {}) {
1600
2047
  selectedBy,
1601
2048
  fallbacks: runtimeFallbacks,
1602
2049
  tags: runtimeTags,
2050
+ retryPolicy,
2051
+ allowFallbackOnRetry,
1603
2052
  budget:
1604
2053
  runtimeBudget.turns !== null || runtimeBudget.minutes !== null ? runtimeBudget : null,
1605
2054
  fallbackUsed: false,
@@ -1742,12 +2191,12 @@ export function buildManifest(lanePaths, waves) {
1742
2191
  })
1743
2192
  .toSorted((a, b) => a.path.localeCompare(b.path));
1744
2193
 
1745
- return {
2194
+ return normalizeManifest({
1746
2195
  generatedAt: new Date().toISOString(),
1747
2196
  source: `${path.relative(REPO_ROOT, lanePaths.docsDir).replaceAll(path.sep, "/")}/**/*`,
1748
2197
  waves,
1749
2198
  docs,
1750
- };
2199
+ });
1751
2200
  }
1752
2201
 
1753
2202
  export function validateWaveComponentPromotions(wave, summariesByAgentId = {}, options = {}) {
@@ -1762,21 +2211,43 @@ export function validateWaveComponentPromotions(wave, summariesByAgentId = {}, o
1762
2211
  };
1763
2212
  }
1764
2213
  const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
2214
+ const roles = laneProfile.roles || {};
2215
+ const contQaAgentId = roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
2216
+ const contEvalAgentId = roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
2217
+ const integrationAgentId = roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
2218
+ const documentationAgentId =
2219
+ roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
2220
+ const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
2221
+ const implementationOwningAgents = (wave.agents || []).filter((agent) =>
2222
+ isImplementationOwningWaveAgent(agent, {
2223
+ contQaAgentId,
2224
+ contEvalAgentId,
2225
+ integrationAgentId,
2226
+ documentationAgentId,
2227
+ securityRolePromptPath,
2228
+ }),
2229
+ );
1765
2230
  if (promotions.length === 0) {
1766
2231
  return {
1767
- ok: false,
1768
- statusCode: "missing-component-promotions",
1769
- detail: `Wave ${wave.wave} is missing component promotions.`,
2232
+ ok: implementationOwningAgents.length === 0,
2233
+ statusCode:
2234
+ implementationOwningAgents.length === 0 ? "pass" : "missing-component-promotions",
2235
+ detail:
2236
+ implementationOwningAgents.length === 0
2237
+ ? `Wave ${wave.wave} has no implementation-owned component promotions to prove.`
2238
+ : `Wave ${wave.wave} is missing component promotions.`,
1770
2239
  componentId: null,
1771
2240
  };
1772
2241
  }
1773
- const roles = laneProfile.roles || {};
1774
- const evaluatorAgentId = roles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1775
- const documentationAgentId =
1776
- roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
1777
2242
  const satisfied = new Set();
1778
2243
  for (const agent of wave.agents) {
1779
- if ([evaluatorAgentId, documentationAgentId].includes(agent.agentId)) {
2244
+ if (!isImplementationOwningWaveAgent(agent, {
2245
+ contQaAgentId,
2246
+ contEvalAgentId,
2247
+ integrationAgentId,
2248
+ documentationAgentId,
2249
+ securityRolePromptPath,
2250
+ })) {
1780
2251
  continue;
1781
2252
  }
1782
2253
  const summary = summariesByAgentId[agent.agentId] || null;
@@ -1815,14 +2286,33 @@ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
1815
2286
  const laneProfile = resolveLaneProfileForOptions(options);
1816
2287
  const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
1817
2288
  const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
2289
+ const roles = laneProfile.roles || {};
2290
+ const contQaAgentId = roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
2291
+ const contEvalAgentId = roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
2292
+ const integrationAgentId = roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
2293
+ const documentationAgentId = roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
2294
+ const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
2295
+ const implementationOwningAgents = (wave.agents || []).filter((agent) =>
2296
+ isImplementationOwningWaveAgent(agent, {
2297
+ contQaAgentId,
2298
+ contEvalAgentId,
2299
+ integrationAgentId,
2300
+ documentationAgentId,
2301
+ securityRolePromptPath,
2302
+ }),
2303
+ );
1818
2304
  if (
1819
2305
  promotions.length === 0 &&
1820
- (componentThreshold === null || wave.wave < componentThreshold)
2306
+ ((componentThreshold === null || wave.wave < componentThreshold) ||
2307
+ implementationOwningAgents.length === 0)
1821
2308
  ) {
1822
2309
  return {
1823
2310
  ok: true,
1824
2311
  statusCode: "pass",
1825
- detail: "Component current-level gate is not active for this wave.",
2312
+ detail:
2313
+ implementationOwningAgents.length === 0
2314
+ ? `Wave ${wave.wave} has no implementation-owned component promotions to reconcile.`
2315
+ : "Component current-level gate is not active for this wave.",
1826
2316
  componentId: null,
1827
2317
  };
1828
2318
  }
@@ -1861,7 +2351,7 @@ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
1861
2351
  }
1862
2352
 
1863
2353
  export function writeManifest(manifestPath, manifest) {
1864
- writeJsonAtomic(manifestPath, manifest);
2354
+ writeJsonAtomic(manifestPath, normalizeManifest(manifest));
1865
2355
  }
1866
2356
 
1867
2357
  export function normalizeCompletedWaves(values) {
@@ -1877,28 +2367,253 @@ export function normalizeCompletedWaves(values) {
1877
2367
  ).toSorted((a, b) => a - b);
1878
2368
  }
1879
2369
 
1880
- export function readRunState(runStatePath) {
1881
- const payload = readJsonOrNull(runStatePath);
2370
+ function fileHashOrNull(filePath) {
2371
+ if (!filePath || !fs.existsSync(filePath)) {
2372
+ return null;
2373
+ }
2374
+ return hashText(fs.readFileSync(filePath, "utf8"));
2375
+ }
2376
+
2377
+ function relativeRepoPathOrNull(filePath) {
2378
+ return filePath ? path.relative(REPO_ROOT, filePath) : null;
2379
+ }
2380
+
2381
+ function normalizeRunStateWaveEntry(rawEntry, waveNumber) {
2382
+ const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
2383
+ const normalizedWave = normalizeCompletedWaves([waveNumber])[0] ?? normalizeCompletedWaves([source.wave])[0] ?? null;
2384
+ return {
2385
+ wave: normalizedWave,
2386
+ currentState: String(source.currentState || "completed").trim().toLowerCase() || "completed",
2387
+ lastTransitionAt:
2388
+ typeof source.lastTransitionAt === "string"
2389
+ ? source.lastTransitionAt
2390
+ : typeof source.updatedAt === "string"
2391
+ ? source.updatedAt
2392
+ : typeof source.completedAt === "string"
2393
+ ? source.completedAt
2394
+ : null,
2395
+ lastSource: typeof source.lastSource === "string" ? source.lastSource : null,
2396
+ lastReasonCode: typeof source.lastReasonCode === "string" ? source.lastReasonCode : null,
2397
+ lastDetail: typeof source.lastDetail === "string" ? source.lastDetail : "",
2398
+ lastEvidence:
2399
+ source.lastEvidence && typeof source.lastEvidence === "object" && !Array.isArray(source.lastEvidence)
2400
+ ? source.lastEvidence
2401
+ : null,
2402
+ };
2403
+ }
2404
+
2405
+ function normalizeRunStateHistoryEntry(rawEntry, seqFallback) {
2406
+ const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
2407
+ return {
2408
+ seq: normalizeCompletedWaves([source.seq])[0] ?? seqFallback,
2409
+ at: typeof source.at === "string" ? source.at : toIsoTimestamp(),
2410
+ wave: normalizeCompletedWaves([source.wave])[0] ?? null,
2411
+ fromState: typeof source.fromState === "string" ? source.fromState : null,
2412
+ toState: typeof source.toState === "string" ? source.toState : null,
2413
+ source: typeof source.source === "string" ? source.source : null,
2414
+ reasonCode: typeof source.reasonCode === "string" ? source.reasonCode : null,
2415
+ detail: typeof source.detail === "string" ? source.detail : "",
2416
+ evidence:
2417
+ source.evidence && typeof source.evidence === "object" && !Array.isArray(source.evidence)
2418
+ ? source.evidence
2419
+ : null,
2420
+ };
2421
+ }
2422
+
2423
+ function completedWavesFromStateEntries(waves) {
2424
+ return normalizeCompletedWaves(
2425
+ Object.values(waves || {})
2426
+ .filter((entry) => entry?.currentState === "completed")
2427
+ .map((entry) => entry.wave),
2428
+ );
2429
+ }
2430
+
2431
+ function normalizeRunStateWaves(rawWaves, completedWaves, lastUpdatedAt) {
2432
+ const normalized = {};
2433
+ for (const waveNumber of completedWaves) {
2434
+ normalized[String(waveNumber)] = {
2435
+ wave: waveNumber,
2436
+ currentState: "completed",
2437
+ lastTransitionAt: lastUpdatedAt,
2438
+ lastSource: "legacy-run-state",
2439
+ lastReasonCode: "legacy-completed-wave",
2440
+ lastDetail: "Imported from legacy completedWaves state.",
2441
+ lastEvidence: null,
2442
+ };
2443
+ }
2444
+ if (!rawWaves || typeof rawWaves !== "object" || Array.isArray(rawWaves)) {
2445
+ return normalized;
2446
+ }
2447
+ for (const [waveKey, rawEntry] of Object.entries(rawWaves)) {
2448
+ const waveNumber = normalizeCompletedWaves([waveKey])[0];
2449
+ if (waveNumber === undefined) {
2450
+ continue;
2451
+ }
2452
+ normalized[String(waveNumber)] = normalizeRunStateWaveEntry(rawEntry, waveNumber);
2453
+ }
2454
+ return normalized;
2455
+ }
2456
+
2457
+ function normalizeRunStateInternal(payload) {
2458
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
2459
+ const completedWaves = normalizeCompletedWaves(source.completedWaves);
2460
+ const lastUpdatedAt = typeof source.lastUpdatedAt === "string" ? source.lastUpdatedAt : undefined;
2461
+ const waves = normalizeRunStateWaves(source.waves, completedWaves, lastUpdatedAt);
2462
+ const history = Array.isArray(source.history)
2463
+ ? source.history
2464
+ .map((entry, index) => normalizeRunStateHistoryEntry(entry, index + 1))
2465
+ .filter((entry) => Number.isFinite(entry.seq) && entry.wave !== null)
2466
+ : [];
1882
2467
  return {
1883
- completedWaves: normalizeCompletedWaves(payload?.completedWaves),
1884
- lastUpdatedAt: typeof payload?.lastUpdatedAt === "string" ? payload.lastUpdatedAt : undefined,
2468
+ schemaVersion: RUN_STATE_SCHEMA_VERSION,
2469
+ kind: RUN_STATE_KIND,
2470
+ completedWaves: completedWavesFromStateEntries(waves),
2471
+ lastUpdatedAt,
2472
+ waves,
2473
+ history,
1885
2474
  };
1886
2475
  }
1887
2476
 
2477
+ export function readRunState(runStatePath) {
2478
+ return normalizeRunStateInternal(readJsonOrNull(runStatePath));
2479
+ }
2480
+
1888
2481
  export function writeRunState(runStatePath, state) {
1889
2482
  ensureDirectory(path.dirname(runStatePath));
2483
+ const normalized = normalizeRunStateInternal(state);
1890
2484
  const payload = {
1891
- completedWaves: normalizeCompletedWaves(state.completedWaves),
2485
+ ...normalized,
2486
+ completedWaves: completedWavesFromStateEntries(normalized.waves),
1892
2487
  lastUpdatedAt: new Date().toISOString(),
1893
2488
  };
1894
2489
  writeJsonAtomic(runStatePath, payload);
1895
2490
  return payload;
1896
2491
  }
1897
2492
 
1898
- export function markWaveCompleted(runStatePath, waveNumber) {
2493
+ function nextRunStateSequence(history) {
2494
+ return (history || []).reduce((max, entry) => Math.max(max, Number(entry?.seq) || 0), 0) + 1;
2495
+ }
2496
+
2497
+ function appendRunStateTransition(state, {
2498
+ waveNumber,
2499
+ toState,
2500
+ source,
2501
+ reasonCode,
2502
+ detail,
2503
+ evidence = null,
2504
+ at = toIsoTimestamp(),
2505
+ }) {
2506
+ const nextState = normalizeRunStateInternal(state);
2507
+ const waveKey = String(waveNumber);
2508
+ const previousEntry = nextState.waves[waveKey] || null;
2509
+ const currentState = previousEntry?.currentState || null;
2510
+ const currentEvidence = previousEntry?.lastEvidence || null;
2511
+ const effectiveDetail = String(detail || "").trim();
2512
+ const effectiveEvidence =
2513
+ evidence && typeof evidence === "object" && !Array.isArray(evidence) ? evidence : null;
2514
+ if (
2515
+ previousEntry &&
2516
+ currentState === toState &&
2517
+ previousEntry.lastSource === source &&
2518
+ previousEntry.lastReasonCode === reasonCode &&
2519
+ previousEntry.lastDetail === effectiveDetail &&
2520
+ JSON.stringify(currentEvidence || null) === JSON.stringify(effectiveEvidence || null)
2521
+ ) {
2522
+ return nextState;
2523
+ }
2524
+ const historyEntry = {
2525
+ seq: nextRunStateSequence(nextState.history),
2526
+ at,
2527
+ wave: waveNumber,
2528
+ fromState: currentState,
2529
+ toState,
2530
+ source,
2531
+ reasonCode,
2532
+ detail: effectiveDetail,
2533
+ evidence: effectiveEvidence,
2534
+ };
2535
+ nextState.waves[waveKey] = {
2536
+ wave: waveNumber,
2537
+ currentState: toState,
2538
+ lastTransitionAt: at,
2539
+ lastSource: source,
2540
+ lastReasonCode: reasonCode,
2541
+ lastDetail: effectiveDetail,
2542
+ lastEvidence: effectiveEvidence,
2543
+ };
2544
+ nextState.history = [...nextState.history, historyEntry];
2545
+ nextState.completedWaves = completedWavesFromStateEntries(nextState.waves);
2546
+ return nextState;
2547
+ }
2548
+
2549
+ export function buildRunStateEvidence({
2550
+ wave,
2551
+ agentRuns = [],
2552
+ statusEntries = [],
2553
+ coordinationLogPath = null,
2554
+ assignmentsPath = null,
2555
+ dependencySnapshotPath = null,
2556
+ gateSnapshot = null,
2557
+ traceDir = null,
2558
+ blockedReasons = [],
2559
+ }) {
2560
+ const observations =
2561
+ statusEntries.length > 0
2562
+ ? statusEntries
2563
+ : agentRuns.map((run) => ({
2564
+ agentId: run.agent?.agentId || null,
2565
+ statusPath: run.statusPath,
2566
+ summaryPath: agentSummaryPathFromStatusPath(run.statusPath),
2567
+ statusRecord: readStatusRecordIfPresent(run.statusPath),
2568
+ }));
2569
+ return {
2570
+ waveFileHash: wave?.file ? fileHashOrNull(path.resolve(REPO_ROOT, wave.file)) : null,
2571
+ traceDir: relativeRepoPathOrNull(traceDir),
2572
+ statusFiles: observations
2573
+ .filter((entry) => entry?.statusPath)
2574
+ .map((entry) => ({
2575
+ agentId: entry.agentId || null,
2576
+ path: relativeRepoPathOrNull(entry.statusPath),
2577
+ promptHash: entry.statusRecord?.promptHash || null,
2578
+ code:
2579
+ entry.statusRecord && Number.isFinite(Number(entry.statusRecord.code))
2580
+ ? Number(entry.statusRecord.code)
2581
+ : null,
2582
+ completedAt: entry.statusRecord?.completedAt || null,
2583
+ sha256: fileHashOrNull(entry.statusPath),
2584
+ })),
2585
+ summaryFiles: observations
2586
+ .filter((entry) => entry?.summaryPath && fs.existsSync(entry.summaryPath))
2587
+ .map((entry) => ({
2588
+ agentId: entry.agentId || null,
2589
+ path: relativeRepoPathOrNull(entry.summaryPath),
2590
+ sha256: fileHashOrNull(entry.summaryPath),
2591
+ })),
2592
+ coordinationLogSha256: fileHashOrNull(coordinationLogPath),
2593
+ assignmentsSha256: fileHashOrNull(assignmentsPath),
2594
+ dependencySnapshotSha256: fileHashOrNull(dependencySnapshotPath),
2595
+ gateSnapshotSha256: gateSnapshot ? hashText(JSON.stringify(gateSnapshot)) : null,
2596
+ blockedReasons: Array.isArray(blockedReasons)
2597
+ ? blockedReasons.map((reason) => ({
2598
+ code: String(reason?.code || "").trim(),
2599
+ detail: String(reason?.detail || "").trim(),
2600
+ }))
2601
+ : [],
2602
+ };
2603
+ }
2604
+
2605
+ export function markWaveCompleted(runStatePath, waveNumber, options = {}) {
1899
2606
  const state = readRunState(runStatePath);
1900
- state.completedWaves = normalizeCompletedWaves([...state.completedWaves, waveNumber]);
1901
- return writeRunState(runStatePath, state);
2607
+ const nextState = appendRunStateTransition(state, {
2608
+ waveNumber,
2609
+ toState: "completed",
2610
+ source: options.source || "live-launcher",
2611
+ reasonCode: options.reasonCode || "wave-complete",
2612
+ detail: options.detail || `Wave ${waveNumber} completed.`,
2613
+ evidence: options.evidence || null,
2614
+ at: options.at || toIsoTimestamp(),
2615
+ });
2616
+ return writeRunState(runStatePath, nextState);
1902
2617
  }
1903
2618
 
1904
2619
  export function resolveAutoNextWaveStart(allWaves, runStatePath) {
@@ -1924,52 +2639,51 @@ export function arraysEqual(a, b) {
1924
2639
  return true;
1925
2640
  }
1926
2641
 
1927
- export function readWaveEvaluatorArtifacts(wave, { logsDir, evaluatorAgentId } = {}) {
1928
- const resolvedEvaluatorAgentId = evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
1929
- const evaluator =
1930
- wave.agents.find((agent) => agent.agentId === resolvedEvaluatorAgentId) ?? null;
1931
- if (!evaluator) {
2642
+ export function readWaveContQaArtifacts(wave, { logsDir, contQaAgentId } = {}) {
2643
+ const resolvedContQaAgentId = contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
2644
+ const contQa = wave.agents.find((agent) => agent.agentId === resolvedContQaAgentId) ?? null;
2645
+ if (!contQa) {
1932
2646
  return {
1933
2647
  ok: false,
1934
- statusCode: "missing-evaluator",
1935
- detail: `Agent ${resolvedEvaluatorAgentId} is missing.`,
2648
+ statusCode: "missing-cont-qa",
2649
+ detail: `Agent ${resolvedContQaAgentId} is missing.`,
1936
2650
  };
1937
2651
  }
1938
- const evaluatorReportPath = wave.evaluatorReportPath
1939
- ? path.resolve(REPO_ROOT, wave.evaluatorReportPath)
2652
+ const contQaReportPath = wave.contQaReportPath
2653
+ ? path.resolve(REPO_ROOT, wave.contQaReportPath)
1940
2654
  : null;
1941
2655
  const reportText =
1942
- evaluatorReportPath && fs.existsSync(evaluatorReportPath)
1943
- ? fs.readFileSync(evaluatorReportPath, "utf8")
2656
+ contQaReportPath && fs.existsSync(contQaReportPath)
2657
+ ? fs.readFileSync(contQaReportPath, "utf8")
1944
2658
  : "";
1945
2659
  const reportVerdict = parseVerdictFromText(reportText, REPORT_VERDICT_REGEX);
1946
2660
  if (reportVerdict.verdict) {
1947
2661
  return {
1948
2662
  ok: reportVerdict.verdict === "pass",
1949
- statusCode: reportVerdict.verdict === "pass" ? "pass" : `evaluator-${reportVerdict.verdict}`,
1950
- detail: reportVerdict.detail || "Verdict read from evaluator report.",
2663
+ statusCode: reportVerdict.verdict === "pass" ? "pass" : `cont-qa-${reportVerdict.verdict}`,
2664
+ detail: reportVerdict.detail || "Verdict read from cont-QA report.",
1951
2665
  };
1952
2666
  }
1953
- const evaluatorLogPath = logsDir
1954
- ? path.join(logsDir, `wave-${wave.wave}-${evaluator.slug}.log`)
2667
+ const contQaLogPath = logsDir
2668
+ ? path.join(logsDir, `wave-${wave.wave}-${contQa.slug}.log`)
1955
2669
  : null;
1956
2670
  const logVerdict = parseVerdictFromText(
1957
- evaluatorLogPath ? readFileTail(evaluatorLogPath, 30000) : "",
2671
+ contQaLogPath ? readFileTail(contQaLogPath, 30000) : "",
1958
2672
  WAVE_VERDICT_REGEX,
1959
2673
  );
1960
2674
  if (logVerdict.verdict) {
1961
2675
  return {
1962
2676
  ok: logVerdict.verdict === "pass",
1963
- statusCode: logVerdict.verdict === "pass" ? "pass" : `evaluator-${logVerdict.verdict}`,
1964
- detail: logVerdict.detail || "Verdict read from evaluator log marker.",
2677
+ statusCode: logVerdict.verdict === "pass" ? "pass" : `cont-qa-${logVerdict.verdict}`,
2678
+ detail: logVerdict.detail || "Verdict read from cont-QA log marker.",
1965
2679
  };
1966
2680
  }
1967
2681
  return {
1968
2682
  ok: false,
1969
- statusCode: "missing-evaluator-verdict",
1970
- detail: evaluatorReportPath
1971
- ? `Missing evaluator verdict in ${path.relative(REPO_ROOT, evaluatorReportPath)}.`
1972
- : "Missing evaluator report path and evaluator log verdict.",
2683
+ statusCode: "missing-cont-qa-verdict",
2684
+ detail: contQaReportPath
2685
+ ? `Missing cont-QA verdict in ${path.relative(REPO_ROOT, contQaReportPath)}.`
2686
+ : "Missing cont-QA report path and cont-QA log verdict.",
1973
2687
  };
1974
2688
  }
1975
2689
 
@@ -1991,7 +2705,12 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
1991
2705
  const logsDir = options.logsDir || path.join(path.resolve(statusDir, ".."), "logs");
1992
2706
  const coordinationDir =
1993
2707
  options.coordinationDir || path.join(path.resolve(statusDir, ".."), "coordination");
1994
- const evaluatorAgentId = options.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
2708
+ const assignmentsDir =
2709
+ options.assignmentsDir || path.join(path.resolve(statusDir, ".."), "assignments");
2710
+ const dependencySnapshotsDir =
2711
+ options.dependencySnapshotsDir || path.join(path.resolve(statusDir, ".."), "dependencies");
2712
+ const contQaAgentId = options.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
2713
+ const contEvalAgentId = options.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
1995
2714
  const integrationAgentId = options.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
1996
2715
  const documentationAgentId =
1997
2716
  options.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
@@ -2005,8 +2724,12 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2005
2724
 
2006
2725
  const reasons = [];
2007
2726
  const summariesByAgentId = {};
2727
+ const statusEntries = [];
2008
2728
  const missingStatusAgents = [];
2009
2729
  let statusesReady = wave.agents.length > 0;
2730
+ const coordinationLogPath = path.join(coordinationDir, `wave-${wave.wave}.jsonl`);
2731
+ const assignmentsPath = path.join(assignmentsDir, `wave-${wave.wave}.json`);
2732
+ const dependencySnapshotPath = path.join(dependencySnapshotsDir, `wave-${wave.wave}.json`);
2010
2733
 
2011
2734
  for (const agent of wave.agents) {
2012
2735
  const statusPath = path.join(statusDir, `wave-${wave.wave}-${agent.slug}.status`);
@@ -2016,6 +2739,13 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2016
2739
  statusesReady = false;
2017
2740
  continue;
2018
2741
  }
2742
+ const summaryPath = agentSummaryPathFromStatusPath(statusPath);
2743
+ statusEntries.push({
2744
+ agentId: agent.agentId,
2745
+ statusPath,
2746
+ summaryPath,
2747
+ statusRecord,
2748
+ });
2019
2749
  const expectedPromptHash = hashAgentPromptFingerprint(agent);
2020
2750
  if (statusRecord.code !== 0) {
2021
2751
  pushWaveCompletionReason(
@@ -2035,22 +2765,67 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2035
2765
  statusesReady = false;
2036
2766
  continue;
2037
2767
  }
2038
- const summary = readAgentExecutionSummary(statusPath);
2768
+ const summary = materializeLiveExecutionSummaryIfMissing({
2769
+ wave,
2770
+ agent,
2771
+ statusPath,
2772
+ statusRecord,
2773
+ logsDir,
2774
+ contQaAgentId,
2775
+ contEvalAgentId,
2776
+ });
2039
2777
  summariesByAgentId[agent.agentId] = summary;
2040
- if (agent.agentId === evaluatorAgentId) {
2041
- if (summary) {
2042
- const validation = validateEvaluatorSummary(agent, summary);
2043
- if (!validation.ok) {
2778
+ if (agent.agentId === contQaAgentId) {
2779
+ const validation = validateContQaSummary(agent, summary, { mode: "live" });
2780
+ if (!validation.ok) {
2781
+ pushWaveCompletionReason(
2782
+ reasons,
2783
+ "invalid-cont-qa-summary",
2784
+ `${agent.agentId}: ${validation.statusCode}: ${validation.detail}`,
2785
+ );
2786
+ statusesReady = false;
2787
+ }
2788
+ continue;
2789
+ }
2790
+ if (agent.agentId === contEvalAgentId) {
2791
+ const evalValidation = validateContEvalSummary(agent, summary, {
2792
+ mode: "live",
2793
+ evalTargets: wave.evalTargets,
2794
+ benchmarkCatalogPath: laneProfile.paths.benchmarkCatalogPath,
2795
+ });
2796
+ if (!evalValidation.ok) {
2797
+ pushWaveCompletionReason(
2798
+ reasons,
2799
+ "invalid-cont-eval-summary",
2800
+ `${agent.agentId}: ${evalValidation.statusCode}: ${evalValidation.detail}`,
2801
+ );
2802
+ statusesReady = false;
2803
+ }
2804
+ if (isContEvalImplementationOwningAgent(agent, { contEvalAgentId })) {
2805
+ const implementationValidation = validateImplementationSummary(agent, summary);
2806
+ if (!implementationValidation.ok) {
2044
2807
  pushWaveCompletionReason(
2045
2808
  reasons,
2046
- "invalid-evaluator-summary",
2047
- `${agent.agentId}: ${validation.statusCode}: ${validation.detail}`,
2809
+ "invalid-cont-eval-implementation-summary",
2810
+ `${agent.agentId}: ${implementationValidation.statusCode}: ${implementationValidation.detail}`,
2048
2811
  );
2049
2812
  statusesReady = false;
2050
2813
  }
2051
2814
  }
2052
2815
  continue;
2053
2816
  }
2817
+ if (isSecurityReviewAgent(agent)) {
2818
+ const validation = validateSecuritySummary(agent, summary);
2819
+ if (!validation.ok) {
2820
+ pushWaveCompletionReason(
2821
+ reasons,
2822
+ "invalid-security-summary",
2823
+ `${agent.agentId}: ${validation.statusCode}: ${validation.detail}`,
2824
+ );
2825
+ statusesReady = false;
2826
+ }
2827
+ continue;
2828
+ }
2054
2829
  if (
2055
2830
  agent.agentId === integrationAgentId &&
2056
2831
  integrationThreshold !== null &&
@@ -2126,19 +2901,8 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2126
2901
  }
2127
2902
  }
2128
2903
 
2129
- if (statusesReady) {
2130
- const evaluatorArtifacts = readWaveEvaluatorArtifacts(wave, {
2131
- logsDir,
2132
- evaluatorAgentId,
2133
- });
2134
- if (!evaluatorArtifacts.ok) {
2135
- pushWaveCompletionReason(reasons, evaluatorArtifacts.statusCode, evaluatorArtifacts.detail);
2136
- statusesReady = false;
2137
- }
2138
- }
2139
-
2140
2904
  const coordinationState = readMaterializedCoordinationState(
2141
- path.join(coordinationDir, `wave-${wave.wave}.jsonl`),
2905
+ coordinationLogPath,
2142
2906
  );
2143
2907
  const openClarificationIds = coordinationState.clarifications
2144
2908
  .filter((record) => isOpenCoordinationStatus(record.status))
@@ -2180,10 +2944,66 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2180
2944
  `Open human feedback records: ${openHumanFeedbackIds.join(", ")}.`,
2181
2945
  );
2182
2946
  }
2947
+ const capabilityAssignments = readAssignmentSnapshot(assignmentsPath, {
2948
+ lane: options.lane || null,
2949
+ wave: wave.wave,
2950
+ });
2951
+ const blockingAssignments = Array.isArray(capabilityAssignments?.assignments)
2952
+ ? capabilityAssignments.assignments.filter((assignment) => assignment?.blocking)
2953
+ : [];
2954
+ const unresolvedAssignments = blockingAssignments.filter((assignment) => !assignment?.assignedAgentId);
2955
+ if (unresolvedAssignments.length > 0) {
2956
+ pushWaveCompletionReason(
2957
+ reasons,
2958
+ "helper-assignment-unresolved",
2959
+ `Helper assignments remain unresolved (${unresolvedAssignments.map((assignment) => assignment.requestId || assignment.id).join(", ")}).`,
2960
+ );
2961
+ } else if (blockingAssignments.length > 0) {
2962
+ pushWaveCompletionReason(
2963
+ reasons,
2964
+ "helper-assignment-open",
2965
+ `Helper assignments remain open (${blockingAssignments.map((assignment) => assignment.requestId || assignment.id).join(", ")}).`,
2966
+ );
2967
+ }
2968
+ const dependencySnapshot = readDependencySnapshot(dependencySnapshotPath, {
2969
+ lane: options.lane || null,
2970
+ wave: wave.wave,
2971
+ });
2972
+ const unresolvedInboundAssignments = Array.isArray(dependencySnapshot?.unresolvedInboundAssignments)
2973
+ ? dependencySnapshot.unresolvedInboundAssignments
2974
+ : [];
2975
+ if (unresolvedInboundAssignments.length > 0) {
2976
+ pushWaveCompletionReason(
2977
+ reasons,
2978
+ "dependency-assignment-unresolved",
2979
+ `Required inbound dependencies are not assigned (${unresolvedInboundAssignments.map((record) => record.id || record).join(", ")}).`,
2980
+ );
2981
+ }
2982
+ const requiredInbound = Array.isArray(dependencySnapshot?.requiredInbound)
2983
+ ? dependencySnapshot.requiredInbound
2984
+ : [];
2985
+ const requiredOutbound = Array.isArray(dependencySnapshot?.requiredOutbound)
2986
+ ? dependencySnapshot.requiredOutbound
2987
+ : [];
2988
+ if (requiredInbound.length > 0 || requiredOutbound.length > 0) {
2989
+ pushWaveCompletionReason(
2990
+ reasons,
2991
+ "dependency-open",
2992
+ `Open required dependencies remain (${[...requiredInbound, ...requiredOutbound].map((record) => record.id || record).join(", ")}).`,
2993
+ );
2994
+ }
2183
2995
 
2184
2996
  return {
2185
2997
  ok: reasons.length === 0,
2186
2998
  reasons,
2999
+ evidence: buildRunStateEvidence({
3000
+ wave,
3001
+ statusEntries,
3002
+ coordinationLogPath,
3003
+ assignmentsPath,
3004
+ dependencySnapshotPath,
3005
+ blockedReasons: reasons,
3006
+ }),
2187
3007
  };
2188
3008
  }
2189
3009
 
@@ -2206,13 +3026,39 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
2206
3026
  .filter((diagnostic) => diagnostic.ok)
2207
3027
  .map((diagnostic) => diagnostic.wave);
2208
3028
  const before = readRunState(runStatePath);
2209
- const firstMerge = normalizeCompletedWaves([...before.completedWaves, ...completedFromStatus]);
3029
+ const firstMerge = normalizeCompletedWaves(
3030
+ diagnostics
3031
+ .filter((diagnostic) => diagnostic.ok)
3032
+ .map((diagnostic) => diagnostic.wave)
3033
+ .concat(
3034
+ before.completedWaves.filter((waveNumber) => {
3035
+ const diagnostic = diagnostics.find((entry) => entry.wave === waveNumber);
3036
+ return !diagnostic || diagnostic.ok;
3037
+ }),
3038
+ ),
3039
+ );
2210
3040
  const latest = readRunState(runStatePath);
2211
- const merged = normalizeCompletedWaves([...latest.completedWaves, ...completedFromStatus]);
2212
- let state = latest;
2213
- if (!arraysEqual(merged, latest.completedWaves)) {
2214
- state = writeRunState(runStatePath, { completedWaves: merged });
3041
+ let nextState = latest;
3042
+ for (const diagnostic of diagnostics) {
3043
+ const toState = diagnostic.ok ? "completed" : "blocked";
3044
+ const reasonCode = diagnostic.ok
3045
+ ? "status-reconcile-complete"
3046
+ : diagnostic.reasons[0]?.code || "status-reconcile-blocked";
3047
+ const detail = diagnostic.ok
3048
+ ? `Wave ${diagnostic.wave} reconstructed as complete from status files.`
3049
+ : diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
3050
+ nextState = appendRunStateTransition(nextState, {
3051
+ waveNumber: diagnostic.wave,
3052
+ toState,
3053
+ source: "status-reconcile",
3054
+ reasonCode,
3055
+ detail,
3056
+ evidence: diagnostic.evidence || null,
3057
+ at: diagnostic.evidence?.statusFiles?.find((entry) => entry.completedAt)?.completedAt || toIsoTimestamp(),
3058
+ });
2215
3059
  }
3060
+ const state = writeRunState(runStatePath, nextState);
3061
+ const merged = state.completedWaves;
2216
3062
  return {
2217
3063
  completedFromStatus,
2218
3064
  addedFromBefore: firstMerge.filter((waveNumber) => !before.completedWaves.includes(waveNumber)),