@chllming/wave-orchestration 0.5.4 → 0.6.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 +52 -3
- package/README.md +33 -5
- package/docs/README.md +18 -4
- package/docs/agents/wave-cont-eval-role.md +36 -0
- package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
- package/docs/agents/wave-documentation-role.md +1 -1
- package/docs/agents/wave-infra-role.md +1 -1
- package/docs/agents/wave-integration-role.md +3 -3
- package/docs/agents/wave-launcher-role.md +4 -3
- package/docs/agents/wave-security-role.md +40 -0
- package/docs/concepts/context7-vs-skills.md +1 -1
- package/docs/concepts/what-is-a-wave.md +56 -6
- package/docs/evals/README.md +166 -0
- package/docs/evals/benchmark-catalog.json +663 -0
- package/docs/guides/author-and-run-waves.md +135 -0
- package/docs/guides/planner.md +5 -0
- package/docs/guides/terminal-surfaces.md +2 -0
- package/docs/plans/component-cutover-matrix.json +1 -1
- package/docs/plans/component-cutover-matrix.md +1 -1
- package/docs/plans/current-state.md +19 -1
- package/docs/plans/examples/wave-example-live-proof.md +435 -0
- package/docs/plans/migration.md +42 -0
- package/docs/plans/wave-orchestrator.md +46 -7
- package/docs/plans/waves/wave-0.md +4 -4
- package/docs/reference/live-proof-waves.md +177 -0
- package/docs/reference/migration-0.2-to-0.5.md +26 -19
- package/docs/reference/npmjs-trusted-publishing.md +6 -5
- package/docs/reference/runtime-config/README.md +14 -4
- package/docs/reference/sample-waves.md +87 -0
- package/docs/reference/skills.md +110 -42
- package/docs/research/agent-context-sources.md +130 -11
- package/docs/research/coordination-failure-review.md +266 -0
- package/docs/roadmap.md +6 -2
- package/package.json +2 -2
- package/releases/manifest.json +35 -2
- package/scripts/research/agent-context-archive.mjs +83 -1
- package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
- package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
- package/scripts/wave-orchestrator/agent-state.mjs +358 -6
- package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
- package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
- package/scripts/wave-orchestrator/config.mjs +48 -12
- package/scripts/wave-orchestrator/context7.mjs +2 -0
- package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
- package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
- package/scripts/wave-orchestrator/coordination.mjs +83 -9
- package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
- package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
- package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
- package/scripts/wave-orchestrator/evals.mjs +451 -0
- package/scripts/wave-orchestrator/feedback.mjs +15 -1
- package/scripts/wave-orchestrator/install.mjs +32 -9
- package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
- package/scripts/wave-orchestrator/launcher.mjs +709 -601
- package/scripts/wave-orchestrator/ledger.mjs +123 -20
- package/scripts/wave-orchestrator/local-executor.mjs +99 -12
- package/scripts/wave-orchestrator/planner.mjs +177 -42
- package/scripts/wave-orchestrator/replay.mjs +6 -3
- package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
- package/scripts/wave-orchestrator/shared.mjs +75 -11
- package/scripts/wave-orchestrator/skills.mjs +637 -106
- package/scripts/wave-orchestrator/traces.mjs +71 -48
- package/scripts/wave-orchestrator/wave-files.mjs +947 -101
- package/scripts/wave.mjs +9 -0
- package/skills/README.md +202 -0
- package/skills/provider-aws/SKILL.md +111 -0
- package/skills/provider-aws/adapters/claude.md +1 -0
- package/skills/provider-aws/adapters/codex.md +1 -0
- package/skills/provider-aws/references/service-verification.md +39 -0
- package/skills/provider-aws/skill.json +50 -1
- package/skills/provider-custom-deploy/SKILL.md +59 -0
- package/skills/provider-custom-deploy/skill.json +46 -1
- package/skills/provider-docker-compose/SKILL.md +90 -0
- package/skills/provider-docker-compose/adapters/local.md +1 -0
- package/skills/provider-docker-compose/skill.json +49 -1
- package/skills/provider-github-release/SKILL.md +116 -1
- package/skills/provider-github-release/adapters/claude.md +1 -0
- package/skills/provider-github-release/adapters/codex.md +1 -0
- package/skills/provider-github-release/skill.json +51 -1
- package/skills/provider-kubernetes/SKILL.md +137 -0
- package/skills/provider-kubernetes/adapters/claude.md +1 -0
- package/skills/provider-kubernetes/adapters/codex.md +1 -0
- package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
- package/skills/provider-kubernetes/skill.json +48 -1
- package/skills/provider-railway/SKILL.md +118 -1
- package/skills/provider-railway/references/verification-commands.md +39 -0
- package/skills/provider-railway/skill.json +67 -1
- package/skills/provider-ssh-manual/SKILL.md +91 -0
- package/skills/provider-ssh-manual/skill.json +50 -1
- package/skills/repo-coding-rules/SKILL.md +84 -0
- package/skills/repo-coding-rules/skill.json +30 -1
- package/skills/role-cont-eval/SKILL.md +90 -0
- package/skills/role-cont-eval/adapters/codex.md +1 -0
- package/skills/role-cont-eval/skill.json +36 -0
- package/skills/role-cont-qa/SKILL.md +93 -0
- package/skills/role-cont-qa/adapters/claude.md +1 -0
- package/skills/role-cont-qa/skill.json +36 -0
- package/skills/role-deploy/SKILL.md +90 -0
- package/skills/role-deploy/skill.json +32 -1
- package/skills/role-documentation/SKILL.md +66 -0
- package/skills/role-documentation/skill.json +32 -1
- package/skills/role-implementation/SKILL.md +62 -0
- package/skills/role-implementation/skill.json +32 -1
- package/skills/role-infra/SKILL.md +74 -0
- package/skills/role-infra/skill.json +32 -1
- package/skills/role-integration/SKILL.md +79 -1
- package/skills/role-integration/skill.json +32 -1
- package/skills/role-research/SKILL.md +58 -0
- package/skills/role-research/skill.json +32 -1
- package/skills/role-security/SKILL.md +60 -0
- package/skills/role-security/skill.json +36 -0
- package/skills/runtime-claude/SKILL.md +60 -1
- package/skills/runtime-claude/skill.json +32 -1
- package/skills/runtime-codex/SKILL.md +52 -1
- package/skills/runtime-codex/skill.json +32 -1
- package/skills/runtime-local/SKILL.md +39 -0
- package/skills/runtime-local/skill.json +32 -1
- package/skills/runtime-opencode/SKILL.md +51 -0
- package/skills/runtime-opencode/skill.json +32 -1
- package/skills/wave-core/SKILL.md +107 -0
- package/skills/wave-core/references/marker-syntax.md +62 -0
- package/skills/wave-core/skill.json +31 -1
- package/wave.config.json +35 -6
- package/skills/role-evaluator/SKILL.md +0 -6
- package/skills/role-evaluator/skill.json +0 -5
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
parseStructuredSignalsFromLog,
|
|
4
|
+
refreshWaveDashboardAgentStates,
|
|
5
|
+
setWaveDashboardAgent,
|
|
6
|
+
updateWaveDashboardMessageBoard,
|
|
7
|
+
} from "./dashboard-state.mjs";
|
|
8
|
+
import { REPO_ROOT, toIsoTimestamp } from "./shared.mjs";
|
|
9
|
+
import { isSecurityReviewAgent } from "./role-helpers.mjs";
|
|
10
|
+
import { summarizeResolvedSkills } from "./skills.mjs";
|
|
11
|
+
|
|
12
|
+
function failureResultFromGate(gate, fallbackLogPath) {
|
|
13
|
+
return {
|
|
14
|
+
failures: [
|
|
15
|
+
{
|
|
16
|
+
agentId: gate.agentId,
|
|
17
|
+
statusCode: gate.statusCode,
|
|
18
|
+
logPath: gate.logPath || fallbackLogPath,
|
|
19
|
+
detail: gate.detail,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
timedOut: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function recordClosureGateFailure({
|
|
27
|
+
wave,
|
|
28
|
+
lanePaths,
|
|
29
|
+
gate,
|
|
30
|
+
label,
|
|
31
|
+
recordCombinedEvent,
|
|
32
|
+
appendCoordination,
|
|
33
|
+
actionRequested,
|
|
34
|
+
}) {
|
|
35
|
+
recordCombinedEvent({
|
|
36
|
+
level: "error",
|
|
37
|
+
agentId: gate.agentId,
|
|
38
|
+
message: `${label} blocked wave ${wave.wave}: ${gate.detail}`,
|
|
39
|
+
});
|
|
40
|
+
appendCoordination({
|
|
41
|
+
event: "wave_gate_blocked",
|
|
42
|
+
waves: [wave.wave],
|
|
43
|
+
status: "blocked",
|
|
44
|
+
details: `agent=${gate.agentId}; reason=${gate.statusCode}; ${gate.detail}`,
|
|
45
|
+
actionRequested:
|
|
46
|
+
actionRequested ||
|
|
47
|
+
`Lane ${lanePaths.lane} owners should resolve the ${label.toLowerCase()} before wave progression.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runClosureSweepPhase({
|
|
52
|
+
lanePaths,
|
|
53
|
+
wave,
|
|
54
|
+
closureRuns,
|
|
55
|
+
coordinationLogPath,
|
|
56
|
+
refreshDerivedState,
|
|
57
|
+
dashboardState,
|
|
58
|
+
recordCombinedEvent,
|
|
59
|
+
flushDashboards,
|
|
60
|
+
options,
|
|
61
|
+
feedbackStateByRequestId,
|
|
62
|
+
appendCoordination,
|
|
63
|
+
launchAgentSessionFn,
|
|
64
|
+
waitForWaveCompletionFn,
|
|
65
|
+
readWaveContEvalGateFn,
|
|
66
|
+
readWaveSecurityGateFn,
|
|
67
|
+
readWaveIntegrationBarrierFn,
|
|
68
|
+
readWaveDocumentationGateFn,
|
|
69
|
+
readWaveComponentMatrixGateFn,
|
|
70
|
+
readWaveContQaGateFn,
|
|
71
|
+
materializeAgentExecutionSummaryForRunFn,
|
|
72
|
+
monitorWaveHumanFeedbackFn,
|
|
73
|
+
}) {
|
|
74
|
+
const contQaAgentId = wave.contQaAgentId || "A0";
|
|
75
|
+
const contEvalAgentId = wave.contEvalAgentId || lanePaths.contEvalAgentId || "E0";
|
|
76
|
+
const integrationAgentId = wave.integrationAgentId || lanePaths.integrationAgentId || "A8";
|
|
77
|
+
const documentationAgentId = wave.documentationAgentId || "A9";
|
|
78
|
+
const stagedRuns = [
|
|
79
|
+
{
|
|
80
|
+
agentId: contEvalAgentId,
|
|
81
|
+
label: "cont-EVAL gate",
|
|
82
|
+
runs: closureRuns.filter((run) => run.agent.agentId === contEvalAgentId),
|
|
83
|
+
validate: () =>
|
|
84
|
+
readWaveContEvalGateFn(wave, closureRuns, {
|
|
85
|
+
contEvalAgentId,
|
|
86
|
+
mode: "live",
|
|
87
|
+
evalTargets: wave.evalTargets,
|
|
88
|
+
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
89
|
+
}),
|
|
90
|
+
actionRequested:
|
|
91
|
+
`Lane ${lanePaths.lane} owners should resolve cont-EVAL tuning gaps before integration closure.`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
agentId: "security",
|
|
95
|
+
label: "Security review",
|
|
96
|
+
runs: closureRuns.filter((run) => isSecurityReviewAgent(run.agent)),
|
|
97
|
+
validate: () => readWaveSecurityGateFn(wave, closureRuns),
|
|
98
|
+
actionRequested:
|
|
99
|
+
`Lane ${lanePaths.lane} owners should resolve blocked security findings or missing approvals before integration closure.`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
agentId: integrationAgentId,
|
|
103
|
+
label: "Integration gate",
|
|
104
|
+
runs: closureRuns.filter((run) => run.agent.agentId === integrationAgentId),
|
|
105
|
+
validate: () =>
|
|
106
|
+
readWaveIntegrationBarrierFn(
|
|
107
|
+
wave,
|
|
108
|
+
closureRuns,
|
|
109
|
+
refreshDerivedState?.(dashboardState?.attempt || 0),
|
|
110
|
+
{
|
|
111
|
+
integrationAgentId,
|
|
112
|
+
requireIntegrationStewardFromWave: lanePaths.requireIntegrationStewardFromWave,
|
|
113
|
+
},
|
|
114
|
+
),
|
|
115
|
+
actionRequested:
|
|
116
|
+
`Lane ${lanePaths.lane} owners should resolve integration contradictions or blockers before documentation and cont-QA closure.`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
agentId: documentationAgentId,
|
|
120
|
+
label: "Documentation closure",
|
|
121
|
+
runs: closureRuns.filter((run) => run.agent.agentId === documentationAgentId),
|
|
122
|
+
validate: () => {
|
|
123
|
+
const documentationGate = readWaveDocumentationGateFn(wave, closureRuns);
|
|
124
|
+
if (!documentationGate.ok) {
|
|
125
|
+
return documentationGate;
|
|
126
|
+
}
|
|
127
|
+
return readWaveComponentMatrixGateFn(wave, closureRuns, {
|
|
128
|
+
laneProfile: lanePaths.laneProfile,
|
|
129
|
+
documentationAgentId,
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
actionRequested:
|
|
133
|
+
`Lane ${lanePaths.lane} owners should resolve the shared-plan or component-matrix closure state before cont-QA progression.`,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
agentId: contQaAgentId,
|
|
137
|
+
label: "cont-QA gate",
|
|
138
|
+
runs: closureRuns.filter((run) => run.agent.agentId === contQaAgentId),
|
|
139
|
+
validate: () => readWaveContQaGateFn(wave, closureRuns, { contQaAgentId, mode: "live" }),
|
|
140
|
+
actionRequested:
|
|
141
|
+
`Lane ${lanePaths.lane} owners should resolve the cont-QA gate before wave progression.`,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
for (const stage of stagedRuns) {
|
|
145
|
+
if (stage.runs.length === 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for (const runInfo of stage.runs) {
|
|
149
|
+
const existing = dashboardState.agents.find((entry) => entry.agentId === runInfo.agent.agentId);
|
|
150
|
+
setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
|
|
151
|
+
state: "launching",
|
|
152
|
+
attempts: (existing?.attempts || 0) + 1,
|
|
153
|
+
startedAt: existing?.startedAt || toIsoTimestamp(),
|
|
154
|
+
completedAt: null,
|
|
155
|
+
exitCode: null,
|
|
156
|
+
detail: "Launching closure sweep",
|
|
157
|
+
});
|
|
158
|
+
flushDashboards();
|
|
159
|
+
const launchResult = await launchAgentSessionFn(lanePaths, {
|
|
160
|
+
wave: wave.wave,
|
|
161
|
+
waveDefinition: wave,
|
|
162
|
+
agent: runInfo.agent,
|
|
163
|
+
sessionName: runInfo.sessionName,
|
|
164
|
+
promptPath: runInfo.promptPath,
|
|
165
|
+
logPath: runInfo.logPath,
|
|
166
|
+
statusPath: runInfo.statusPath,
|
|
167
|
+
messageBoardPath: runInfo.messageBoardPath,
|
|
168
|
+
messageBoardSnapshot: runInfo.messageBoardSnapshot || "",
|
|
169
|
+
sharedSummaryPath: runInfo.sharedSummaryPath,
|
|
170
|
+
sharedSummaryText: runInfo.sharedSummaryText,
|
|
171
|
+
inboxPath: runInfo.inboxPath,
|
|
172
|
+
inboxText: runInfo.inboxText,
|
|
173
|
+
orchestratorId: options.orchestratorId,
|
|
174
|
+
executorMode: options.executorMode,
|
|
175
|
+
codexSandboxMode: options.codexSandboxMode,
|
|
176
|
+
agentRateLimitRetries: options.agentRateLimitRetries,
|
|
177
|
+
agentRateLimitBaseDelaySeconds: options.agentRateLimitBaseDelaySeconds,
|
|
178
|
+
agentRateLimitMaxDelaySeconds: options.agentRateLimitMaxDelaySeconds,
|
|
179
|
+
context7Enabled: options.context7Enabled,
|
|
180
|
+
});
|
|
181
|
+
runInfo.lastLaunchAttempt = dashboardState?.attempt || null;
|
|
182
|
+
runInfo.lastPromptHash = launchResult?.promptHash || null;
|
|
183
|
+
runInfo.lastContext7 = launchResult?.context7 || null;
|
|
184
|
+
runInfo.lastExecutorId = launchResult?.executorId || runInfo.agent.executorResolved?.id || null;
|
|
185
|
+
runInfo.lastSkillProjection =
|
|
186
|
+
launchResult?.skills || summarizeResolvedSkills(runInfo.agent.skillsResolved);
|
|
187
|
+
setWaveDashboardAgent(dashboardState, runInfo.agent.agentId, {
|
|
188
|
+
state: "running",
|
|
189
|
+
detail: `Closure sweep launched${launchResult?.context7?.mode ? ` (${launchResult.context7.mode})` : ""}`,
|
|
190
|
+
});
|
|
191
|
+
recordCombinedEvent({
|
|
192
|
+
agentId: runInfo.agent.agentId,
|
|
193
|
+
message: `Closure sweep launched in tmux session ${runInfo.sessionName}`,
|
|
194
|
+
});
|
|
195
|
+
flushDashboards();
|
|
196
|
+
const result = await waitForWaveCompletionFn(
|
|
197
|
+
lanePaths,
|
|
198
|
+
[runInfo],
|
|
199
|
+
options.timeoutMinutes,
|
|
200
|
+
({ pendingAgentIds }) => {
|
|
201
|
+
refreshWaveDashboardAgentStates(dashboardState, [runInfo], pendingAgentIds, (event) =>
|
|
202
|
+
recordCombinedEvent(event),
|
|
203
|
+
);
|
|
204
|
+
monitorWaveHumanFeedbackFn({
|
|
205
|
+
lanePaths,
|
|
206
|
+
waveNumber: wave.wave,
|
|
207
|
+
agentRuns: [runInfo],
|
|
208
|
+
orchestratorId: options.orchestratorId,
|
|
209
|
+
coordinationLogPath,
|
|
210
|
+
feedbackStateByRequestId,
|
|
211
|
+
recordCombinedEvent,
|
|
212
|
+
appendCoordination,
|
|
213
|
+
});
|
|
214
|
+
updateWaveDashboardMessageBoard(dashboardState, runInfo.messageBoardPath);
|
|
215
|
+
flushDashboards();
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
materializeAgentExecutionSummaryForRunFn(wave, runInfo);
|
|
219
|
+
refreshDerivedState?.(dashboardState?.attempt || 0);
|
|
220
|
+
if (result.failures.length > 0) {
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const gate = stage.validate();
|
|
225
|
+
if (!gate.ok) {
|
|
226
|
+
recordClosureGateFailure({
|
|
227
|
+
wave,
|
|
228
|
+
lanePaths,
|
|
229
|
+
gate,
|
|
230
|
+
label: stage.label,
|
|
231
|
+
recordCombinedEvent,
|
|
232
|
+
appendCoordination,
|
|
233
|
+
actionRequested: stage.actionRequested,
|
|
234
|
+
});
|
|
235
|
+
return failureResultFromGate(
|
|
236
|
+
gate,
|
|
237
|
+
stage.runs[0]?.logPath ? path.relative(REPO_ROOT, stage.runs[0].logPath) : null,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { failures: [], timedOut: false };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const NON_BLOCKING_INFRA_SIGNAL_STATES = new Set([
|
|
245
|
+
"conformant",
|
|
246
|
+
"setup-required",
|
|
247
|
+
"setup-in-progress",
|
|
248
|
+
"action-required",
|
|
249
|
+
"action-approved",
|
|
250
|
+
"action-complete",
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
export function readWaveInfraGate(agentRuns) {
|
|
254
|
+
for (const run of agentRuns) {
|
|
255
|
+
const signals = parseStructuredSignalsFromLog(run.logPath);
|
|
256
|
+
if (!signals?.infra) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const infra = signals.infra;
|
|
260
|
+
const normalizedState = String(infra.state || "")
|
|
261
|
+
.trim()
|
|
262
|
+
.toLowerCase();
|
|
263
|
+
if (NON_BLOCKING_INFRA_SIGNAL_STATES.has(normalizedState)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
agentId: run.agent.agentId,
|
|
269
|
+
statusCode: `infra-${normalizedState || "blocked"}`,
|
|
270
|
+
detail: `Infra signal ${infra.kind || "unknown"} on ${infra.target || "unknown"} ended in state ${normalizedState || "unknown"}${infra.detail ? ` (${infra.detail})` : ""}.`,
|
|
271
|
+
logPath: path.relative(REPO_ROOT, run.logPath),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
agentId: null,
|
|
277
|
+
statusCode: "pass",
|
|
278
|
+
detail: "No blocking infra signals detected.",
|
|
279
|
+
logPath: null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildExecutionPrompt } from "./coordination.mjs";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AGENT_RATE_LIMIT_BASE_DELAY_SECONDS,
|
|
6
|
+
DEFAULT_AGENT_RATE_LIMIT_MAX_DELAY_SECONDS,
|
|
7
|
+
DEFAULT_WAIT_PROGRESS_INTERVAL_MS,
|
|
8
|
+
REPO_ROOT,
|
|
9
|
+
ensureDirectory,
|
|
10
|
+
shellQuote,
|
|
11
|
+
writeJsonAtomic,
|
|
12
|
+
} from "./shared.mjs";
|
|
13
|
+
import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
|
|
14
|
+
import { buildExecutorLaunchSpec } from "./executors.mjs";
|
|
15
|
+
import { hashAgentPromptFingerprint, prefetchContext7ForSelection } from "./context7.mjs";
|
|
16
|
+
import { killTmuxSessionIfExists } from "./terminals.mjs";
|
|
17
|
+
import {
|
|
18
|
+
resolveAgentSkills,
|
|
19
|
+
summarizeResolvedSkills,
|
|
20
|
+
writeResolvedSkillArtifacts,
|
|
21
|
+
} from "./skills.mjs";
|
|
22
|
+
|
|
23
|
+
export function refreshResolvedSkillsForRun(runInfo, waveDefinition, lanePaths) {
|
|
24
|
+
runInfo.agent.skillsResolved = resolveAgentSkills(
|
|
25
|
+
runInfo.agent,
|
|
26
|
+
waveDefinition || { deployEnvironments: [] },
|
|
27
|
+
{ laneProfile: lanePaths.laneProfile },
|
|
28
|
+
);
|
|
29
|
+
return runInfo.agent.skillsResolved;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function collectUnexpectedSessionFailures(
|
|
33
|
+
lanePaths,
|
|
34
|
+
agentRuns,
|
|
35
|
+
pendingAgentIds,
|
|
36
|
+
{ listLaneTmuxSessionNamesFn },
|
|
37
|
+
) {
|
|
38
|
+
const activeSessionNames = new Set(listLaneTmuxSessionNamesFn(lanePaths));
|
|
39
|
+
const failures = [];
|
|
40
|
+
for (const run of agentRuns) {
|
|
41
|
+
if (!pendingAgentIds.has(run.agent.agentId) || fs.existsSync(run.statusPath)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (activeSessionNames.has(run.sessionName)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
failures.push({
|
|
48
|
+
agentId: run.agent.agentId,
|
|
49
|
+
statusCode: "session-missing",
|
|
50
|
+
logPath: path.relative(REPO_ROOT, run.logPath),
|
|
51
|
+
detail: `tmux session ${run.sessionName} disappeared before ${path.relative(REPO_ROOT, run.statusPath)} was written.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return failures;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
58
|
+
const {
|
|
59
|
+
wave,
|
|
60
|
+
waveDefinition = null,
|
|
61
|
+
agent,
|
|
62
|
+
sessionName,
|
|
63
|
+
promptPath,
|
|
64
|
+
logPath,
|
|
65
|
+
statusPath,
|
|
66
|
+
messageBoardPath,
|
|
67
|
+
messageBoardSnapshot,
|
|
68
|
+
sharedSummaryPath,
|
|
69
|
+
sharedSummaryText,
|
|
70
|
+
inboxPath,
|
|
71
|
+
inboxText,
|
|
72
|
+
orchestratorId,
|
|
73
|
+
agentRateLimitRetries,
|
|
74
|
+
agentRateLimitBaseDelaySeconds,
|
|
75
|
+
agentRateLimitMaxDelaySeconds,
|
|
76
|
+
context7Enabled,
|
|
77
|
+
dryRun = false,
|
|
78
|
+
} = params;
|
|
79
|
+
ensureDirectory(path.dirname(promptPath));
|
|
80
|
+
ensureDirectory(path.dirname(logPath));
|
|
81
|
+
ensureDirectory(path.dirname(statusPath));
|
|
82
|
+
fs.rmSync(statusPath, { force: true });
|
|
83
|
+
|
|
84
|
+
const context7 = await prefetchContext7ForSelection(agent.context7Resolved, {
|
|
85
|
+
cacheDir: lanePaths.context7CacheDir,
|
|
86
|
+
disabled: !context7Enabled,
|
|
87
|
+
});
|
|
88
|
+
const overlayDir = path.join(lanePaths.executorOverlaysDir, `wave-${wave}`, agent.slug);
|
|
89
|
+
ensureDirectory(overlayDir);
|
|
90
|
+
const resolvedWaveDefinition = waveDefinition || { deployEnvironments: [] };
|
|
91
|
+
const skillsResolved =
|
|
92
|
+
agent.skillsResolved ||
|
|
93
|
+
resolveAgentSkills(agent, resolvedWaveDefinition, {
|
|
94
|
+
laneProfile: lanePaths.laneProfile,
|
|
95
|
+
});
|
|
96
|
+
agent.skillsResolved = skillsResolved;
|
|
97
|
+
const skillArtifacts = writeResolvedSkillArtifacts(overlayDir, skillsResolved);
|
|
98
|
+
if (skillArtifacts) {
|
|
99
|
+
agent.skillsResolved = {
|
|
100
|
+
...skillsResolved,
|
|
101
|
+
artifacts: skillArtifacts,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const prompt = buildExecutionPrompt({
|
|
105
|
+
lane: lanePaths.lane,
|
|
106
|
+
wave,
|
|
107
|
+
agent,
|
|
108
|
+
orchestratorId,
|
|
109
|
+
messageBoardPath,
|
|
110
|
+
messageBoardSnapshot,
|
|
111
|
+
sharedSummaryPath,
|
|
112
|
+
sharedSummaryText,
|
|
113
|
+
inboxPath,
|
|
114
|
+
inboxText,
|
|
115
|
+
context7,
|
|
116
|
+
componentPromotions: resolvedWaveDefinition.componentPromotions,
|
|
117
|
+
evalTargets: resolvedWaveDefinition.evalTargets,
|
|
118
|
+
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
119
|
+
sharedPlanDocs: lanePaths.sharedPlanDocs,
|
|
120
|
+
contQaAgentId: lanePaths.contQaAgentId,
|
|
121
|
+
contEvalAgentId: lanePaths.contEvalAgentId,
|
|
122
|
+
integrationAgentId: lanePaths.integrationAgentId,
|
|
123
|
+
documentationAgentId: lanePaths.documentationAgentId,
|
|
124
|
+
});
|
|
125
|
+
const promptHash = hashAgentPromptFingerprint(agent);
|
|
126
|
+
fs.writeFileSync(promptPath, `${prompt}\n`, "utf8");
|
|
127
|
+
const launchSpec = buildExecutorLaunchSpec({
|
|
128
|
+
agent,
|
|
129
|
+
promptPath,
|
|
130
|
+
logPath,
|
|
131
|
+
overlayDir,
|
|
132
|
+
skillProjection: agent.skillsResolved,
|
|
133
|
+
});
|
|
134
|
+
const resolvedExecutorMode = launchSpec.executorId || agent.executorResolved?.id || "codex";
|
|
135
|
+
if (dryRun) {
|
|
136
|
+
writeJsonAtomic(path.join(overlayDir, "launch-preview.json"), {
|
|
137
|
+
executorId: resolvedExecutorMode,
|
|
138
|
+
command: launchSpec.command,
|
|
139
|
+
env: launchSpec.env || {},
|
|
140
|
+
useRateLimitRetries: launchSpec.useRateLimitRetries === true,
|
|
141
|
+
invocationLines: launchSpec.invocationLines,
|
|
142
|
+
skills: summarizeResolvedSkills(agent.skillsResolved),
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
promptHash,
|
|
146
|
+
context7,
|
|
147
|
+
executorId: resolvedExecutorMode,
|
|
148
|
+
launchSpec,
|
|
149
|
+
dryRun: true,
|
|
150
|
+
skills: summarizeResolvedSkills(agent.skillsResolved),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
killTmuxSessionIfExists(lanePaths.tmuxSocketName, sessionName);
|
|
154
|
+
|
|
155
|
+
const executionLines = [];
|
|
156
|
+
if (launchSpec.env) {
|
|
157
|
+
for (const [key, value] of Object.entries(launchSpec.env)) {
|
|
158
|
+
executionLines.push(`export ${key}=${shellQuote(value)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!launchSpec.useRateLimitRetries) {
|
|
162
|
+
executionLines.push(...launchSpec.invocationLines);
|
|
163
|
+
executionLines.push("status=$?");
|
|
164
|
+
} else {
|
|
165
|
+
executionLines.push(`: > ${shellQuote(logPath)}`);
|
|
166
|
+
executionLines.push(
|
|
167
|
+
`max_rate_attempts=${Math.max(1, Number.parseInt(String(agentRateLimitRetries || 0), 10) + 1)}`,
|
|
168
|
+
);
|
|
169
|
+
executionLines.push(
|
|
170
|
+
`rate_delay_base=${Math.max(1, Number.parseInt(String(agentRateLimitBaseDelaySeconds || DEFAULT_AGENT_RATE_LIMIT_BASE_DELAY_SECONDS), 10))}`,
|
|
171
|
+
);
|
|
172
|
+
executionLines.push(
|
|
173
|
+
`rate_delay_max=${Math.max(1, Number.parseInt(String(agentRateLimitMaxDelaySeconds || DEFAULT_AGENT_RATE_LIMIT_MAX_DELAY_SECONDS), 10))}`,
|
|
174
|
+
);
|
|
175
|
+
executionLines.push("rate_attempt=1");
|
|
176
|
+
executionLines.push("status=1");
|
|
177
|
+
executionLines.push('while [ "$rate_attempt" -le "$max_rate_attempts" ]; do');
|
|
178
|
+
for (const line of launchSpec.invocationLines) {
|
|
179
|
+
executionLines.push(` ${line}`);
|
|
180
|
+
}
|
|
181
|
+
executionLines.push(" status=$?");
|
|
182
|
+
executionLines.push(' if [ "$status" -eq 0 ]; then');
|
|
183
|
+
executionLines.push(" break");
|
|
184
|
+
executionLines.push(" fi");
|
|
185
|
+
executionLines.push(' if [ "$rate_attempt" -ge "$max_rate_attempts" ]; then');
|
|
186
|
+
executionLines.push(" break");
|
|
187
|
+
executionLines.push(" fi");
|
|
188
|
+
executionLines.push(
|
|
189
|
+
` if tail -n 120 ${shellQuote(logPath)} | grep -Eqi '429 Too Many Requests|exceeded retry limit|last status: 429|rate limit'; then`,
|
|
190
|
+
);
|
|
191
|
+
executionLines.push(" sleep_seconds=$((rate_delay_base * (2 ** (rate_attempt - 1))))");
|
|
192
|
+
executionLines.push(
|
|
193
|
+
' if [ "$sleep_seconds" -gt "$rate_delay_max" ]; then sleep_seconds=$rate_delay_max; fi',
|
|
194
|
+
);
|
|
195
|
+
executionLines.push(" jitter=$((RANDOM % 5))");
|
|
196
|
+
executionLines.push(" sleep_seconds=$((sleep_seconds + jitter))");
|
|
197
|
+
executionLines.push(
|
|
198
|
+
` echo "[${lanePaths.lane}-wave-launcher] rate-limit detected for ${agent.agentId}; retry \${rate_attempt}/\${max_rate_attempts} after \${sleep_seconds}s" | tee -a ${shellQuote(logPath)}`,
|
|
199
|
+
);
|
|
200
|
+
executionLines.push(' sleep "$sleep_seconds"');
|
|
201
|
+
executionLines.push(" rate_attempt=$((rate_attempt + 1))");
|
|
202
|
+
executionLines.push(" continue");
|
|
203
|
+
executionLines.push(" fi");
|
|
204
|
+
executionLines.push(" break");
|
|
205
|
+
executionLines.push("done");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const command = [
|
|
209
|
+
`cd ${shellQuote(REPO_ROOT)}`,
|
|
210
|
+
"set -o pipefail",
|
|
211
|
+
`export WAVE_ORCHESTRATOR_ID=${shellQuote(orchestratorId || "")}`,
|
|
212
|
+
`export WAVE_EXECUTOR_MODE=${shellQuote(resolvedExecutorMode)}`,
|
|
213
|
+
...executionLines,
|
|
214
|
+
`node -e ${shellQuote(
|
|
215
|
+
"const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
|
|
216
|
+
)} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")}`,
|
|
217
|
+
`echo "[${lanePaths.lane}-wave-launcher] ${sessionName} finished with code $status"`,
|
|
218
|
+
"exec bash -l",
|
|
219
|
+
].join("\n");
|
|
220
|
+
|
|
221
|
+
runTmuxFn(
|
|
222
|
+
lanePaths,
|
|
223
|
+
["new-session", "-d", "-s", sessionName, `bash -lc ${shellQuote(command)}`],
|
|
224
|
+
`launch session ${sessionName}`,
|
|
225
|
+
);
|
|
226
|
+
return {
|
|
227
|
+
promptHash,
|
|
228
|
+
context7,
|
|
229
|
+
executorId: resolvedExecutorMode,
|
|
230
|
+
skills: summarizeResolvedSkills(agent.skillsResolved),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function waitForWaveCompletion(
|
|
235
|
+
lanePaths,
|
|
236
|
+
agentRuns,
|
|
237
|
+
timeoutMinutes,
|
|
238
|
+
onProgress = null,
|
|
239
|
+
{ collectUnexpectedSessionFailuresFn },
|
|
240
|
+
) {
|
|
241
|
+
const defaultTimeoutMs = timeoutMinutes * 60 * 1000;
|
|
242
|
+
const startedAt = Date.now();
|
|
243
|
+
const timeoutAtByAgentId = new Map(
|
|
244
|
+
agentRuns.map((run) => {
|
|
245
|
+
const budgetMinutes = Number(run.agent.executorResolved?.budget?.minutes || 0);
|
|
246
|
+
const effectiveBudgetMs =
|
|
247
|
+
Number.isFinite(budgetMinutes) && budgetMinutes > 0
|
|
248
|
+
? Math.min(defaultTimeoutMs, budgetMinutes * 60 * 1000)
|
|
249
|
+
: defaultTimeoutMs;
|
|
250
|
+
return [run.agent.agentId, startedAt + effectiveBudgetMs];
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
const pending = new Set(agentRuns.map((run) => run.agent.agentId));
|
|
254
|
+
const timedOutAgentIds = new Set();
|
|
255
|
+
let sessionFailures = [];
|
|
256
|
+
|
|
257
|
+
const refreshPending = () => {
|
|
258
|
+
for (const run of agentRuns) {
|
|
259
|
+
if (pending.has(run.agent.agentId) && fs.existsSync(run.statusPath)) {
|
|
260
|
+
pending.delete(run.agent.agentId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
const interval = setInterval(() => {
|
|
267
|
+
refreshPending();
|
|
268
|
+
onProgress?.({ pendingAgentIds: new Set(pending), timedOut: false });
|
|
269
|
+
if (pending.size === 0) {
|
|
270
|
+
clearInterval(interval);
|
|
271
|
+
resolve();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
sessionFailures = collectUnexpectedSessionFailuresFn(lanePaths, agentRuns, pending);
|
|
275
|
+
if (sessionFailures.length > 0) {
|
|
276
|
+
onProgress?.({
|
|
277
|
+
pendingAgentIds: new Set(pending),
|
|
278
|
+
timedOut: false,
|
|
279
|
+
failures: sessionFailures,
|
|
280
|
+
});
|
|
281
|
+
clearInterval(interval);
|
|
282
|
+
resolve();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
for (const run of agentRuns) {
|
|
287
|
+
if (!pending.has(run.agent.agentId)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const deadline = timeoutAtByAgentId.get(run.agent.agentId) || startedAt + defaultTimeoutMs;
|
|
291
|
+
if (now <= deadline) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
timedOutAgentIds.add(run.agent.agentId);
|
|
295
|
+
pending.delete(run.agent.agentId);
|
|
296
|
+
killTmuxSessionIfExists(lanePaths.tmuxSocketName, run.sessionName);
|
|
297
|
+
}
|
|
298
|
+
if (pending.size === 0) {
|
|
299
|
+
clearInterval(interval);
|
|
300
|
+
resolve();
|
|
301
|
+
}
|
|
302
|
+
}, DEFAULT_WAIT_PROGRESS_INTERVAL_MS);
|
|
303
|
+
refreshPending();
|
|
304
|
+
onProgress?.({ pendingAgentIds: new Set(pending), timedOut: false });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (sessionFailures.length > 0) {
|
|
308
|
+
onProgress?.({ pendingAgentIds: new Set(), timedOut: false, failures: sessionFailures });
|
|
309
|
+
return { failures: sessionFailures, timedOut: false };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const failures = [];
|
|
313
|
+
for (const run of agentRuns) {
|
|
314
|
+
const code = readStatusCodeIfPresent(run.statusPath);
|
|
315
|
+
if (code === 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (code === null || timedOutAgentIds.has(run.agent.agentId)) {
|
|
319
|
+
failures.push({
|
|
320
|
+
agentId: run.agent.agentId,
|
|
321
|
+
statusCode: timedOutAgentIds.has(run.agent.agentId) ? "timeout-no-status" : "missing-status",
|
|
322
|
+
logPath: path.relative(REPO_ROOT, run.logPath),
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
failures.push({
|
|
327
|
+
agentId: run.agent.agentId,
|
|
328
|
+
statusCode: String(code),
|
|
329
|
+
logPath: path.relative(REPO_ROOT, run.logPath),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
onProgress?.({ pendingAgentIds: new Set(), timedOut: timedOutAgentIds.size > 0 });
|
|
333
|
+
return { failures, timedOut: timedOutAgentIds.size > 0 };
|
|
334
|
+
}
|