@chllming/wave-orchestration 0.9.13 → 0.9.15
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 +27 -0
- package/README.md +7 -7
- package/docs/README.md +3 -3
- package/docs/concepts/operating-modes.md +1 -1
- package/docs/guides/author-and-run-waves.md +1 -1
- package/docs/guides/planner.md +2 -2
- package/docs/guides/recommendations-0.9.15.md +83 -0
- package/docs/guides/sandboxed-environments.md +2 -2
- package/docs/guides/signal-wrappers.md +10 -0
- package/docs/plans/agent-first-closure-hardening.md +612 -0
- package/docs/plans/current-state.md +3 -3
- package/docs/plans/end-state-architecture.md +1 -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 +75 -20
- package/docs/reference/cli-reference.md +34 -1
- package/docs/reference/coordination-and-closure.md +16 -1
- package/docs/reference/npmjs-token-publishing.md +3 -3
- package/docs/reference/package-publishing-flow.md +13 -11
- package/docs/reference/runtime-config/README.md +2 -2
- package/docs/reference/sample-waves.md +5 -5
- package/docs/reference/skills.md +1 -1
- package/docs/reference/wave-control.md +1 -1
- package/docs/roadmap.md +5 -3
- package/package.json +1 -1
- package/releases/manifest.json +35 -0
- package/scripts/wave-orchestrator/agent-state.mjs +221 -313
- package/scripts/wave-orchestrator/artifact-schemas.mjs +37 -2
- package/scripts/wave-orchestrator/closure-adjudicator.mjs +311 -0
- package/scripts/wave-orchestrator/control-cli.mjs +212 -18
- package/scripts/wave-orchestrator/dashboard-state.mjs +40 -0
- package/scripts/wave-orchestrator/derived-state-engine.mjs +3 -0
- package/scripts/wave-orchestrator/gate-engine.mjs +140 -3
- package/scripts/wave-orchestrator/install.mjs +1 -1
- package/scripts/wave-orchestrator/launcher.mjs +49 -10
- package/scripts/wave-orchestrator/signal-cli.mjs +271 -0
- package/scripts/wave-orchestrator/structured-signal-parser.mjs +499 -0
- package/scripts/wave-orchestrator/task-entity.mjs +13 -4
- package/scripts/wave.mjs +9 -0
|
@@ -124,6 +124,24 @@ export function normalizePhaseState(value) {
|
|
|
124
124
|
: null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function deriveWaveProjectionTriplet(status) {
|
|
128
|
+
const normalized = String(status || "").trim().toLowerCase();
|
|
129
|
+
if (["running", "retrying"].includes(normalized)) {
|
|
130
|
+
return { executionState: "active", closureState: "evaluating", controllerState: "active" };
|
|
131
|
+
}
|
|
132
|
+
if (normalized === "completed") {
|
|
133
|
+
return { executionState: "settled", closureState: "passed", controllerState: "idle" };
|
|
134
|
+
}
|
|
135
|
+
if (["blocked", "failed", "timed_out"].includes(normalized)) {
|
|
136
|
+
return {
|
|
137
|
+
executionState: "settled",
|
|
138
|
+
closureState: "blocked",
|
|
139
|
+
controllerState: normalized === "blocked" ? "relaunch-planned" : "stale",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { executionState: "pending", closureState: "pending", controllerState: "idle" };
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
export function buildWaveDashboardState({
|
|
128
146
|
lane,
|
|
129
147
|
wave,
|
|
@@ -134,12 +152,16 @@ export function buildWaveDashboardState({
|
|
|
134
152
|
agentRuns,
|
|
135
153
|
}) {
|
|
136
154
|
const now = toIsoTimestamp();
|
|
155
|
+
const projectionStates = deriveWaveProjectionTriplet("running");
|
|
137
156
|
return normalizeWaveDashboardState({
|
|
138
157
|
lane,
|
|
139
158
|
wave,
|
|
140
159
|
waveFile,
|
|
141
160
|
runTag,
|
|
142
161
|
status: "running",
|
|
162
|
+
executionState: projectionStates.executionState,
|
|
163
|
+
closureState: projectionStates.closureState,
|
|
164
|
+
controllerState: projectionStates.controllerState,
|
|
143
165
|
attempt: 0,
|
|
144
166
|
maxAttempts,
|
|
145
167
|
startedAt: now,
|
|
@@ -192,10 +214,14 @@ export function buildGlobalDashboardState({
|
|
|
192
214
|
feedbackRequestsDir,
|
|
193
215
|
}) {
|
|
194
216
|
const now = toIsoTimestamp();
|
|
217
|
+
const projectionStates = deriveWaveProjectionTriplet("running");
|
|
195
218
|
return normalizeGlobalDashboardState({
|
|
196
219
|
lane,
|
|
197
220
|
runId: Math.random().toString(16).slice(2, 14),
|
|
198
221
|
status: "running",
|
|
222
|
+
executionState: projectionStates.executionState,
|
|
223
|
+
closureState: projectionStates.closureState,
|
|
224
|
+
controllerState: projectionStates.controllerState,
|
|
199
225
|
startedAt: now,
|
|
200
226
|
updatedAt: now,
|
|
201
227
|
options: {
|
|
@@ -223,6 +249,9 @@ export function buildGlobalDashboardState({
|
|
|
223
249
|
wave: wave.wave,
|
|
224
250
|
waveFile: wave.file,
|
|
225
251
|
status: "pending",
|
|
252
|
+
executionState: "pending",
|
|
253
|
+
closureState: "pending",
|
|
254
|
+
controllerState: "idle",
|
|
226
255
|
attempt: 0,
|
|
227
256
|
maxAttempts: options.maxRetriesPerWave + 1,
|
|
228
257
|
dashboardPath: path.relative(
|
|
@@ -260,8 +289,12 @@ export function buildGlobalDashboardState({
|
|
|
260
289
|
|
|
261
290
|
export function writeWaveDashboard(dashboardPath, state) {
|
|
262
291
|
ensureDirectory(path.dirname(dashboardPath));
|
|
292
|
+
const projectionStates = deriveWaveProjectionTriplet(state?.status);
|
|
263
293
|
const normalized = normalizeWaveDashboardState({
|
|
264
294
|
...state,
|
|
295
|
+
executionState: state?.executionState || projectionStates.executionState,
|
|
296
|
+
closureState: state?.closureState || projectionStates.closureState,
|
|
297
|
+
controllerState: state?.controllerState || projectionStates.controllerState,
|
|
265
298
|
updatedAt: toIsoTimestamp(),
|
|
266
299
|
});
|
|
267
300
|
Object.assign(state, normalized);
|
|
@@ -270,8 +303,12 @@ export function writeWaveDashboard(dashboardPath, state) {
|
|
|
270
303
|
|
|
271
304
|
export function writeGlobalDashboard(globalDashboardPath, state) {
|
|
272
305
|
ensureDirectory(path.dirname(globalDashboardPath));
|
|
306
|
+
const projectionStates = deriveWaveProjectionTriplet(state?.status);
|
|
273
307
|
const normalized = normalizeGlobalDashboardState({
|
|
274
308
|
...state,
|
|
309
|
+
executionState: state?.executionState || projectionStates.executionState,
|
|
310
|
+
closureState: state?.closureState || projectionStates.closureState,
|
|
311
|
+
controllerState: state?.controllerState || projectionStates.controllerState,
|
|
275
312
|
updatedAt: toIsoTimestamp(),
|
|
276
313
|
});
|
|
277
314
|
Object.assign(state, normalized);
|
|
@@ -316,6 +353,9 @@ export function syncGlobalWaveFromWaveDashboard(globalState, waveDashboard) {
|
|
|
316
353
|
return;
|
|
317
354
|
}
|
|
318
355
|
entry.status = waveDashboard.status;
|
|
356
|
+
entry.executionState = waveDashboard.executionState || null;
|
|
357
|
+
entry.closureState = waveDashboard.closureState || null;
|
|
358
|
+
entry.controllerState = waveDashboard.controllerState || null;
|
|
319
359
|
entry.attempt = waveDashboard.attempt;
|
|
320
360
|
entry.startedAt = waveDashboard.startedAt;
|
|
321
361
|
if (WAVE_TERMINAL_STATES.has(waveDashboard.status)) {
|
|
@@ -430,6 +430,9 @@ function buildIntegrationEvidence({
|
|
|
430
430
|
) {
|
|
431
431
|
const validation = validateImplementationSummary(agent, summary);
|
|
432
432
|
if (!validation.ok) {
|
|
433
|
+
if (validation.failureClass === "transport-failure" && validation.eligibleForAdjudication) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
433
436
|
const entry = summarizeGap(agent.agentId, validation.detail, "Implementation validation failed.");
|
|
434
437
|
if (["missing-doc-delta", "doc-impact-gap", "invalid-doc-delta-format"].includes(validation.statusCode)) {
|
|
435
438
|
docGapEntries.push(entry);
|
|
@@ -57,6 +57,11 @@ import {
|
|
|
57
57
|
evaluateDocumentationAutoClosure,
|
|
58
58
|
evaluateInferredIntegrationClosure,
|
|
59
59
|
} from "./closure-policy.mjs";
|
|
60
|
+
import {
|
|
61
|
+
evaluateClosureAdjudication,
|
|
62
|
+
persistClosureAdjudication,
|
|
63
|
+
closureAdjudicationPath,
|
|
64
|
+
} from "./closure-adjudicator.mjs";
|
|
60
65
|
|
|
61
66
|
function contradictionList(value) {
|
|
62
67
|
if (value instanceof Map) {
|
|
@@ -142,6 +147,34 @@ function validateEnvelopeForRun(runInfo, envelope, options = {}) {
|
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
function adjudicatedGateResult(validation, runInfo, summary, adjudication, adjudicationPathValue = null) {
|
|
151
|
+
if (adjudication.status === "pass") {
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
agentId: runInfo.agent.agentId,
|
|
155
|
+
statusCode: validation.statusCode,
|
|
156
|
+
detail: adjudication.detail,
|
|
157
|
+
logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
|
|
158
|
+
adjudicated: true,
|
|
159
|
+
adjudicationStatus: adjudication.status,
|
|
160
|
+
adjudicationArtifactPath: adjudicationPathValue ? path.relative(REPO_ROOT, adjudicationPathValue) : null,
|
|
161
|
+
failureClass: validation.failureClass || null,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
agentId: runInfo.agent.agentId,
|
|
167
|
+
statusCode: adjudication.status === "ambiguous" ? "awaiting-adjudication" : validation.statusCode,
|
|
168
|
+
detail: adjudication.detail || validation.detail,
|
|
169
|
+
logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
|
|
170
|
+
adjudicated: true,
|
|
171
|
+
adjudicationStatus: adjudication.status,
|
|
172
|
+
adjudicationArtifactPath: adjudicationPathValue ? path.relative(REPO_ROOT, adjudicationPathValue) : null,
|
|
173
|
+
failureClass: validation.failureClass || null,
|
|
174
|
+
eligibleForAdjudication: validation.eligibleForAdjudication === true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
145
178
|
|
|
146
179
|
// --- Laddered gate modes (0.9.4) ---
|
|
147
180
|
export function resolveGateMode(waveNumber, thresholds) {
|
|
@@ -612,13 +645,14 @@ export function readWaveImplementationGate(wave, agentRuns, options = {}) {
|
|
|
612
645
|
logPath: path.relative(REPO_ROOT, runInfo.logPath),
|
|
613
646
|
};
|
|
614
647
|
}
|
|
648
|
+
const statusRecord = runInfo?.statusPath ? readStatusRecordIfPresent(runInfo.statusPath) : null;
|
|
615
649
|
const summary = envelopeResult.valid
|
|
616
650
|
? projectLegacySummaryFromEnvelope(
|
|
617
651
|
envelopeResult.envelope,
|
|
618
652
|
buildEnvelopeReadOptions(
|
|
619
653
|
runInfo,
|
|
620
654
|
wave,
|
|
621
|
-
|
|
655
|
+
statusRecord,
|
|
622
656
|
resolveRunReportPath(wave, runInfo, options),
|
|
623
657
|
),
|
|
624
658
|
)
|
|
@@ -628,12 +662,45 @@ export function readWaveImplementationGate(wave, agentRuns, options = {}) {
|
|
|
628
662
|
});
|
|
629
663
|
const validation = validateImplementationSummary(runInfo.agent, summary);
|
|
630
664
|
if (!validation.ok) {
|
|
665
|
+
if (validation.eligibleForAdjudication) {
|
|
666
|
+
const adjudication = evaluateClosureAdjudication({
|
|
667
|
+
wave,
|
|
668
|
+
lanePaths: options.lanePaths || null,
|
|
669
|
+
gate: validation,
|
|
670
|
+
summary,
|
|
671
|
+
derivedState: options.derivedState || null,
|
|
672
|
+
agentRun: runInfo,
|
|
673
|
+
envelope: envelopeResult.valid ? envelopeResult.envelope : null,
|
|
674
|
+
});
|
|
675
|
+
let adjudicationArtifactPath = null;
|
|
676
|
+
if (options.lanePaths?.statusDir) {
|
|
677
|
+
const persisted = persistClosureAdjudication({
|
|
678
|
+
lanePaths: options.lanePaths,
|
|
679
|
+
waveNumber: wave.wave,
|
|
680
|
+
attempt: statusRecord?.attempt || envelopeResult.envelope?.attempt || 1,
|
|
681
|
+
agentId: runInfo.agent.agentId,
|
|
682
|
+
payload: {
|
|
683
|
+
status: adjudication.status,
|
|
684
|
+
failureClass: validation.failureClass,
|
|
685
|
+
reason: adjudication.reason,
|
|
686
|
+
detail: adjudication.detail,
|
|
687
|
+
evidence: adjudication.evidence,
|
|
688
|
+
synthesizedSignals: adjudication.synthesizedSignals,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
adjudicationArtifactPath = persisted.filePath;
|
|
692
|
+
}
|
|
693
|
+
return adjudicatedGateResult(validation, runInfo, summary, adjudication, adjudicationArtifactPath);
|
|
694
|
+
}
|
|
631
695
|
return {
|
|
632
696
|
ok: false,
|
|
633
697
|
agentId: runInfo.agent.agentId,
|
|
634
698
|
statusCode: validation.statusCode,
|
|
635
699
|
detail: validation.detail,
|
|
636
700
|
logPath: summary?.logPath || path.relative(REPO_ROOT, runInfo.logPath),
|
|
701
|
+
failureClass: validation.failureClass || null,
|
|
702
|
+
eligibleForAdjudication: validation.eligibleForAdjudication === true,
|
|
703
|
+
adjudicationHint: validation.adjudicationHint || null,
|
|
637
704
|
};
|
|
638
705
|
}
|
|
639
706
|
}
|
|
@@ -922,7 +989,29 @@ export function readWaveDocumentationGate(wave, agentRuns, options = {}) {
|
|
|
922
989
|
mode,
|
|
923
990
|
securityRolePromptPath: options.securityRolePromptPath,
|
|
924
991
|
});
|
|
925
|
-
const validation = validateDocumentationClosureSummary(docRun.agent, summary
|
|
992
|
+
const validation = validateDocumentationClosureSummary(docRun.agent, summary, {
|
|
993
|
+
allowFallbackOnEmptyRun: true,
|
|
994
|
+
});
|
|
995
|
+
// Fallback: when doc steward had an empty run but integration/QA passed,
|
|
996
|
+
// auto-close instead of blocking the wave.
|
|
997
|
+
if (!validation.ok && validation.eligibleForFallback) {
|
|
998
|
+
const integrationSummary = options.derivedState?.integrationSummary;
|
|
999
|
+
const integrationReady =
|
|
1000
|
+
integrationSummary?.recommendation === "ready-for-doc-closure";
|
|
1001
|
+
const contQaPassed =
|
|
1002
|
+
options.derivedState?.ledger?.contQa?.verdict === "pass" ||
|
|
1003
|
+
options.derivedState?.ledger?.contQa?.verdict === "PASS";
|
|
1004
|
+
if (integrationReady || contQaPassed) {
|
|
1005
|
+
return {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
agentId: docRun.agent.agentId,
|
|
1008
|
+
statusCode: "fallback-auto-closed",
|
|
1009
|
+
detail: `Documentation steward ${docRun.agent.agentId} had an empty run. Auto-closed because ${integrationReady ? "integration is ready-for-doc-closure" : "cont-QA passed"}.`,
|
|
1010
|
+
logPath: summary?.logPath || path.relative(REPO_ROOT, docRun.logPath),
|
|
1011
|
+
docClosureState: "no-change",
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
926
1015
|
return {
|
|
927
1016
|
ok: validation.ok,
|
|
928
1017
|
agentId: docRun.agent.agentId,
|
|
@@ -1311,12 +1400,37 @@ export function readWaveImplementationGatePure(wave, agentResults, options = {})
|
|
|
1311
1400
|
const summary = agentResults?.[agent.agentId] || null;
|
|
1312
1401
|
const validation = validateImplementationSummary(agent, summary);
|
|
1313
1402
|
if (!validation.ok) {
|
|
1403
|
+
if (validation.eligibleForAdjudication) {
|
|
1404
|
+
const adjudication = evaluateClosureAdjudication({
|
|
1405
|
+
wave,
|
|
1406
|
+
lanePaths: options.lanePaths || null,
|
|
1407
|
+
gate: validation,
|
|
1408
|
+
summary,
|
|
1409
|
+
derivedState: options.derivedState || null,
|
|
1410
|
+
agentRun: { agent, logPath: summary?.logPath ? path.resolve(REPO_ROOT, summary.logPath) : null },
|
|
1411
|
+
envelope: null,
|
|
1412
|
+
});
|
|
1413
|
+
return {
|
|
1414
|
+
ok: adjudication.status === "pass",
|
|
1415
|
+
agentId: agent.agentId,
|
|
1416
|
+
statusCode: adjudication.status === "ambiguous" ? "awaiting-adjudication" : validation.statusCode,
|
|
1417
|
+
detail: adjudication.detail || validation.detail,
|
|
1418
|
+
logPath: summary?.logPath || null,
|
|
1419
|
+
adjudicated: true,
|
|
1420
|
+
adjudicationStatus: adjudication.status,
|
|
1421
|
+
failureClass: validation.failureClass || null,
|
|
1422
|
+
eligibleForAdjudication: true,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1314
1425
|
return {
|
|
1315
1426
|
ok: false,
|
|
1316
1427
|
agentId: agent.agentId,
|
|
1317
1428
|
statusCode: validation.statusCode,
|
|
1318
1429
|
detail: validation.detail,
|
|
1319
1430
|
logPath: summary?.logPath || null,
|
|
1431
|
+
failureClass: validation.failureClass || null,
|
|
1432
|
+
eligibleForAdjudication: validation.eligibleForAdjudication === true,
|
|
1433
|
+
adjudicationHint: validation.adjudicationHint || null,
|
|
1320
1434
|
};
|
|
1321
1435
|
}
|
|
1322
1436
|
}
|
|
@@ -1501,7 +1615,30 @@ export function readWaveDocumentationGatePure(wave, agentResults, options = {})
|
|
|
1501
1615
|
}
|
|
1502
1616
|
const summary = agentResults?.[documentationAgentId] || null;
|
|
1503
1617
|
const agent = { agentId: documentationAgentId };
|
|
1504
|
-
const validation = validateDocumentationClosureSummary(agent, summary
|
|
1618
|
+
const validation = validateDocumentationClosureSummary(agent, summary, {
|
|
1619
|
+
allowFallbackOnEmptyRun: true,
|
|
1620
|
+
});
|
|
1621
|
+
// When the doc steward had an empty run (broker collision, no output) but
|
|
1622
|
+
// integration is already ready-for-doc-closure, auto-close documentation
|
|
1623
|
+
// instead of failing the entire wave.
|
|
1624
|
+
if (!validation.ok && validation.eligibleForFallback) {
|
|
1625
|
+
const integrationSummary = options.derivedState?.integrationSummary;
|
|
1626
|
+
const integrationReady =
|
|
1627
|
+
integrationSummary?.recommendation === "ready-for-doc-closure";
|
|
1628
|
+
const contQaPassed =
|
|
1629
|
+
options.derivedState?.ledger?.contQa?.verdict === "pass" ||
|
|
1630
|
+
options.derivedState?.ledger?.contQa?.verdict === "PASS";
|
|
1631
|
+
if (integrationReady || contQaPassed) {
|
|
1632
|
+
return {
|
|
1633
|
+
ok: true,
|
|
1634
|
+
agentId: documentationAgentId,
|
|
1635
|
+
statusCode: "fallback-auto-closed",
|
|
1636
|
+
detail: `Documentation steward ${documentationAgentId} had an empty run. Auto-closed because ${integrationReady ? "integration is ready-for-doc-closure" : "cont-QA passed"}.`,
|
|
1637
|
+
logPath: summary?.logPath || null,
|
|
1638
|
+
docClosureState: "no-change",
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1505
1642
|
return { ok: validation.ok, agentId: documentationAgentId, statusCode: validation.statusCode,
|
|
1506
1643
|
detail: validation.detail, logPath: summary?.logPath || null };
|
|
1507
1644
|
}
|
|
@@ -69,7 +69,7 @@ export const STARTER_TEMPLATE_PATHS = [
|
|
|
69
69
|
"docs/guides/author-and-run-waves.md",
|
|
70
70
|
"docs/guides/monorepo-projects.md",
|
|
71
71
|
"docs/guides/planner.md",
|
|
72
|
-
"docs/guides/recommendations-0.9.
|
|
72
|
+
"docs/guides/recommendations-0.9.15.md",
|
|
73
73
|
"docs/guides/sandboxed-environments.md",
|
|
74
74
|
"docs/guides/signal-wrappers.md",
|
|
75
75
|
"docs/guides/terminal-surfaces.md",
|
|
@@ -715,6 +715,12 @@ function failuresAreRecoverable(failures) {
|
|
|
715
715
|
return Array.isArray(failures) && failures.length > 0 && failures.every((failure) => failure?.recoverable);
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
+
function failuresAwaitAdjudication(failures) {
|
|
719
|
+
return Array.isArray(failures) && failures.length > 0 && failures.every(
|
|
720
|
+
(failure) => String(failure?.statusCode || "").trim().toLowerCase() === "awaiting-adjudication",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
718
724
|
function appendRepairCoordinationRequests({
|
|
719
725
|
coordinationLogPath,
|
|
720
726
|
lanePaths,
|
|
@@ -771,16 +777,23 @@ export async function runLauncherCli(argv) {
|
|
|
771
777
|
const projectId = options.project || config.defaultProject;
|
|
772
778
|
if (!readProjectProfile({ config, project: projectId })) {
|
|
773
779
|
const { stderr: stderrStream } = await import("node:process");
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
"
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
780
|
+
if (options.dryRun) {
|
|
781
|
+
stderrStream.write(
|
|
782
|
+
"\n No project profile found — continuing dry-run with config defaults.\n" +
|
|
783
|
+
" Live launches will still prompt for first-time setup, or you can run: wave project setup\n\n",
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
stderrStream.write(
|
|
787
|
+
"\n No project profile found — running first-time setup.\n" +
|
|
788
|
+
" You can re-run this later with: wave project setup\n\n",
|
|
789
|
+
);
|
|
790
|
+
const { runPlannerCli } = await import("./planner.mjs");
|
|
791
|
+
await runPlannerCli(["project", "setup", ...(projectId ? ["--project", projectId] : [])]);
|
|
792
|
+
// Re-read the terminal surface from the newly created profile.
|
|
793
|
+
const freshProfile = readProjectProfile({ config, project: projectId });
|
|
794
|
+
if (freshProfile) {
|
|
795
|
+
options.terminalSurface = resolveDefaultTerminalSurface(freshProfile);
|
|
796
|
+
}
|
|
784
797
|
}
|
|
785
798
|
}
|
|
786
799
|
|
|
@@ -1907,6 +1920,8 @@ export async function runLauncherCli(argv) {
|
|
|
1907
1920
|
if (failures.length === 0) {
|
|
1908
1921
|
const implementationGate = readWaveImplementationGate(wave, agentRuns, {
|
|
1909
1922
|
securityRolePromptPath: lanePaths.securityRolePromptPath,
|
|
1923
|
+
lanePaths,
|
|
1924
|
+
derivedState,
|
|
1910
1925
|
});
|
|
1911
1926
|
if (!implementationGate.ok) {
|
|
1912
1927
|
failures = [
|
|
@@ -2449,6 +2464,30 @@ export async function runLauncherCli(argv) {
|
|
|
2449
2464
|
throw error;
|
|
2450
2465
|
}
|
|
2451
2466
|
|
|
2467
|
+
if (failuresAwaitAdjudication(failures)) {
|
|
2468
|
+
clearWaveRelaunchPlan(lanePaths, wave.wave);
|
|
2469
|
+
dashboardState.status = "blocked";
|
|
2470
|
+
for (const failure of failures) {
|
|
2471
|
+
setWaveDashboardAgent(dashboardState, failure.agentId, {
|
|
2472
|
+
state: "failed",
|
|
2473
|
+
detail: failure.detail || "Closure is awaiting deterministic adjudication.",
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
flushDashboards();
|
|
2477
|
+
appendCoordination({
|
|
2478
|
+
event: "wave_adjudication_pending",
|
|
2479
|
+
waves: [wave.wave],
|
|
2480
|
+
status: "blocked",
|
|
2481
|
+
details: failures.map((failure) => `${failure.agentId || "wave"}:${failure.statusCode}`).join(", "),
|
|
2482
|
+
actionRequested: `Lane ${lanePaths.lane} owners should inspect the persisted closure adjudication artifact before any rerun decision.`,
|
|
2483
|
+
});
|
|
2484
|
+
const error = new Error(
|
|
2485
|
+
`Wave ${wave.wave} is waiting on deterministic closure adjudication; no retry was queued.`,
|
|
2486
|
+
);
|
|
2487
|
+
error.exitCode = 43;
|
|
2488
|
+
throw error;
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2452
2491
|
const failedAgentIds = new Set(failures.map((failure) => failure.agentId));
|
|
2453
2492
|
const failedList = Array.from(failedAgentIds).join(", ");
|
|
2454
2493
|
recordAttemptState(lanePaths, wave.wave, attempt, "failed", {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDirectory } from "./shared.mjs";
|
|
4
|
+
import {
|
|
5
|
+
EXIT_CONTRACT_COMPLETION_VALUES,
|
|
6
|
+
EXIT_CONTRACT_DURABILITY_VALUES,
|
|
7
|
+
EXIT_CONTRACT_PROOF_VALUES,
|
|
8
|
+
EXIT_CONTRACT_DOC_IMPACT_VALUES,
|
|
9
|
+
} from "./agent-state.mjs";
|
|
10
|
+
import { parseStructuredSignalCandidate } from "./structured-signal-parser.mjs";
|
|
11
|
+
|
|
12
|
+
const PROOF_STATES = ["met", "complete", "gap"];
|
|
13
|
+
const DOC_CLOSURE_STATES = ["closed", "no-change", "delta"];
|
|
14
|
+
const INTEGRATION_STATES = ["ready-for-doc-closure", "needs-more-work"];
|
|
15
|
+
const COMPONENT_ID_REGEX = /^[a-z0-9._-]+$/;
|
|
16
|
+
const COMPONENT_LEVEL_REGEX = /^[a-z0-9._-]+$/;
|
|
17
|
+
const UNSAFE_KEY_VALUE_FRAGMENT_REGEX = /(^|\s)[a-z][a-z0-9_-]*=/i;
|
|
18
|
+
const PARSER_KIND_BY_SIGNAL_KIND = {
|
|
19
|
+
proof: "proof",
|
|
20
|
+
"doc-delta": "docDelta",
|
|
21
|
+
component: "component",
|
|
22
|
+
integration: "integration",
|
|
23
|
+
"doc-closure": "docClosure",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function printUsage() {
|
|
27
|
+
console.log(`Usage:
|
|
28
|
+
wave signal proof --completion <level> --durability <level> --proof <level> --state <met|complete|gap> [--detail <text>] [--json] [--append-file <path>]
|
|
29
|
+
wave signal doc-delta --state <none|owned|shared-plan> [--path <file> ...] [--detail <text>] [--json] [--append-file <path>]
|
|
30
|
+
wave signal component --id <component> --level <level> --state <met|complete|gap> [--detail <text>] [--json] [--append-file <path>]
|
|
31
|
+
wave signal integration --state <ready-for-doc-closure|needs-more-work> --claims <n> --conflicts <n> --blockers <n> [--detail <text>] [--json] [--append-file <path>]
|
|
32
|
+
wave signal doc-closure --state <closed|no-change|delta> [--path <file> ...] [--detail <text>] [--json] [--append-file <path>]
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeState(value) {
|
|
37
|
+
return String(value || "").trim().toLowerCase() === "complete" ? "met" : String(value || "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanText(value) {
|
|
41
|
+
return String(value || "").trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateAllowedValue(label, value, allowedValues) {
|
|
45
|
+
const normalized = cleanText(value).toLowerCase();
|
|
46
|
+
if (!allowedValues.includes(normalized)) {
|
|
47
|
+
throw new Error(`${label} must be one of: ${allowedValues.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateSafeField(label, value, { allowCommas = true } = {}) {
|
|
53
|
+
const normalized = cleanText(value);
|
|
54
|
+
if (!normalized) {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
if (/[\r\n]/.test(normalized)) {
|
|
58
|
+
throw new Error(`${label} cannot contain newlines`);
|
|
59
|
+
}
|
|
60
|
+
if (!allowCommas && normalized.includes(",")) {
|
|
61
|
+
throw new Error(`${label} cannot contain commas`);
|
|
62
|
+
}
|
|
63
|
+
if (UNSAFE_KEY_VALUE_FRAGMENT_REGEX.test(normalized)) {
|
|
64
|
+
throw new Error(`${label} cannot contain embedded key=value fragments`);
|
|
65
|
+
}
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validatePathList(paths) {
|
|
70
|
+
return paths.map((item, index) =>
|
|
71
|
+
validateSafeField(`path ${index + 1}`, item, { allowCommas: false }),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseArgs(argv) {
|
|
76
|
+
const kind = String(argv[0] || "").trim().toLowerCase();
|
|
77
|
+
const options = {
|
|
78
|
+
completion: "",
|
|
79
|
+
durability: "",
|
|
80
|
+
proof: "",
|
|
81
|
+
state: "",
|
|
82
|
+
detail: "",
|
|
83
|
+
componentId: "",
|
|
84
|
+
level: "",
|
|
85
|
+
paths: [],
|
|
86
|
+
claims: "0",
|
|
87
|
+
conflicts: "0",
|
|
88
|
+
blockers: "0",
|
|
89
|
+
appendFile: "",
|
|
90
|
+
json: false,
|
|
91
|
+
};
|
|
92
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
93
|
+
const arg = argv[index];
|
|
94
|
+
if (arg === "--completion") {
|
|
95
|
+
options.completion = String(argv[++index] || "").trim();
|
|
96
|
+
} else if (arg === "--durability") {
|
|
97
|
+
options.durability = String(argv[++index] || "").trim();
|
|
98
|
+
} else if (arg === "--proof") {
|
|
99
|
+
options.proof = String(argv[++index] || "").trim();
|
|
100
|
+
} else if (arg === "--state") {
|
|
101
|
+
options.state = String(argv[++index] || "").trim();
|
|
102
|
+
} else if (arg === "--detail") {
|
|
103
|
+
options.detail = String(argv[++index] || "").trim();
|
|
104
|
+
} else if (arg === "--id") {
|
|
105
|
+
options.componentId = String(argv[++index] || "").trim();
|
|
106
|
+
} else if (arg === "--level") {
|
|
107
|
+
options.level = String(argv[++index] || "").trim();
|
|
108
|
+
} else if (arg === "--path") {
|
|
109
|
+
options.paths.push(String(argv[++index] || "").trim());
|
|
110
|
+
} else if (arg === "--claims") {
|
|
111
|
+
options.claims = String(argv[++index] || "0").trim();
|
|
112
|
+
} else if (arg === "--conflicts") {
|
|
113
|
+
options.conflicts = String(argv[++index] || "0").trim();
|
|
114
|
+
} else if (arg === "--blockers") {
|
|
115
|
+
options.blockers = String(argv[++index] || "0").trim();
|
|
116
|
+
} else if (arg === "--append-file") {
|
|
117
|
+
options.appendFile = String(argv[++index] || "").trim();
|
|
118
|
+
} else if (arg === "--json") {
|
|
119
|
+
options.json = true;
|
|
120
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
121
|
+
return { help: true, kind, options };
|
|
122
|
+
} else if (arg) {
|
|
123
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { help: false, kind, options };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildLine(kind, options) {
|
|
130
|
+
if (kind === "proof") {
|
|
131
|
+
return [
|
|
132
|
+
"[wave-proof]",
|
|
133
|
+
`completion=${options.completion}`,
|
|
134
|
+
`durability=${options.durability}`,
|
|
135
|
+
`proof=${options.proof}`,
|
|
136
|
+
`state=${normalizeState(options.state)}`,
|
|
137
|
+
...(options.detail ? [`detail=${options.detail}`] : []),
|
|
138
|
+
].join(" ");
|
|
139
|
+
}
|
|
140
|
+
if (kind === "doc-delta") {
|
|
141
|
+
return [
|
|
142
|
+
"[wave-doc-delta]",
|
|
143
|
+
`state=${options.state}`,
|
|
144
|
+
...(options.paths.length > 0 ? [`paths=${options.paths.join(",")}`] : []),
|
|
145
|
+
...(options.detail ? [`detail=${options.detail}`] : []),
|
|
146
|
+
].join(" ");
|
|
147
|
+
}
|
|
148
|
+
if (kind === "component") {
|
|
149
|
+
return [
|
|
150
|
+
"[wave-component]",
|
|
151
|
+
`component=${options.componentId}`,
|
|
152
|
+
`level=${options.level}`,
|
|
153
|
+
`state=${normalizeState(options.state)}`,
|
|
154
|
+
...(options.detail ? [`detail=${options.detail}`] : []),
|
|
155
|
+
].join(" ");
|
|
156
|
+
}
|
|
157
|
+
if (kind === "integration") {
|
|
158
|
+
return [
|
|
159
|
+
"[wave-integration]",
|
|
160
|
+
`state=${options.state}`,
|
|
161
|
+
`claims=${options.claims}`,
|
|
162
|
+
`conflicts=${options.conflicts}`,
|
|
163
|
+
`blockers=${options.blockers}`,
|
|
164
|
+
...(options.detail ? [`detail=${options.detail}`] : []),
|
|
165
|
+
].join(" ");
|
|
166
|
+
}
|
|
167
|
+
if (kind === "doc-closure") {
|
|
168
|
+
return [
|
|
169
|
+
"[wave-doc-closure]",
|
|
170
|
+
`state=${options.state}`,
|
|
171
|
+
...(options.paths.length > 0 ? [`paths=${options.paths.join(",")}`] : []),
|
|
172
|
+
...(options.detail ? [`detail=${options.detail}`] : []),
|
|
173
|
+
].join(" ");
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Unknown signal kind: ${kind}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertCanonicalRoundTrip(kind, line) {
|
|
179
|
+
const candidate = parseStructuredSignalCandidate(line);
|
|
180
|
+
const parserKind = PARSER_KIND_BY_SIGNAL_KIND[kind] || kind;
|
|
181
|
+
if (!candidate?.accepted || candidate.kind !== parserKind || candidate.normalizedLine !== line) {
|
|
182
|
+
throw new Error(`wave signal ${kind} could not round-trip through the shipped structured-signal parser`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validate(kind, options) {
|
|
187
|
+
if (kind === "proof") {
|
|
188
|
+
if (!options.completion || !options.durability || !options.proof || !options.state) {
|
|
189
|
+
throw new Error("wave signal proof requires --completion, --durability, --proof, and --state");
|
|
190
|
+
}
|
|
191
|
+
options.completion = validateAllowedValue("--completion", options.completion, EXIT_CONTRACT_COMPLETION_VALUES);
|
|
192
|
+
options.durability = validateAllowedValue("--durability", options.durability, EXIT_CONTRACT_DURABILITY_VALUES);
|
|
193
|
+
options.proof = validateAllowedValue("--proof", options.proof, EXIT_CONTRACT_PROOF_VALUES);
|
|
194
|
+
options.state = validateAllowedValue("--state", options.state, PROOF_STATES);
|
|
195
|
+
options.detail = validateSafeField("--detail", options.detail);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (kind === "doc-delta" || kind === "doc-closure") {
|
|
199
|
+
if (!options.state) {
|
|
200
|
+
throw new Error(`wave signal ${kind} requires --state`);
|
|
201
|
+
}
|
|
202
|
+
options.state = validateAllowedValue(
|
|
203
|
+
"--state",
|
|
204
|
+
options.state,
|
|
205
|
+
kind === "doc-delta" ? EXIT_CONTRACT_DOC_IMPACT_VALUES : DOC_CLOSURE_STATES,
|
|
206
|
+
);
|
|
207
|
+
options.paths = validatePathList(options.paths);
|
|
208
|
+
options.detail = validateSafeField("--detail", options.detail);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (kind === "component") {
|
|
212
|
+
if (!options.componentId || !options.level || !options.state) {
|
|
213
|
+
throw new Error("wave signal component requires --id, --level, and --state");
|
|
214
|
+
}
|
|
215
|
+
options.componentId = validateSafeField("--id", options.componentId, { allowCommas: false });
|
|
216
|
+
options.level = validateSafeField("--level", options.level, { allowCommas: false }).toLowerCase();
|
|
217
|
+
options.state = validateAllowedValue("--state", options.state, PROOF_STATES);
|
|
218
|
+
options.detail = validateSafeField("--detail", options.detail);
|
|
219
|
+
if (!COMPONENT_ID_REGEX.test(options.componentId)) {
|
|
220
|
+
throw new Error("--id must use a canonical component token like runtime-render-snapshot");
|
|
221
|
+
}
|
|
222
|
+
if (!COMPONENT_LEVEL_REGEX.test(options.level)) {
|
|
223
|
+
throw new Error("--level must use a canonical component level token");
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (kind === "integration") {
|
|
228
|
+
if (!options.state) {
|
|
229
|
+
throw new Error("wave signal integration requires --state");
|
|
230
|
+
}
|
|
231
|
+
options.state = validateAllowedValue("--state", options.state, INTEGRATION_STATES);
|
|
232
|
+
options.detail = validateSafeField("--detail", options.detail);
|
|
233
|
+
for (const field of ["claims", "conflicts", "blockers"]) {
|
|
234
|
+
const value = Number.parseInt(options[field], 10);
|
|
235
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
236
|
+
throw new Error(`--${field} must be a non-negative integer`);
|
|
237
|
+
}
|
|
238
|
+
options[field] = String(value);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`Unknown signal kind: ${kind}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function runSignalCli(argv) {
|
|
246
|
+
if (["--help", "-h", "help"].includes(String(argv[0] || "").trim().toLowerCase())) {
|
|
247
|
+
printUsage();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const parsed = parseArgs(argv);
|
|
251
|
+
if (parsed.help || !parsed.kind) {
|
|
252
|
+
printUsage();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
validate(parsed.kind, parsed.options);
|
|
256
|
+
const line = buildLine(parsed.kind, parsed.options);
|
|
257
|
+
assertCanonicalRoundTrip(parsed.kind, line);
|
|
258
|
+
if (parsed.options.appendFile) {
|
|
259
|
+
ensureDirectory(path.dirname(parsed.options.appendFile));
|
|
260
|
+
fs.appendFileSync(parsed.options.appendFile, `${line}\n`, "utf8");
|
|
261
|
+
}
|
|
262
|
+
if (parsed.options.json) {
|
|
263
|
+
console.log(JSON.stringify({
|
|
264
|
+
kind: parsed.kind,
|
|
265
|
+
line,
|
|
266
|
+
appendFile: parsed.options.appendFile || null,
|
|
267
|
+
}, null, 2));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.log(line);
|
|
271
|
+
}
|