@chllming/wave-orchestration 0.6.0 → 0.6.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 (31) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +75 -30
  3. package/docs/README.md +15 -3
  4. package/docs/concepts/context7-vs-skills.md +24 -0
  5. package/docs/concepts/runtime-agnostic-orchestration.md +17 -2
  6. package/docs/concepts/what-is-a-wave.md +28 -0
  7. package/docs/evals/README.md +4 -2
  8. package/docs/guides/terminal-surfaces.md +2 -0
  9. package/docs/plans/current-state.md +1 -1
  10. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  11. package/docs/plans/migration.md +4 -4
  12. package/docs/plans/wave-orchestrator.md +11 -3
  13. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  14. package/docs/reference/runtime-config/README.md +4 -4
  15. package/docs/reference/runtime-config/claude.md +6 -1
  16. package/docs/reference/runtime-config/codex.md +2 -2
  17. package/docs/reference/runtime-config/opencode.md +1 -1
  18. package/docs/reference/sample-waves.md +4 -4
  19. package/docs/research/agent-context-sources.md +2 -0
  20. package/docs/research/coordination-failure-review.md +37 -13
  21. package/package.json +1 -1
  22. package/releases/manifest.json +33 -0
  23. package/scripts/wave-orchestrator/agent-state.mjs +10 -3
  24. package/scripts/wave-orchestrator/config.mjs +19 -0
  25. package/scripts/wave-orchestrator/dashboard-renderer.mjs +150 -20
  26. package/scripts/wave-orchestrator/dashboard-state.mjs +8 -0
  27. package/scripts/wave-orchestrator/executors.mjs +67 -4
  28. package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -0
  29. package/scripts/wave-orchestrator/launcher.mjs +245 -10
  30. package/scripts/wave-orchestrator/terminals.mjs +25 -0
  31. package/scripts/wave-orchestrator/wave-files.mjs +31 -0
@@ -81,6 +81,7 @@ import {
81
81
  writeTextAtomic,
82
82
  } from "./shared.mjs";
83
83
  import {
84
+ createCurrentWaveDashboardTerminalEntry,
84
85
  appendTerminalEntries,
85
86
  createGlobalDashboardTerminalEntry,
86
87
  createTemporaryTerminalEntries,
@@ -1449,11 +1450,94 @@ export function readWaveImplementationGate(wave, agentRuns) {
1449
1450
  };
1450
1451
  }
1451
1452
 
