@chllming/wave-orchestration 0.7.0 → 0.7.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.
@@ -198,6 +198,22 @@ export function formatReconcileBlockedWaveLine(blockedWave) {
198
198
  }`;
199
199
  }
200
200
 
201
+ export function formatReconcilePreservedWaveLine(preservedWave) {
202
+ const parts = Array.isArray(preservedWave?.reasons)
203
+ ? preservedWave.reasons
204
+ .map((reason) => {
205
+ const code = compactSingleLine(reason?.code || "", 80);
206
+ const detail = compactSingleLine(reason?.detail || "", 240);
207
+ return code && detail ? `${code}=${detail}` : "";
208
+ })
209
+ .filter(Boolean)
210
+ : [];
211
+ const previousState = compactSingleLine(preservedWave?.previousState || "completed", 80);
212
+ return `[reconcile] wave ${preservedWave?.wave ?? "unknown"} preserved as ${previousState}: ${
213
+ parts.join("; ") || "unknown reason"
214
+ }`;
215
+ }
216
+
201
217
  function printUsage(lanePaths, terminalSurface) {
202
218
  console.log(`Usage: pnpm exec wave launch [options]
203
219
 
@@ -206,6 +222,7 @@ Options:
206
222
  --start-wave <n> Start from wave number (default: 0)
207
223
  --end-wave <n> End at wave number (default: last available)
208
224
  --auto-next Start from the next unfinished wave and continue forward
225
+ --resume-control-state Preserve the prior auto-generated relaunch plan for this wave
209
226
  --reconcile-status Reconcile run-state from agent status files and exit
210
227
  --state-file <path> Path to run-state JSON (default: ${path.relative(REPO_ROOT, lanePaths.defaultRunStatePath)})
211
228
  --timeout-minutes <n> Max minutes to wait per wave (default: ${DEFAULT_TIMEOUT_MINUTES})
@@ -252,6 +269,7 @@ function parseArgs(argv) {
252
269
  startWave: 0,
253
270
  endWave: null,
254
271
  autoNext: false,
272
+ resumeControlState: false,
255
273
  reconcileStatus: false,
256
274
  runStatePath: lanePaths.defaultRunStatePath,
257
275
  timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
@@ -301,6 +319,8 @@ function parseArgs(argv) {
301
319
  options.cleanupSessions = false;
302
320
  } else if (arg === "--auto-next") {
303
321
  options.autoNext = true;
322
+ } else if (arg === "--resume-control-state") {
323
+ options.resumeControlState = true;
304
324
  } else if (arg === "--reconcile-status") {
305
325
  options.reconcileStatus = true;
306
326
  } else if (arg === "--keep-terminals") {
@@ -563,6 +583,32 @@ function materializeAgentExecutionSummaryForRun(wave, runInfo) {
563
583
  reportPath,
564
584
  });
565
585
  writeAgentExecutionSummary(runInfo.statusPath, summary);
586
+ if (runInfo?.previewPath && fs.existsSync(runInfo.previewPath)) {
587
+ const previewPayload = readJsonOrNull(runInfo.previewPath);
588
+ if (previewPayload && typeof previewPayload === "object") {
589
+ const nextLimits =
590
+ previewPayload.limits && typeof previewPayload.limits === "object" && !Array.isArray(previewPayload.limits)
591
+ ? { ...previewPayload.limits }
592
+ : {};
593
+ const observedTurnLimit = Number(summary?.terminationObservedTurnLimit);
594
+ if (Number.isFinite(observedTurnLimit) && observedTurnLimit > 0) {
595
+ nextLimits.observedTurnLimit = observedTurnLimit;
596
+ nextLimits.observedTurnLimitSource = "runtime-log";
597
+ if (runInfo.agent.executorResolved?.id === "codex") {
598
+ const existingNotes = Array.isArray(nextLimits.notes) ? nextLimits.notes.slice() : [];
599
+ const observedNote = `Observed runtime stop at ${observedTurnLimit} turns from executor log output.`;
600
+ if (!existingNotes.includes(observedNote)) {
601
+ existingNotes.push(observedNote);
602
+ }
603
+ nextLimits.notes = existingNotes;
604
+ }
605
+ }
606
+ writeJsonAtomic(runInfo.previewPath, {
607
+ ...previewPayload,
608
+ limits: nextLimits,
609
+ });
610
+ }
611
+ }
566
612
  return summary;
567
613
  }
