@chllming/wave-orchestration 0.9.0 → 0.9.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.
- package/CHANGELOG.md +57 -0
- package/LICENSE.md +21 -0
- package/README.md +133 -20
- package/docs/README.md +12 -4
- package/docs/agents/wave-security-role.md +1 -0
- 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.2.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 +11 -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 +70 -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 +18 -1
- package/docs/reference/corridor.md +225 -0
- 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 +61 -3
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/reference/wave-control.md +358 -27
- package/docs/roadmap.md +39 -204
- package/package.json +1 -1
- package/releases/manifest.json +38 -0
- package/scripts/wave-cli-bootstrap.mjs +52 -1
- 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/config.mjs +199 -3
- package/scripts/wave-orchestrator/context7.mjs +231 -29
- package/scripts/wave-orchestrator/control-cli.mjs +42 -5
- package/scripts/wave-orchestrator/coordination.mjs +14 -0
- package/scripts/wave-orchestrator/corridor.mjs +363 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +115 -43
- package/scripts/wave-orchestrator/derived-state-engine.mjs +44 -4
- package/scripts/wave-orchestrator/gate-engine.mjs +126 -38
- package/scripts/wave-orchestrator/install.mjs +46 -0
- package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +290 -75
- package/scripts/wave-orchestrator/launcher.mjs +201 -53
- package/scripts/wave-orchestrator/ledger.mjs +7 -2
- package/scripts/wave-orchestrator/planner.mjs +1 -0
- package/scripts/wave-orchestrator/projection-writer.mjs +36 -1
- package/scripts/wave-orchestrator/provider-runtime.mjs +104 -0
- 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/shared.mjs +1 -0
- 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/traces.mjs +25 -0
- package/scripts/wave-orchestrator/wave-control-client.mjs +14 -1
- package/scripts/wave-orchestrator/wave-files.mjs +38 -5
- package/scripts/wave.mjs +13 -0
|
@@ -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 {
|
|
@@ -7,10 +6,11 @@ import {
|
|
|
7
6
|
REPO_ROOT,
|
|
8
7
|
TERMINAL_COLOR,
|
|
9
8
|
TERMINAL_ICON,
|
|
10
|
-
TMUX_COMMAND_TIMEOUT_MS,
|
|
11
9
|
ensureDirectory,
|
|
10
|
+
shellQuote,
|
|
12
11
|
writeJsonAtomic,
|
|
13
12
|
} from "./shared.mjs";
|
|
13
|
+
import { killSessionIfExists } from "./tmux-adapter.mjs";
|
|
14
14
|
|
|
15
15
|
export const TERMINAL_SURFACES = ["vscode", "tmux", "none"];
|
|
16
16
|
|
|
@@ -95,13 +95,15 @@ export function createWaveAgentSessionName(lanePaths, wave, agentSlug) {
|
|
|
95
95
|
export function createTemporaryTerminalEntries(
|
|
96
96
|
lanePaths,
|
|
97
97
|
wave,
|
|
98
|
-
|
|
98
|
+
agentRuns,
|
|
99
99
|
runTag,
|
|
100
100
|
includeDashboard = false,
|
|
101
101
|
) {
|
|
102
|
-
const agentEntries =
|
|
102
|
+
const agentEntries = (agentRuns || []).map((run) => {
|
|
103
|
+
const agent = run?.agent || run;
|
|
103
104
|
const terminalName = `${lanePaths.terminalNamePrefix}${wave}-${agent.slug}`;
|
|
104
|
-
const sessionName = createWaveAgentSessionName(lanePaths, wave, agent.slug);
|
|
105
|
+
const sessionName = run?.sessionName || createWaveAgentSessionName(lanePaths, wave, agent.slug);
|
|
106
|
+
const logPath = String(run?.logPath || "").trim();
|
|
105
107
|
return {
|
|
106
108
|
terminalName,
|
|
107
109
|
sessionName,
|
|
@@ -109,7 +111,9 @@ export function createTemporaryTerminalEntries(
|
|
|
109
111
|
name: terminalName,
|
|
110
112
|
icon: TERMINAL_ICON,
|
|
111
113
|
color: TERMINAL_COLOR,
|
|
112
|
-
command:
|
|
114
|
+
command: logPath
|
|
115
|
+
? `bash -lc "mkdir -p ${shellQuote(path.dirname(logPath))} && touch ${shellQuote(logPath)} && tail -n 200 -F ${shellQuote(logPath)}"`
|
|
116
|
+
: `TMUX= tmux -L ${lanePaths.tmuxSocketName} new -As ${sessionName}`,
|
|
113
117
|
},
|
|
114
118
|
};
|
|
115
119
|
});
|
|
@@ -221,30 +225,6 @@ export function pruneOrphanLaneTemporaryTerminalEntries(
|
|
|
221
225
|
};
|
|
222
226
|
}
|
|
223
227
|
|
|
224
|
-
export function killTmuxSessionIfExists(socketName, sessionName) {
|
|
225
|
-
|
|
226
|
-
cwd: REPO_ROOT,
|
|
227
|
-
encoding: "utf8",
|
|
228
|
-
env: { ...process.env, TMUX: "" },
|
|
229
|
-
timeout: TMUX_COMMAND_TIMEOUT_MS,
|
|
230
|
-
});
|
|
231
|
-
if (result.error) {
|
|
232
|
-
if (result.error.code === "ETIMEDOUT") {
|
|
233
|
-
throw new Error(`kill existing session ${sessionName} failed: tmux command timed out`);
|
|
234
|
-
}
|
|
235
|
-
throw new Error(`kill existing session ${sessionName} failed: ${result.error.message}`);
|
|
236
|
-
}
|
|
237
|
-
if (result.status === 0) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
|
|
241
|
-
if (
|
|
242
|
-
combined.includes("can't find session") ||
|
|
243
|
-
combined.includes("no server running") ||
|
|
244
|
-
combined.includes("no current target") ||
|
|
245
|
-
combined.includes("error connecting")
|
|
246
|
-
) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
throw new Error(`kill existing session ${sessionName} failed: ${(result.stderr || "").trim()}`);
|
|
228
|
+
export async function killTmuxSessionIfExists(socketName, sessionName) {
|
|
229
|
+
await killSessionIfExists(socketName, sessionName);
|
|
250
230
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
REPO_ROOT,
|
|
4
|
+
TMUX_COMMAND_TIMEOUT_MS,
|
|
5
|
+
sleep,
|
|
6
|
+
} from "./shared.mjs";
|
|
7
|
+
|
|
8
|
+
const RETRYABLE_TMUX_ERROR_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE"]);
|
|
9
|
+
const MISSING_SESSION_MARKERS = [
|
|
10
|
+
"can't find session",
|
|
11
|
+
"no current target",
|
|
12
|
+
];
|
|
13
|
+
const NO_SERVER_MARKERS = [
|
|
14
|
+
"no server running",
|
|
15
|
+
"failed to connect",
|
|
16
|
+
"error connecting",
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_TMUX_RETRY_ATTEMPTS = 4;
|
|
19
|
+
|
|
20
|
+
function tmuxCombinedOutput(stdout = "", stderr = "") {
|
|
21
|
+
return `${String(stderr || "").toLowerCase()}\n${String(stdout || "").toLowerCase()}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function classifyTmuxOutput(stdout = "", stderr = "") {
|
|
25
|
+
const combined = tmuxCombinedOutput(stdout, stderr);
|
|
26
|
+
return {
|
|
27
|
+
combined,
|
|
28
|
+
missingSession: MISSING_SESSION_MARKERS.some((marker) => combined.includes(marker)),
|
|
29
|
+
noServer: NO_SERVER_MARKERS.some((marker) => combined.includes(marker)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildTmuxError(message, details = {}) {
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
Object.assign(error, details);
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function retryDelayMs(attemptIndex) {
|
|
40
|
+
const baseDelay = Math.min(250, 25 * (2 ** attemptIndex));
|
|
41
|
+
return baseDelay + Math.floor(Math.random() * 25);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function defaultSpawnTmux(socketName, args, { stdio = "pipe", timeoutMs = TMUX_COMMAND_TIMEOUT_MS } = {}) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawn("tmux", ["-L", socketName, ...args], {
|
|
47
|
+
cwd: REPO_ROOT,
|
|
48
|
+
env: { ...process.env, TMUX: "" },
|
|
49
|
+
stdio,
|
|
50
|
+
});
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
let timedOut = false;
|
|
54
|
+
if (child.stdout) {
|
|
55
|
+
child.stdout.on("data", (chunk) => {
|
|
56
|
+
stdout += chunk.toString();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (child.stderr) {
|
|
60
|
+
child.stderr.on("data", (chunk) => {
|
|
61
|
+
stderr += chunk.toString();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
timedOut = true;
|
|
66
|
+
child.kill("SIGKILL");
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
child.once("error", (error) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
reject(
|
|
71
|
+
buildTmuxError(`tmux process failed: ${error.message}`, {
|
|
72
|
+
code: error?.code || null,
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
child.once("close", (status) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
if (timedOut) {
|
|
81
|
+
reject(
|
|
82
|
+
buildTmuxError(`tmux command timed out after ${timeoutMs}ms`, {
|
|
83
|
+
code: "ETIMEDOUT",
|
|
84
|
+
status,
|
|
85
|
+
stdout,
|
|
86
|
+
stderr,
|
|
87
|
+
tmuxTimedOut: true,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
resolve({
|
|
93
|
+
status: typeof status === "number" ? status : 1,
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createTmuxAdapter({
|
|
102
|
+
spawnTmuxFn = defaultSpawnTmux,
|
|
103
|
+
sleepFn = sleep,
|
|
104
|
+
retryAttempts = DEFAULT_TMUX_RETRY_ATTEMPTS,
|
|
105
|
+
} = {}) {
|
|
106
|
+
let mutationQueue = Promise.resolve();
|
|
107
|
+
|
|
108
|
+
const enqueueMutation = (callback) => {
|
|
109
|
+
const run = mutationQueue.then(callback, callback);
|
|
110
|
+
mutationQueue = run.catch(() => {});
|
|
111
|
+
return run;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const runWithRetry = async (callback, { description = "tmux command" } = {}) => {
|
|
115
|
+
for (let attemptIndex = 0; attemptIndex < retryAttempts; attemptIndex += 1) {
|
|
116
|
+
try {
|
|
117
|
+
return await callback();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!RETRYABLE_TMUX_ERROR_CODES.has(String(error?.code || "")) || attemptIndex === retryAttempts - 1) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
await sleepFn(retryDelayMs(attemptIndex));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`${description} failed after ${retryAttempts} attempts.`);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const runTmuxCommand = async (
|
|
129
|
+
socketName,
|
|
130
|
+
args,
|
|
131
|
+
{ description = "tmux command", stdio = "pipe", mutate = false } = {},
|
|
132
|
+
) => {
|
|
133
|
+
const invoke = async () => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await spawnTmuxFn(socketName, args, { stdio, timeoutMs: TMUX_COMMAND_TIMEOUT_MS });
|
|
136
|
+
if (result.status === 0) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
const classification = classifyTmuxOutput(result.stdout, result.stderr);
|
|
140
|
+
throw buildTmuxError(
|
|
141
|
+
`${description} failed: ${(result.stderr || result.stdout || "tmux command failed").trim() || "tmux command failed"}`,
|
|
142
|
+
{
|
|
143
|
+
status: result.status,
|
|
144
|
+
stdout: result.stdout,
|
|
145
|
+
stderr: result.stderr,
|
|
146
|
+
tmuxMissingSession: classification.missingSession,
|
|
147
|
+
tmuxNoServer: classification.noServer,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error?.tmuxTimedOut || error?.tmuxMissingSession || error?.tmuxNoServer) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
if (error?.code === "ENOENT") {
|
|
155
|
+
throw buildTmuxError(`${description} failed: tmux is not installed or not on PATH`, {
|
|
156
|
+
code: "ENOENT",
|
|
157
|
+
tmuxMissingBinary: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (error?.code === "ETIMEDOUT") {
|
|
161
|
+
throw buildTmuxError(
|
|
162
|
+
`${description} failed: tmux command timed out after ${TMUX_COMMAND_TIMEOUT_MS}ms`,
|
|
163
|
+
{
|
|
164
|
+
code: "ETIMEDOUT",
|
|
165
|
+
tmuxTimedOut: true,
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof Error) {
|
|
170
|
+
throw buildTmuxError(`${description} failed: ${error.message}`, {
|
|
171
|
+
code: error?.code || null,
|
|
172
|
+
stdout: error?.stdout || "",
|
|
173
|
+
stderr: error?.stderr || "",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const runner = () => runWithRetry(invoke, { description });
|
|
180
|
+
return mutate ? enqueueMutation(runner) : runner();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const listSessions = async (socketName) => {
|
|
184
|
+
try {
|
|
185
|
+
const result = await runTmuxCommand(socketName, ["list-sessions", "-F", "#{session_name}"], {
|
|
186
|
+
description: "list tmux sessions",
|
|
187
|
+
});
|
|
188
|
+
return String(result.stdout || "")
|
|
189
|
+
.split(/\r?\n/)
|
|
190
|
+
.map((line) => line.trim())
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.tmuxMissingBinary || error?.tmuxNoServer) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const hasSession = async (socketName, sessionName, { allowMissingBinary = true } = {}) => {
|
|
201
|
+
try {
|
|
202
|
+
await runTmuxCommand(socketName, ["has-session", "-t", sessionName], {
|
|
203
|
+
description: `lookup tmux session ${sessionName}`,
|
|
204
|
+
});
|
|
205
|
+
return true;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error?.tmuxMissingSession || error?.tmuxNoServer) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (error?.tmuxMissingBinary && allowMissingBinary) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const killSessionIfExists = async (socketName, sessionName) => {
|
|
218
|
+
try {
|
|
219
|
+
await runTmuxCommand(socketName, ["kill-session", "-t", sessionName], {
|
|
220
|
+
description: `kill existing session ${sessionName}`,
|
|
221
|
+
mutate: true,
|
|
222
|
+
});
|
|
223
|
+
return true;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error?.tmuxMissingBinary || error?.tmuxMissingSession || error?.tmuxNoServer) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const createSession = async (socketName, sessionName, command, { description = `launch session ${sessionName}` } = {}) => {
|
|
233
|
+
await runTmuxCommand(socketName, ["new-session", "-d", "-s", sessionName, command], {
|
|
234
|
+
description,
|
|
235
|
+
mutate: true,
|
|
236
|
+
});
|
|
237
|
+
return sessionName;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const attachSession = async (socketName, sessionName) => {
|
|
241
|
+
const exists = await hasSession(socketName, sessionName, { allowMissingBinary: false });
|
|
242
|
+
if (!exists) {
|
|
243
|
+
throw buildTmuxError(`No live tmux session named ${sessionName}.`, {
|
|
244
|
+
tmuxMissingSession: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await runTmuxCommand(socketName, ["attach", "-t", sessionName], {
|
|
249
|
+
description: `attach tmux session ${sessionName}`,
|
|
250
|
+
stdio: "inherit",
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error?.tmuxMissingBinary) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
const stillExists = await hasSession(socketName, sessionName);
|
|
257
|
+
if (!stillExists || error?.tmuxMissingSession || error?.tmuxNoServer) {
|
|
258
|
+
throw buildTmuxError(`No live tmux session named ${sessionName}.`, {
|
|
259
|
+
tmuxMissingSession: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
runTmuxCommand,
|
|
268
|
+
createSession,
|
|
269
|
+
killSessionIfExists,
|
|
270
|
+
listSessions,
|
|
271
|
+
hasSession,
|
|
272
|
+
attachSession,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const defaultTmuxAdapter = createTmuxAdapter();
|
|
277
|
+
|
|
278
|
+
export function runTmuxCommand(socketName, args, options = {}) {
|
|
279
|
+
return defaultTmuxAdapter.runTmuxCommand(socketName, args, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function createSession(socketName, sessionName, command, options = {}) {
|
|
283
|
+
return defaultTmuxAdapter.createSession(socketName, sessionName, command, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function killSessionIfExists(socketName, sessionName) {
|
|
287
|
+
return defaultTmuxAdapter.killSessionIfExists(socketName, sessionName);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function listSessions(socketName) {
|
|
291
|
+
return defaultTmuxAdapter.listSessions(socketName);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function hasSession(socketName, sessionName, options = {}) {
|
|
295
|
+
return defaultTmuxAdapter.hasSession(socketName, sessionName, options);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function attachSession(socketName, sessionName) {
|
|
299
|
+
return defaultTmuxAdapter.attachSession(socketName, sessionName);
|
|
300
|
+
}
|
|
@@ -798,6 +798,8 @@ export function writeTraceBundle({
|
|
|
798
798
|
capabilityAssignments = [],
|
|
799
799
|
dependencySnapshot = null,
|
|
800
800
|
securitySummary = null,
|
|
801
|
+
corridorSummary = null,
|
|
802
|
+
corridorSummaryPath = null,
|
|
801
803
|
integrationSummary,
|
|
802
804
|
integrationMarkdownPath,
|
|
803
805
|
proofRegistryPath = null,
|
|
@@ -860,6 +862,28 @@ export function writeTraceBundle({
|
|
|
860
862
|
"json",
|
|
861
863
|
true,
|
|
862
864
|
);
|
|
865
|
+
const corridorArtifact =
|
|
866
|
+
corridorSummaryPath || corridorSummary
|
|
867
|
+
? corridorSummaryPath
|
|
868
|
+
? copyArtifactDescriptor(
|
|
869
|
+
dir,
|
|
870
|
+
corridorSummaryPath,
|
|
871
|
+
path.join(dir, "corridor.json"),
|
|
872
|
+
false,
|
|
873
|
+
)
|
|
874
|
+
: writeArtifactDescriptor(
|
|
875
|
+
dir,
|
|
876
|
+
path.join(dir, "corridor.json"),
|
|
877
|
+
corridorSummary || {},
|
|
878
|
+
"json",
|
|
879
|
+
false,
|
|
880
|
+
)
|
|
881
|
+
: {
|
|
882
|
+
path: "corridor.json",
|
|
883
|
+
required: false,
|
|
884
|
+
present: false,
|
|
885
|
+
sha256: null,
|
|
886
|
+
};
|
|
863
887
|
const integrationArtifact = writeArtifactDescriptor(
|
|
864
888
|
dir,
|
|
865
889
|
path.join(dir, "integration.json"),
|
|
@@ -1023,6 +1047,7 @@ export function writeTraceBundle({
|
|
|
1023
1047
|
capabilityAssignments: capabilityAssignmentsArtifact,
|
|
1024
1048
|
dependencySnapshot: dependencySnapshotArtifact,
|
|
1025
1049
|
security: securityArtifact,
|
|
1050
|
+
corridor: corridorArtifact,
|
|
1026
1051
|
integration: integrationArtifact,
|
|
1027
1052
|
integrationMarkdown: integrationMarkdownArtifact,
|
|
1028
1053
|
proofRegistry: proofRegistryArtifact,
|
|
@@ -154,6 +154,19 @@ function resolveWaveControlConfig(lanePaths, overrides = {}) {
|
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function resolveWaveControlAuthToken(config) {
|
|
158
|
+
const authVars = Array.isArray(config.authTokenEnvVars)
|
|
159
|
+
? config.authTokenEnvVars
|
|
160
|
+
: [config.authTokenEnvVar].filter(Boolean);
|
|
161
|
+
for (const envVar of authVars) {
|
|
162
|
+
const value = envVar ? String(process.env[envVar] || "").trim() : "";
|
|
163
|
+
if (value) {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
function buildWorkspaceId(lanePaths, config) {
|
|
158
171
|
return normalizeText(config.workspaceId, buildWorkspaceTmuxToken(REPO_ROOT));
|
|
159
172
|
}
|
|
@@ -505,7 +518,7 @@ export async function flushWaveControlQueue(lanePaths, options = {}) {
|
|
|
505
518
|
|
|
506
519
|
try {
|
|
507
520
|
const ingestUrl = resolveIngestUrl(config.endpoint);
|
|
508
|
-
const authToken = config
|
|
521
|
+
const authToken = resolveWaveControlAuthToken(config);
|
|
509
522
|
await postBatch(
|
|
510
523
|
ingestUrl,
|
|
511
524
|
authToken,
|
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
resolveDesignReportPath,
|
|
69
69
|
isSecurityRolePromptPath,
|
|
70
70
|
isSecurityReviewAgent,
|
|
71
|
+
resolveAgentClosureRoleKeys,
|
|
72
|
+
resolveWaveRoleBindings,
|
|
71
73
|
resolveSecurityReviewReportPath,
|
|
72
74
|
} from "./role-helpers.mjs";
|
|
73
75
|
import {
|
|
@@ -107,6 +109,13 @@ const COMPONENT_MATURITY_ORDER = Object.fromEntries(
|
|
|
107
109
|
);
|
|
108
110
|
const PROOF_CENTRIC_COMPONENT_LEVEL = "pilot-live";
|
|
109
111
|
const RETRY_POLICY_VALUES = new Set(["sticky", "fallback-allowed"]);
|
|
112
|
+
const CLOSURE_ROLE_LABELS = {
|
|
113
|
+
"cont-eval": "cont-EVAL",
|
|
114
|
+
"security-review": "security review",
|
|
115
|
+
integration: "integration steward",
|
|
116
|
+
documentation: "documentation steward",
|
|
117
|
+
"cont-qa": "cont-QA",
|
|
118
|
+
};
|
|
110
119
|
|
|
111
120
|
function resolveLaneProfileForOptions(options = {}) {
|
|
112
121
|
if (options.laneProfile) {
|
|
@@ -1209,7 +1218,11 @@ function isImplementationOwningWaveAgent(
|
|
|
1209
1218
|
);
|
|
1210
1219
|
}
|
|
1211
1220
|
|
|
1212
|
-
function resolveAgentSummaryReportPath(
|
|
1221
|
+
function resolveAgentSummaryReportPath(
|
|
1222
|
+
wave,
|
|
1223
|
+
agentId,
|
|
1224
|
+
{ contQaAgentId, contEvalAgentId, securityRolePromptPath } = {},
|
|
1225
|
+
) {
|
|
1213
1226
|
if (agentId === contQaAgentId && wave.contQaReportPath) {
|
|
1214
1227
|
return path.resolve(REPO_ROOT, wave.contQaReportPath);
|
|
1215
1228
|
}
|
|
@@ -1223,7 +1236,7 @@ function resolveAgentSummaryReportPath(wave, agentId, { contQaAgentId, contEvalA
|
|
|
1223
1236
|
return path.resolve(REPO_ROOT, designReportPath);
|
|
1224
1237
|
}
|
|
1225
1238
|
}
|
|
1226
|
-
if (isSecurityReviewAgent(agent)) {
|
|
1239
|
+
if (isSecurityReviewAgent(agent, { securityRolePromptPath })) {
|
|
1227
1240
|
const securityReportPath = resolveSecurityReviewReportPath(agent);
|
|
1228
1241
|
if (securityReportPath) {
|
|
1229
1242
|
return path.resolve(REPO_ROOT, securityReportPath);
|
|
@@ -1240,6 +1253,7 @@ function materializeLiveExecutionSummaryIfMissing({
|
|
|
1240
1253
|
logsDir,
|
|
1241
1254
|
contQaAgentId,
|
|
1242
1255
|
contEvalAgentId,
|
|
1256
|
+
securityRolePromptPath = null,
|
|
1243
1257
|
}) {
|
|
1244
1258
|
const logPath = logsDir ? path.join(logsDir, `wave-${wave.wave}-${agent.slug}.log`) : null;
|
|
1245
1259
|
const existing = readAgentExecutionSummary(statusPath, {
|
|
@@ -1250,6 +1264,7 @@ function materializeLiveExecutionSummaryIfMissing({
|
|
|
1250
1264
|
reportPath: resolveAgentSummaryReportPath(wave, agent.agentId, {
|
|
1251
1265
|
contQaAgentId,
|
|
1252
1266
|
contEvalAgentId,
|
|
1267
|
+
securityRolePromptPath,
|
|
1253
1268
|
}),
|
|
1254
1269
|
});
|
|
1255
1270
|
if (existing) {
|
|
@@ -1265,6 +1280,7 @@ function materializeLiveExecutionSummaryIfMissing({
|
|
|
1265
1280
|
reportPath: resolveAgentSummaryReportPath(wave, agent.agentId, {
|
|
1266
1281
|
contQaAgentId,
|
|
1267
1282
|
contEvalAgentId,
|
|
1283
|
+
securityRolePromptPath,
|
|
1268
1284
|
}),
|
|
1269
1285
|
});
|
|
1270
1286
|
writeAgentExecutionSummary(statusPath, summary);
|
|
@@ -1746,6 +1762,19 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1746
1762
|
errors.push(`Design agent ${designAgent.agentId} must stay docs/spec-only unless it explicitly owns implementation files`);
|
|
1747
1763
|
}
|
|
1748
1764
|
}
|
|
1765
|
+
const closureRoleBindings = resolveWaveRoleBindings(wave, laneProfile.roles, wave.agents);
|
|
1766
|
+
for (const agent of wave.agents) {
|
|
1767
|
+
const closureRoles = resolveAgentClosureRoleKeys(
|
|
1768
|
+
agent,
|
|
1769
|
+
closureRoleBindings,
|
|
1770
|
+
laneProfile.roles,
|
|
1771
|
+
);
|
|
1772
|
+
if (closureRoles.length > 1) {
|
|
1773
|
+
errors.push(
|
|
1774
|
+
`Agent ${agent.agentId} must not overlap closure roles (${closureRoles.map((role) => CLOSURE_ROLE_LABELS[role] || role).join(", ")})`,
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1749
1778
|
if (integrationRuleActive) {
|
|
1750
1779
|
const integrationStewards = wave.agents.filter((agent) =>
|
|
1751
1780
|
agent.rolePromptPaths?.includes(laneProfile.roles.integrationRolePromptPath),
|
|
@@ -2006,7 +2035,9 @@ function inferAgentRuntimeRole(agent, laneProfile) {
|
|
|
2006
2035
|
) {
|
|
2007
2036
|
return "design";
|
|
2008
2037
|
}
|
|
2009
|
-
if (isSecurityReviewAgent(agent
|
|
2038
|
+
if (isSecurityReviewAgent(agent, {
|
|
2039
|
+
securityRolePromptPath: laneProfile?.roles?.securityRolePromptPath,
|
|
2040
|
+
})) {
|
|
2010
2041
|
return "security";
|
|
2011
2042
|
}
|
|
2012
2043
|
const capabilities = Array.isArray(agent?.capabilities)
|
|
@@ -2203,7 +2234,7 @@ export function resolveAgentExecutor(agent, options = {}) {
|
|
|
2203
2234
|
profile?.codex?.sandbox ||
|
|
2204
2235
|
(executorId === "codex"
|
|
2205
2236
|
? normalizeCodexSandboxMode(
|
|
2206
|
-
options.codexSandboxMode
|
|
2237
|
+
options.codexSandboxMode ?? laneProfile.executors.codex.sandbox ?? DEFAULT_CODEX_SANDBOX_MODE,
|
|
2207
2238
|
"executor.codex.sandbox",
|
|
2208
2239
|
)
|
|
2209
2240
|
: laneProfile.executors.codex.sandbox || DEFAULT_CODEX_SANDBOX_MODE),
|
|
@@ -2924,6 +2955,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2924
2955
|
const componentThreshold =
|
|
2925
2956
|
options.requireComponentPromotionsFromWave ??
|
|
2926
2957
|
laneProfile.validation.requireComponentPromotionsFromWave;
|
|
2958
|
+
const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
|
|
2927
2959
|
|
|
2928
2960
|
const reasons = [];
|
|
2929
2961
|
const summariesByAgentId = {};
|
|
@@ -2980,6 +3012,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2980
3012
|
logsDir,
|
|
2981
3013
|
contQaAgentId,
|
|
2982
3014
|
contEvalAgentId,
|
|
3015
|
+
securityRolePromptPath,
|
|
2983
3016
|
});
|
|
2984
3017
|
summariesByAgentId[agent.agentId] = summary;
|
|
2985
3018
|
if (agent.agentId === contQaAgentId) {
|
|
@@ -3021,7 +3054,7 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
3021
3054
|
}
|
|
3022
3055
|
continue;
|
|
3023
3056
|
}
|
|
3024
|
-
if (isSecurityReviewAgent(agent)) {
|
|
3057
|
+
if (isSecurityReviewAgent(agent, { securityRolePromptPath })) {
|
|
3025
3058
|
const validation = validateSecuritySummary(agent, summary);
|
|
3026
3059
|
if (!validation.ok) {
|
|
3027
3060
|
pushWaveCompletionReason(
|
package/scripts/wave.mjs
CHANGED
|
@@ -20,6 +20,11 @@ function printHelp() {
|
|
|
20
20
|
wave draft [draft options]
|
|
21
21
|
wave adhoc [adhoc options]
|
|
22
22
|
wave launch [launcher options]
|
|
23
|
+
wave submit [launcher options]
|
|
24
|
+
wave supervise [supervisor options]
|
|
25
|
+
wave status --run-id <id> [--json]
|
|
26
|
+
wave wait --run-id <id> [--timeout-seconds <n>] [--json]
|
|
27
|
+
wave attach --run-id <id> (--agent <id> | --dashboard)
|
|
23
28
|
wave autonomous [autonomous options]
|
|
24
29
|
wave feedback [feedback options]
|
|
25
30
|
wave dashboard [dashboard options]
|
|
@@ -73,6 +78,14 @@ if (["init", "upgrade", "self-update", "changelog", "doctor"].includes(subcomman
|
|
|
73
78
|
console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
|
|
74
79
|
process.exit(Number.isInteger(error?.exitCode) ? error.exitCode : 1);
|
|
75
80
|
}
|
|
81
|
+
} else if (["submit", "supervise", "status", "wait", "attach"].includes(subcommand)) {
|
|
82
|
+
try {
|
|
83
|
+
const { runSupervisorCli } = await import("./wave-orchestrator/supervisor-cli.mjs");
|
|
84
|
+
await runSupervisorCli(subcommand, rest);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
process.exit(Number.isInteger(error?.exitCode) ? error.exitCode : 1);
|
|
88
|
+
}
|
|
76
89
|
} else if (subcommand === "autonomous") {
|
|
77
90
|
const { runAutonomousCli } = await import("./wave-orchestrator/autonomous.mjs");
|
|
78
91
|
try {
|