@chllming/wave-orchestration 0.6.1 → 0.6.3
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.
- package/CHANGELOG.md +16 -0
- package/README.md +79 -30
- package/docs/README.md +15 -3
- package/docs/concepts/context7-vs-skills.md +24 -0
- package/docs/concepts/runtime-agnostic-orchestration.md +17 -2
- package/docs/concepts/what-is-a-wave.md +28 -0
- package/docs/evals/README.md +2 -0
- package/docs/guides/terminal-surfaces.md +2 -0
- package/docs/plans/current-state.md +2 -1
- package/docs/plans/wave-orchestrator.md +22 -3
- package/docs/reference/runtime-config/README.md +4 -4
- package/docs/reference/runtime-config/claude.md +6 -1
- package/docs/reference/runtime-config/codex.md +2 -2
- package/docs/reference/runtime-config/opencode.md +1 -1
- package/docs/research/agent-context-sources.md +2 -0
- package/docs/research/coordination-failure-review.md +37 -13
- package/package.json +1 -1
- package/releases/manifest.json +33 -0
- package/scripts/wave-autonomous.mjs +2 -4
- package/scripts/wave-orchestrator/adhoc.mjs +32 -11
- package/scripts/wave-orchestrator/agent-state.mjs +10 -3
- package/scripts/wave-orchestrator/autonomous.mjs +20 -6
- package/scripts/wave-orchestrator/config.mjs +19 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +150 -20
- package/scripts/wave-orchestrator/dashboard-state.mjs +8 -0
- package/scripts/wave-orchestrator/executors.mjs +67 -4
- package/scripts/wave-orchestrator/install.mjs +198 -25
- package/scripts/wave-orchestrator/launcher-runtime.mjs +1 -0
- package/scripts/wave-orchestrator/launcher.mjs +249 -10
- package/scripts/wave-orchestrator/package-update-notice.mjs +230 -0
- package/scripts/wave-orchestrator/package-version.mjs +32 -0
- package/scripts/wave-orchestrator/terminals.mjs +25 -0
- package/scripts/wave-orchestrator/wave-files.mjs +31 -0
- package/scripts/wave.mjs +12 -2
|
@@ -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,
|
|
@@ -97,6 +98,7 @@ import {
|
|
|
97
98
|
commandForExecutor,
|
|
98
99
|
isExecutorCommandAvailable,
|
|
99
100
|
} from "./executors.mjs";
|
|
101
|
+
import { maybeAnnouncePackageUpdate } from "./package-update-notice.mjs";
|
|
100
102
|
import {
|
|
101
103
|
agentRequiresProofCentricValidation,
|
|
102
104
|
buildRunStateEvidence,
|
|
@@ -1449,11 +1451,94 @@ export function readWaveImplementationGate(wave, agentRuns) {
|
|
|
1449
1451
|
};
|
|
1450
1452
|
}
|
|
1451
1453
|
|
|
1454
|
+
function analyzePromotedComponentOwners(componentId, agentRuns, summariesByAgentId) {
|
|
1455
|
+
const ownerRuns = (agentRuns || []).filter((runInfo) =>
|
|
1456
|
+
runInfo.agent.components?.includes(componentId),
|
|
1457
|
+
);
|
|
1458
|
+
const ownerAgentIds = ownerRuns.map((runInfo) => runInfo.agent.agentId);
|
|
1459
|
+
const satisfiedAgentIds = [];
|
|
1460
|
+
const waitingOnAgentIds = [];
|
|
1461
|
+
const failedOwnContractAgentIds = [];
|
|
1462
|
+
for (const runInfo of ownerRuns) {
|
|
1463
|
+
const summary = summariesByAgentId?.[runInfo.agent.agentId] || null;
|
|
1464
|
+
const implementationValidation = validateImplementationSummary(runInfo.agent, summary);
|
|
1465
|
+
const componentMarkers = new Map(
|
|
1466
|
+
Array.isArray(summary?.components)
|
|
1467
|
+
? summary.components.map((component) => [component.componentId, component])
|
|
1468
|
+
: [],
|
|
1469
|
+
);
|
|
1470
|
+
const marker = componentMarkers.get(componentId);
|
|
1471
|
+
const expectedLevel = runInfo.agent.componentTargets?.[componentId] || null;
|
|
1472
|
+
const componentSatisfied =
|
|
1473
|
+
marker &&
|
|
1474
|
+
marker.state === "met" &&
|
|
1475
|
+
(!expectedLevel || marker.level === expectedLevel);
|
|
1476
|
+
if (implementationValidation.ok && componentSatisfied) {
|
|
1477
|
+
satisfiedAgentIds.push(runInfo.agent.agentId);
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
waitingOnAgentIds.push(runInfo.agent.agentId);
|
|
1481
|
+
if (!implementationValidation.ok) {
|
|
1482
|
+
failedOwnContractAgentIds.push(runInfo.agent.agentId);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
componentId,
|
|
1487
|
+
ownerRuns,
|
|
1488
|
+
ownerAgentIds,
|
|
1489
|
+
satisfiedAgentIds,
|
|
1490
|
+
waitingOnAgentIds,
|
|
1491
|
+
failedOwnContractAgentIds,
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function buildSharedComponentSiblingPendingFailure(componentState) {
|
|
1496
|
+
if (
|
|
1497
|
+
!componentState ||
|
|
1498
|
+
componentState.satisfiedAgentIds.length === 0 ||
|
|
1499
|
+
componentState.waitingOnAgentIds.length === 0
|
|
1500
|
+
) {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
const landedSummary =
|
|
1504
|
+
componentState.satisfiedAgentIds.length === 1
|
|
1505
|
+
? `${componentState.satisfiedAgentIds[0]} desired-state slice landed`
|
|
1506
|
+
: `${componentState.satisfiedAgentIds.join(", ")} desired-state slices landed`;
|
|
1507
|
+
const ownerRun =
|
|
1508
|
+
componentState.ownerRuns.find((runInfo) =>
|
|
1509
|
+
componentState.waitingOnAgentIds.includes(runInfo.agent.agentId),
|
|
1510
|
+
) ||
|
|
1511
|
+
componentState.ownerRuns[0] ||
|
|
1512
|
+
null;
|
|
1513
|
+
return {
|
|
1514
|
+
ok: false,
|
|
1515
|
+
agentId: componentState.waitingOnAgentIds[0] || ownerRun?.agent?.agentId || null,
|
|
1516
|
+
componentId: componentState.componentId || null,
|
|
1517
|
+
statusCode: "shared-component-sibling-pending",
|
|
1518
|
+
detail: `${landedSummary}; shared component closure still depends on ${componentState.waitingOnAgentIds.join("/")}.`,
|
|
1519
|
+
logPath: ownerRun ? path.relative(REPO_ROOT, ownerRun.logPath) : null,
|
|
1520
|
+
ownerAgentIds: componentState.ownerAgentIds,
|
|
1521
|
+
satisfiedAgentIds: componentState.satisfiedAgentIds,
|
|
1522
|
+
waitingOnAgentIds: componentState.waitingOnAgentIds,
|
|
1523
|
+
failedOwnContractAgentIds: componentState.failedOwnContractAgentIds,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1452
1527
|
export function readWaveComponentGate(wave, agentRuns, options = {}) {
|
|
1453
1528
|
const summariesByAgentId = Object.fromEntries(
|
|
1454
1529
|
agentRuns.map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)]),
|
|
1455
1530
|
);
|
|
1456
1531
|
const validation = validateWaveComponentPromotions(wave, summariesByAgentId, options);
|
|
1532
|
+
const sharedPending = (wave.componentPromotions || [])
|
|
1533
|
+
.map((promotion) =>
|
|
1534
|
+
buildSharedComponentSiblingPendingFailure(
|
|
1535
|
+
analyzePromotedComponentOwners(promotion.componentId, agentRuns, summariesByAgentId),
|
|
1536
|
+
),
|
|
1537
|
+
)
|
|
1538
|
+
.find(Boolean);
|
|
1539
|
+
if (sharedPending) {
|
|
1540
|
+
return sharedPending;
|
|
1541
|
+
}
|
|
1457
1542
|
if (validation.ok) {
|
|
1458
1543
|
return {
|
|
1459
1544
|
ok: true,
|
|
@@ -1464,8 +1549,12 @@ export function readWaveComponentGate(wave, agentRuns, options = {}) {
|
|
|
1464
1549
|
logPath: null,
|
|
1465
1550
|
};
|
|
1466
1551
|
}
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1552
|
+
const componentState = analyzePromotedComponentOwners(
|
|
1553
|
+
validation.componentId,
|
|
1554
|
+
agentRuns,
|
|
1555
|
+
summariesByAgentId,
|
|
1556
|
+
);
|
|
1557
|
+
const ownerRun = componentState.ownerRuns[0] ?? null;
|
|
1469
1558
|
return {
|
|
1470
1559
|
ok: false,
|
|
1471
1560
|
agentId: ownerRun?.agent?.agentId || null,
|
|
@@ -1473,6 +1562,10 @@ export function readWaveComponentGate(wave, agentRuns, options = {}) {
|
|
|
1473
1562
|
statusCode: validation.statusCode,
|
|
1474
1563
|
detail: validation.detail,
|
|
1475
1564
|
logPath: ownerRun ? path.relative(REPO_ROOT, ownerRun.logPath) : null,
|
|
1565
|
+
ownerAgentIds: componentState.ownerAgentIds,
|
|
1566
|
+
satisfiedAgentIds: componentState.satisfiedAgentIds,
|
|
1567
|
+
waitingOnAgentIds: componentState.waitingOnAgentIds,
|
|
1568
|
+
failedOwnContractAgentIds: componentState.failedOwnContractAgentIds,
|
|
1476
1569
|
};
|
|
1477
1570
|
}
|
|
1478
1571
|
|
|
@@ -1799,6 +1892,38 @@ function removeOrphanWaveDashboards(lanePaths, activeSessionNames) {
|
|
|
1799
1892
|
return removedDashboardPaths;
|
|
1800
1893
|
}
|
|
1801
1894
|
|
|
1895
|
+
function pruneDryRunExecutorPreviewDirs(lanePaths, waves) {
|
|
1896
|
+
if (!fs.existsSync(lanePaths.executorOverlaysDir)) {
|
|
1897
|
+
return [];
|
|
1898
|
+
}
|
|
1899
|
+
const expectedSlugsByWave = new Map(
|
|
1900
|
+
(waves || []).map((wave) => [wave.wave, new Set((wave.agents || []).map((agent) => agent.slug))]),
|
|
1901
|
+
);
|
|
1902
|
+
const removedPaths = [];
|
|
1903
|
+
for (const entry of fs.readdirSync(lanePaths.executorOverlaysDir, { withFileTypes: true })) {
|
|
1904
|
+
if (!entry.isDirectory() || !/^wave-\d+$/.test(entry.name)) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const waveNumber = Number.parseInt(entry.name.slice("wave-".length), 10);
|
|
1908
|
+
const waveDir = path.join(lanePaths.executorOverlaysDir, entry.name);
|
|
1909
|
+
const expectedSlugs = expectedSlugsByWave.get(waveNumber);
|
|
1910
|
+
if (!expectedSlugs) {
|
|
1911
|
+
fs.rmSync(waveDir, { recursive: true, force: true });
|
|
1912
|
+
removedPaths.push(path.relative(REPO_ROOT, waveDir));
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
for (const child of fs.readdirSync(waveDir, { withFileTypes: true })) {
|
|
1916
|
+
if (!child.isDirectory() || expectedSlugs.has(child.name)) {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const childPath = path.join(waveDir, child.name);
|
|
1920
|
+
fs.rmSync(childPath, { recursive: true, force: true });
|
|
1921
|
+
removedPaths.push(path.relative(REPO_ROOT, childPath));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return removedPaths.toSorted();
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1802
1927
|
export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
|
|
1803
1928
|
const outcome = {
|
|
1804
1929
|
removedLock: false,
|
|
@@ -2162,9 +2287,63 @@ function relaunchReasonBuckets(runs, failures, derivedState) {
|
|
|
2162
2287
|
closureGate: (failures || []).some(
|
|
2163
2288
|
(failure) => failure.agentId && selectedAgentIds.has(failure.agentId),
|
|
2164
2289
|
),
|
|
2290
|
+
sharedComponentSiblingWait: (failures || []).some(
|
|
2291
|
+
(failure) =>
|
|
2292
|
+
failure.statusCode === "shared-component-sibling-pending" &&
|
|
2293
|
+
(failure.waitingOnAgentIds || []).some((agentId) => selectedAgentIds.has(agentId)),
|
|
2294
|
+
),
|
|
2165
2295
|
};
|
|
2166
2296
|
}
|
|
2167
2297
|
|
|
2298
|
+
function applySharedComponentWaitStateToDashboard(componentGate, dashboardState) {
|
|
2299
|
+
const waitingSummary = (componentGate?.waitingOnAgentIds || []).join("/");
|
|
2300
|
+
if (!waitingSummary) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
for (const agentId of componentGate?.satisfiedAgentIds || []) {
|
|
2304
|
+
setWaveDashboardAgent(dashboardState, agentId, {
|
|
2305
|
+
state: "completed",
|
|
2306
|
+
detail: `Desired-state slice landed; waiting on ${waitingSummary} for shared component closure`,
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function reconcileFailuresAgainstSharedComponentState(wave, agentRuns, failures) {
|
|
2312
|
+
if (!Array.isArray(failures) || failures.length === 0) {
|
|
2313
|
+
return failures;
|
|
2314
|
+
}
|
|
2315
|
+
const summariesByAgentId = Object.fromEntries(
|
|
2316
|
+
(agentRuns || []).map((runInfo) => [runInfo.agent.agentId, readRunExecutionSummary(runInfo, wave)]),
|
|
2317
|
+
);
|
|
2318
|
+
const failureAgentIds = new Set(failures.map((failure) => failure.agentId).filter(Boolean));
|
|
2319
|
+
const consumedSatisfiedAgentIds = new Set();
|
|
2320
|
+
const synthesizedFailures = [];
|
|
2321
|
+
for (const promotion of wave?.componentPromotions || []) {
|
|
2322
|
+
const componentState = analyzePromotedComponentOwners(
|
|
2323
|
+
promotion.componentId,
|
|
2324
|
+
agentRuns,
|
|
2325
|
+
summariesByAgentId,
|
|
2326
|
+
);
|
|
2327
|
+
if (
|
|
2328
|
+
componentState.satisfiedAgentIds.length === 0 ||
|
|
2329
|
+
componentState.waitingOnAgentIds.length === 0 ||
|
|
2330
|
+
!componentState.satisfiedAgentIds.some((agentId) => failureAgentIds.has(agentId))
|
|
2331
|
+
) {
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
for (const agentId of componentState.satisfiedAgentIds) {
|
|
2335
|
+
if (failureAgentIds.has(agentId)) {
|
|
2336
|
+
consumedSatisfiedAgentIds.add(agentId);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
synthesizedFailures.push(buildSharedComponentSiblingPendingFailure(componentState));
|
|
2340
|
+
}
|
|
2341
|
+
return [
|
|
2342
|
+
...synthesizedFailures.filter(Boolean),
|
|
2343
|
+
...failures.filter((failure) => !consumedSatisfiedAgentIds.has(failure.agentId)),
|
|
2344
|
+
];
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2168
2347
|
export function hasReusableSuccessStatus(agent, statusPath, options = {}) {
|
|
2169
2348
|
const statusRecord = readStatusRecordIfPresent(statusPath);
|
|
2170
2349
|
const basicReuseOk = Boolean(
|
|
@@ -2272,7 +2451,11 @@ function buildFallbackExecutorState(executorState, executorId, attempt, reason)
|
|
|
2272
2451
|
}
|
|
2273
2452
|
|
|
2274
2453
|
function applyRetryFallbacks(agentRuns, failures, lanePaths, attemptNumber, waveDefinition = null) {
|
|
2275
|
-
const failedAgentIds = new Set(
|
|
2454
|
+
const failedAgentIds = new Set(
|
|
2455
|
+
failures
|
|
2456
|
+
.filter((failure) => failure.statusCode !== "shared-component-sibling-pending")
|
|
2457
|
+
.map((failure) => failure.agentId),
|
|
2458
|
+
);
|
|
2276
2459
|
let changed = false;
|
|
2277
2460
|
const outcomes = new Map();
|
|
2278
2461
|
for (const run of agentRuns) {
|
|
@@ -2733,6 +2916,20 @@ export function resolveRelaunchRuns(agentRuns, failures, derivedState, lanePaths
|
|
|
2733
2916
|
barrier: null,
|
|
2734
2917
|
};
|
|
2735
2918
|
}
|
|
2919
|
+
const sharedComponentWaitingAgentIds = new Set(
|
|
2920
|
+
(failures || [])
|
|
2921
|
+
.filter((failure) => failure.statusCode === "shared-component-sibling-pending")
|
|
2922
|
+
.flatMap((failure) => failure.waitingOnAgentIds || [])
|
|
2923
|
+
.filter((agentId) => runsByAgentId.has(agentId)),
|
|
2924
|
+
);
|
|
2925
|
+
if (sharedComponentWaitingAgentIds.size > 0) {
|
|
2926
|
+
return {
|
|
2927
|
+
runs: Array.from(sharedComponentWaitingAgentIds)
|
|
2928
|
+
.map((agentId) => runsByAgentId.get(agentId))
|
|
2929
|
+
.filter(Boolean),
|
|
2930
|
+
barrier: null,
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2736
2933
|
const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
|
|
2737
2934
|
return {
|
|
2738
2935
|
runs: agentRuns.filter((run) => failedAgentIds.has(run.agent.agentId)),
|
|
@@ -2775,10 +2972,15 @@ export async function runLauncherCli(argv) {
|
|
|
2775
2972
|
return;
|
|
2776
2973
|
}
|
|
2777
2974
|
const { lanePaths, options } = parsed;
|
|
2975
|
+
if (!options.reconcileStatus) {
|
|
2976
|
+
await maybeAnnouncePackageUpdate();
|
|
2977
|
+
}
|
|
2778
2978
|
let lockHeld = false;
|
|
2779
2979
|
let globalDashboard = null;
|
|
2780
2980
|
let globalDashboardTerminalEntry = null;
|
|
2781
2981
|
let globalDashboardTerminalAppended = false;
|
|
2982
|
+
let currentWaveDashboardTerminalEntry = null;
|
|
2983
|
+
let currentWaveDashboardTerminalAppended = false;
|
|
2782
2984
|
let selectedWavesForCoordination = [];
|
|
2783
2985
|
|
|
2784
2986
|
const appendCoordination = ({
|
|
@@ -2976,6 +3178,7 @@ export async function runLauncherCli(argv) {
|
|
|
2976
3178
|
});
|
|
2977
3179
|
|
|
2978
3180
|
if (options.dryRun) {
|
|
3181
|
+
pruneDryRunExecutorPreviewDirs(lanePaths, allWaves);
|
|
2979
3182
|
for (const wave of filteredWaves) {
|
|
2980
3183
|
const derivedState = writeWaveDerivedState({
|
|
2981
3184
|
lanePaths,
|
|
@@ -3072,9 +3275,14 @@ export async function runLauncherCli(argv) {
|
|
|
3072
3275
|
lanePaths,
|
|
3073
3276
|
globalDashboard.runId || "global",
|
|
3074
3277
|
);
|
|
3278
|
+
currentWaveDashboardTerminalEntry = createCurrentWaveDashboardTerminalEntry(lanePaths);
|
|
3075
3279
|
if (terminalRegistryEnabled) {
|
|
3076
|
-
appendTerminalEntries(lanePaths.terminalsPath, [
|
|
3280
|
+
appendTerminalEntries(lanePaths.terminalsPath, [
|
|
3281
|
+
globalDashboardTerminalEntry,
|
|
3282
|
+
currentWaveDashboardTerminalEntry,
|
|
3283
|
+
]);
|
|
3077
3284
|
globalDashboardTerminalAppended = true;
|
|
3285
|
+
currentWaveDashboardTerminalAppended = true;
|
|
3078
3286
|
}
|
|
3079
3287
|
launchWaveDashboardSession(lanePaths, {
|
|
3080
3288
|
sessionName: globalDashboardTerminalEntry.sessionName,
|
|
@@ -3152,7 +3360,7 @@ export async function runLauncherCli(argv) {
|
|
|
3152
3360
|
wave.wave,
|
|
3153
3361
|
wave.agents,
|
|
3154
3362
|
runTag,
|
|
3155
|
-
|
|
3363
|
+
false,
|
|
3156
3364
|
);
|
|
3157
3365
|
if (terminalRegistryEnabled) {
|
|
3158
3366
|
appendTerminalEntries(lanePaths.terminalsPath, terminalEntries);
|
|
@@ -3265,12 +3473,9 @@ export async function runLauncherCli(argv) {
|
|
|
3265
3473
|
}
|
|
3266
3474
|
flushDashboards();
|
|
3267
3475
|
|
|
3268
|
-
|
|
3269
|
-
(entry) => entry.terminalName === `${lanePaths.dashboardTerminalNamePrefix}${wave.wave}`,
|
|
3270
|
-
);
|
|
3271
|
-
if (options.dashboard && dashboardEntry) {
|
|
3476
|
+
if (options.dashboard && currentWaveDashboardTerminalEntry) {
|
|
3272
3477
|
launchWaveDashboardSession(lanePaths, {
|
|
3273
|
-
sessionName:
|
|
3478
|
+
sessionName: currentWaveDashboardTerminalEntry.sessionName,
|
|
3274
3479
|
dashboardPath,
|
|
3275
3480
|
messageBoardPath,
|
|
3276
3481
|
});
|
|
@@ -3442,6 +3647,12 @@ export async function runLauncherCli(argv) {
|
|
|
3442
3647
|
|
|
3443
3648
|
materializeAgentExecutionSummaries(wave, agentRuns);
|
|
3444
3649
|
refreshDerivedState(attempt);
|
|
3650
|
+
failures = reconcileFailuresAgainstSharedComponentState(wave, agentRuns, failures);
|
|
3651
|
+
for (const failure of failures) {
|
|
3652
|
+
if (failure.statusCode === "shared-component-sibling-pending") {
|
|
3653
|
+
applySharedComponentWaitStateToDashboard(failure, dashboardState);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3445
3656
|
|
|
3446
3657
|
if (failures.length > 0) {
|
|
3447
3658
|
for (const failure of failures) {
|
|
@@ -3483,12 +3694,20 @@ export async function runLauncherCli(argv) {
|
|
|
3483
3694
|
laneProfile: lanePaths.laneProfile,
|
|
3484
3695
|
});
|
|
3485
3696
|
if (!componentGate.ok) {
|
|
3697
|
+
if (componentGate.statusCode === "shared-component-sibling-pending") {
|
|
3698
|
+
applySharedComponentWaitStateToDashboard(componentGate, dashboardState);
|
|
3699
|
+
}
|
|
3486
3700
|
failures = [
|
|
3487
3701
|
{
|
|
3488
3702
|
agentId: componentGate.agentId,
|
|
3489
3703
|
statusCode: componentGate.statusCode,
|
|
3490
3704
|
logPath:
|
|
3491
3705
|
componentGate.logPath || path.relative(REPO_ROOT, messageBoardPath),
|
|
3706
|
+
detail: componentGate.detail,
|
|
3707
|
+
ownerAgentIds: componentGate.ownerAgentIds || [],
|
|
3708
|
+
satisfiedAgentIds: componentGate.satisfiedAgentIds || [],
|
|
3709
|
+
waitingOnAgentIds: componentGate.waitingOnAgentIds || [],
|
|
3710
|
+
failedOwnContractAgentIds: componentGate.failedOwnContractAgentIds || [],
|
|
3492
3711
|
},
|
|
3493
3712
|
];
|
|
3494
3713
|
recordCombinedEvent({
|
|
@@ -4045,6 +4264,9 @@ export async function runLauncherCli(argv) {
|
|
|
4045
4264
|
if (globalDashboardTerminalEntry) {
|
|
4046
4265
|
excludeSessionNames.add(globalDashboardTerminalEntry.sessionName);
|
|
4047
4266
|
}
|
|
4267
|
+
if (currentWaveDashboardTerminalEntry) {
|
|
4268
|
+
excludeSessionNames.add(currentWaveDashboardTerminalEntry.sessionName);
|
|
4269
|
+
}
|
|
4048
4270
|
cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames });
|
|
4049
4271
|
}
|
|
4050
4272
|
if (globalWave && globalWave.status === "running") {
|
|
@@ -4079,6 +4301,13 @@ export async function runLauncherCli(argv) {
|
|
|
4079
4301
|
if (globalDashboardTerminalAppended && globalDashboardTerminalEntry && !options.keepTerminals) {
|
|
4080
4302
|
removeTerminalEntries(lanePaths.terminalsPath, [globalDashboardTerminalEntry]);
|
|
4081
4303
|
}
|
|
4304
|
+
if (
|
|
4305
|
+
currentWaveDashboardTerminalAppended &&
|
|
4306
|
+
currentWaveDashboardTerminalEntry &&
|
|
4307
|
+
!options.keepTerminals
|
|
4308
|
+
) {
|
|
4309
|
+
removeTerminalEntries(lanePaths.terminalsPath, [currentWaveDashboardTerminalEntry]);
|
|
4310
|
+
}
|
|
4082
4311
|
if (options.cleanupSessions && globalDashboardTerminalEntry) {
|
|
4083
4312
|
try {
|
|
4084
4313
|
killTmuxSessionIfExists(lanePaths.tmuxSocketName, globalDashboardTerminalEntry.sessionName);
|
|
@@ -4086,6 +4315,16 @@ export async function runLauncherCli(argv) {
|
|
|
4086
4315
|
// no-op
|
|
4087
4316
|
}
|
|
4088
4317
|
}
|
|
4318
|
+
if (options.cleanupSessions && currentWaveDashboardTerminalEntry) {
|
|
4319
|
+
try {
|
|
4320
|
+
killTmuxSessionIfExists(
|
|
4321
|
+
lanePaths.tmuxSocketName,
|
|
4322
|
+
currentWaveDashboardTerminalEntry.sessionName,
|
|
4323
|
+
);
|
|
4324
|
+
} catch {
|
|
4325
|
+
// no-op
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4089
4328
|
if (lockHeld) {
|
|
4090
4329
|
releaseLauncherLock(lanePaths.launcherLockPath);
|
|
4091
4330
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDirectory, readJsonOrNull, REPO_ROOT, writeJsonAtomic } from "./shared.mjs";
|
|
4
|
+
import {
|
|
5
|
+
compareVersions,
|
|
6
|
+
readInstalledPackageMetadata,
|
|
7
|
+
WAVE_PACKAGE_NAME,
|
|
8
|
+
} from "./package-version.mjs";
|
|
9
|
+
|
|
10
|
+
export const PACKAGE_UPDATE_CHECK_SCHEMA_VERSION = 1;
|
|
11
|
+
export const PACKAGE_UPDATE_CHECK_PATH = path.join(REPO_ROOT, ".wave", "package-update-check.json");
|
|
12
|
+
export const PACKAGE_UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
13
|
+
export const PACKAGE_UPDATE_CHECK_TIMEOUT_MS = 2000;
|
|
14
|
+
export const WAVE_SKIP_UPDATE_CHECK_ENV = "WAVE_SKIP_UPDATE_CHECK";
|
|
15
|
+
export const WAVE_SUPPRESS_UPDATE_NOTICE_ENV = "WAVE_SUPPRESS_UPDATE_NOTICE";
|
|
16
|
+
export const NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org";
|
|
17
|
+
|
|
18
|
+
function isTruthyEnvValue(value) {
|
|
19
|
+
const normalized = String(value ?? "")
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parsePackageManagerId(value) {
|
|
26
|
+
const normalized = String(value || "")
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
if (normalized.startsWith("pnpm@")) {
|
|
30
|
+
return "pnpm";
|
|
31
|
+
}
|
|
32
|
+
if (normalized.startsWith("npm@")) {
|
|
33
|
+
return "npm";
|
|
34
|
+
}
|
|
35
|
+
if (normalized.startsWith("yarn@")) {
|
|
36
|
+
return "yarn";
|
|
37
|
+
}
|
|
38
|
+
if (normalized.startsWith("bun@")) {
|
|
39
|
+
return "bun";
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runtimeSelfUpdateCommand(workspaceRoot = REPO_ROOT) {
|
|
45
|
+
const workspacePackage = readJsonOrNull(path.join(workspaceRoot, "package.json"));
|
|
46
|
+
const packageManagerId = parsePackageManagerId(workspacePackage?.packageManager);
|
|
47
|
+
if (packageManagerId === "pnpm") {
|
|
48
|
+
return "pnpm exec wave self-update";
|
|
49
|
+
}
|
|
50
|
+
if (packageManagerId === "npm") {
|
|
51
|
+
return "npm exec -- wave self-update";
|
|
52
|
+
}
|
|
53
|
+
if (packageManagerId === "yarn") {
|
|
54
|
+
return "yarn exec wave self-update";
|
|
55
|
+
}
|
|
56
|
+
if (packageManagerId === "bun") {
|
|
57
|
+
return "bun x wave self-update";
|
|
58
|
+
}
|
|
59
|
+
if (fs.existsSync(path.join(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
60
|
+
return "pnpm exec wave self-update";
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(path.join(workspaceRoot, "yarn.lock"))) {
|
|
63
|
+
return "yarn exec wave self-update";
|
|
64
|
+
}
|
|
65
|
+
if (fs.existsSync(path.join(workspaceRoot, "bun.lock")) || fs.existsSync(path.join(workspaceRoot, "bun.lockb"))) {
|
|
66
|
+
return "bun x wave self-update";
|
|
67
|
+
}
|
|
68
|
+
return "npm exec -- wave self-update";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildPackageLatestUrl(packageName) {
|
|
72
|
+
return `${NPM_REGISTRY_LATEST_URL}/${encodeURIComponent(String(packageName || WAVE_PACKAGE_NAME)).replace("%40", "@")}/latest`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readUpdateCheckCache(cachePath = PACKAGE_UPDATE_CHECK_PATH) {
|
|
76
|
+
const payload = readJsonOrNull(cachePath);
|
|
77
|
+
return payload && typeof payload === "object" ? payload : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeUpdateCheckCache(cachePath, payload) {
|
|
81
|
+
ensureDirectory(path.dirname(cachePath));
|
|
82
|
+
writeJsonAtomic(cachePath, payload);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildNoticeLines(packageName, currentVersion, latestVersion, workspaceRoot = REPO_ROOT) {
|
|
86
|
+
return [
|
|
87
|
+
`[wave:update] newer ${packageName} available: installed ${currentVersion}, latest ${latestVersion}`,
|
|
88
|
+
`[wave:update] update now with: ${runtimeSelfUpdateCommand(workspaceRoot)}`,
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function emitNotice(packageName, currentVersion, latestVersion, emit = console.error, workspaceRoot = REPO_ROOT) {
|
|
93
|
+
for (const line of buildNoticeLines(packageName, currentVersion, latestVersion, workspaceRoot)) {
|
|
94
|
+
emit(line);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function fetchLatestPackageVersion(
|
|
99
|
+
packageName = WAVE_PACKAGE_NAME,
|
|
100
|
+
{
|
|
101
|
+
fetchImpl = globalThis.fetch,
|
|
102
|
+
timeoutMs = PACKAGE_UPDATE_CHECK_TIMEOUT_MS,
|
|
103
|
+
} = {},
|
|
104
|
+
) {
|
|
105
|
+
if (typeof fetchImpl !== "function") {
|
|
106
|
+
throw new Error("Package update check is unavailable in this Node runtime.");
|
|
107
|
+
}
|
|
108
|
+
const abortController = new AbortController();
|
|
109
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetchImpl(buildPackageLatestUrl(packageName), {
|
|
112
|
+
signal: abortController.signal,
|
|
113
|
+
headers: {
|
|
114
|
+
Accept: "application/json",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (!response?.ok) {
|
|
118
|
+
throw new Error(`Upstream package check failed with status ${response?.status || "unknown"}.`);
|
|
119
|
+
}
|
|
120
|
+
const payload = await response.json();
|
|
121
|
+
const latestVersion = String(payload?.version || "").trim();
|
|
122
|
+
if (!latestVersion) {
|
|
123
|
+
throw new Error("Upstream package check returned no version.");
|
|
124
|
+
}
|
|
125
|
+
return latestVersion;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function maybeAnnouncePackageUpdate(options = {}) {
|
|
132
|
+
const env = options.env || process.env;
|
|
133
|
+
if (
|
|
134
|
+
isTruthyEnvValue(env[WAVE_SKIP_UPDATE_CHECK_ENV]) ||
|
|
135
|
+
isTruthyEnvValue(env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV])
|
|
136
|
+
) {
|
|
137
|
+
return {
|
|
138
|
+
skipped: true,
|
|
139
|
+
reason: "disabled",
|
|
140
|
+
updateAvailable: false,
|
|
141
|
+
latestVersion: null,
|
|
142
|
+
currentVersion: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const metadata = options.packageMetadata || readInstalledPackageMetadata();
|
|
147
|
+
const packageName = String(metadata.name || WAVE_PACKAGE_NAME);
|
|
148
|
+
const currentVersion = String(metadata.version || "").trim();
|
|
149
|
+
const cachePath = options.cachePath || PACKAGE_UPDATE_CHECK_PATH;
|
|
150
|
+
const workspaceRoot = options.workspaceRoot || REPO_ROOT;
|
|
151
|
+
const cacheTtlMs = options.cacheTtlMs ?? PACKAGE_UPDATE_CHECK_TTL_MS;
|
|
152
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
153
|
+
const emit = options.emit || console.error;
|
|
154
|
+
const cache = readUpdateCheckCache(cachePath);
|
|
155
|
+
const cachedCheckedAtMs = Date.parse(String(cache?.checkedAt || ""));
|
|
156
|
+
const cacheMatchesCurrentVersion = cache?.currentVersion === currentVersion;
|
|
157
|
+
const cachedUpdateAvailable =
|
|
158
|
+
cacheMatchesCurrentVersion &&
|
|
159
|
+
typeof cache?.latestVersion === "string" &&
|
|
160
|
+
compareVersions(cache.latestVersion, currentVersion) > 0;
|
|
161
|
+
const cacheFresh =
|
|
162
|
+
cacheMatchesCurrentVersion &&
|
|
163
|
+
Number.isFinite(cachedCheckedAtMs) &&
|
|
164
|
+
nowMs - cachedCheckedAtMs <= cacheTtlMs;
|
|
165
|
+
let emitted = false;
|
|
166
|
+
|
|
167
|
+
if (cachedUpdateAvailable) {
|
|
168
|
+
emitNotice(packageName, currentVersion, cache.latestVersion, emit, workspaceRoot);
|
|
169
|
+
emitted = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (cacheFresh) {
|
|
173
|
+
return {
|
|
174
|
+
skipped: false,
|
|
175
|
+
source: "cache",
|
|
176
|
+
updateAvailable: cachedUpdateAvailable,
|
|
177
|
+
latestVersion: cache?.latestVersion || currentVersion,
|
|
178
|
+
currentVersion,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const latestVersion = await fetchLatestPackageVersion(packageName, options);
|
|
184
|
+
const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
185
|
+
writeUpdateCheckCache(cachePath, {
|
|
186
|
+
schemaVersion: PACKAGE_UPDATE_CHECK_SCHEMA_VERSION,
|
|
187
|
+
packageName,
|
|
188
|
+
checkedAt: new Date(nowMs).toISOString(),
|
|
189
|
+
currentVersion,
|
|
190
|
+
latestVersion,
|
|
191
|
+
updateAvailable,
|
|
192
|
+
lastErrorAt: null,
|
|
193
|
+
lastErrorMessage: null,
|
|
194
|
+
});
|
|
195
|
+
if (updateAvailable && !emitted) {
|
|
196
|
+
emitNotice(packageName, currentVersion, latestVersion, emit, workspaceRoot);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
skipped: false,
|
|
200
|
+
source: "network",
|
|
201
|
+
updateAvailable,
|
|
202
|
+
latestVersion,
|
|
203
|
+
currentVersion,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
writeUpdateCheckCache(cachePath, {
|
|
207
|
+
schemaVersion: PACKAGE_UPDATE_CHECK_SCHEMA_VERSION,
|
|
208
|
+
packageName,
|
|
209
|
+
checkedAt: new Date(nowMs).toISOString(),
|
|
210
|
+
currentVersion,
|
|
211
|
+
latestVersion:
|
|
212
|
+
cacheMatchesCurrentVersion && typeof cache?.latestVersion === "string"
|
|
213
|
+
? cache.latestVersion
|
|
214
|
+
: currentVersion,
|
|
215
|
+
updateAvailable: cachedUpdateAvailable,
|
|
216
|
+
lastErrorAt: new Date(nowMs).toISOString(),
|
|
217
|
+
lastErrorMessage: error instanceof Error ? error.message : String(error),
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
skipped: false,
|
|
221
|
+
source: "error",
|
|
222
|
+
updateAvailable: cachedUpdateAvailable,
|
|
223
|
+
latestVersion:
|
|
224
|
+
cacheMatchesCurrentVersion && typeof cache?.latestVersion === "string"
|
|
225
|
+
? cache.latestVersion
|
|
226
|
+
: currentVersion,
|
|
227
|
+
currentVersion,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { PACKAGE_ROOT, readJsonOrNull } from "./shared.mjs";
|
|
3
|
+
|
|
4
|
+
export const WAVE_PACKAGE_NAME = "@chllming/wave-orchestration";
|
|
5
|
+
export const PACKAGE_METADATA_PATH = path.join(PACKAGE_ROOT, "package.json");
|
|
6
|
+
|
|
7
|
+
export function readInstalledPackageMetadata(metadataPath = PACKAGE_METADATA_PATH) {
|
|
8
|
+
const payload = readJsonOrNull(metadataPath);
|
|
9
|
+
if (!payload?.name || !payload?.version) {
|
|
10
|
+
throw new Error(`Invalid package metadata: ${metadataPath}`);
|
|
11
|
+
}
|
|
12
|
+
return payload;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeVersionParts(version) {
|
|
16
|
+
return String(version || "")
|
|
17
|
+
.split(".")
|
|
18
|
+
.map((part) => Number.parseInt(part.replace(/[^0-9].*$/, ""), 10) || 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compareVersions(a, b) {
|
|
22
|
+
const left = normalizeVersionParts(a);
|
|
23
|
+
const right = normalizeVersionParts(b);
|
|
24
|
+
const length = Math.max(left.length, right.length);
|
|
25
|
+
for (let index = 0; index < length; index += 1) {
|
|
26
|
+
const diff = (left[index] || 0) - (right[index] || 0);
|
|
27
|
+
if (diff !== 0) {
|
|
28
|
+
return diff;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|