@chllming/wave-orchestration 0.7.0 → 0.7.2

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 (42) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +9 -8
  3. package/docs/guides/planner.md +19 -0
  4. package/docs/guides/terminal-surfaces.md +12 -0
  5. package/docs/plans/component-cutover-matrix.json +50 -3
  6. package/docs/plans/current-state.md +1 -1
  7. package/docs/plans/end-state-architecture.md +927 -0
  8. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  9. package/docs/plans/migration.md +26 -0
  10. package/docs/plans/wave-orchestrator.md +4 -7
  11. package/docs/plans/waves/wave-1.md +376 -0
  12. package/docs/plans/waves/wave-2.md +292 -0
  13. package/docs/plans/waves/wave-3.md +342 -0
  14. package/docs/plans/waves/wave-4.md +391 -0
  15. package/docs/plans/waves/wave-5.md +382 -0
  16. package/docs/plans/waves/wave-6.md +321 -0
  17. package/docs/reference/cli-reference.md +547 -0
  18. package/docs/reference/coordination-and-closure.md +1 -1
  19. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  20. package/docs/reference/runtime-config/README.md +2 -2
  21. package/docs/reference/runtime-config/codex.md +2 -1
  22. package/docs/reference/sample-waves.md +4 -4
  23. package/package.json +1 -1
  24. package/releases/manifest.json +43 -2
  25. package/scripts/wave-orchestrator/agent-state.mjs +458 -35
  26. package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
  27. package/scripts/wave-orchestrator/control-cli.mjs +119 -20
  28. package/scripts/wave-orchestrator/coordination.mjs +11 -10
  29. package/scripts/wave-orchestrator/dashboard-renderer.mjs +82 -2
  30. package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
  31. package/scripts/wave-orchestrator/install.mjs +120 -3
  32. package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
  33. package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
  34. package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
  35. package/scripts/wave-orchestrator/launcher-runtime.mjs +9 -9
  36. package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
  37. package/scripts/wave-orchestrator/launcher.mjs +317 -2999
  38. package/scripts/wave-orchestrator/task-entity.mjs +557 -0
  39. package/scripts/wave-orchestrator/terminals.mjs +1 -1
  40. package/scripts/wave-orchestrator/wave-files.mjs +138 -20
  41. package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
  42. package/wave.config.json +1 -1
