@chllming/wave-orchestration 0.6.3 → 0.7.0
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 -1
- package/README.md +39 -7
- package/docs/agents/wave-orchestrator-role.md +50 -0
- package/docs/agents/wave-planner-role.md +39 -0
- package/docs/context7/bundles.json +9 -0
- package/docs/context7/planner-agent/README.md +25 -0
- package/docs/context7/planner-agent/manifest.json +83 -0
- package/docs/context7/planner-agent/papers/cooperbench-why-coding-agents-cannot-be-your-teammates-yet.md +3283 -0
- package/docs/context7/planner-agent/papers/dova-deliberation-first-multi-agent-orchestration-for-autonomous-research-automation.md +1699 -0
- package/docs/context7/planner-agent/papers/dpbench-large-language-models-struggle-with-simultaneous-coordination.md +2251 -0
- package/docs/context7/planner-agent/papers/incremental-planning-to-control-a-blackboard-based-problem-solver.md +1729 -0
- package/docs/context7/planner-agent/papers/silo-bench-a-scalable-environment-for-evaluating-distributed-coordination-in-multi-agent-llm-systems.md +3747 -0
- package/docs/context7/planner-agent/papers/todoevolve-learning-to-architect-agent-planning-systems.md +1675 -0
- package/docs/context7/planner-agent/papers/verified-multi-agent-orchestration-a-plan-execute-verify-replan-framework-for-complex-query-resolution.md +1173 -0
- package/docs/context7/planner-agent/papers/why-do-multi-agent-llm-systems-fail.md +5211 -0
- package/docs/context7/planner-agent/topics/planning-and-orchestration.md +24 -0
- package/docs/evals/README.md +96 -1
- package/docs/evals/arm-templates/README.md +13 -0
- package/docs/evals/arm-templates/full-wave.json +15 -0
- package/docs/evals/arm-templates/single-agent.json +15 -0
- package/docs/evals/benchmark-catalog.json +7 -0
- package/docs/evals/cases/README.md +47 -0
- package/docs/evals/cases/wave-blackboard-inbox-targeting.json +73 -0
- package/docs/evals/cases/wave-contradiction-conflict.json +104 -0
- package/docs/evals/cases/wave-expert-routing-preservation.json +69 -0
- package/docs/evals/cases/wave-hidden-profile-private-evidence.json +81 -0
- package/docs/evals/cases/wave-premature-closure-guard.json +71 -0
- package/docs/evals/cases/wave-silo-cross-agent-state.json +77 -0
- package/docs/evals/cases/wave-simultaneous-lockstep.json +92 -0
- package/docs/evals/cooperbench/real-world-mitigation.md +341 -0
- package/docs/evals/external-benchmarks.json +85 -0
- package/docs/evals/external-command-config.sample.json +9 -0
- package/docs/evals/external-command-config.swe-bench-pro.json +8 -0
- package/docs/evals/pilots/README.md +47 -0
- package/docs/evals/pilots/swe-bench-pro-public-full-wave-review-10.json +64 -0
- package/docs/evals/pilots/swe-bench-pro-public-pilot.json +111 -0
- package/docs/evals/wave-benchmark-program.md +302 -0
- package/docs/guides/planner.md +48 -11
- package/docs/plans/context7-wave-orchestrator.md +20 -0
- package/docs/plans/current-state.md +8 -1
- package/docs/plans/examples/wave-benchmark-improvement.md +108 -0
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/examples/wave-example-rollout-fidelity.md +340 -0
- package/docs/plans/wave-orchestrator.md +62 -11
- package/docs/plans/waves/reviews/wave-1-benchmark-operator.md +118 -0
- package/docs/reference/coordination-and-closure.md +436 -0
- package/docs/reference/live-proof-waves.md +25 -3
- package/docs/reference/npmjs-trusted-publishing.md +3 -3
- package/docs/reference/proof-metrics.md +90 -0
- package/docs/reference/runtime-config/README.md +61 -0
- package/docs/reference/sample-waves.md +29 -18
- package/docs/reference/wave-control.md +164 -0
- package/docs/reference/wave-planning-lessons.md +131 -0
- package/package.json +5 -4
- package/releases/manifest.json +18 -0
- package/scripts/research/agent-context-archive.mjs +18 -0
- package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +17 -0
- package/scripts/research/sync-planner-context7-bundle.mjs +133 -0
- package/scripts/wave-orchestrator/artifact-schemas.mjs +232 -0
- package/scripts/wave-orchestrator/autonomous.mjs +7 -0
- package/scripts/wave-orchestrator/benchmark-cases.mjs +374 -0
- package/scripts/wave-orchestrator/benchmark-external.mjs +1384 -0
- package/scripts/wave-orchestrator/benchmark.mjs +972 -0
- package/scripts/wave-orchestrator/clarification-triage.mjs +78 -12
- package/scripts/wave-orchestrator/config.mjs +175 -0
- package/scripts/wave-orchestrator/control-cli.mjs +1123 -0
- package/scripts/wave-orchestrator/control-plane.mjs +697 -0
- package/scripts/wave-orchestrator/coord-cli.mjs +360 -2
- package/scripts/wave-orchestrator/coordination-store.mjs +211 -9
- package/scripts/wave-orchestrator/coordination.mjs +84 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +38 -3
- package/scripts/wave-orchestrator/dashboard-state.mjs +22 -0
- package/scripts/wave-orchestrator/evals.mjs +23 -0
- package/scripts/wave-orchestrator/executors.mjs +3 -2
- package/scripts/wave-orchestrator/feedback.mjs +55 -0
- package/scripts/wave-orchestrator/install.mjs +55 -1
- package/scripts/wave-orchestrator/launcher-closure.mjs +4 -1
- package/scripts/wave-orchestrator/launcher-runtime.mjs +24 -21
- package/scripts/wave-orchestrator/launcher.mjs +796 -35
- package/scripts/wave-orchestrator/planner-context.mjs +75 -0
- package/scripts/wave-orchestrator/planner.mjs +2270 -136
- package/scripts/wave-orchestrator/proof-cli.mjs +195 -0
- package/scripts/wave-orchestrator/proof-registry.mjs +317 -0
- package/scripts/wave-orchestrator/replay.mjs +10 -4
- package/scripts/wave-orchestrator/retry-cli.mjs +184 -0
- package/scripts/wave-orchestrator/retry-control.mjs +225 -0
- package/scripts/wave-orchestrator/shared.mjs +26 -0
- package/scripts/wave-orchestrator/swe-bench-pro-task.mjs +1004 -0
- package/scripts/wave-orchestrator/traces.mjs +157 -2
- package/scripts/wave-orchestrator/wave-control-client.mjs +532 -0
- package/scripts/wave-orchestrator/wave-control-schema.mjs +309 -0
- package/scripts/wave-orchestrator/wave-files.mjs +17 -5
- package/scripts/wave.mjs +27 -0
- package/skills/repo-coding-rules/SKILL.md +1 -0
- package/skills/role-cont-eval/SKILL.md +1 -0
- package/skills/role-cont-qa/SKILL.md +13 -6
- package/skills/role-deploy/SKILL.md +1 -0
- package/skills/role-documentation/SKILL.md +4 -0
- package/skills/role-implementation/SKILL.md +4 -0
- package/skills/role-infra/SKILL.md +2 -1
- package/skills/role-integration/SKILL.md +15 -8
- package/skills/role-planner/SKILL.md +39 -0
- package/skills/role-planner/skill.json +21 -0
- package/skills/role-research/SKILL.md +1 -0
- package/skills/role-security/SKILL.md +2 -2
- package/skills/runtime-claude/SKILL.md +2 -1
- package/skills/runtime-codex/SKILL.md +1 -0
- package/skills/runtime-local/SKILL.md +2 -0
- package/skills/runtime-opencode/SKILL.md +1 -0
- package/skills/wave-core/SKILL.md +25 -6
- package/skills/wave-core/references/marker-syntax.md +16 -8
- package/wave.config.json +45 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
readWaveControlDeliveryState,
|
|
6
|
+
writeWaveControlDeliveryState,
|
|
7
|
+
} from "./artifact-schemas.mjs";
|
|
8
|
+
import { REPO_ROOT, buildWorkspaceTmuxToken, ensureDirectory, toIsoTimestamp } from "./shared.mjs";
|
|
9
|
+
import {
|
|
10
|
+
buildWaveControlConfigAttestationHash,
|
|
11
|
+
normalizeWaveControlArtifactDescriptor,
|
|
12
|
+
normalizeWaveControlEventEnvelope,
|
|
13
|
+
} from "./wave-control-schema.mjs";
|
|
14
|
+
import { readInstalledPackageMetadata } from "./package-version.mjs";
|
|
15
|
+
|
|
16
|
+
const MAX_INLINE_ARTIFACT_BYTES = 512 * 1024;
|
|
17
|
+
|
|
18
|
+
function normalizeText(value, fallback = null) {
|
|
19
|
+
const normalized = String(value ?? "").trim();
|
|
20
|
+
return normalized || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTelemetryId(value, fallback = null) {
|
|
24
|
+
const normalized = String(value ?? "")
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
28
|
+
.replace(/-+/g, "-")
|
|
29
|
+
.replace(/^-+|-+$/g, "");
|
|
30
|
+
return normalized || fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeToken(value, fallback = "item") {
|
|
34
|
+
const token = String(value ?? "")
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
37
|
+
.replace(/-+/g, "-")
|
|
38
|
+
.replace(/^-+|-+$/g, "");
|
|
39
|
+
return token || fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function telemetryPaths(lanePaths) {
|
|
43
|
+
const telemetryDir = lanePaths?.telemetryDir || path.join(lanePaths.controlPlaneDir, "telemetry");
|
|
44
|
+
return {
|
|
45
|
+
telemetryDir,
|
|
46
|
+
pendingDir: path.join(telemetryDir, "pending"),
|
|
47
|
+
sentDir: path.join(telemetryDir, "sent"),
|
|
48
|
+
failedDir: path.join(telemetryDir, "failed"),
|
|
49
|
+
eventsPath: path.join(telemetryDir, "events.jsonl"),
|
|
50
|
+
deliveryStatePath: path.join(telemetryDir, "delivery-state.json"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function listPendingQueueFiles(paths) {
|
|
55
|
+
return fs
|
|
56
|
+
.readdirSync(paths.pendingDir)
|
|
57
|
+
.filter((fileName) => fileName.endsWith(".json"))
|
|
58
|
+
.sort()
|
|
59
|
+
.map((fileName) => path.join(paths.pendingDir, fileName));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureTelemetryDirs(paths) {
|
|
63
|
+
ensureDirectory(paths.telemetryDir);
|
|
64
|
+
ensureDirectory(paths.pendingDir);
|
|
65
|
+
ensureDirectory(paths.sentDir);
|
|
66
|
+
ensureDirectory(paths.failedDir);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function repoRelativePathOrNull(filePath) {
|
|
70
|
+
if (!filePath) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const absolute = path.resolve(String(filePath));
|
|
74
|
+
const relative = path.relative(REPO_ROOT, absolute);
|
|
75
|
+
if (!relative || relative.startsWith("..")) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return relative.replaceAll(path.sep, "/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fileHashOrNull(filePath) {
|
|
82
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function contentTypeForPath(filePath) {
|
|
89
|
+
const ext = path.extname(String(filePath || "").toLowerCase());
|
|
90
|
+
if (ext === ".json") {
|
|
91
|
+
return "application/json";
|
|
92
|
+
}
|
|
93
|
+
if (ext === ".jsonl") {
|
|
94
|
+
return "application/x-ndjson";
|
|
95
|
+
}
|
|
96
|
+
if (ext === ".md") {
|
|
97
|
+
return "text/markdown";
|
|
98
|
+
}
|
|
99
|
+
if ([".txt", ".log", ".status"].includes(ext)) {
|
|
100
|
+
return "text/plain";
|
|
101
|
+
}
|
|
102
|
+
if (ext === ".html") {
|
|
103
|
+
return "text/html";
|
|
104
|
+
}
|
|
105
|
+
if ([".js", ".mjs", ".ts"].includes(ext)) {
|
|
106
|
+
return "text/plain";
|
|
107
|
+
}
|
|
108
|
+
return "application/octet-stream";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveWaveControlConfig(lanePaths, overrides = {}) {
|
|
112
|
+
return {
|
|
113
|
+
...(lanePaths?.waveControl || lanePaths?.laneProfile?.waveControl || {}),
|
|
114
|
+
...(overrides || {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildWorkspaceId(lanePaths, config) {
|
|
119
|
+
return normalizeText(config.workspaceId, buildWorkspaceTmuxToken(REPO_ROOT));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildProjectId(lanePaths, config) {
|
|
123
|
+
const projectName =
|
|
124
|
+
lanePaths?.projectId ||
|
|
125
|
+
config.projectId ||
|
|
126
|
+
lanePaths?.config?.projectId ||
|
|
127
|
+
lanePaths?.config?.projectName ||
|
|
128
|
+
path.basename(REPO_ROOT);
|
|
129
|
+
return normalizeTelemetryId(projectName, "wave");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildRuntimeVersion(lanePaths) {
|
|
133
|
+
return normalizeText(
|
|
134
|
+
lanePaths?.runtimeVersion,
|
|
135
|
+
normalizeText(readInstalledPackageMetadata()?.version, null),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function shouldUploadArtifactBody(descriptor, config) {
|
|
140
|
+
if (!descriptor?.present) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (descriptor.uploadPolicy === "local-only" || descriptor.uploadPolicy === "metadata-only") {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const allowedKinds = Array.isArray(config.uploadArtifactKinds) ? config.uploadArtifactKinds : [];
|
|
147
|
+
const kindAllowed = allowedKinds.includes(descriptor.kind);
|
|
148
|
+
if (descriptor.uploadPolicy === "full") {
|
|
149
|
+
return config.reportMode === "full-artifact-upload" && (allowedKinds.length === 0 || kindAllowed);
|
|
150
|
+
}
|
|
151
|
+
if (!kindAllowed) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return ["metadata-plus-selected", "full-artifact-upload"].includes(config.reportMode);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildInlineArtifactPayload(artifactDescriptor, sourcePath, config) {
|
|
158
|
+
if (!shouldUploadArtifactBody(artifactDescriptor, config) || !sourcePath || !fs.existsSync(sourcePath)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const stat = fs.statSync(sourcePath);
|
|
162
|
+
if (!stat.isFile() || stat.size > MAX_INLINE_ARTIFACT_BYTES) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const contentType = artifactDescriptor.contentType || contentTypeForPath(sourcePath);
|
|
166
|
+
const textLike = contentType.startsWith("text/") || contentType === "application/json";
|
|
167
|
+
const content = textLike
|
|
168
|
+
? fs.readFileSync(sourcePath, "utf8")
|
|
169
|
+
: fs.readFileSync(sourcePath).toString("base64");
|
|
170
|
+
return {
|
|
171
|
+
artifactId: artifactDescriptor.artifactId,
|
|
172
|
+
contentType,
|
|
173
|
+
encoding: textLike ? "utf8" : "base64",
|
|
174
|
+
content,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function deliveryStateDefaults(lanePaths, config, paths) {
|
|
179
|
+
return {
|
|
180
|
+
workspaceId: buildWorkspaceId(lanePaths, config),
|
|
181
|
+
lane: lanePaths?.lane || null,
|
|
182
|
+
runId: lanePaths?.runId || null,
|
|
183
|
+
runKind: lanePaths?.runKind || "unknown",
|
|
184
|
+
reportMode: config.reportMode || "metadata-plus-selected",
|
|
185
|
+
endpoint: config.endpoint || null,
|
|
186
|
+
queuePath: repoRelativePathOrNull(paths.pendingDir),
|
|
187
|
+
eventsPath: repoRelativePathOrNull(paths.eventsPath),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readDeliveryStateOrDefault(lanePaths, config, paths) {
|
|
192
|
+
return (
|
|
193
|
+
readWaveControlDeliveryState(paths.deliveryStatePath, deliveryStateDefaults(lanePaths, config, paths)) ||
|
|
194
|
+
writeWaveControlDeliveryState(paths.deliveryStatePath, {}, deliveryStateDefaults(lanePaths, config, paths))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeDeliveryState(lanePaths, config, paths, payload) {
|
|
199
|
+
return writeWaveControlDeliveryState(
|
|
200
|
+
paths.deliveryStatePath,
|
|
201
|
+
payload,
|
|
202
|
+
deliveryStateDefaults(lanePaths, config, paths),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function attestationForEvent(rawEvent, config) {
|
|
207
|
+
if (!rawEvent?.attestation && !rawEvent?.data?.attestation) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const attestationInput = rawEvent.attestation || rawEvent.data?.attestation;
|
|
211
|
+
return {
|
|
212
|
+
...(typeof attestationInput === "object" && attestationInput ? attestationInput : {}),
|
|
213
|
+
configHash: buildWaveControlConfigAttestationHash(attestationInput),
|
|
214
|
+
reportMode: config.reportMode,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function queueFilePath(paths, event) {
|
|
219
|
+
return path.join(
|
|
220
|
+
paths.pendingDir,
|
|
221
|
+
`${event.recordedAt.replace(/[-:.TZ]/g, "").slice(0, 14)}-${sanitizeToken(event.id)}.json`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sentFilePath(paths, event) {
|
|
226
|
+
return path.join(paths.sentDir, `${sanitizeToken(event.id)}.json`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function failedFilePath(paths, event) {
|
|
230
|
+
return path.join(paths.failedDir, `${sanitizeToken(event.id)}.json`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function overflowQueueFilePath(paths, filePath) {
|
|
234
|
+
return path.join(paths.failedDir, `overflow-${path.basename(filePath)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function enforcePendingQueueCap(paths, config) {
|
|
238
|
+
const maxPendingEvents = Number(config.maxPendingEvents || 0);
|
|
239
|
+
const pendingFiles = listPendingQueueFiles(paths);
|
|
240
|
+
if (!maxPendingEvents || pendingFiles.length <= maxPendingEvents) {
|
|
241
|
+
return { dropped: 0, pendingCount: pendingFiles.length };
|
|
242
|
+
}
|
|
243
|
+
const overflowFiles = pendingFiles.slice(0, pendingFiles.length - maxPendingEvents);
|
|
244
|
+
for (const filePath of overflowFiles) {
|
|
245
|
+
fs.renameSync(filePath, overflowQueueFilePath(paths, filePath));
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
dropped: overflowFiles.length,
|
|
249
|
+
pendingCount: maxPendingEvents,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildWaveControlArtifactFromPath(filePath, options = {}) {
|
|
254
|
+
const absolutePath = path.isAbsolute(String(filePath || ""))
|
|
255
|
+
? String(filePath)
|
|
256
|
+
: path.resolve(REPO_ROOT, String(filePath || ""));
|
|
257
|
+
const relativePath = repoRelativePathOrNull(absolutePath) || normalizeText(options.path, null);
|
|
258
|
+
const stat = absolutePath && fs.existsSync(absolutePath) ? fs.statSync(absolutePath) : null;
|
|
259
|
+
return normalizeWaveControlArtifactDescriptor({
|
|
260
|
+
path: relativePath,
|
|
261
|
+
kind: normalizeText(options.kind, "artifact"),
|
|
262
|
+
required: options.required === true,
|
|
263
|
+
present: Boolean(stat?.isFile()),
|
|
264
|
+
sha256: stat?.isFile() ? fileHashOrNull(absolutePath) : null,
|
|
265
|
+
bytes: stat?.isFile() ? stat.size : null,
|
|
266
|
+
contentType: normalizeText(options.contentType, contentTypeForPath(absolutePath)),
|
|
267
|
+
uploadPolicy: normalizeText(options.uploadPolicy, "metadata-only"),
|
|
268
|
+
label: normalizeText(options.label, null),
|
|
269
|
+
recordedAt: normalizeText(options.recordedAt, null),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function queueWaveControlEvent(lanePaths, rawEvent, options = {}) {
|
|
274
|
+
const config = resolveWaveControlConfig(lanePaths, options.config);
|
|
275
|
+
if (config.enabled === false) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const paths = telemetryPaths(lanePaths);
|
|
279
|
+
ensureTelemetryDirs(paths);
|
|
280
|
+
const artifacts = (Array.isArray(rawEvent?.artifacts) ? rawEvent.artifacts : []).map((artifact) =>
|
|
281
|
+
normalizeWaveControlArtifactDescriptor(artifact),
|
|
282
|
+
);
|
|
283
|
+
const event = normalizeWaveControlEventEnvelope(
|
|
284
|
+
{
|
|
285
|
+
...rawEvent,
|
|
286
|
+
identity: {
|
|
287
|
+
workspaceId: buildWorkspaceId(lanePaths, config),
|
|
288
|
+
projectId: buildProjectId(lanePaths, config),
|
|
289
|
+
runId: lanePaths?.runId || null,
|
|
290
|
+
runKind: lanePaths?.runKind || "roadmap",
|
|
291
|
+
lane: lanePaths?.lane || null,
|
|
292
|
+
orchestratorId: lanePaths?.orchestratorId || null,
|
|
293
|
+
runtimeVersion: buildRuntimeVersion(lanePaths),
|
|
294
|
+
...(rawEvent?.identity || {}),
|
|
295
|
+
},
|
|
296
|
+
data: {
|
|
297
|
+
...(rawEvent?.data || {}),
|
|
298
|
+
...(attestationForEvent(rawEvent, config)
|
|
299
|
+
? { attestation: attestationForEvent(rawEvent, config) }
|
|
300
|
+
: {}),
|
|
301
|
+
},
|
|
302
|
+
artifacts,
|
|
303
|
+
},
|
|
304
|
+
options.defaults || {},
|
|
305
|
+
);
|
|
306
|
+
const sourcePaths = Object.fromEntries(
|
|
307
|
+
artifacts
|
|
308
|
+
.map((artifact, index) => {
|
|
309
|
+
const sourcePath =
|
|
310
|
+
rawEvent?.artifactSourcePaths?.[artifact.artifactId] ||
|
|
311
|
+
rawEvent?.artifactSourcePaths?.[artifact.path] ||
|
|
312
|
+
rawEvent?.artifacts?.[index]?.sourcePath ||
|
|
313
|
+
(artifact.path ? path.resolve(REPO_ROOT, artifact.path) : null);
|
|
314
|
+
return sourcePath ? [artifact.artifactId, sourcePath] : null;
|
|
315
|
+
})
|
|
316
|
+
.filter(Boolean),
|
|
317
|
+
);
|
|
318
|
+
const queuePath = queueFilePath(paths, event);
|
|
319
|
+
fs.appendFileSync(paths.eventsPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
queuePath,
|
|
322
|
+
`${JSON.stringify(
|
|
323
|
+
{
|
|
324
|
+
schemaVersion: 1,
|
|
325
|
+
kind: "wave-control-queued-event",
|
|
326
|
+
enqueuedAt: toIsoTimestamp(),
|
|
327
|
+
event,
|
|
328
|
+
artifactSourcePaths: sourcePaths,
|
|
329
|
+
},
|
|
330
|
+
null,
|
|
331
|
+
2,
|
|
332
|
+
)}\n`,
|
|
333
|
+
"utf8",
|
|
334
|
+
);
|
|
335
|
+
const deliveryState = readDeliveryStateOrDefault(lanePaths, config, paths);
|
|
336
|
+
const queueCap = enforcePendingQueueCap(paths, config);
|
|
337
|
+
const queueOverflowMessage =
|
|
338
|
+
queueCap.dropped > 0
|
|
339
|
+
? `Wave Control pending queue exceeded maxPendingEvents=${config.maxPendingEvents}; dropped ${queueCap.dropped} oldest pending event(s) from remote delivery queue.`
|
|
340
|
+
: null;
|
|
341
|
+
writeDeliveryState(lanePaths, config, paths, {
|
|
342
|
+
...deliveryState,
|
|
343
|
+
pendingCount: queueCap.pendingCount,
|
|
344
|
+
failedCount: (deliveryState.failedCount || 0) + queueCap.dropped,
|
|
345
|
+
lastEnqueuedAt: event.recordedAt,
|
|
346
|
+
...(queueOverflowMessage
|
|
347
|
+
? {
|
|
348
|
+
lastError: {
|
|
349
|
+
message: queueOverflowMessage,
|
|
350
|
+
failedAt: toIsoTimestamp(),
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
354
|
+
recentEventIds: [...(deliveryState.recentEventIds || []).slice(-19), event.id],
|
|
355
|
+
updatedAt: toIsoTimestamp(),
|
|
356
|
+
});
|
|
357
|
+
return event;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function safeQueueWaveControlEvent(lanePaths, rawEvent, options = {}) {
|
|
361
|
+
try {
|
|
362
|
+
return queueWaveControlEvent(lanePaths, rawEvent, options);
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveIngestUrl(endpoint) {
|
|
369
|
+
const normalized = normalizeText(endpoint, null);
|
|
370
|
+
if (!normalized) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (/\/ingest\/batches\/?$/.test(normalized)) {
|
|
374
|
+
return normalized;
|
|
375
|
+
}
|
|
376
|
+
return `${normalized.replace(/\/$/, "")}/ingest/batches`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function postBatch(url, token, timeoutMs, payload) {
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
382
|
+
try {
|
|
383
|
+
const response = await fetch(url, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: {
|
|
386
|
+
"content-type": "application/json",
|
|
387
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify(payload),
|
|
390
|
+
signal: controller.signal,
|
|
391
|
+
});
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
const body = await response.text();
|
|
394
|
+
throw new Error(`Wave Control ingest failed (${response.status}): ${body || response.statusText}`);
|
|
395
|
+
}
|
|
396
|
+
return response;
|
|
397
|
+
} finally {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function flushWaveControlQueue(lanePaths, options = {}) {
|
|
403
|
+
const config = resolveWaveControlConfig(lanePaths, options.config);
|
|
404
|
+
if (config.enabled === false) {
|
|
405
|
+
return { attempted: 0, sent: 0, failed: 0, pending: 0, disabled: true };
|
|
406
|
+
}
|
|
407
|
+
const paths = telemetryPaths(lanePaths);
|
|
408
|
+
ensureTelemetryDirs(paths);
|
|
409
|
+
const deliveryState = readDeliveryStateOrDefault(lanePaths, config, paths);
|
|
410
|
+
const pendingFiles = listPendingQueueFiles(paths)
|
|
411
|
+
.slice(0, options.limit || config.flushBatchSize || 25)
|
|
412
|
+
if (!config.endpoint) {
|
|
413
|
+
writeDeliveryState(lanePaths, config, paths, {
|
|
414
|
+
...deliveryState,
|
|
415
|
+
pendingCount: listPendingQueueFiles(paths).length,
|
|
416
|
+
updatedAt: toIsoTimestamp(),
|
|
417
|
+
});
|
|
418
|
+
return { attempted: 0, sent: 0, failed: 0, pending: listPendingQueueFiles(paths).length };
|
|
419
|
+
}
|
|
420
|
+
if (pendingFiles.length === 0) {
|
|
421
|
+
return { attempted: 0, sent: 0, failed: 0, pending: 0 };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const queuedEvents = pendingFiles.map((filePath) => {
|
|
425
|
+
const payload = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
426
|
+
return { filePath, payload };
|
|
427
|
+
});
|
|
428
|
+
const hydratedEvents = queuedEvents.map(({ payload }) => {
|
|
429
|
+
const artifactUploads = (payload.event.artifacts || [])
|
|
430
|
+
.map((artifact) =>
|
|
431
|
+
buildInlineArtifactPayload(
|
|
432
|
+
artifact,
|
|
433
|
+
payload.artifactSourcePaths?.[artifact.artifactId] || null,
|
|
434
|
+
config,
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
.filter(Boolean);
|
|
438
|
+
return {
|
|
439
|
+
...payload.event,
|
|
440
|
+
artifactUploads,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const ingestUrl = resolveIngestUrl(config.endpoint);
|
|
446
|
+
const authToken = config.authTokenEnvVar ? process.env[config.authTokenEnvVar] || "" : "";
|
|
447
|
+
await postBatch(
|
|
448
|
+
ingestUrl,
|
|
449
|
+
authToken,
|
|
450
|
+
options.timeoutMs || config.requestTimeoutMs || 5000,
|
|
451
|
+
{
|
|
452
|
+
workspaceId: buildWorkspaceId(lanePaths, config),
|
|
453
|
+
lane: lanePaths?.lane || null,
|
|
454
|
+
runKind: lanePaths?.runKind || "unknown",
|
|
455
|
+
runId: lanePaths?.runId || null,
|
|
456
|
+
sentAt: toIsoTimestamp(),
|
|
457
|
+
events: hydratedEvents,
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
for (const { filePath, payload } of queuedEvents) {
|
|
461
|
+
fs.renameSync(filePath, sentFilePath(paths, payload.event));
|
|
462
|
+
}
|
|
463
|
+
writeDeliveryState(lanePaths, config, paths, {
|
|
464
|
+
...deliveryState,
|
|
465
|
+
pendingCount: listPendingQueueFiles(paths).length,
|
|
466
|
+
sentCount: (deliveryState.sentCount || 0) + pendingFiles.length,
|
|
467
|
+
lastFlushAt: toIsoTimestamp(),
|
|
468
|
+
lastSuccessAt: toIsoTimestamp(),
|
|
469
|
+
lastError: null,
|
|
470
|
+
updatedAt: toIsoTimestamp(),
|
|
471
|
+
});
|
|
472
|
+
return {
|
|
473
|
+
attempted: pendingFiles.length,
|
|
474
|
+
sent: pendingFiles.length,
|
|
475
|
+
failed: 0,
|
|
476
|
+
pending: listPendingQueueFiles(paths).length,
|
|
477
|
+
};
|
|
478
|
+
} catch (error) {
|
|
479
|
+
for (const { filePath, payload } of queuedEvents) {
|
|
480
|
+
fs.copyFileSync(filePath, failedFilePath(paths, payload.event));
|
|
481
|
+
}
|
|
482
|
+
writeDeliveryState(lanePaths, config, paths, {
|
|
483
|
+
...deliveryState,
|
|
484
|
+
pendingCount: listPendingQueueFiles(paths).length,
|
|
485
|
+
failedCount: (deliveryState.failedCount || 0) + pendingFiles.length,
|
|
486
|
+
lastFlushAt: toIsoTimestamp(),
|
|
487
|
+
lastError: {
|
|
488
|
+
message: error instanceof Error ? error.message : String(error),
|
|
489
|
+
failedAt: toIsoTimestamp(),
|
|
490
|
+
},
|
|
491
|
+
updatedAt: toIsoTimestamp(),
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
attempted: pendingFiles.length,
|
|
495
|
+
sent: 0,
|
|
496
|
+
failed: pendingFiles.length,
|
|
497
|
+
pending: listPendingQueueFiles(paths).length,
|
|
498
|
+
error,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function readWaveControlQueueState(lanePaths, options = {}) {
|
|
504
|
+
const config = resolveWaveControlConfig(lanePaths, options.config);
|
|
505
|
+
if (config.enabled === false) {
|
|
506
|
+
return {
|
|
507
|
+
workspaceId: buildWorkspaceId(lanePaths, config),
|
|
508
|
+
lane: lanePaths?.lane || null,
|
|
509
|
+
runId: lanePaths?.runId || null,
|
|
510
|
+
runKind: lanePaths?.runKind || "unknown",
|
|
511
|
+
reportMode: config.reportMode || "metadata-plus-selected",
|
|
512
|
+
endpoint: config.endpoint || null,
|
|
513
|
+
pendingCount: 0,
|
|
514
|
+
sentCount: 0,
|
|
515
|
+
failedCount: 0,
|
|
516
|
+
telemetryDir: repoRelativePathOrNull(
|
|
517
|
+
lanePaths?.telemetryDir || path.join(lanePaths.controlPlaneDir, "telemetry"),
|
|
518
|
+
),
|
|
519
|
+
disabled: true,
|
|
520
|
+
updatedAt: toIsoTimestamp(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const paths = telemetryPaths(lanePaths);
|
|
524
|
+
ensureTelemetryDirs(paths);
|
|
525
|
+
const state = readDeliveryStateOrDefault(lanePaths, config, paths);
|
|
526
|
+
const pendingFiles = fs.readdirSync(paths.pendingDir).filter((fileName) => fileName.endsWith(".json"));
|
|
527
|
+
return {
|
|
528
|
+
...state,
|
|
529
|
+
pendingCount: pendingFiles.length,
|
|
530
|
+
telemetryDir: repoRelativePathOrNull(paths.telemetryDir),
|
|
531
|
+
};
|
|
532
|
+
}
|