568
614
 
@@ -652,6 +698,25 @@ function clearWaveRelaunchPlan(lanePaths, waveNumber) {
652
698
  }
653
699
  }
654
700
 
701
+ export function resetPersistedWaveLaunchState(lanePaths, waveNumber, options = {}) {
702
+ if (options?.dryRun || options?.resumeControlState) {
703
+ return {
704
+ clearedRelaunchPlan: false,
705
+ };
706
+ }
707
+ const persistedRelaunchPlan = readWaveRelaunchPlan(lanePaths, waveNumber);
708
+ if (!persistedRelaunchPlan) {
709
+ return {
710
+ clearedRelaunchPlan: false,
711
+ };
712
+ }
713
+ clearWaveRelaunchPlan(lanePaths, waveNumber);
714
+ return {
715
+ clearedRelaunchPlan: true,
716
+ relaunchPlan: persistedRelaunchPlan,
717
+ };
718
+ }
719
+
655
720
  function waveSecurityPath(lanePaths, waveNumber) {
656
721
  return path.join(lanePaths.securityDir, `wave-${waveNumber}.json`);
657
722
  }
@@ -3469,6 +3534,9 @@ export async function runLauncherCli(argv) {
3469
3534
  for (const blockedWave of reconciliation.blockedFromStatus || []) {
3470
3535
  console.log(formatReconcileBlockedWaveLine(blockedWave));
3471
3536
  }
3537
+ for (const preservedWave of reconciliation.preservedWithDrift || []) {
3538
+ console.log(formatReconcilePreservedWaveLine(preservedWave));
3539
+ }
3472
3540
  console.log(`[reconcile] completed waves now: ${completedSummary}`);
3473
3541
  return;
3474
3542
  }
@@ -3628,7 +3696,7 @@ export async function runLauncherCli(argv) {
3628
3696
  dashboardPath: lanePaths.globalDashboardPath,
3629
3697
  });
3630
3698
  console.log(
3631
- `[dashboard] tmux -L ${lanePaths.tmuxSocketName} attach -t ${globalDashboardTerminalEntry.sessionName}`,
3699
+ `[dashboard] attach global: pnpm exec wave dashboard --lane ${lanePaths.lane} --attach global`,
3632
3700
  );
3633
3701
  }
3634
3702
 
@@ -3730,6 +3798,12 @@ export async function runLauncherCli(argv) {
3730
3798
  promptPath: path.join(lanePaths.promptsDir, `${safeName}.prompt.md`),
3731
3799
  logPath: path.join(lanePaths.logsDir, `${safeName}.log`),
3732
3800
  statusPath: path.join(lanePaths.statusDir, `${safeName}.status`),
3801
+ previewPath: path.join(
3802
+ lanePaths.executorOverlaysDir,
3803
+ `wave-${wave.wave}`,
3804
+ agent.slug,
3805
+ "launch-preview.json",
3806
+ ),
3733
3807
  messageBoardPath,
3734
3808
  messageBoardSnapshot: derivedState.messageBoardText,
3735
3809
  sharedSummaryPath: derivedState.sharedSummaryPath,
@@ -3777,6 +3851,16 @@ export async function runLauncherCli(argv) {
3777
3851
  };
3778
3852
 
3779
3853
  refreshDerivedState(0);
3854
+ const launchStateReset = resetPersistedWaveLaunchState(lanePaths, wave.wave, options);
3855
+ if (launchStateReset.clearedRelaunchPlan) {
3856
+ appendCoordination({
3857
+ event: "wave_launch_state_reset",
3858
+ waves: [wave.wave],
3859
+ status: "running",
3860
+ details: `cleared_relaunch_plan=yes; previous_agents=${(launchStateReset.relaunchPlan?.selectedAgentIds || []).join(",") || "none"}`,
3861
+ actionRequested: "None",
3862
+ });
3863
+ }
3780
3864
  let persistedRelaunchPlan = readWaveRelaunchPlan(lanePaths, wave.wave);
3781
3865
  let retryOverride = readWaveRetryOverride(lanePaths, wave.wave);
3782
3866
 
@@ -3891,6 +3975,9 @@ export async function runLauncherCli(argv) {
3891
3975
  dashboardPath,
3892
3976
  messageBoardPath,
3893
3977
  });
