@chllming/wave-orchestration 0.9.0 → 0.9.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.
- package/CHANGELOG.md +28 -0
- package/README.md +119 -18
- package/docs/README.md +7 -3
- package/docs/architecture/README.md +1498 -0
- package/docs/concepts/operating-modes.md +2 -2
- package/docs/guides/author-and-run-waves.md +14 -4
- package/docs/guides/planner.md +2 -2
- package/docs/guides/{recommendations-0.9.0.md → recommendations-0.9.1.md} +8 -7
- package/docs/guides/sandboxed-environments.md +158 -0
- package/docs/guides/terminal-surfaces.md +14 -12
- package/docs/plans/current-state.md +5 -3
- package/docs/plans/end-state-architecture.md +3 -1
- package/docs/plans/examples/wave-example-design-handoff.md +1 -1
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +46 -19
- package/docs/plans/sandbox-end-state-architecture.md +153 -0
- package/docs/reference/cli-reference.md +71 -7
- package/docs/reference/coordination-and-closure.md +1 -1
- package/docs/reference/github-packages-setup.md +1 -1
- package/docs/reference/migration-0.2-to-0.5.md +9 -7
- package/docs/reference/npmjs-token-publishing.md +53 -0
- package/docs/reference/npmjs-trusted-publishing.md +4 -50
- package/docs/reference/package-publishing-flow.md +272 -0
- package/docs/reference/runtime-config/README.md +2 -2
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/roadmap.md +43 -201
- package/package.json +1 -1
- package/releases/manifest.json +19 -0
- package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
- package/scripts/wave-orchestrator/agent-state.mjs +0 -1
- package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
- package/scripts/wave-orchestrator/autonomous.mjs +47 -14
- package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
- package/scripts/wave-orchestrator/control-cli.mjs +42 -5
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
- package/scripts/wave-orchestrator/derived-state-engine.mjs +6 -3
- package/scripts/wave-orchestrator/gate-engine.mjs +106 -38
- package/scripts/wave-orchestrator/install.mjs +13 -0
- package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +179 -68
- package/scripts/wave-orchestrator/launcher.mjs +201 -53
- package/scripts/wave-orchestrator/ledger.mjs +7 -2
- package/scripts/wave-orchestrator/projection-writer.mjs +13 -1
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
- package/scripts/wave-orchestrator/retry-control.mjs +3 -3
- package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
- package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
- package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
- package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
- package/scripts/wave-orchestrator/terminals.mjs +12 -32
- package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
- package/scripts/wave-orchestrator/wave-files.mjs +38 -5
- package/scripts/wave.mjs +13 -0
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
isDocsOnlyDesignAgent,
|
|
37
37
|
isDesignAgent,
|
|
38
38
|
isImplementationOwningDesignAgent,
|
|
39
|
-
|
|
39
|
+
isSecurityReviewAgentForLane,
|
|
40
40
|
resolveWaveRoleBindings,
|
|
41
41
|
} from "./role-helpers.mjs";
|
|
42
42
|
import {
|
|
@@ -283,7 +283,10 @@ export function reconcileFailuresAgainstSharedComponentState(wave, agentRuns, fa
|
|
|
283
283
|
const summariesByAgentId = Object.fromEntries(
|
|
284
284
|
(agentRuns || []).map((runInfo) => [
|
|
285
285
|
runInfo.agent.agentId,
|
|
286
|
-
readRunExecutionSummary(runInfo, wave, {
|
|
286
|
+
readRunExecutionSummary(runInfo, wave, {
|
|
287
|
+
mode: "live",
|
|
288
|
+
securityRolePromptPath: lanePaths?.securityRolePromptPath,
|
|
289
|
+
}),
|
|
287
290
|
]),
|
|
288
291
|
);
|
|
289
292
|
const failureAgentIds = new Set(failures.map((failure) => failure.agentId).filter(Boolean));
|
|
@@ -407,7 +410,7 @@ function isClosureAgentId(agent, lanePaths, waveDefinition = null) {
|
|
|
407
410
|
return (
|
|
408
411
|
resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents).closureAgentIds.includes(
|
|
409
412
|
agent?.agentId,
|
|
410
|
-
) ||
|
|
413
|
+
) || isSecurityReviewAgentForLane(agent, lanePaths)
|
|
411
414
|
);
|
|
412
415
|
}
|
|
413
416
|
|
|
@@ -658,7 +661,7 @@ function resolveRunsForResumePhase(agentRuns, lanePaths, resumePhase, waveDefini
|
|
|
658
661
|
return runsFromAgentIds(agentRuns, [roleBindings.integrationAgentId]);
|
|
659
662
|
}
|
|
660
663
|
if (resumePhase === "security-review") {
|
|
661
|
-
return (agentRuns || []).filter((run) =>
|
|
664
|
+
return (agentRuns || []).filter((run) => isSecurityReviewAgentForLane(run.agent, lanePaths));
|
|
662
665
|
}
|
|
663
666
|
if (resumePhase === "docs-closure") {
|
|
664
667
|
return runsFromAgentIds(agentRuns, [roleBindings.documentationAgentId]);
|
|
@@ -820,7 +823,7 @@ function resolveRelaunchRunsLegacy(agentRuns, failures, derivedState, lanePaths,
|
|
|
820
823
|
}
|
|
821
824
|
if (derivedState?.ledger?.phase === "security-review") {
|
|
822
825
|
return {
|
|
823
|
-
runs: agentRuns.filter((run) =>
|
|
826
|
+
runs: agentRuns.filter((run) => isSecurityReviewAgentForLane(run.agent, lanePaths)),
|
|
824
827
|
barrier: null,
|
|
825
828
|
};
|
|
826
829
|
}
|
|
@@ -1196,6 +1199,66 @@ function collectResumeExecutorChanges(waveState) {
|
|
|
1196
1199
|
}));
|
|
1197
1200
|
}
|
|
1198
1201
|
|
|
1202
|
+
const CLOSURE_STAGE_ORDER = [
|
|
1203
|
+
"cont-eval",
|
|
1204
|
+
"security-review",
|
|
1205
|
+
"integrating",
|
|
1206
|
+
"docs-closure",
|
|
1207
|
+
"cont-qa-closure",
|
|
1208
|
+
];
|
|
1209
|
+
|
|
1210
|
+
function closureStageForAgentId(agentId, roleBindings) {
|
|
1211
|
+
if (!agentId) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
if (agentId === roleBindings?.contEvalAgentId) {
|
|
1215
|
+
return "cont-eval";
|
|
1216
|
+
}
|
|
1217
|
+
if ((roleBindings?.securityReviewerAgentIds || []).includes(agentId)) {
|
|
1218
|
+
return "security-review";
|
|
1219
|
+
}
|
|
1220
|
+
if (agentId === roleBindings?.integrationAgentId) {
|
|
1221
|
+
return "integrating";
|
|
1222
|
+
}
|
|
1223
|
+
if (agentId === roleBindings?.documentationAgentId) {
|
|
1224
|
+
return "docs-closure";
|
|
1225
|
+
}
|
|
1226
|
+
if (agentId === roleBindings?.contQaAgentId) {
|
|
1227
|
+
return "cont-qa-closure";
|
|
1228
|
+
}
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function collectForwardedClosureGaps(waveState, waveDefinition, lanePaths) {
|
|
1233
|
+
const roleBindings = resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents);
|
|
1234
|
+
return normalizeRetryTargets(waveState?.retryTargetSet)
|
|
1235
|
+
.filter((target) => target?.statusCode === "wave-proof-gap" || target?.reason === "wave-proof-gap")
|
|
1236
|
+
.map((target) => {
|
|
1237
|
+
const stageKey = closureStageForAgentId(target.agentId, roleBindings);
|
|
1238
|
+
if (!stageKey) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
stageKey,
|
|
1243
|
+
agentId: target.agentId,
|
|
1244
|
+
attempt: waveState?.attempt ?? null,
|
|
1245
|
+
detail: target.detail || target.reason || target.statusCode || null,
|
|
1246
|
+
targets: normalizeRetryTargets(waveState?.retryTargetSet)
|
|
1247
|
+
.filter((entry) => closureStageForAgentId(entry.agentId, roleBindings))
|
|
1248
|
+
.map((entry) => entry.agentId)
|
|
1249
|
+
.filter((agentId) => {
|
|
1250
|
+
const agentStage = closureStageForAgentId(agentId, roleBindings);
|
|
1251
|
+
return CLOSURE_STAGE_ORDER.indexOf(agentStage) >= CLOSURE_STAGE_ORDER.indexOf(stageKey);
|
|
1252
|
+
}),
|
|
1253
|
+
resolved: false,
|
|
1254
|
+
};
|
|
1255
|
+
})
|
|
1256
|
+
.filter(Boolean)
|
|
1257
|
+
.sort(
|
|
1258
|
+
(left, right) => CLOSURE_STAGE_ORDER.indexOf(left.stageKey) - CLOSURE_STAGE_ORDER.indexOf(right.stageKey),
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1199
1262
|
/**
|
|
1200
1263
|
* Deterministic resume planner operating on reducer output (WaveState).
|
|
1201
1264
|
* Pure function — no file I/O.
|
|
@@ -1207,8 +1270,29 @@ export function buildResumePlan(waveState, options = {}) {
|
|
|
1207
1270
|
const canResume = reason !== "all-gates-pass";
|
|
1208
1271
|
const pendingAgentIds = waveState.closureEligibility?.pendingAgentIds || [];
|
|
1209
1272
|
const provenAgentIds = waveState.closureEligibility?.ownedSliceProvenAgentIds || [];
|
|
1273
|
+
const forwardedClosureGaps = collectForwardedClosureGaps(waveState, waveDefinition, lanePaths);
|
|
1210
1274
|
const invalidatedAgentIds = [...pendingAgentIds].sort();
|
|
1211
1275
|
const reusableAgentIds = [...provenAgentIds].sort();
|
|
1276
|
+
if (forwardedClosureGaps.length > 0) {
|
|
1277
|
+
const roleBindings = resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents);
|
|
1278
|
+
const earliestGap = forwardedClosureGaps[0];
|
|
1279
|
+
const invalidatedClosureAgentIds = roleBindings.closureAgentIds.filter((agentId) => {
|
|
1280
|
+
const stageKey = closureStageForAgentId(agentId, roleBindings);
|
|
1281
|
+
return CLOSURE_STAGE_ORDER.indexOf(stageKey) >= CLOSURE_STAGE_ORDER.indexOf(earliestGap.stageKey);
|
|
1282
|
+
});
|
|
1283
|
+
for (const agentId of invalidatedClosureAgentIds) {
|
|
1284
|
+
if (!invalidatedAgentIds.includes(agentId)) {
|
|
1285
|
+
invalidatedAgentIds.push(agentId);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
invalidatedAgentIds.sort();
|
|
1289
|
+
for (const agentId of invalidatedClosureAgentIds) {
|
|
1290
|
+
const index = reusableAgentIds.indexOf(agentId);
|
|
1291
|
+
if (index >= 0) {
|
|
1292
|
+
reusableAgentIds.splice(index, 1);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1212
1296
|
const proofBundles =
|
|
1213
1297
|
waveState.closureEligibility?.proofBundles ||
|
|
1214
1298
|
waveState.proofAvailability?.activeProofBundles ||
|
|
@@ -1219,7 +1303,9 @@ export function buildResumePlan(waveState, options = {}) {
|
|
|
1219
1303
|
.sort();
|
|
1220
1304
|
const gateSnapshot = waveState.gateSnapshot || {};
|
|
1221
1305
|
let resumeFromPhase = "completed";
|
|
1222
|
-
if (
|
|
1306
|
+
if (forwardedClosureGaps.length > 0) {
|
|
1307
|
+
resumeFromPhase = forwardedClosureGaps[0].stageKey;
|
|
1308
|
+
} else if (canResume && gateSnapshot.overall && !gateSnapshot.overall.ok) {
|
|
1223
1309
|
resumeFromPhase = phaseFromGate(gateSnapshot.overall.gate);
|
|
1224
1310
|
} else if (canResume) {
|
|
1225
1311
|
resumeFromPhase = "implementation";
|
|
@@ -1239,6 +1325,7 @@ export function buildResumePlan(waveState, options = {}) {
|
|
|
1239
1325
|
humanInputBlockers: collectResumeHumanInputBlockers(waveState),
|
|
1240
1326
|
gateBlockers: collectResumeGateBlockers(gateSnapshot),
|
|
1241
1327
|
closureEligibility: waveState.closureEligibility || null,
|
|
1328
|
+
forwardedClosureGaps,
|
|
1242
1329
|
deterministic: true,
|
|
1243
1330
|
createdAt: toIsoTimestamp(),
|
|
1244
1331
|
};
|
|
@@ -105,6 +105,12 @@ export function isSecurityReviewAgent(
|
|
|
105
105
|
return capabilities.includes("security-review");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function isSecurityReviewAgentForLane(agent, lanePaths = null) {
|
|
109
|
+
return isSecurityReviewAgent(agent, {
|
|
110
|
+
securityRolePromptPath: lanePaths?.securityRolePromptPath,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
export function isDesignAgent(
|
|
109
115
|
agent,
|
|
110
116
|
{ designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH } = {},
|
|
@@ -202,6 +208,30 @@ export function resolveWaveRoleBindings(wave = {}, lanePaths = {}, agents = wave
|
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
export function resolveAgentClosureRoleKeys(agent, roleBindings = {}, lanePaths = {}) {
|
|
212
|
+
const roles = [];
|
|
213
|
+
if (agent?.agentId === roleBindings?.contEvalAgentId) {
|
|
214
|
+
roles.push("cont-eval");
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
isSecurityReviewAgent(agent, {
|
|
218
|
+
securityRolePromptPath: lanePaths?.securityRolePromptPath,
|
|
219
|
+
})
|
|
220
|
+
) {
|
|
221
|
+
roles.push("security-review");
|
|
222
|
+
}
|
|
223
|
+
if (agent?.agentId === roleBindings?.integrationAgentId) {
|
|
224
|
+
roles.push("integration");
|
|
225
|
+
}
|
|
226
|
+
if (agent?.agentId === roleBindings?.documentationAgentId) {
|
|
227
|
+
roles.push("documentation");
|
|
228
|
+
}
|
|
229
|
+
if (agent?.agentId === roleBindings?.contQaAgentId) {
|
|
230
|
+
roles.push("cont-qa");
|
|
231
|
+
}
|
|
232
|
+
return roles;
|
|
233
|
+
}
|
|
234
|
+
|
|
205
235
|
export function isClosureRoleAgentId(agentId, roleBindings) {
|
|
206
236
|
return (roleBindings?.closureAgentIds || []).includes(agentId);
|
|
207
237
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import {
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
ensureDirectory,
|
|
23
22
|
shellQuote,
|
|
24
23
|
PACKAGE_ROOT,
|
|
25
|
-
TMUX_COMMAND_TIMEOUT_MS,
|
|
26
24
|
toIsoTimestamp,
|
|
27
25
|
writeJsonAtomic,
|
|
28
26
|
} from "./shared.mjs";
|
|
@@ -32,15 +30,25 @@ import {
|
|
|
32
30
|
terminalSurfaceUsesTerminalRegistry,
|
|
33
31
|
pruneOrphanLaneTemporaryTerminalEntries,
|
|
34
32
|
} from "./terminals.mjs";
|
|
33
|
+
import {
|
|
34
|
+
createSession as createTmuxSession,
|
|
35
|
+
listSessions as listTmuxSessions,
|
|
36
|
+
runTmuxCommand,
|
|
37
|
+
} from "./tmux-adapter.mjs";
|
|
35
38
|
import {
|
|
36
39
|
recordGlobalDashboardEvent,
|
|
37
40
|
} from "./dashboard-state.mjs";
|
|
38
41
|
import { buildHumanFeedbackWorkflowUpdate } from "./human-input-workflow.mjs";
|
|
39
42
|
import {
|
|
40
|
-
|
|
43
|
+
collectUnexpectedSessionWarnings as collectUnexpectedSessionWarningsImpl,
|
|
41
44
|
launchAgentSession as launchAgentSessionImpl,
|
|
42
45
|
waitForWaveCompletion as waitForWaveCompletionImpl,
|
|
43
46
|
} from "./launcher-runtime.mjs";
|
|
47
|
+
import { terminateAgentProcessRuntime } from "./agent-process-runner.mjs";
|
|
48
|
+
import {
|
|
49
|
+
buildSupervisorPaths,
|
|
50
|
+
supervisorAgentRuntimePathForRun,
|
|
51
|
+
} from "./supervisor-cli.mjs";
|
|
44
52
|
import {
|
|
45
53
|
agentUsesSignalHygiene,
|
|
46
54
|
buildSignalStatusLine,
|
|
@@ -65,6 +73,13 @@ function relativeArtifactPath(filePath) {
|
|
|
65
73
|
return filePath ? path.relative(REPO_ROOT, filePath) : null;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
function readRuntimeRecord(run) {
|
|
77
|
+
if (!run?.runtimePath || !fs.existsSync(run.runtimePath)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return readJsonOrNull(run.runtimePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
export function recordWaveRunState(lanePaths, waveNumber, state, data = {}) {
|
|
69
84
|
return appendWaveControlEvent(lanePaths, waveNumber, {
|
|
70
85
|
entityType: "wave_run",
|
|
@@ -235,8 +250,8 @@ function isLaneSessionName(lanePaths, sessionName) {
|
|
|
235
250
|
);
|
|
236
251
|
}
|
|
237
252
|
|
|
238
|
-
function listLaneTmuxSessionNames(lanePaths) {
|
|
239
|
-
return listTmuxSessionNames(lanePaths).filter((sessionName) =>
|
|
253
|
+
async function listLaneTmuxSessionNames(lanePaths) {
|
|
254
|
+
return (await listTmuxSessionNames(lanePaths)).filter((sessionName) =>
|
|
240
255
|
isLaneSessionName(lanePaths, sessionName),
|
|
241
256
|
);
|
|
242
257
|
}
|
|
@@ -362,6 +377,7 @@ export function buildResidentOrchestratorRun({
|
|
|
362
377
|
promptPath: path.join(lanePaths.promptsDir, `${baseName}.prompt.md`),
|
|
363
378
|
logPath: path.join(lanePaths.logsDir, `${baseName}.log`),
|
|
364
379
|
statusPath: path.join(lanePaths.statusDir, `${baseName}.status`),
|
|
380
|
+
runtimePath: path.join(lanePaths.statusDir, `${baseName}.runtime.json`),
|
|
365
381
|
promptOverride: buildResidentOrchestratorPrompt({
|
|
366
382
|
lane: lanePaths.lane,
|
|
367
383
|
wave: wave.wave,
|
|
@@ -415,20 +431,28 @@ export function monitorResidentOrchestratorSession({
|
|
|
415
431
|
});
|
|
416
432
|
return true;
|
|
417
433
|
}
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
434
|
+
const runtimeRecord = readRuntimeRecord(run);
|
|
435
|
+
if (
|
|
436
|
+
runtimeRecord &&
|
|
437
|
+
["completed", "failed", "terminated"].includes(
|
|
438
|
+
String(runtimeRecord.terminalDisposition || ""),
|
|
439
|
+
)
|
|
440
|
+
) {
|
|
420
441
|
sessionState.closed = true;
|
|
442
|
+
const exitCode = Number.parseInt(String(runtimeRecord.exitCode ?? ""), 10);
|
|
421
443
|
recordCombinedEvent({
|
|
422
|
-
level: "warn",
|
|
444
|
+
level: Number.isFinite(exitCode) && exitCode === 0 ? "info" : "warn",
|
|
423
445
|
agentId: run.agent.agentId,
|
|
424
446
|
message:
|
|
425
|
-
|
|
447
|
+
Number.isFinite(exitCode) && exitCode === 0
|
|
448
|
+
? "Resident orchestrator ended via runtime record before writing a status file; launcher continues as the control plane."
|
|
449
|
+
: "Resident orchestrator ended via runtime record before writing a status file; launcher continues as the control plane.",
|
|
426
450
|
});
|
|
427
451
|
appendCoordination({
|
|
428
|
-
event: "
|
|
452
|
+
event: "resident_orchestrator_runtime_terminal",
|
|
429
453
|
waves: [waveNumber],
|
|
430
|
-
status: "warn",
|
|
431
|
-
details: `
|
|
454
|
+
status: Number.isFinite(exitCode) && exitCode === 0 ? "resolved" : "warn",
|
|
455
|
+
details: `runtime record reached ${runtimeRecord.terminalDisposition} before ${path.relative(REPO_ROOT, run.statusPath)} was written.`,
|
|
432
456
|
actionRequested: "None",
|
|
433
457
|
});
|
|
434
458
|
return true;
|
|
@@ -505,7 +529,7 @@ export function pruneDryRunExecutorPreviewDirs(lanePaths, waves) {
|
|
|
505
529
|
return removedPaths.toSorted();
|
|
506
530
|
}
|
|
507
531
|
|
|
508
|
-
export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
|
|
532
|
+
export async function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
|
|
509
533
|
const outcome = {
|
|
510
534
|
removedLock: false,
|
|
511
535
|
removedSessions: [],
|
|
@@ -527,8 +551,8 @@ export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
|
|
|
527
551
|
outcome.removedLock = true;
|
|
528
552
|
}
|
|
529
553
|
|
|
530
|
-
outcome.removedSessions = cleanupLaneTmuxSessions(lanePaths);
|
|
531
|
-
const activeSessionNames = new Set(listLaneTmuxSessionNames(lanePaths));
|
|
554
|
+
outcome.removedSessions = await cleanupLaneTmuxSessions(lanePaths);
|
|
555
|
+
const activeSessionNames = new Set(await listLaneTmuxSessionNames(lanePaths));
|
|
532
556
|
if (terminalSurfaceUsesTerminalRegistry(options.terminalSurface || "vscode")) {
|
|
533
557
|
const terminalCleanup = pruneOrphanLaneTemporaryTerminalEntries(
|
|
534
558
|
lanePaths.terminalsPath,
|
|
@@ -562,87 +586,35 @@ export function reconcileStaleLauncherArtifacts(lanePaths, options = {}) {
|
|
|
562
586
|
}
|
|
563
587
|
|
|
564
588
|
export function runTmux(lanePaths, args, description) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
env: { ...process.env, TMUX: "" },
|
|
569
|
-
timeout: TMUX_COMMAND_TIMEOUT_MS,
|
|
589
|
+
return runTmuxCommand(lanePaths.tmuxSocketName, args, {
|
|
590
|
+
description,
|
|
591
|
+
mutate: ["new-session", "kill-session"].includes(String(args?.[0] || "")),
|
|
570
592
|
});
|
|
571
|
-
if (result.error) {
|
|
572
|
-
if (result.error.code === "ETIMEDOUT") {
|
|
573
|
-
throw new Error(
|
|
574
|
-
`${description} failed: tmux command timed out after ${TMUX_COMMAND_TIMEOUT_MS}ms`,
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
throw new Error(`${description} failed: ${result.error.message}`);
|
|
578
|
-
}
|
|
579
|
-
if (result.status !== 0) {
|
|
580
|
-
throw new Error(
|
|
581
|
-
`${description} failed: ${(result.stderr || "").trim() || "tmux command failed"}`,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
593
|
}
|
|
585
594
|
|
|
586
595
|
function listTmuxSessionNames(lanePaths) {
|
|
587
|
-
|
|
588
|
-
"tmux",
|
|
589
|
-
["-L", lanePaths.tmuxSocketName, "list-sessions", "-F", "#{session_name}"],
|
|
590
|
-
{
|
|
591
|
-
cwd: REPO_ROOT,
|
|
592
|
-
encoding: "utf8",
|
|
593
|
-
env: { ...process.env, TMUX: "" },
|
|
594
|
-
timeout: TMUX_COMMAND_TIMEOUT_MS,
|
|
595
|
-
},
|
|
596
|
-
);
|
|
597
|
-
if (result.error) {
|
|
598
|
-
if (result.error.code === "ENOENT") {
|
|
599
|
-
return [];
|
|
600
|
-
}
|
|
601
|
-
if (result.error.code === "ETIMEDOUT") {
|
|
602
|
-
throw new Error(`list tmux sessions failed: timed out after ${TMUX_COMMAND_TIMEOUT_MS}ms`);
|
|
603
|
-
}
|
|
604
|
-
throw new Error(`list tmux sessions failed: ${result.error.message}`);
|
|
605
|
-
}
|
|
606
|
-
if (result.status !== 0) {
|
|
607
|
-
const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
|
|
608
|
-
if (
|
|
609
|
-
combined.includes("no server running") ||
|
|
610
|
-
combined.includes("failed to connect") ||
|
|
611
|
-
combined.includes("error connecting")
|
|
612
|
-
) {
|
|
613
|
-
return [];
|
|
614
|
-
}
|
|
615
|
-
throw new Error(
|
|
616
|
-
`list tmux sessions failed: ${(result.stderr || "").trim() || "unknown error"}`,
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
return String(result.stdout || "")
|
|
620
|
-
.split(/\r?\n/)
|
|
621
|
-
.map((line) => line.trim())
|
|
622
|
-
.filter(Boolean);
|
|
596
|
+
return listTmuxSessions(lanePaths.tmuxSocketName);
|
|
623
597
|
}
|
|
624
598
|
|
|
625
|
-
export function cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames = new Set() } = {}) {
|
|
626
|
-
const sessionNames = listTmuxSessionNames(lanePaths);
|
|
599
|
+
export async function cleanupLaneTmuxSessions(lanePaths, { excludeSessionNames = new Set() } = {}) {
|
|
600
|
+
const sessionNames = await listTmuxSessionNames(lanePaths);
|
|
627
601
|
const killed = [];
|
|
628
602
|
for (const sessionName of sessionNames) {
|
|
629
603
|
if (excludeSessionNames.has(sessionName) || !isLaneSessionName(lanePaths, sessionName)) {
|
|
630
604
|
continue;
|
|
631
605
|
}
|
|
632
|
-
killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
|
|
606
|
+
await killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
|
|
633
607
|
killed.push(sessionName);
|
|
634
608
|
}
|
|
635
609
|
return killed;
|
|
636
610
|
}
|
|
637
611
|
|
|
638
|
-
export function
|
|
639
|
-
return
|
|
640
|
-
listLaneTmuxSessionNamesFn: listLaneTmuxSessionNames,
|
|
641
|
-
});
|
|
612
|
+
export function collectUnexpectedSessionWarnings(lanePaths, agentRuns, pendingAgentIds) {
|
|
613
|
+
return collectUnexpectedSessionWarningsImpl(lanePaths, agentRuns, pendingAgentIds, {});
|
|
642
614
|
}
|
|
643
615
|
|
|
644
|
-
export function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPath, messageBoardPath }) {
|
|
645
|
-
killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
|
|
616
|
+
export async function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPath, messageBoardPath }) {
|
|
617
|
+
await killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
|
|
646
618
|
const messageBoardArg = messageBoardPath
|
|
647
619
|
? ` --message-board ${shellQuote(messageBoardPath)}`
|
|
648
620
|
: "";
|
|
@@ -653,15 +625,31 @@ export function launchWaveDashboardSession(lanePaths, { sessionName, dashboardPa
|
|
|
653
625
|
)}${messageBoardArg} --lane ${shellQuote(lanePaths.lane)} --watch`,
|
|
654
626
|
"exec bash -l",
|
|
655
627
|
].join("; ");
|
|
656
|
-
|
|
657
|
-
lanePaths,
|
|
658
|
-
|
|
659
|
-
`
|
|
628
|
+
await createTmuxSession(
|
|
629
|
+
lanePaths.tmuxSocketName,
|
|
630
|
+
sessionName,
|
|
631
|
+
`bash -lc ${shellQuote(command)}`,
|
|
632
|
+
{ description: `launch dashboard session ${sessionName}` },
|
|
660
633
|
);
|
|
661
634
|
}
|
|
662
635
|
|
|
663
636
|
export async function launchAgentSession(lanePaths, params) {
|
|
664
|
-
const
|
|
637
|
+
const supervisorRunId = String(process.env.WAVE_SUPERVISOR_RUN_ID || "").trim();
|
|
638
|
+
const runtimePath = supervisorRunId
|
|
639
|
+
? supervisorAgentRuntimePathForRun(
|
|
640
|
+
buildSupervisorPaths(lanePaths),
|
|
641
|
+
supervisorRunId,
|
|
642
|
+
params?.agent?.agentId || "unknown-agent",
|
|
643
|
+
)
|
|
644
|
+
: params?.runtimePath || null;
|
|
645
|
+
const result = await launchAgentSessionImpl(
|
|
646
|
+
lanePaths,
|
|
647
|
+
{
|
|
648
|
+
...params,
|
|
649
|
+
runtimePath,
|
|
650
|
+
},
|
|
651
|
+
{ runTmuxFn: runTmux },
|
|
652
|
+
);
|
|
665
653
|
const controlPlane = params?.controlPlane || null;
|
|
666
654
|
if (!params?.dryRun && controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {
|
|
667
655
|
recordAgentRunStarted(lanePaths, {
|
|
@@ -669,11 +657,32 @@ export async function launchAgentSession(lanePaths, params) {
|
|
|
669
657
|
attempt: controlPlane.attempt,
|
|
670
658
|
runInfo: {
|
|
671
659
|
...params,
|
|
660
|
+
runtimePath,
|
|
672
661
|
lastExecutorId: result?.executorId || params?.agent?.executorResolved?.id || null,
|
|
673
662
|
},
|
|
674
663
|
});
|
|
675
664
|
}
|
|
676
|
-
return
|
|
665
|
+
return {
|
|
666
|
+
...result,
|
|
667
|
+
runtimePath,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export async function cleanupLaunchedRun(
|
|
672
|
+
lanePaths,
|
|
673
|
+
run,
|
|
674
|
+
{
|
|
675
|
+
terminateRuntimeFn = terminateAgentProcessRuntime,
|
|
676
|
+
killSessionFn = killTmuxSessionIfExists,
|
|
677
|
+
} = {},
|
|
678
|
+
) {
|
|
679
|
+
const runtimeRecord = readRuntimeRecord(run);
|
|
680
|
+
if (runtimeRecord && typeof runtimeRecord === "object") {
|
|
681
|
+
await terminateRuntimeFn(runtimeRecord);
|
|
682
|
+
}
|
|
683
|
+
if (run?.sessionName) {
|
|
684
|
+
await killSessionFn(lanePaths.tmuxSocketName, run.sessionName);
|
|
685
|
+
}
|
|
677
686
|
}
|
|
678
687
|
|
|
679
688
|
export async function waitForWaveCompletion(
|
|
@@ -684,7 +693,7 @@ export async function waitForWaveCompletion(
|
|
|
684
693
|
options = {},
|
|
685
694
|
) {
|
|
686
695
|
const result = await waitForWaveCompletionImpl(lanePaths, agentRuns, timeoutMinutes, onProgress, {
|
|
687
|
-
|
|
696
|
+
collectUnexpectedSessionWarningsFn: collectUnexpectedSessionWarnings,
|
|
688
697
|
});
|
|
689
698
|
const controlPlane = options?.controlPlane || null;
|
|
690
699
|
if (controlPlane?.waveNumber !== undefined && controlPlane?.attempt) {
|