1453
+ function analyzePromotedComponentOwners(componentId, agentRuns, summariesByAgentId) {
1454
+ const ownerRuns = (agentRuns || []).filter((runInfo) =>
1455
+ runInfo.agent.components?.includes(componentId),
1456
+ );
1457
+ const ownerAgentIds = ownerRuns.map((runInfo) => runInfo.agent.agentId);
1458
+ const satisfiedAgentIds = [];
1459
+ const waitingOnAgentIds = [];
1460
+ const failedOwnContractAgentIds = [];
1461
+ for (const runInfo of ownerRuns) {
1462
+ const summary = summariesByAgentId?.[runInfo.agent.agentId] || null;
1463
+ const implementationValidation = validateImplementationSummary(runInfo.agent, summary);
1464
+ const componentMarkers = new Map(
1465
+ Array.isArray(summary?.components)
1466
+ ? summary.components.map((component) => [component.componentId, component])
1467
+ : [],
1468
+ );
1469
+ const marker = componentMarkers.get(componentId);
1470
+ const expectedLevel = runInfo.agent.componentTargets?.[componentId] || null;
1471
+ const componentSatisfied =
1472
+ marker &&
1473
+ marker.state === "met" &&
1474
+ (!expectedLevel || marker.level === expectedLevel);
1475
+ if (implementationValidation.ok && componentSatisfied) {
1476
+ satisfiedAgentIds.push(runInfo.agent.agentId);
1477
+ continue;
1478
+ }
1479
+ waitingOnAgentIds.push(runInfo.agent.agentId);
1480
+ if (!implementationValidation.ok) {
1481
+ failedOwnContractAgentIds.push(runInfo.agent.agentId);
1482
+ }
1483
+ }
1484
+ return {
1485
+ componentId,
1486
+ ownerRuns,
1487
+ ownerAgentIds,
1488
+ satisfiedAgentIds,
1489
+ waitingOnAgentIds,
1490
+ failedOwnContractAgentIds,
1491
+ };
1492
+ }
1493
+
1494
+ function buildSharedComponentSiblingPendingFailure(componentState) {
1495
+ if (
1496
+ !componentState ||
1497
+ componentState.satisfiedAgentIds.length === 0 ||
1498
+ componentState.waitingOnAgentIds.length === 0
1499
+ ) {
1500
+ return null;
1501
+ }
1502
+ const landedSummary =
1503
+ componentState.satisfiedAgentIds.length === 1
1504
+ ? `${componentState.satisfiedAgentIds[0]} desired-state slice landed`
1505
+ : `${componentState.satisfiedAgentIds.join(", ")} desired-state slices landed`;
1506
+ const ownerRun =
1507
+ componentState.ownerRuns.find((runInfo) =>
1508
+ componentState.waitingOnAgentIds.includes(runInfo.agent.agentId),
1509
+ ) ||
1510
+ componentState.ownerRuns[0] ||
1511
+ null;
1512
+ return {
1513
+ ok: false,
1514
+ agentId: componentState.waitingOnAgentIds[0] || ownerRun?.agent?.agentId || null,
1515
+ componentId: componentState.componentId || null,
1516
+ statusCode: "shared-component-sibling-pending",
1517
+ detail: `${landedSummary}; shared component closure still depends on ${componentState.waitingOnAgentIds.join("/")}.`,
1518
+ logPath: ownerRun ? path.relative(REPO_ROOT, ownerRun.logPath) : null,
1519
+ ownerAgentIds: componentState.ownerAgentIds,
1520
+ satisfiedAgentIds: componentState.satisfiedAgentIds,
1521
+ waitingOnAgentIds: componentState.waitingOnAgentIds,
1522
+ failedOwnContractAgentIds: componentState.failedOwnContractAgentIds,
1523
+ };
1524
+ }
1525
+
1452
1526
  export function readWaveComponentGate(wave, agentRuns, options = {}) {
1453
1527
  const summariesByAgentId = Object.fromEntries(
1454
1528
  agentRuns.map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)]),
1455
1529
  );
1456
1530
  const validation = validateWaveComponentPromotions(wave, summariesByAgentId, options);