@@ -1202,11 +1202,20 @@ function materializeLiveExecutionSummaryIfMissing({
1202
1202
  contQaAgentId,
1203
1203
  contEvalAgentId,
1204
1204
  }) {
1205
- const existing = readAgentExecutionSummary(statusPath);
1205
+ const logPath = logsDir ? path.join(logsDir, `wave-${wave.wave}-${agent.slug}.log`) : null;
1206
+ const existing = readAgentExecutionSummary(statusPath, {
1207
+ agent,
1208
+ statusPath,
1209
+ statusRecord,
1210
+ logPath,
1211
+ reportPath: resolveAgentSummaryReportPath(wave, agent.agentId, {
1212
+ contQaAgentId,
1213
+ contEvalAgentId,
1214
+ }),
1215
+ });
1206
1216
  if (existing) {
1207
1217
  return existing;
1208
1218
  }
1209
- const logPath = logsDir ? path.join(logsDir, `wave-${wave.wave}-${agent.slug}.log`) : null;
1210
1219
  if (!statusRecord || !logPath || !fs.existsSync(logPath)) {
1211
1220
  return null;
1212
1221
  }
@@ -2421,6 +2430,17 @@ function relativeRepoPathOrNull(filePath) {
2421
2430
  return filePath ? path.relative(REPO_ROOT, filePath) : null;
2422
2431
  }
2423
2432
 
2433
+ const RUN_STATE_COMPLETED_VALUES = new Set(["completed", "completed_with_drift"]);
2434
+ const PROMPT_DRIFT_REASON_CODES = new Set(["prompt-hash-mismatch", "prompt-hash-missing"]);
2435
+
2436
+ function isCompletedRunStateValue(value) {
2437
+ return RUN_STATE_COMPLETED_VALUES.has(String(value || "").trim().toLowerCase());
2438
+ }
2439
+
2440
+ function completedRunStateEntries(waves) {
2441
+ return Object.values(waves || {}).filter((entry) => isCompletedRunStateValue(entry?.currentState));
2442
+ }
2443
+
2424
2444
  function normalizeRunStateWaveEntry(rawEntry, waveNumber) {
2425
2445
  const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
2426
2446
  const normalizedWave = normalizeCompletedWaves([waveNumber])[0] ?? normalizeCompletedWaves([source.wave])[0] ?? null;
@@ -2465,9 +2485,7 @@ function normalizeRunStateHistoryEntry(rawEntry, seqFallback) {
2465
2485
 
2466
2486
  function completedWavesFromStateEntries(waves) {
2467
2487
  return normalizeCompletedWaves(
2468
- Object.values(waves || {})
2469
- .filter((entry) => entry?.currentState === "completed")
2470
- .map((entry) => entry.wave),
2488
+ completedRunStateEntries(waves).map((entry) => entry.wave),
2471
2489
  );
2472
2490
  }
2473
2491
 
@@ -2744,6 +2762,64 @@ function pushWaveCompletionReason(reasons, code, detail) {
2744
2762
  reasons.push({ code: normalizedCode, detail: normalizedDetail });
2745
2763
  }
2746
2764
 
2765
+ function promptDriftReasonForStatus(agent, statusPath, statusRecord, expectedPromptHash) {
2766
+ const actualPromptHash = String(statusRecord?.promptHash || "").trim();
2767
+ if (!actualPromptHash) {
2768
+ return {
2769
+ code: "prompt-hash-missing",
2770
+ detail: `${agent.agentId} status in ${path.relative(REPO_ROOT, statusPath)} is missing prompt-hash metadata required to match the current prompt fingerprint.`,
2771
+ };
2772
+ }
2773
+ if (actualPromptHash !== expectedPromptHash) {
2774
+ return {
2775
+ code: "prompt-hash-mismatch",
2776
+ detail: `${agent.agentId} status in ${path.relative(REPO_ROOT, statusPath)} does not match the current prompt fingerprint.`,
2777
+ };
2778
+ }
2779
+ return null;
2780
+ }
2781
+
2782
+ function diagnosticHasOnlyPromptDriftReasons(diagnostic) {
2783
+ return (
2784
+ Array.isArray(diagnostic?.reasons) &&
2785
+ diagnostic.reasons.length > 0 &&
2786
+ diagnostic.reasons.every((reason) => PROMPT_DRIFT_REASON_CODES.has(String(reason?.code || "").trim()))
2787
+ );
2788
+ }
2789
+
2790
+ function isAuthoritativeCompletedRunStateEntry(entry) {
2791
+ if (!isCompletedRunStateValue(entry?.currentState)) {
2792
+ return false;
2793
+ }
2794
+ const source = String(entry?.lastSource || "").trim().toLowerCase();
2795
+ return source !== "" && source !== "legacy-run-state";
2796
+ }
2797
+
2798
+ function buildPreservedCompletionEvidence(previousEntry, diagnostic) {
2799
+ const baseEvidence =
2800
+ diagnostic?.evidence && typeof diagnostic.evidence === "object" && !Array.isArray(diagnostic.evidence)
2801
+ ? { ...diagnostic.evidence }
2802
+ : {};
2803
+ baseEvidence.preservedCompletion = {
2804
+ preserved: true,
2805
+ preservedFromState: previousEntry?.currentState || "completed",
2806
+ preservedFromSource: previousEntry?.lastSource || null,
2807
+ preservedFromReasonCode: previousEntry?.lastReasonCode || null,
2808
+ driftReasons: (diagnostic?.reasons || [])
2809
+ .filter((reason) => PROMPT_DRIFT_REASON_CODES.has(String(reason?.code || "").trim()))
2810
+ .map((reason) => ({
2811
+ code: String(reason?.code || "").trim(),
2812
+ detail: String(reason?.detail || "").trim(),
2813
+ })),
2814
+ previousEvidence: previousEntry?.lastEvidence || null,
2815
+ };
2816
+ return baseEvidence;
2817
+ }
2818
+
2819
+ function shouldPreserveCompletedWave(previousEntry, diagnostic) {
2820
+ return isAuthoritativeCompletedRunStateEntry(previousEntry) && diagnosticHasOnlyPromptDriftReasons(diagnostic);
2821
+ }
2822
+
2747
2823
  function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2748
2824
  const logsDir = options.logsDir || path.join(path.resolve(statusDir, ".."), "logs");
2749
2825
  const coordinationDir =
@@ -2770,6 +2846,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2770
2846
  const statusEntries = [];
2771
2847
  const missingStatusAgents = [];
2772
2848
  let statusesReady = wave.agents.length > 0;
2849
+ let summaryValidationReady = wave.agents.length > 0;
2773
2850
  const coordinationLogPath = path.join(coordinationDir, `wave-${wave.wave}.jsonl`);
2774
2851
  const assignmentsPath = path.join(assignmentsDir, `wave-${wave.wave}.json`);
2775
2852
  const dependencySnapshotPath = path.join(dependencySnapshotsDir, `wave-${wave.wave}.json`);
@@ -2780,6 +2857,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2780
2857
  if (!statusRecord) {
2781
2858
  missingStatusAgents.push(agent.agentId);
2782
2859
  statusesReady = false;
2860
+ summaryValidationReady = false;
2783
2861
  continue;
2784
2862
  }
2785
2863
  const summaryPath = agentSummaryPathFromStatusPath(statusPath);
@@ -2797,16 +2875,18 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2797
2875
  `${agent.agentId} exited ${statusRecord.code} in ${path.relative(REPO_ROOT, statusPath)}.`,
2798
2876
  );
2799
2877
  statusesReady = false;
2878
+ summaryValidationReady = false;
2800
2879
  continue;
2801
2880
  }
2802
- if (statusRecord.promptHash !== expectedPromptHash) {
2803
- pushWaveCompletionReason(
2804
- reasons,
2805
- "prompt-hash-mismatch",
2806
- `${agent.agentId} status in ${path.relative(REPO_ROOT, statusPath)} does not match the current prompt fingerprint.`,
2807
- );
2881
+ const promptDriftReason = promptDriftReasonForStatus(
2882
+ agent,
2883
+ statusPath,
2884
+ statusRecord,
2885
+ expectedPromptHash,
2886
+ );
2887
+ if (promptDriftReason) {
2888
+ pushWaveCompletionReason(reasons, promptDriftReason.code, promptDriftReason.detail);
2808
2889
  statusesReady = false;
2809
- continue;
2810
2890
  }
2811
2891
  const summary = materializeLiveExecutionSummaryIfMissing({
2812
2892
  wave,
@@ -2917,7 +2997,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2917
2997
  }
2918
2998
 
2919
2999
  if (
2920
- statusesReady &&
3000
+ summaryValidationReady &&
2921
3001
  componentThreshold !== null &&
2922
3002
  wave.wave >= componentThreshold
2923
3003
  ) {
@@ -3071,32 +3151,63 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
3071
3151
  const before = readRunState(runStatePath);
3072
3152
  const firstMerge = normalizeCompletedWaves(
3073
3153
  diagnostics
3074
- .filter((diagnostic) => diagnostic.ok)
3154
+ .filter((diagnostic) => {
3155
+ if (diagnostic.ok) {
3156
+ return true;
3157
+ }
3158
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3159
+ return shouldPreserveCompletedWave(previousEntry, diagnostic);
3160
+ })
3075
3161
  .map((diagnostic) => diagnostic.wave)
3076
3162
  .concat(
3077
3163
  before.completedWaves.filter((waveNumber) => {
3078
3164
  const diagnostic = diagnostics.find((entry) => entry.wave === waveNumber);
3079
- return !diagnostic || diagnostic.ok;
3165
+ if (!diagnostic) {
3166
+ return true;
3167
+ }
3168
+ const previousEntry = before.waves[String(waveNumber)] || null;
3169
+ return diagnostic.ok || shouldPreserveCompletedWave(previousEntry, diagnostic);
3080
3170
  }),
3081
3171
  ),
3082
3172
  );
3083
3173
  const latest = readRunState(runStatePath);
3084
3174
  let nextState = latest;
3175
+ const preservedWithDrift = [];
3085
3176
  for (const diagnostic of diagnostics) {
3086
- const toState = diagnostic.ok ? "completed" : "blocked";
3177
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3178
+ const preserveCompleted = shouldPreserveCompletedWave(previousEntry, diagnostic);
3179
+ const toState = diagnostic.ok
3180
+ ? "completed"
3181
+ : preserveCompleted
3182
+ ? "completed_with_drift"
3183
+ : "blocked";
3087
3184
  const reasonCode = diagnostic.ok
3088
3185
  ? "status-reconcile-complete"
3089
- : diagnostic.reasons[0]?.code || "status-reconcile-blocked";
3186
+ : preserveCompleted
3187
+ ? "status-reconcile-completed-with-drift"
3188
+ : diagnostic.reasons[0]?.code || "status-reconcile-blocked";
3090
3189
  const detail = diagnostic.ok
3091
3190
  ? `Wave ${diagnostic.wave} reconstructed as complete from status files.`
3092
- : diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
3191
+ : preserveCompleted
3192
+ ? `Wave ${diagnostic.wave} preserved as completed with prompt drift: ${diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ")}`
3193
+ : diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
3194
+ const evidence = preserveCompleted
3195
+ ? buildPreservedCompletionEvidence(previousEntry, diagnostic)
3196
+ : diagnostic.evidence || null;
3197
+ if (preserveCompleted) {
3198
+ preservedWithDrift.push({
3199
+ wave: diagnostic.wave,
3200
+ reasons: diagnostic.reasons,
3201
+ previousState: previousEntry?.currentState || "completed",
3202
+ });
3203
+ }
3093
3204
  nextState = appendRunStateTransition(nextState, {
3094
3205
  waveNumber: diagnostic.wave,
3095
3206
  toState,
3096
3207
  source: "status-reconcile",
3097
3208
  reasonCode,
3098
3209
  detail,
3099
- evidence: diagnostic.evidence || null,
3210
+ evidence,
3100
3211
  at: diagnostic.evidence?.statusFiles?.find((entry) => entry.completedAt)?.completedAt || toIsoTimestamp(),
3101
3212
  });
3102
3213
  }
@@ -3106,7 +3217,14 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
3106
3217
  completedFromStatus,
3107
3218
  addedFromBefore: firstMerge.filter((waveNumber) => !before.completedWaves.includes(waveNumber)),
3108
3219
  addedFromLatest: merged.filter((waveNumber) => !latest.completedWaves.includes(waveNumber)),
3109
- blockedFromStatus: diagnostics.filter((diagnostic) => !diagnostic.ok),
3220
+ blockedFromStatus: diagnostics.filter((diagnostic) => {
3221
+ if (diagnostic.ok) {
3222
+ return false;
3223
+ }
3224
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3225
+ return !shouldPreserveCompletedWave(previousEntry, diagnostic);
3226
+ }),
3227
+ preservedWithDrift,
3110
3228
  state,
3111
3229
  };
3112
3230
  }