@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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_CORRIDOR_BASE_URL,
|
|
5
|
+
DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
|
|
6
|
+
DEFAULT_WAVE_CONTROL_ENDPOINT,
|
|
7
|
+
} from "./config.mjs";
|
|
8
|
+
import { writeJsonAtomic, readJsonOrNull } from "./shared.mjs";
|
|
9
|
+
import {
|
|
10
|
+
isDefaultWaveControlEndpoint,
|
|
11
|
+
readJsonResponse,
|
|
12
|
+
resolveWaveControlAuthToken,
|
|
13
|
+
} from "./provider-runtime.mjs";
|
|
14
|
+
import {
|
|
15
|
+
isContEvalImplementationOwningAgent,
|
|
16
|
+
isDesignAgent,
|
|
17
|
+
isSecurityReviewAgent,
|
|
18
|
+
} from "./role-helpers.mjs";
|
|
19
|
+
|
|
20
|
+
const SEVERITY_RANK = {
|
|
21
|
+
low: 1,
|
|
22
|
+
medium: 2,
|
|
23
|
+
high: 3,
|
|
24
|
+
critical: 4,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function normalizeOwnedPath(value) {
|
|
28
|
+
return String(value || "").trim().replaceAll("\\", "/").replace(/\/+$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRelevantOwnedPath(value) {
|
|
32
|
+
const normalized = normalizeOwnedPath(value);
|
|
33
|
+
if (!normalized || normalized.startsWith(".tmp/")) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (normalized.startsWith("docs/")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return !/\.(?:md|txt)$/i.test(normalized);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function matchesOwnedPath(findingPath, ownedPath) {
|
|
43
|
+
const normalizedFinding = normalizeOwnedPath(findingPath);
|
|
44
|
+
const normalizedOwned = normalizeOwnedPath(ownedPath);
|
|
45
|
+
if (!normalizedFinding || !normalizedOwned) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return normalizedFinding === normalizedOwned || normalizedFinding.startsWith(`${normalizedOwned}/`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldIncludeImplementationOwnedPaths(agent, lanePaths = {}) {
|
|
52
|
+
if (!agent || isSecurityReviewAgent(agent) || isDesignAgent(agent)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (agent.agentId === lanePaths.contQaAgentId || agent.agentId === lanePaths.documentationAgentId) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (agent.agentId === lanePaths.integrationAgentId) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (agent.agentId === lanePaths.contEvalAgentId) {
|
|
62
|
+
return isContEvalImplementationOwningAgent(agent, {
|
|
63
|
+
contEvalAgentId: lanePaths.contEvalAgentId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function waveCorridorContextPath(lanePaths, waveNumber) {
|
|
70
|
+
return path.join(lanePaths.securityDir, `wave-${waveNumber}-corridor.json`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readWaveCorridorContext(lanePaths, waveNumber) {
|
|
74
|
+
return readJsonOrNull(waveCorridorContextPath(lanePaths, waveNumber));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function corridorArtifactBase({ lanePaths, wave, ownedPaths, providerMode, source }) {
|
|
78
|
+
return {
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
wave,
|
|
81
|
+
lane: lanePaths.lane,
|
|
82
|
+
projectId: lanePaths.project,
|
|
83
|
+
providerMode,
|
|
84
|
+
source,
|
|
85
|
+
requiredAtClosure: lanePaths.externalProviders?.corridor?.requiredAtClosure !== false,
|
|
86
|
+
severityThreshold:
|
|
87
|
+
lanePaths.externalProviders?.corridor?.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
|
|
88
|
+
fetchedAt: new Date().toISOString(),
|
|
89
|
+
relevantOwnedPaths: ownedPaths,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function summarizeCorridorPayload(base, guardrails, findings) {
|
|
94
|
+
const thresholdRank = SEVERITY_RANK[String(base.severityThreshold || "critical").toLowerCase()] || 4;
|
|
95
|
+
const matchedFindings = (Array.isArray(findings) ? findings : [])
|
|
96
|
+
.map((finding) => {
|
|
97
|
+
const matchedOwnedPaths = base.relevantOwnedPaths.filter((ownedPath) =>
|
|
98
|
+
matchesOwnedPath(finding.affectedFile, ownedPath),
|
|
99
|
+
);
|
|
100
|
+
if (matchedOwnedPaths.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
id: finding.id || null,
|
|
105
|
+
title: finding.title || null,
|
|
106
|
+
affectedFile: finding.affectedFile || null,
|
|
107
|
+
cwe: finding.cwe || null,
|
|
108
|
+
severity: finding.severity || null,
|
|
109
|
+
state: finding.state || null,
|
|
110
|
+
createdAt: finding.createdAt || null,
|
|
111
|
+
matchedOwnedPaths,
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
const blockingFindings = matchedFindings.filter((finding) => {
|
|
116
|
+
const rank = SEVERITY_RANK[String(finding.severity || "").toLowerCase()] || 0;
|
|
117
|
+
return rank >= thresholdRank;
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
ok: true,
|
|
122
|
+
guardrails: Array.isArray(guardrails?.reports) ? guardrails.reports : [],
|
|
123
|
+
matchedFindings,
|
|
124
|
+
blockingFindings,
|
|
125
|
+
blocking: blockingFindings.length > 0,
|
|
126
|
+
error: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function failureCorridorPayload(base, error) {
|
|
131
|
+
return {
|
|
132
|
+
...base,
|
|
133
|
+
ok: false,
|
|
134
|
+
guardrails: [],
|
|
135
|
+
matchedFindings: [],
|
|
136
|
+
blockingFindings: [],
|
|
137
|
+
blocking: base.requiredAtClosure === true,
|
|
138
|
+
error: error instanceof Error ? error.message : String(error),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function requestCorridorJson(fetchImpl, url, token) {
|
|
143
|
+
const response = await fetchImpl(url, {
|
|
144
|
+
method: "GET",
|
|
145
|
+
headers: {
|
|
146
|
+
authorization: `Bearer ${token}`,
|
|
147
|
+
accept: "application/json",
|
|
148
|
+
},
|
|
149
|
+
signal: AbortSignal.timeout(10000),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const payload = await readJsonResponse(response, null);
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Corridor request failed (${response.status}): ${payload?.error || payload?.message || response.statusText || "unknown error"}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return response.json();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function listCorridorFindings(fetchImpl, baseUrl, projectId, token, findingStates) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
const states = findingStates.size > 0 ? [...findingStates] : [null];
|
|
163
|
+
for (const state of states) {
|
|
164
|
+
let nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
|
|
165
|
+
if (state) {
|
|
166
|
+
nextUrl.searchParams.set("state", state);
|
|
167
|
+
}
|
|
168
|
+
let pages = 0;
|
|
169
|
+
while (nextUrl && pages < 10) {
|
|
170
|
+
const payload = await requestCorridorJson(fetchImpl, nextUrl, token);
|
|
171
|
+
if (Array.isArray(payload)) {
|
|
172
|
+
findings.push(...payload);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const items = Array.isArray(payload?.items)
|
|
176
|
+
? payload.items
|
|
177
|
+
: Array.isArray(payload?.findings)
|
|
178
|
+
? payload.findings
|
|
179
|
+
: Array.isArray(payload?.data)
|
|
180
|
+
? payload.data
|
|
181
|
+
: [];
|
|
182
|
+
findings.push(...items);
|
|
183
|
+
if (payload?.nextPageUrl) {
|
|
184
|
+
nextUrl = new URL(payload.nextPageUrl);
|
|
185
|
+
} else if (payload?.nextCursor) {
|
|
186
|
+
nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
|
|
187
|
+
if (state) {
|
|
188
|
+
nextUrl.searchParams.set("state", state);
|
|
189
|
+
}
|
|
190
|
+
nextUrl.searchParams.set("cursor", String(payload.nextCursor));
|
|
191
|
+
} else if (payload?.page && payload?.totalPages && Number(payload.page) < Number(payload.totalPages)) {
|
|
192
|
+
nextUrl = new URL(`${baseUrl}/projects/${projectId}/findings`);
|
|
193
|
+
if (state) {
|
|
194
|
+
nextUrl.searchParams.set("state", state);
|
|
195
|
+
}
|
|
196
|
+
nextUrl.searchParams.set("page", String(Number(payload.page) + 1));
|
|
197
|
+
} else {
|
|
198
|
+
nextUrl = null;
|
|
199
|
+
}
|
|
200
|
+
pages += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return findings;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths) {
|
|
207
|
+
const corridor = lanePaths.externalProviders?.corridor || {};
|
|
208
|
+
const token =
|
|
209
|
+
process.env[corridor.apiTokenEnvVar || "CORRIDOR_API_TOKEN"] ||
|
|
210
|
+
process.env[corridor.apiKeyFallbackEnvVar || "CORRIDOR_API_KEY"] ||
|
|
211
|
+
"";
|
|
212
|
+
if (!token) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Corridor token is missing; set ${corridor.apiTokenEnvVar || "CORRIDOR_API_TOKEN"} or ${corridor.apiKeyFallbackEnvVar || "CORRIDOR_API_KEY"}.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const baseUrl = String(corridor.baseUrl || DEFAULT_CORRIDOR_BASE_URL).replace(/\/$/, "");
|
|
218
|
+
const findingStates = new Set((corridor.findingStates || []).map((state) => String(state).trim().toLowerCase()));
|
|
219
|
+
const [guardrails, findings] = await Promise.all([
|
|
220
|
+
requestCorridorJson(fetchImpl, `${baseUrl}/projects/${corridor.projectId}/reports`, token),
|
|
221
|
+
listCorridorFindings(fetchImpl, baseUrl, corridor.projectId, token, findingStates),
|
|
222
|
+
]);
|
|
223
|
+
const filteredFindings = (Array.isArray(findings) ? findings : []).filter((finding) =>
|
|
224
|
+
findingStates.size === 0 || findingStates.has(String(finding.state || "").trim().toLowerCase()),
|
|
225
|
+
);
|
|
226
|
+
return summarizeCorridorPayload(
|
|
227
|
+
corridorArtifactBase({
|
|
228
|
+
lanePaths,
|
|
229
|
+
wave: null,
|
|
230
|
+
ownedPaths,
|
|
231
|
+
providerMode: "direct",
|
|
232
|
+
source: "corridor-api",
|
|
233
|
+
}),
|
|
234
|
+
guardrails,
|
|
235
|
+
filteredFindings,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths) {
|
|
240
|
+
const waveControl = lanePaths.waveControl || {};
|
|
241
|
+
const endpoint = String(waveControl.endpoint || DEFAULT_WAVE_CONTROL_ENDPOINT).trim();
|
|
242
|
+
if (!endpoint || isDefaultWaveControlEndpoint(endpoint)) {
|
|
243
|
+
throw new Error("Corridor broker mode requires an owned Wave Control endpoint.");
|
|
244
|
+
}
|
|
245
|
+
const authToken = resolveWaveControlAuthToken(waveControl);
|
|
246
|
+
if (!authToken) {
|
|
247
|
+
throw new Error("WAVE_API_TOKEN is not set; Corridor broker mode is unavailable.");
|
|
248
|
+
}
|
|
249
|
+
const response = await fetchImpl(`${endpoint.replace(/\/$/, "")}/providers/corridor/context`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: {
|
|
252
|
+
authorization: `Bearer ${authToken}`,
|
|
253
|
+
"content-type": "application/json",
|
|
254
|
+
accept: "application/json",
|
|
255
|
+
},
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
projectId: lanePaths.project,
|
|
258
|
+
wave: waveNumber,
|
|
259
|
+
ownedPaths,
|
|
260
|
+
severityThreshold:
|
|
261
|
+
lanePaths.externalProviders?.corridor?.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD,
|
|
262
|
+
findingStates: lanePaths.externalProviders?.corridor?.findingStates || [],
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
if (!response.ok) {
|
|
266
|
+
const payload = await readJsonResponse(response, null);
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Corridor broker request failed (${response.status}): ${payload?.error || payload?.message || response.statusText || "unknown error"}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return response.json();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function materializeWaveCorridorContext(
|
|
275
|
+
lanePaths,
|
|
276
|
+
waveDefinition,
|
|
277
|
+
{
|
|
278
|
+
fetchImpl = globalThis.fetch,
|
|
279
|
+
} = {},
|
|
280
|
+
) {
|
|
281
|
+
const corridor = lanePaths.externalProviders?.corridor || {};
|
|
282
|
+
const waveNumber = waveDefinition?.wave ?? 0;
|
|
283
|
+
const artifactPath = waveCorridorContextPath(lanePaths, waveNumber);
|
|
284
|
+
if (!corridor.enabled) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const ownedPaths = (Array.isArray(waveDefinition?.agents) ? waveDefinition.agents : [])
|
|
288
|
+
.filter((agent) => shouldIncludeImplementationOwnedPaths(agent, lanePaths))
|
|
289
|
+
.flatMap((agent) => (Array.isArray(agent.ownedPaths) ? agent.ownedPaths : []))
|
|
290
|
+
.map(normalizeOwnedPath)
|
|
291
|
+
.filter(isRelevantOwnedPath);
|
|
292
|
+
const base = corridorArtifactBase({
|
|
293
|
+
lanePaths,
|
|
294
|
+
wave: waveNumber,
|
|
295
|
+
ownedPaths,
|
|
296
|
+
providerMode: corridor.mode || "direct",
|
|
297
|
+
source: null,
|
|
298
|
+
});
|
|
299
|
+
if (ownedPaths.length === 0) {
|
|
300
|
+
const payload = {
|
|
301
|
+
...base,
|
|
302
|
+
ok: true,
|
|
303
|
+
guardrails: [],
|
|
304
|
+
matchedFindings: [],
|
|
305
|
+
blockingFindings: [],
|
|
306
|
+
blocking: false,
|
|
307
|
+
error: null,
|
|
308
|
+
detail: "No implementation-owned paths were eligible for Corridor matching in this wave.",
|
|
309
|
+
};
|
|
310
|
+
writeJsonAtomic(artifactPath, payload);
|
|
311
|
+
return payload;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
let payload;
|
|
315
|
+
if (corridor.mode === "broker") {
|
|
316
|
+
payload = await fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths);
|
|
317
|
+
} else if (corridor.mode === "hybrid") {
|
|
318
|
+
try {
|
|
319
|
+
payload = await fetchCorridorBroker(fetchImpl, lanePaths, waveNumber, ownedPaths);
|
|
320
|
+
} catch {
|
|
321
|
+
payload = await fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
payload = await fetchCorridorDirect(fetchImpl, lanePaths, ownedPaths);
|
|
325
|
+
}
|
|
326
|
+
const mergedPayload = {
|
|
327
|
+
...base,
|
|
328
|
+
...payload,
|
|
329
|
+
wave: waveNumber,
|
|
330
|
+
lane: lanePaths.lane,
|
|
331
|
+
projectId: lanePaths.project,
|
|
332
|
+
relevantOwnedPaths: ownedPaths,
|
|
333
|
+
requiredAtClosure: corridor.requiredAtClosure !== false,
|
|
334
|
+
};
|
|
335
|
+
writeJsonAtomic(artifactPath, mergedPayload);
|
|
336
|
+
return mergedPayload;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const payload = failureCorridorPayload(base, error);
|
|
339
|
+
writeJsonAtomic(artifactPath, payload);
|
|
340
|
+
return payload;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function renderCorridorPromptContext(corridorContext) {
|
|
345
|
+
if (!corridorContext || corridorContext.ok !== true) {
|
|
346
|
+
if (corridorContext?.error) {
|
|
347
|
+
return `Corridor provider fetch failed: ${corridorContext.error}`;
|
|
348
|
+
}
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
const lines = [
|
|
352
|
+
`Corridor source: ${corridorContext.source || corridorContext.providerMode || "unknown"}`,
|
|
353
|
+
`Corridor blocking: ${corridorContext.blocking ? "yes" : "no"}`,
|
|
354
|
+
`Corridor threshold: ${corridorContext.severityThreshold || DEFAULT_CORRIDOR_SEVERITY_THRESHOLD}`,
|
|
355
|
+
`Corridor matched findings: ${(corridorContext.matchedFindings || []).length}`,
|
|
356
|
+
];
|
|
357
|
+
for (const finding of (corridorContext.blockingFindings || []).slice(0, 5)) {
|
|
358
|
+
lines.push(
|
|
359
|
+
`- ${finding.severity || "unknown"} ${finding.affectedFile || "unknown-file"}: ${finding.title || "finding"}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
@@ -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 { loadWaveConfig } from "./config.mjs";
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
formatAgeFromTimestamp,
|
|
15
14
|
formatElapsed,
|
|
16
15
|
pad,
|
|
16
|
+
readJsonOrNull,
|
|
17
17
|
sleep,
|
|
18
18
|
truncate,
|
|
19
19
|
} from "./shared.mjs";
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
createCurrentWaveDashboardTerminalEntry,
|
|
22
22
|
createGlobalDashboardTerminalEntry,
|
|
23
23
|
} from "./terminals.mjs";
|
|
24
|
+
import {
|
|
25
|
+
attachSession as attachTmuxSession,
|
|
26
|
+
hasSession as hasTmuxSession,
|
|
27
|
+
} from "./tmux-adapter.mjs";
|
|
24
28
|
|
|
25
29
|
const DASHBOARD_ATTACH_TARGETS = ["current", "global"];
|
|
26
30
|
|
|
@@ -78,30 +82,7 @@ export function parseDashboardArgs(argv) {
|
|
|
78
82
|
return { help: false, options };
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
function
|
|
82
|
-
const result = spawnSync("tmux", ["-L", socketName, "has-session", "-t", sessionName], {
|
|
83
|
-
cwd: REPO_ROOT,
|
|
84
|
-
encoding: "utf8",
|
|
85
|
-
env: { ...process.env, TMUX: "" },
|
|
86
|
-
});
|
|
87
|
-
if (result.error) {
|
|
88
|
-
throw new Error(`tmux session lookup failed: ${result.error.message}`);
|
|
89
|
-
}
|
|
90
|
-
if (result.status === 0) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
|
|
94
|
-
if (
|
|
95
|
-
combined.includes("can't find session") ||
|
|
96
|
-
combined.includes("no server running") ||
|
|
97
|
-
combined.includes("error connecting")
|
|
98
|
-
) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
throw new Error((result.stderr || result.stdout || "tmux has-session failed").trim());
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function attachDashboardSession(project, lane, target) {
|
|
85
|
+
async function attachDashboardSession(project, lane, target) {
|
|
105
86
|
const config = loadWaveConfig();
|
|
106
87
|
const lanePaths = buildLanePaths(lane, {
|
|
107
88
|
config,
|
|
@@ -111,25 +92,112 @@ function attachDashboardSession(project, lane, target) {
|
|
|
111
92
|
target === "global"
|
|
112
93
|
? createGlobalDashboardTerminalEntry(lanePaths, "current")
|
|
113
94
|
: createCurrentWaveDashboardTerminalEntry(lanePaths);
|
|
114
|
-
if (!
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
if (!await hasTmuxSession(lanePaths.tmuxSocketName, entry.sessionName, { allowMissingBinary: false })) {
|
|
96
|
+
const fallback = resolveDashboardAttachFallback(lanePaths, target);
|
|
97
|
+
if (fallback) {
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(buildMissingDashboardAttachError(lanePaths, target));
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await attachTmuxSession(lanePaths.tmuxSocketName, entry.sessionName);
|
|
104
|
+
return null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.tmuxMissingSession) {
|
|
107
|
+
const fallback = resolveDashboardAttachFallback(lanePaths, target);
|
|
108
|
+
if (fallback) {
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(buildMissingDashboardAttachError(lanePaths, target));
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildMissingDashboardAttachError(lanePaths, target) {
|
|
118
|
+
const dashboardsRel = path.relative(REPO_ROOT, path.dirname(lanePaths.globalDashboardPath));
|
|
119
|
+
return `No ${target} dashboard session is live for lane ${lanePaths.lane}. Launch a dashboarded run on that lane, then inspect ${dashboardsRel} if you need the last written dashboard state.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function waveDashboardPathForNumber(lanePaths, waveNumber) {
|
|
123
|
+
if (!Number.isFinite(Number(waveNumber))) {
|
|
124
|
+
return null;
|
|
119
125
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
const candidate = path.join(lanePaths.dashboardsDir, `wave-${Number(waveNumber)}.json`);
|
|
127
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function selectCurrentWaveFromGlobalDashboard(globalState) {
|
|
131
|
+
const waves = Array.isArray(globalState?.waves) ? globalState.waves : [];
|
|
132
|
+
const candidates = waves
|
|
133
|
+
.map((wave) => ({
|
|
134
|
+
waveNumber: Number.parseInt(String(wave?.wave ?? ""), 10),
|
|
135
|
+
status: String(wave?.status || "").trim().toLowerCase(),
|
|
136
|
+
updatedAt: Date.parse(
|
|
137
|
+
String(wave?.updatedAt || wave?.completedAt || wave?.startedAt || ""),
|
|
138
|
+
),
|
|
139
|
+
}))
|
|
140
|
+
.filter((entry) => Number.isFinite(entry.waveNumber));
|
|
141
|
+
if (candidates.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
candidates.sort((left, right) => {
|
|
145
|
+
const leftTerminal = TERMINAL_STATES.has(left.status);
|
|
146
|
+
const rightTerminal = TERMINAL_STATES.has(right.status);
|
|
147
|
+
if (leftTerminal !== rightTerminal) {
|
|
148
|
+
return leftTerminal ? 1 : -1;
|
|
149
|
+
}
|
|
150
|
+
const leftUpdatedAt = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
|
|
151
|
+
const rightUpdatedAt = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
|
|
152
|
+
if (leftUpdatedAt !== rightUpdatedAt) {
|
|
153
|
+
return rightUpdatedAt - leftUpdatedAt;
|
|
154
|
+
}
|
|
155
|
+
return right.waveNumber - left.waveNumber;
|
|
124
156
|
});
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
return candidates[0].waveNumber;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveDashboardAttachFallback(lanePaths, target) {
|
|
161
|
+
if (target === "global") {
|
|
162
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
163
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
164
|
+
: null;
|
|
127
165
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
const globalState = readJsonOrNull(lanePaths.globalDashboardPath);
|
|
167
|
+
const preferredWaveNumber = selectCurrentWaveFromGlobalDashboard(globalState);
|
|
168
|
+
const preferredWavePath = waveDashboardPathForNumber(lanePaths, preferredWaveNumber);
|
|
169
|
+
if (preferredWavePath) {
|
|
170
|
+
return { dashboardFile: preferredWavePath };
|
|
132
171
|
}
|
|
172
|
+
if (!fs.existsSync(lanePaths.dashboardsDir)) {
|
|
173
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
174
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
175
|
+
: null;
|
|
176
|
+
}
|
|
177
|
+
const candidates = fs.readdirSync(lanePaths.dashboardsDir, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isFile())
|
|
179
|
+
.map((entry) => ({
|
|
180
|
+
filePath: path.join(lanePaths.dashboardsDir, entry.name),
|
|
181
|
+
match: entry.name.match(/^wave-(\d+)\.json$/),
|
|
182
|
+
}))
|
|
183
|
+
.filter((entry) => entry.match)
|
|
184
|
+
.map((entry) => ({
|
|
185
|
+
dashboardFile: entry.filePath,
|
|
186
|
+
waveNumber: Number.parseInt(entry.match[1], 10),
|
|
187
|
+
mtimeMs: fs.statSync(entry.filePath).mtimeMs,
|
|
188
|
+
}))
|
|
189
|
+
.sort((left, right) => {
|
|
190
|
+
if (left.mtimeMs !== right.mtimeMs) {
|
|
191
|
+
return right.mtimeMs - left.mtimeMs;
|
|
192
|
+
}
|
|
193
|
+
return right.waveNumber - left.waveNumber;
|
|
194
|
+
});
|
|
195
|
+
if (candidates.length > 0) {
|
|
196
|
+
return { dashboardFile: candidates[0].dashboardFile };
|
|
197
|
+
}
|
|
198
|
+
return fs.existsSync(lanePaths.globalDashboardPath)
|
|
199
|
+
? { dashboardFile: lanePaths.globalDashboardPath }
|
|
200
|
+
: null;
|
|
133
201
|
}
|
|
134
202
|
|
|
135
203
|
function readMessageBoardTail(messageBoardPath, maxLines = 24) {
|
|
@@ -460,7 +528,7 @@ Options:
|
|
|
460
528
|
--dashboard-file <path> Path to wave/global dashboard JSON
|
|
461
529
|
--message-board <path> Optional message board path override
|
|
462
530
|
--attach <current|global>
|
|
463
|
-
Attach to the stable
|
|
531
|
+
Attach to the stable dashboard session for the lane, or follow the last written dashboard file when no live session exists
|
|
464
532
|
--watch Refresh continuously
|
|
465
533
|
--refresh-ms <n> Refresh interval in ms (default: ${DEFAULT_REFRESH_MS})
|
|
466
534
|
`);
|
|
@@ -468,8 +536,12 @@ Options:
|
|
|
468
536
|
}
|
|
469
537
|
|
|
470
538
|
if (options.attach) {
|
|
471
|
-
attachDashboardSession(options.project, options.lane, options.attach);
|
|
472
|
-
|
|
539
|
+
const fallback = await attachDashboardSession(options.project, options.lane, options.attach);
|
|
540
|
+
if (!fallback) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
options.dashboardFile = fallback.dashboardFile;
|
|
544
|
+
options.watch = true;
|
|
473
545
|
}
|
|
474
546
|
|
|
475
547
|
let terminalStateReachedAt = null;
|
|
@@ -26,7 +26,7 @@ import { deriveWaveLedger, readWaveLedger } from "./ledger.mjs";
|
|
|
26
26
|
import { buildDocsQueue, readDocsQueue } from "./docs-queue.mjs";
|
|
27
27
|
import { parseStructuredSignalsFromLog } from "./dashboard-state.mjs";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
isSecurityReviewAgentForLane,
|
|
30
30
|
resolveSecurityReviewReportPath,
|
|
31
31
|
isContEvalImplementationOwningAgent,
|
|
32
32
|
resolveWaveRoleBindings,
|
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
describeContext7Libraries,
|
|
48
48
|
loadContext7BundleIndex,
|
|
49
49
|
} from "./context7.mjs";
|
|
50
|
+
import {
|
|
51
|
+
readWaveCorridorContext,
|
|
52
|
+
waveCorridorContextPath,
|
|
53
|
+
} from "./corridor.mjs";
|
|
50
54
|
|
|
51
55
|
export function waveCoordinationLogPath(lanePaths, waveNumber) {
|
|
52
56
|
return path.join(lanePaths.coordinationDir, `wave-${waveNumber}.jsonl`);
|
|
@@ -212,9 +216,12 @@ export function buildWaveSecuritySummary({
|
|
|
212
216
|
wave,
|
|
213
217
|
attempt,
|
|
214
218
|
summariesByAgentId = {},
|
|
219
|
+
corridorSummary = null,
|
|
215
220
|
}) {
|
|
216
221
|
const createdAt = toIsoTimestamp();
|
|
217
|
-
const securityAgents = (wave.agents || []).filter((agent) =>
|
|
222
|
+
const securityAgents = (wave.agents || []).filter((agent) =>
|
|
223
|
+
isSecurityReviewAgentForLane(agent, lanePaths),
|
|
224
|
+
);
|
|
218
225
|
if (securityAgents.length === 0) {
|
|
219
226
|
return {
|
|
220
227
|
wave: wave.wave,
|
|
@@ -273,7 +280,9 @@ export function buildWaveSecuritySummary({
|
|
|
273
280
|
const totalFindings = agents.reduce((sum, entry) => sum + (entry.findings || 0), 0);
|
|
274
281
|
const totalApprovals = agents.reduce((sum, entry) => sum + (entry.approvals || 0), 0);
|
|
275
282
|
const detail =
|
|
276
|
-
|
|
283
|
+
corridorSummary?.blocking
|
|
284
|
+
? `Corridor matched blocking findings for implementation-owned paths.`
|
|
285
|
+
: overallState === "blocked"
|
|
277
286
|
? `Security review blocked by ${blockedAgentIds.join(", ")}.`
|
|
278
287
|
: overallState === "pending"
|
|
279
288
|
? `Security review output is incomplete for ${pendingAgentIds.join(", ")}.`
|
|
@@ -290,6 +299,17 @@ export function buildWaveSecuritySummary({
|
|
|
290
299
|
concernAgentIds,
|
|
291
300
|
blockedAgentIds,
|
|
292
301
|
detail,
|
|
302
|
+
corridor: corridorSummary
|
|
303
|
+
? {
|
|
304
|
+
ok: corridorSummary.ok === true,
|
|
305
|
+
providerMode: corridorSummary.providerMode || null,
|
|
306
|
+
source: corridorSummary.source || null,
|
|
307
|
+
blocking: corridorSummary.blocking === true,
|
|
308
|
+
blockingFindings: corridorSummary.blockingFindings || [],
|
|
309
|
+
matchedFindings: corridorSummary.matchedFindings || [],
|
|
310
|
+
error: corridorSummary.error || null,
|
|
311
|
+
}
|
|
312
|
+
: null,
|
|
293
313
|
agents,
|
|
294
314
|
createdAt,
|
|
295
315
|
updatedAt: createdAt,
|
|
@@ -377,7 +397,7 @@ function buildIntegrationEvidence({
|
|
|
377
397
|
isContEvalImplementationOwningAgent(agent, {
|
|
378
398
|
contEvalAgentId: roleBindings.contEvalAgentId,
|
|
379
399
|
});
|
|
380
|
-
if (
|
|
400
|
+
if (isSecurityReviewAgentForLane(agent, lanePaths)) {
|
|
381
401
|
continue;
|
|
382
402
|
}
|
|
383
403
|
if (agent.agentId === roleBindings.contEvalAgentId) {
|
|
@@ -490,6 +510,15 @@ function buildIntegrationEvidence({
|
|
|
490
510
|
);
|
|
491
511
|
}
|
|
492
512
|
}
|
|
513
|
+
for (const finding of securitySummary?.corridor?.matchedFindings || []) {
|
|
514
|
+
securityFindingEntries.push(
|
|
515
|
+
summarizeGap(
|
|
516
|
+
"corridor",
|
|
517
|
+
`${finding.severity || "unknown"} ${finding.affectedFile || "unknown-file"}: ${finding.title || "finding"}`,
|
|
518
|
+
"Corridor matched a relevant finding.",
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
493
522
|
|
|
494
523
|
return {
|
|
495
524
|
openClaims: uniqueStringEntries(openClaims),
|
|
@@ -506,6 +535,13 @@ function buildIntegrationEvidence({
|
|
|
506
535
|
securityState: securitySummary?.overallState || "not-applicable",
|
|
507
536
|
securityFindings: uniqueStringEntries(securityFindingEntries),
|
|
508
537
|
securityApprovals: uniqueStringEntries(securityApprovalEntries),
|
|
538
|
+
corridorState: securitySummary?.corridor?.blocking
|
|
539
|
+
? "blocked"
|
|
540
|
+
: securitySummary?.corridor?.ok === false
|
|
541
|
+
? "error"
|
|
542
|
+
: securitySummary?.corridor
|
|
543
|
+
? "clear"
|
|
544
|
+
: "not-configured",
|
|
509
545
|
};
|
|
510
546
|
}
|
|
511
547
|
|
|
@@ -681,6 +717,7 @@ export function buildWaveDerivedState({
|
|
|
681
717
|
wave,
|
|
682
718
|
attempt,
|
|
683
719
|
summariesByAgentId,
|
|
720
|
+
corridorSummary: readWaveCorridorContext(lanePaths, wave.wave),
|
|
684
721
|
});
|
|
685
722
|
const integrationSummary = buildWaveIntegrationSummary({
|
|
686
723
|
lanePaths,
|
|
@@ -710,6 +747,7 @@ export function buildWaveDerivedState({
|
|
|
710
747
|
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
711
748
|
capabilityAssignments,
|
|
712
749
|
dependencySnapshot,
|
|
750
|
+
securityRolePromptPath: lanePaths.securityRolePromptPath,
|
|
713
751
|
});
|
|
714
752
|
const inboxDir = waveInboxDir(lanePaths, wave.wave);
|
|
715
753
|
const sharedSummary = compileSharedSummary({
|
|
@@ -759,6 +797,8 @@ export function buildWaveDerivedState({
|
|
|
759
797
|
dependencySnapshotMarkdownPath: waveDependencySnapshotMarkdownPath(lanePaths, wave.wave),
|
|
760
798
|
securitySummary,
|
|
761
799
|
securitySummaryPath: waveSecurityPath(lanePaths, wave.wave),
|
|
800
|
+
corridorSummary: readWaveCorridorContext(lanePaths, wave.wave),
|
|
801
|
+
corridorSummaryPath: waveCorridorContextPath(lanePaths, wave.wave),
|
|
762
802
|
integrationSummary,
|
|
763
803
|
integrationSummaryPath: waveIntegrationPath(lanePaths, wave.wave),
|
|
764
804
|
integrationMarkdownPath: waveIntegrationMarkdownPath(lanePaths, wave.wave),
|