1531
+ const sharedPending = (wave.componentPromotions || [])
1532
+ .map((promotion) =>
1533
+ buildSharedComponentSiblingPendingFailure(
1534
+ analyzePromotedComponentOwners(promotion.componentId, agentRuns, summariesByAgentId),
1535
+ ),
1536
+ )
1537
+ .find(Boolean);
1538
+ if (sharedPending) {
1539
+ return sharedPending;
1540
+ }
1457
1541
  if (validation.ok) {
1458
1542
  return {
1459
1543
  ok: true,
@@ -1464,8 +1548,12 @@ export function readWaveComponentGate(wave, agentRuns, options = {}) {
1464
1548
  logPath: null,
1465
1549
  };
1466
1550
  }
1467
- const ownerRun =
1468
- agentRuns.find((runInfo) => runInfo.agent.components?.includes(validation.componentId)) ?? null;
1551
+ const componentState = analyzePromotedComponentOwners(
1552
+ validation.componentId,
1553
+ agentRuns,
1554
+ summariesByAgentId,
1555
+ );
1556
+ const ownerRun = componentState.ownerRuns[0] ?? null;
1469
1557
  return {
1470
1558
  ok: false,
1471
1559
  agentId: ownerRun?.agent?.agentId || null,
@@ -1473,6 +1561,10 @@ export function readWaveComponentGate(wave, agentRuns, options = {}) {
1473
1561
  statusCode: validation.statusCode,
1474
1562
  detail: validation.detail,
1475
1563
  logPath: ownerRun ? path.relative(REPO_ROOT, ownerRun.logPath) : null,
1564
+ ownerAgentIds: componentState.ownerAgentIds,
1565
+ satisfiedAgentIds: componentState.satisfiedAgentIds,
1566
+ waitingOnAgentIds: componentState.waitingOnAgentIds,
1567
+ failedOwnContractAgentIds: componentState.failedOwnContractAgentIds,
1476
1568
  };
1477
1569
  }
1478
1570
 
@@ -1799,6 +1891,38 @@ function removeOrphanWaveDashboards(lanePaths, activeSessionNames) {
1799
1891
  return removedDashboardPaths;
1800
1892
  }
1801
1893
 
1894
+ function pruneDryRunExecutorPreviewDirs(lanePaths, waves) {
1895
+ if (!fs.existsSync(lanePaths.executorOverlaysDir)) {
1896
+ return [];
1897
+ }
1898
+ const expectedSlugsByWave = new Map(
1899
+ (waves || []).map((wave) => [wave.wave, new Set((wave.agents || []).map((agent) => agent.slug))]),
1900
+ );
1901
+ const removedPaths = [];
1902
+ for (const entry of fs.readdirSync(lanePaths.executorOverlaysDir, { withFileTypes: true })) {
1903
+ if (!entry.isDirectory() || !/^wave-\d+$/.test(entry.name)) {
1904
+ continue;
1905
+ }
1906
+ const waveNumber = Number.parseInt(entry.name.slice("wave-".length), 10);
1907
+ const waveDir = path.join(lanePaths.executorOverlaysDir, entry.name);
1908
+ const expectedSlugs = expectedSlugsByWave.get(waveNumber);
1909
+ if (!expectedSlugs) {
1910
+ fs.rmSync(waveDir, { recursive: true, force: true });
1911
+ removedPaths.push(path.relative(REPO_ROOT, waveDir));
1912
+ continue;
1913
+ }
1914
+ for (const child of fs.readdirSync(waveDir, { withFileTypes: true })) {
1915
+ if (!child.isDirectory() || expectedSlugs.has(child.name)) {
1916
+ continue;
1917
+ }
1918
+ const childPath = path.join(waveDir, child.name);
1919
+ fs.rmSync(childPath, { recursive: true, force: true });
1920
+ removedPaths.push(path.relative(REPO_ROOT, childPath));
1921
+ }
1922
+ }
1923
+ return removedPaths.toSorted();
1924
+ }
1925
+
1802
1926
  export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
1803
1927
  const outcome = {
1804
1928
  removedLock: false,
@@ -2162,9 +2286,63 @@ function relaunchReasonBuckets(runs, failures, derivedState) {
2162
2286
  closureGate: (failures || []).some(
2163
2287
  (failure) => failure.agentId && selectedAgentIds.has(failure.agentId),
2164
2288
  ),
2289
+ sharedComponentSiblingWait: (failures || []).some(
2290
+ (failure) =>
2291
+ failure.statusCode === "shared-component-sibling-pending" &&
2292
+ (failure.waitingOnAgentIds || []).some((agentId) => selectedAgentIds.has(agentId)),
2293
+ ),
2165
2294
  };
2166
2295
  }
2167
2296
 
2297
+ function applySharedComponentWaitStateToDashboard(componentGate, dashboardState) {
2298
+ const waitingSummary = (componentGate?.waitingOnAgentIds || []).join("/");
2299
+ if (!waitingSummary) {
2300
+ return;
2301
+ }
2302
+ for (const agentId of componentGate?.satisfiedAgentIds || []) {
2303
+ setWaveDashboardAgent(dashboardState, agentId, {
2304
+ state: "completed",
2305
+ detail: `Desired-state slice landed; waiting on ${waitingSummary} for shared component closure`,
2306
+ });
2307
+ }
2308
+ }
2309
+
2310
+ function reconcileFailuresAgainstSharedComponentState(wave, agentRuns, failures) {
2311
+ if (!Array.isArray(failures) || failures.length === 0) {
2312
+ return failures;
2313
+ }
2314
+ const summariesByAgentId = Object.fromEntries(
2315
+ (agentRuns || []).map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)]),
2316
+ );
2317
+ const failureAgentIds = new Set(failures.map((failure) => failure.agentId).filter(Boolean));
2318
+ const consumedSatisfiedAgentIds = new Set();
2319
+ const synthesizedFailures = [];
2320
+ for (const promotion of wave?.componentPromotions || []) {
2321
+ const componentState = analyzePromotedComponentOwners(
2322
+ promotion.componentId,
2323
+ agentRuns,
2324
+ summariesByAgentId,
2325
+ );
2326
+ if (
2327
+ componentState.satisfiedAgentIds.length === 0 ||
2328
+ componentState.waitingOnAgentIds.length === 0 ||
2329
+ !componentState.satisfiedAgentIds.some((agentId) => failureAgentIds.has(agentId))
2330
+ ) {
2331
+ continue;
2332
+ }
2333
+ for (const agentId of componentState.satisfiedAgentIds) {
2334
+ if (failureAgentIds.has(agentId)) {
2335
+ consumedSatisfiedAgentIds.add(agentId);
2336
+ }
2337
+ }
2338
+ synthesizedFailures.push(buildSharedComponentSiblingPendingFailure(componentState));
2339
+ }
2340
+ return [
2341
+ ...synthesizedFailures.filter(Boolean),
2342
+ ...failures.filter((failure) => !consumedSatisfiedAgentIds.has(failure.agentId)),
2343
+ ];
2344
+ }
2345
+
2168
2346
  export function hasReusableSuccessStatus(agent, statusPath, options = {}) {
2169
2347
  const statusRecord = readStatusRecordIfPresent(statusPath);
2170
2348
  const basicReuseOk = Boolean(
@@ -2272,7 +2450,11 @@ function buildFallbackExecutorState(executorState, executorId, attempt, reason)
2272
2450
  }
2273
2451
 
2274
2452
  function applyRetryFallbacks(agentRuns, failures, lanePaths, attemptNumber, waveDefinition = null) {
2275
- const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
2453
+ const failedAgentIds = new Set(
2454
+ failures
2455
+ .filter((failure) => failure.statusCode !== "shared-component-sibling-pending")
2456
+ .map((failure) => failure.agentId),
2457
+ );
2276
2458
  let changed = false;
2277
2459
  const outcomes = new Map();
2278
2460
  for (const run of agentRuns) {
@@ -2733,6 +2915,20 @@ export function resolveRelaunchRuns(agentRuns, failures, derivedState, lanePaths
2733
2915
  barrier: null,
2734
2916
  };
2735
2917
  }
2918
+ const sharedComponentWaitingAgentIds = new Set(
2919
+ (failures || [])
2920
+ .filter((failure) => failure.statusCode === "shared-component-sibling-pending")
2921
+ .flatMap((failure) => failure.waitingOnAgentIds || [])
2922
+ .filter((agentId) => runsByAgentId.has(agentId)),
2923
+ );
2924
+ if (sharedComponentWaitingAgentIds.size > 0) {
2925
+ return {
2926
+ runs: Array.from(sharedComponentWaitingAgentIds)
2927
+ .map((agentId) => runsByAgentId.get(agentId))
2928
+ .filter(Boolean),
2929
+ barrier: null,
2930
+ };
2931
+ }
2736
2932
  const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
2737
2933
  return {
2738
2934
  runs: agentRuns.filter((run) => failedAgentIds.has(run.agent.agentId)),
@@ -2779,6 +2975,8 @@ export async function runLauncherCli(argv) {
2779
2975
  let globalDashboard = null;
2780
2976
  let globalDashboardTerminalEntry = null;
2781
2977
  let globalDashboardTerminalAppended = false;
2978
+ let currentWaveDashboardTerminalEntry = null;
2979
+ let currentWaveDashboardTerminalAppended = false;
2782
2980
  let selectedWavesForCoordination = [];
2783
2981
 
2784
2982
  const appendCoordination = ({
@@ -2976,6 +3174,7 @@ export async function runLauncherCli(argv) {
2976
3174
  });
2977
3175
 
2978
3176
  if (options.dryRun) {
3177
+ pruneDryRunExecutorPreviewDirs(lanePaths, allWaves);
2979
3178
  for (const wave of filteredWaves) {
2980
3179
  const derivedState = writeWaveDerivedState({
2981
3180
  lanePaths,
@@ -3072,9 +3271,14 @@ export async function runLauncherCli(argv) {
3072
3271
  lanePaths,
3073
3272
  globalDashboard.runId || "global",
3074
3273
  );
3274
+ currentWaveDashboardTerminalEntry = createCurrentWaveDashboardTerminalEntry(lanePaths);
3075
3275
  if (terminalRegistryEnabled) {
3076
- appendTerminalEntries(lanePaths.terminalsPath, [globalDashboardTerminalEntry]);
3276
+ appendTerminalEntries(lanePaths.terminalsPath, [
3277
+ globalDashboardTerminalEntry,
3278
+ currentWaveDashboardTerminalEntry,
3279
+ ]);
3077
3280
  globalDashboardTerminalAppended = true;
3281
+ currentWaveDashboardTerminalAppended = true;
3078
3282
  }
3079
3283
  launchWaveDashboardSession(lanePaths, {
3080
3284
  sessionName: globalDashboardTerminalEntry.sessionName,
@@ -3152,7 +3356,7 @@ export async function runLauncherCli(argv) {
3152
3356
  wave.wave,
3153
3357
  wave.agents,
3154
3358
  runTag,
3155
- options.dashboard,
3359
+ false,
3156
3360
  );
3157
3361
  if (terminalRegistryEnabled) {
3158
3362
  appendTerminalEntries(lanePaths.terminalsPath, terminalEntries);
@@ -3265,12 +3469,9 @@ export async function runLauncherCli(argv) {
3265
3469
  }
3266
3470
  flushDashboards();
3267
3471
 
3268
- const dashboardEntry = terminalEntries.find(
3269
- (entry) => entry.terminalName === `${lanePaths.dashboardTerminalNamePrefix}${wave.wave}`,
3270
- );
3271
- if (options.dashboard && dashboardEntry) {
3472
+ if (options.dashboard && currentWaveDashboardTerminalEntry) {
3272
3473
  launchWaveDashboardSession(lanePaths, {
3273
- sessionName: dashboardEntry.sessionName,
3474
+ sessionName: currentWaveDashboardTerminalEntry.sessionName,
3274
3475
  dashboardPath,
3275
3476
  messageBoardPath,
3276
3477
  });
@@ -3442,6 +3643,12 @@ export async function runLauncherCli(argv) {
3442
3643
 
3443
3644
  materializeAgentExecutionSummaries(wave, agentRuns);
3444
3645
  refreshDerivedState(attempt);
3646
+ failures = reconcileFailuresAgainstSharedComponentState(wave, agentRuns, failures);
3647
+ for (const failure of failures) {
3648
+ if (failure.statusCode === "shared-component-sibling-pending") {
3649
+ applySharedComponentWaitStateToDashboard(failure, dashboardState);
3650
+ }
3651
+ }
3445
3652
 
3446
3653
  if (failures.length > 0) {
3447
3654
  for (const failure of failures) {
@@ -3483,12 +3690,20 @@ export async function runLauncherCli(argv) {
3483
3690
  laneProfile: lanePaths.laneProfile,
3484
3691
  });
3485
3692
  if (!componentGate.ok) {
3693
+ if (componentGate.statusCode === "shared-component-sibling-pending") {
3694
+ applySharedComponentWaitStateToDashboard(componentGate, dashboardState);
3695
+ }
3486
3696
  failures = [
3487
3697
  {
3488
3698
  agentId: componentGate.agentId,
3489
3699
  statusCode: componentGate.statusCode,
3490
3700
  logPath:
3491
3701
  componentGate.logPath || path.relative(REPO_ROOT, messageBoardPath),
3702
+ detail: componentGate.detail,
3703
+ ownerAgentIds: componentGate.ownerAgentIds || [],
3704
+ satisfiedAgentIds: componentGate.satisfiedAgentIds || [],
3705
+ waitingOnAgentIds: componentGate.waitingOnAgentIds || [],
3706
+ failedOwnContractAgentIds: componentGate.failedOwnContractAgentIds || [],
3492
3707
  },
3493
3708
  ];
3494
3709
  recordCombinedEvent({
@@ -4045,6 +4260,9 @@ export async function runLauncherCli(argv) {
4045
4260
  if (globalDashboardTerminalEntry) {
4046
4261
  excludeSessionNames.add(globalDashboardTerminalEntry.sessionName);
4047
4262
  }
4263
+ if (currentWaveDashboardTerminalEntry) {
4264
+ excludeSessionNames.add(currentWaveDashboardTerminalEntry.sessionName);
4265
+ }
4048
4266
  cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames });
4049
4267
  }
4050
4268
  if (globalWave && globalWave.status === "running") {
@@ -4079,6 +4297,13 @@ export async function runLauncherCli(argv) {
4079
4297
  if (globalDashboardTerminalAppended && globalDashboardTerminalEntry && !options.keepTerminals) {
4080
4298
  removeTerminalEntries(lanePaths.terminalsPath, [globalDashboardTerminalEntry]);
4081
4299
  }
4300
+ if (
4301
+ currentWaveDashboardTerminalAppended &&
4302
+ currentWaveDashboardTerminalEntry &&
4303
+ !options.keepTerminals
4304
+ ) {
4305
+ removeTerminalEntries(lanePaths.terminalsPath, [currentWaveDashboardTerminalEntry]);
4306
+ }
4082
4307
  if (options.cleanupSessions && globalDashboardTerminalEntry) {
4083
4308
  try {
4084
4309
  killTmuxSessionIfExists(lanePaths.tmuxSocketName, globalDashboardTerminalEntry.sessionName);
@@ -4086,6 +4311,16 @@ export async function runLauncherCli(argv) {
4086
4311
  // no-op
4087
4312
  }
4088
4313
  }
4314
+ if (options.cleanupSessions && currentWaveDashboardTerminalEntry) {
4315
+ try {
4316
+ killTmuxSessionIfExists(
4317
+ lanePaths.tmuxSocketName,
4318
+ currentWaveDashboardTerminalEntry.sessionName,
4319
+ );
4320
+ } catch {
4321
+ // no-op
4322
+ }
4323
+ }
4089
4324
  if (lockHeld) {
4090
4325
  releaseLauncherLock(lanePaths.launcherLockPath);
4091
4326
  }
@@ -61,11 +61,18 @@ export function writeTerminalsConfig(filePath, config) {
61
61
  function isLaneTemporaryTerminalName(name, lanePaths) {
62
62
  return (
63
63
  name === lanePaths.globalDashboardTerminalName ||
64
+ name === currentWaveDashboardTerminalName(lanePaths) ||
64
65
  name.startsWith(lanePaths.terminalNamePrefix) ||
65
66
  name.startsWith(lanePaths.dashboardTerminalNamePrefix)
66
67
  );
67
68
  }
68
69
 
70
+ function currentWaveDashboardTerminalName(lanePaths) {
71
+ return lanePaths.lane === "main"
72
+ ? "Current Wave Dashboard"
73
+ : `Current Wave Dashboard (${lanePaths.lane})`;
74
+ }
75
+
69
76
  function extractTmuxSessionName(command, socketName) {
70
77
  const text = String(command || "").trim();
71
78
  const marker = `tmux -L ${socketName} new -As `;
@@ -138,6 +145,24 @@ export function createGlobalDashboardTerminalEntry(lanePaths, runTag) {
138
145
  };
139
146
  }
140
147
 
148
+ export function createCurrentWaveDashboardTerminalEntry(lanePaths) {
149
+ const sessionName = `${lanePaths.tmuxDashboardSessionPrefix}_current`.replace(
150
+ /[^a-zA-Z0-9:_-]/g,
151
+ "_",
152
+ );
153
+ const terminalName = currentWaveDashboardTerminalName(lanePaths);
154
+ return {
155
+ terminalName,
156
+ sessionName,
157
+ config: {
158
+ name: terminalName,
159
+ icon: DASHBOARD_TERMINAL_ICON,
160
+ color: DASHBOARD_TERMINAL_COLOR,
161
+ command: `TMUX= tmux -L ${lanePaths.tmuxSocketName} new -As ${sessionName}`,
162
+ },
163
+ };
164
+ }
165
+
141
166
  export function appendTerminalEntries(terminalsPath, entries) {
142
167
  const config = readTerminalsConfig(terminalsPath);
143
168
  const namesToReplace = new Set(entries.map((entry) => entry.terminalName));
@@ -13,6 +13,7 @@ import {
13
13
  DEFAULT_INTEGRATION_ROLE_PROMPT_PATH,
14
14
  DEFAULT_SECURITY_ROLE_PROMPT_PATH,
15
15
  DEFAULT_WAVE_LANE,
16
+ normalizeClaudeEffort,
16
17
  loadWaveConfig,
17
18
  normalizeCodexSandboxMode,
18
19
  normalizeExecutorMode,
@@ -700,6 +701,7 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
700
701
  "claude.agent",
701
702
  "claude.permission_mode",
702
703
  "claude.permission_prompt_tool",
704
+ "claude.effort",
703
705
  "claude.max_turns",
704
706
  "claude.mcp_config",
705
707
  "claude.settings",
@@ -827,6 +829,11 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
827
829
  ...(executorConfig.claude || {}),
828
830
  permissionPromptTool: value,
829
831
  };
832
+ } else if (key === "claude.effort") {
833
+ executorConfig.claude = {
834
+ ...(executorConfig.claude || {}),
835
+ effort: normalizeClaudeEffort(value, `${label}.claude.effort`),
836
+ };
830
837
  } else if (key === "claude.max_turns") {
831
838
  executorConfig.claude = {
832
839
  ...(executorConfig.claude || {}),
@@ -2038,6 +2045,28 @@ export function resolveAgentExecutor(agent, options = {}) {
2038
2045
  profile?.budget?.minutes ??
2039
2046
  null,
2040
2047
  };
2048
+ const claudeMaxTurnsSource =
2049
+ executorConfig?.claude?.maxTurns !== null && executorConfig?.claude?.maxTurns !== undefined
2050
+ ? "claude.maxTurns"
2051
+ : profile?.claude?.maxTurns !== null && profile?.claude?.maxTurns !== undefined
2052
+ ? "claude.maxTurns"
2053
+ : runtimeBudget.turns !== null
2054
+ ? "budget.turns"
2055
+ : laneProfile.executors.claude.maxTurns !== null &&
2056
+ laneProfile.executors.claude.maxTurns !== undefined
2057
+ ? "claude.maxTurns"
2058
+ : null;
2059
+ const opencodeStepsSource =
2060
+ executorConfig?.opencode?.steps !== null && executorConfig?.opencode?.steps !== undefined
2061
+ ? "opencode.steps"
2062
+ : profile?.opencode?.steps !== null && profile?.opencode?.steps !== undefined
2063
+ ? "opencode.steps"
2064
+ : runtimeBudget.turns !== null
2065
+ ? "budget.turns"
2066
+ : laneProfile.executors.opencode.steps !== null &&
2067
+ laneProfile.executors.opencode.steps !== undefined
2068
+ ? "opencode.steps"
2069
+ : null;
2041
2070
  return {
2042
2071
  id: executorId,
2043
2072
  initialExecutorId: executorId,
@@ -2122,6 +2151,7 @@ export function resolveAgentExecutor(agent, options = {}) {
2122
2151
  profile?.claude?.maxTurns ??
2123
2152
  runtimeBudget.turns ??
2124
2153
  laneProfile.executors.claude.maxTurns,
2154
+ maxTurnsSource: claudeMaxTurnsSource,
2125
2155
  },
2126
2156
  opencode: {
2127
2157
  ...mergeExecutorSections(
@@ -2139,6 +2169,7 @@ export function resolveAgentExecutor(agent, options = {}) {
2139
2169
  profile?.opencode?.steps ??
2140
2170
  runtimeBudget.turns ??
2141
2171
  laneProfile.executors.opencode.steps,
2172
+ stepsSource: opencodeStepsSource,
2142
2173
  },
2143
2174
  };
2144
2175
  }