3978
+ console.log(
3979
+ `[dashboard] attach current: pnpm exec wave dashboard --lane ${lanePaths.lane} --attach current`,
3980
+ );
3894
3981
  }
3895
3982
 
3896
3983
  if (options.residentOrchestrator) {
@@ -129,7 +129,7 @@ export function createTemporaryTerminalEntries(
129
129
  }
130
130
 
131
131
  export function createGlobalDashboardTerminalEntry(lanePaths, runTag) {
132
- const sessionName = `${lanePaths.tmuxGlobalDashboardSessionPrefix}_${runTag}`.replace(
132
+ const sessionName = `${lanePaths.tmuxGlobalDashboardSessionPrefix}_current`.replace(
133
133
  /[^a-zA-Z0-9:_-]/g,
134
134
  "_",
135
135
  );
@@ -2421,6 +2421,17 @@ function relativeRepoPathOrNull(filePath) {
2421
2421
  return filePath ? path.relative(REPO_ROOT, filePath) : null;
2422
2422
  }
2423
2423
 
2424
+ const RUN_STATE_COMPLETED_VALUES = new Set(["completed", "completed_with_drift"]);
2425
+ const PROMPT_DRIFT_REASON_CODES = new Set(["prompt-hash-mismatch", "prompt-hash-missing"]);
2426
+
2427
+ function isCompletedRunStateValue(value) {
2428
+ return RUN_STATE_COMPLETED_VALUES.has(String(value || "").trim().toLowerCase());
2429
+ }
2430
+
2431
+ function completedRunStateEntries(waves) {
2432
+ return Object.values(waves || {}).filter((entry) => isCompletedRunStateValue(entry?.currentState));
2433
+ }
2434
+
2424
2435
  function normalizeRunStateWaveEntry(rawEntry, waveNumber) {
2425
2436
  const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
2426
2437
  const normalizedWave = normalizeCompletedWaves([waveNumber])[0] ?? normalizeCompletedWaves([source.wave])[0] ?? null;
@@ -2465,9 +2476,7 @@ function normalizeRunStateHistoryEntry(rawEntry, seqFallback) {
2465
2476
 
2466
2477
  function completedWavesFromStateEntries(waves) {
2467
2478
  return normalizeCompletedWaves(
2468
- Object.values(waves || {})
2469
- .filter((entry) => entry?.currentState === "completed")
2470
- .map((entry) => entry.wave),
2479
+ completedRunStateEntries(waves).map((entry) => entry.wave),
2471
2480
  );
2472
2481
  }
2473
2482
 
@@ -2744,6 +2753,64 @@ function pushWaveCompletionReason(reasons, code, detail) {
2744
2753
  reasons.push({ code: normalizedCode, detail: normalizedDetail });
2745
2754
  }
2746
2755
 
2756
+ function promptDriftReasonForStatus(agent, statusPath, statusRecord, expectedPromptHash) {
2757
+ const actualPromptHash = String(statusRecord?.promptHash || "").trim();
2758
+ if (!actualPromptHash) {
2759
+ return {
2760
+ code: "prompt-hash-missing",
2761
+ detail: `${agent.agentId} status in ${path.relative(REPO_ROOT, statusPath)} is missing prompt-hash metadata required to match the current prompt fingerprint.`,
2762
+ };
2763
+ }
2764
+ if (actualPromptHash !== expectedPromptHash) {
2765
+ return {
2766
+ code: "prompt-hash-mismatch",
2767
+ detail: `${agent.agentId} status in ${path.relative(REPO_ROOT, statusPath)} does not match the current prompt fingerprint.`,
2768
+ };
2769
+ }
2770
+ return null;
2771
+ }
2772
+
2773
+ function diagnosticHasOnlyPromptDriftReasons(diagnostic) {
2774
+ return (
2775
+ Array.isArray(diagnostic?.reasons) &&
2776
+ diagnostic.reasons.length > 0 &&
2777
+ diagnostic.reasons.every((reason) => PROMPT_DRIFT_REASON_CODES.has(String(reason?.code || "").trim()))
2778
+ );
2779
+ }
2780
+
2781
+ function isAuthoritativeCompletedRunStateEntry(entry) {
2782
+ if (!isCompletedRunStateValue(entry?.currentState)) {
2783
+ return false;
2784
+ }
2785
+ const source = String(entry?.lastSource || "").trim().toLowerCase();
2786
+ return source !== "" && source !== "legacy-run-state";
2787
+ }
2788
+
2789
+ function buildPreservedCompletionEvidence(previousEntry, diagnostic) {
2790
+ const baseEvidence =
2791
+ diagnostic?.evidence && typeof diagnostic.evidence === "object" && !Array.isArray(diagnostic.evidence)
2792
+ ? { ...diagnostic.evidence }
2793
+ : {};
2794
+ baseEvidence.preservedCompletion = {
2795
+ preserved: true,
2796
+ preservedFromState: previousEntry?.currentState || "completed",
2797
+ preservedFromSource: previousEntry?.lastSource || null,
2798
+ preservedFromReasonCode: previousEntry?.lastReasonCode || null,
2799
+ driftReasons: (diagnostic?.reasons || [])
2800
+ .filter((reason) => PROMPT_DRIFT_REASON_CODES.has(String(reason?.code || "").trim()))
2801
+ .map((reason) => ({
2802
+ code: String(reason?.code || "").trim(),
2803
+ detail: String(reason?.detail || "").trim(),
2804
+ })),
2805
+ previousEvidence: previousEntry?.lastEvidence || null,
2806
+ };
2807
+ return baseEvidence;
2808
+ }
2809
+
2810
+ function shouldPreserveCompletedWave(previousEntry, diagnostic) {
2811
+ return isAuthoritativeCompletedRunStateEntry(previousEntry) && diagnosticHasOnlyPromptDriftReasons(diagnostic);
2812
+ }
2813
+
2747
2814
  function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2748
2815
  const logsDir = options.logsDir || path.join(path.resolve(statusDir, ".."), "logs");
2749
2816
  const coordinationDir =
@@ -2770,6 +2837,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2770
2837
  const statusEntries = [];
2771
2838
  const missingStatusAgents = [];
2772
2839
  let statusesReady = wave.agents.length > 0;
2840
+ let summaryValidationReady = wave.agents.length > 0;
2773
2841
  const coordinationLogPath = path.join(coordinationDir, `wave-${wave.wave}.jsonl`);
2774
2842
  const assignmentsPath = path.join(assignmentsDir, `wave-${wave.wave}.json`);
2775
2843
  const dependencySnapshotPath = path.join(dependencySnapshotsDir, `wave-${wave.wave}.json`);
@@ -2780,6 +2848,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2780
2848
  if (!statusRecord) {
2781
2849
  missingStatusAgents.push(agent.agentId);
2782
2850
  statusesReady = false;
2851
+ summaryValidationReady = false;
2783
2852
  continue;
2784
2853
  }
2785
2854
  const summaryPath = agentSummaryPathFromStatusPath(statusPath);
@@ -2797,16 +2866,18 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2797
2866
  `${agent.agentId} exited ${statusRecord.code} in ${path.relative(REPO_ROOT, statusPath)}.`,
2798
2867
  );
2799
2868
  statusesReady = false;
2869
+ summaryValidationReady = false;
2800
2870
  continue;
2801
2871
  }
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
- );
2872
+ const promptDriftReason = promptDriftReasonForStatus(
2873
+ agent,
2874
+ statusPath,
2875
+ statusRecord,
2876
+ expectedPromptHash,
2877
+ );
2878
+ if (promptDriftReason) {
2879
+ pushWaveCompletionReason(reasons, promptDriftReason.code, promptDriftReason.detail);
2808
2880
  statusesReady = false;
2809
- continue;
2810
2881
  }
2811
2882
  const summary = materializeLiveExecutionSummaryIfMissing({
2812
2883
  wave,
@@ -2917,7 +2988,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
2917
2988
  }
2918
2989
 
2919
2990
  if (
2920
- statusesReady &&
2991
+ summaryValidationReady &&
2921
2992
  componentThreshold !== null &&
2922
2993
  wave.wave >= componentThreshold
2923
2994
  ) {
@@ -3071,32 +3142,63 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
3071
3142
  const before = readRunState(runStatePath);
3072
3143
  const firstMerge = normalizeCompletedWaves(
3073
3144
  diagnostics
3074
- .filter((diagnostic) => diagnostic.ok)
3145
+ .filter((diagnostic) => {
3146
+ if (diagnostic.ok) {
3147
+ return true;
3148
+ }
3149
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3150
+ return shouldPreserveCompletedWave(previousEntry, diagnostic);
3151
+ })
3075
3152
  .map((diagnostic) => diagnostic.wave)
3076
3153
  .concat(
3077
3154
  before.completedWaves.filter((waveNumber) => {
3078
3155
  const diagnostic = diagnostics.find((entry) => entry.wave === waveNumber);
3079
- return !diagnostic || diagnostic.ok;
3156
+ if (!diagnostic) {
3157
+ return true;
3158
+ }
3159
+ const previousEntry = before.waves[String(waveNumber)] || null;
3160
+ return diagnostic.ok || shouldPreserveCompletedWave(previousEntry, diagnostic);
3080
3161
  }),
3081
3162
  ),
3082
3163
  );
3083
3164
  const latest = readRunState(runStatePath);
3084
3165
  let nextState = latest;
3166
+ const preservedWithDrift = [];
3085
3167
  for (const diagnostic of diagnostics) {
3086
- const toState = diagnostic.ok ? "completed" : "blocked";
3168
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3169
+ const preserveCompleted = shouldPreserveCompletedWave(previousEntry, diagnostic);
3170
+ const toState = diagnostic.ok
3171
+ ? "completed"
3172
+ : preserveCompleted
3173
+ ? "completed_with_drift"
3174
+ : "blocked";
3087
3175
  const reasonCode = diagnostic.ok
3088
3176
  ? "status-reconcile-complete"
3089
- : diagnostic.reasons[0]?.code || "status-reconcile-blocked";
3177
+ : preserveCompleted
3178
+ ? "status-reconcile-completed-with-drift"
3179
+ : diagnostic.reasons[0]?.code || "status-reconcile-blocked";
3090
3180
  const detail = diagnostic.ok
3091
3181
  ? `Wave ${diagnostic.wave} reconstructed as complete from status files.`
3092
- : diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
3182
+ : preserveCompleted
3183
+ ? `Wave ${diagnostic.wave} preserved as completed with prompt drift: ${diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ")}`
3184
+ : diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
3185
+ const evidence = preserveCompleted
3186
+ ? buildPreservedCompletionEvidence(previousEntry, diagnostic)
3187
+ : diagnostic.evidence || null;
3188
+ if (preserveCompleted) {
3189
+ preservedWithDrift.push({
3190
+ wave: diagnostic.wave,
3191
+ reasons: diagnostic.reasons,
3192
+ previousState: previousEntry?.currentState || "completed",
3193
+ });
3194
+ }
3093
3195
  nextState = appendRunStateTransition(nextState, {
3094
3196
  waveNumber: diagnostic.wave,
3095
3197
  toState,
3096
3198
  source: "status-reconcile",
3097
3199
  reasonCode,
3098
3200
  detail,
3099
- evidence: diagnostic.evidence || null,
3201
+ evidence,
3100
3202
  at: diagnostic.evidence?.statusFiles?.find((entry) => entry.completedAt)?.completedAt || toIsoTimestamp(),
3101
3203
  });
3102
3204
  }
@@ -3106,7 +3208,14 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
3106
3208
  completedFromStatus,
3107
3209
  addedFromBefore: firstMerge.filter((waveNumber) => !before.completedWaves.includes(waveNumber)),
3108
3210
  addedFromLatest: merged.filter((waveNumber) => !latest.completedWaves.includes(waveNumber)),
3109
- blockedFromStatus: diagnostics.filter((diagnostic) => !diagnostic.ok),
3211
+ blockedFromStatus: diagnostics.filter((diagnostic) => {
3212
+ if (diagnostic.ok) {
3213
+ return false;
3214
+ }
3215
+ const previousEntry = before.waves[String(diagnostic.wave)] || null;
3216
+ return !shouldPreserveCompletedWave(previousEntry, diagnostic);
3217
+ }),
3218
+ preservedWithDrift,
3110
3219
  state,
3111
3220
  };
3112
3221
  }