@chllming/wave-orchestration 0.5.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 +41 -0
- package/README.md +549 -0
- package/docs/agents/wave-deploy-verifier-role.md +34 -0
- package/docs/agents/wave-documentation-role.md +30 -0
- package/docs/agents/wave-evaluator-role.md +43 -0
- package/docs/agents/wave-infra-role.md +34 -0
- package/docs/agents/wave-integration-role.md +32 -0
- package/docs/agents/wave-launcher-role.md +37 -0
- package/docs/context7/bundles.json +91 -0
- package/docs/plans/component-cutover-matrix.json +112 -0
- package/docs/plans/component-cutover-matrix.md +49 -0
- package/docs/plans/context7-wave-orchestrator.md +130 -0
- package/docs/plans/current-state.md +44 -0
- package/docs/plans/master-plan.md +16 -0
- package/docs/plans/migration.md +23 -0
- package/docs/plans/wave-orchestrator.md +254 -0
- package/docs/plans/waves/wave-0.md +165 -0
- package/docs/reference/github-packages-setup.md +52 -0
- package/docs/reference/migration-0.2-to-0.5.md +622 -0
- package/docs/reference/npmjs-trusted-publishing.md +55 -0
- package/docs/reference/repository-guidance.md +18 -0
- package/docs/reference/runtime-config/README.md +85 -0
- package/docs/reference/runtime-config/claude.md +105 -0
- package/docs/reference/runtime-config/codex.md +81 -0
- package/docs/reference/runtime-config/opencode.md +93 -0
- package/docs/research/agent-context-sources.md +57 -0
- package/docs/roadmap.md +626 -0
- package/package.json +53 -0
- package/releases/manifest.json +101 -0
- package/scripts/context7-api-check.sh +21 -0
- package/scripts/context7-export-env.sh +52 -0
- package/scripts/research/agent-context-archive.mjs +472 -0
- package/scripts/research/generate-agent-context-indexes.mjs +85 -0
- package/scripts/research/import-agent-context-archive.mjs +793 -0
- package/scripts/research/manifests/harness-and-blackboard-2026-03-21.mjs +201 -0
- package/scripts/wave-autonomous.mjs +13 -0
- package/scripts/wave-cli-bootstrap.mjs +27 -0
- package/scripts/wave-dashboard.mjs +11 -0
- package/scripts/wave-human-feedback.mjs +11 -0
- package/scripts/wave-launcher.mjs +11 -0
- package/scripts/wave-local-executor.mjs +13 -0
- package/scripts/wave-orchestrator/agent-state.mjs +416 -0
- package/scripts/wave-orchestrator/autonomous.mjs +367 -0
- package/scripts/wave-orchestrator/clarification-triage.mjs +605 -0
- package/scripts/wave-orchestrator/config.mjs +848 -0
- package/scripts/wave-orchestrator/context7.mjs +464 -0
- package/scripts/wave-orchestrator/coord-cli.mjs +286 -0
- package/scripts/wave-orchestrator/coordination-store.mjs +987 -0
- package/scripts/wave-orchestrator/coordination.mjs +768 -0
- package/scripts/wave-orchestrator/dashboard-renderer.mjs +254 -0
- package/scripts/wave-orchestrator/dashboard-state.mjs +473 -0
- package/scripts/wave-orchestrator/dep-cli.mjs +219 -0
- package/scripts/wave-orchestrator/docs-queue.mjs +75 -0
- package/scripts/wave-orchestrator/executors.mjs +385 -0
- package/scripts/wave-orchestrator/feedback.mjs +372 -0
- package/scripts/wave-orchestrator/install.mjs +540 -0
- package/scripts/wave-orchestrator/launcher.mjs +3879 -0
- package/scripts/wave-orchestrator/ledger.mjs +332 -0
- package/scripts/wave-orchestrator/local-executor.mjs +263 -0
- package/scripts/wave-orchestrator/replay.mjs +246 -0
- package/scripts/wave-orchestrator/roots.mjs +10 -0
- package/scripts/wave-orchestrator/routing-state.mjs +542 -0
- package/scripts/wave-orchestrator/shared.mjs +405 -0
- package/scripts/wave-orchestrator/terminals.mjs +209 -0
- package/scripts/wave-orchestrator/traces.mjs +1094 -0
- package/scripts/wave-orchestrator/wave-files.mjs +1923 -0
- package/scripts/wave.mjs +103 -0
- package/wave.config.json +115 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { withFileLock } from "./coordination.mjs";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
7
|
+
DEFAULT_WATCH_REFRESH_MS,
|
|
8
|
+
DEFAULT_WAVE_LANE,
|
|
9
|
+
REPO_ROOT,
|
|
10
|
+
buildLanePaths,
|
|
11
|
+
compactSingleLine,
|
|
12
|
+
ensureDirectory,
|
|
13
|
+
formatAgeFromTimestamp,
|
|
14
|
+
parseNonNegativeInt,
|
|
15
|
+
parsePositiveInt,
|
|
16
|
+
readJsonOrNull,
|
|
17
|
+
sanitizeLaneName,
|
|
18
|
+
sleep,
|
|
19
|
+
toIsoTimestamp,
|
|
20
|
+
truncate,
|
|
21
|
+
writeJsonAtomic,
|
|
22
|
+
} from "./shared.mjs";
|
|
23
|
+
|
|
24
|
+
function sanitizeToken(value) {
|
|
25
|
+
const token = String(value || "")
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/\s+/g, "-")
|
|
28
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
29
|
+
.replace(/-+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "");
|
|
31
|
+
if (!token) {
|
|
32
|
+
throw new Error("Invalid token");
|
|
33
|
+
}
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requestFilePath(feedbackRequestsDir, requestId) {
|
|
38
|
+
return path.join(feedbackRequestsDir, `${requestId}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildRequestId({ lane, wave, agentId }) {
|
|
42
|
+
const ts = new Date().toISOString().replace(/[-:.TZ]/g, "");
|
|
43
|
+
const random = crypto.randomBytes(3).toString("hex");
|
|
44
|
+
return `${ts}-${sanitizeToken(lane)}-w${wave}-${sanitizeToken(agentId)}-${random}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createFeedbackRequest({
|
|
48
|
+
feedbackStateDir,
|
|
49
|
+
feedbackRequestsDir,
|
|
50
|
+
lane,
|
|
51
|
+
wave,
|
|
52
|
+
agentId,
|
|
53
|
+
orchestratorId,
|
|
54
|
+
question,
|
|
55
|
+
context,
|
|
56
|
+
}) {
|
|
57
|
+
ensureDirectory(feedbackRequestsDir);
|
|
58
|
+
const requestId = buildRequestId({ lane, wave, agentId });
|
|
59
|
+
const filePath = requestFilePath(feedbackRequestsDir, requestId);
|
|
60
|
+
const now = toIsoTimestamp();
|
|
61
|
+
const payload = {
|
|
62
|
+
id: requestId,
|
|
63
|
+
createdAt: now,
|
|
64
|
+
updatedAt: now,
|
|
65
|
+
lane,
|
|
66
|
+
wave,
|
|
67
|
+
agentId,
|
|
68
|
+
orchestratorId: orchestratorId || null,
|
|
69
|
+
status: "pending",
|
|
70
|
+
question: String(question || "").trim(),
|
|
71
|
+
context: String(context || "").trim(),
|
|
72
|
+
response: null,
|
|
73
|
+
};
|
|
74
|
+
withFileLock(path.join(feedbackStateDir, "requests.lock"), () => {
|
|
75
|
+
if (fs.existsSync(filePath)) {
|
|
76
|
+
throw new Error(`Request already exists: ${path.relative(REPO_ROOT, filePath)}`);
|
|
77
|
+
}
|
|
78
|
+
writeJsonAtomic(filePath, payload);
|
|
79
|
+
});
|
|
80
|
+
return { requestId, filePath, payload };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function answerFeedbackRequest({
|
|
84
|
+
feedbackStateDir,
|
|
85
|
+
feedbackRequestsDir,
|
|
86
|
+
requestId,
|
|
87
|
+
response,
|
|
88
|
+
operator = "human-operator",
|
|
89
|
+
force = false,
|
|
90
|
+
}) {
|
|
91
|
+
const lockPath = path.join(feedbackStateDir, "requests.lock");
|
|
92
|
+
let answeredPayload = null;
|
|
93
|
+
withFileLock(lockPath, () => {
|
|
94
|
+
const filePath = requestFilePath(feedbackRequestsDir, requestId);
|
|
95
|
+
const existing = readJsonOrNull(filePath);
|
|
96
|
+
if (!existing) {
|
|
97
|
+
throw new Error(`Request not found: ${requestId}`);
|
|
98
|
+
}
|
|
99
|
+
if (existing.status === "answered" && !force) {
|
|
100
|
+
throw new Error(`Request already answered: ${requestId} (use --force to override)`);
|
|
101
|
+
}
|
|
102
|
+
answeredPayload = {
|
|
103
|
+
...existing,
|
|
104
|
+
status: "answered",
|
|
105
|
+
updatedAt: toIsoTimestamp(),
|
|
106
|
+
response: {
|
|
107
|
+
operator,
|
|
108
|
+
text: response,
|
|
109
|
+
answeredAt: toIsoTimestamp(),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
writeJsonAtomic(filePath, answeredPayload);
|
|
113
|
+
});
|
|
114
|
+
return answeredPayload;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readAllRequests(feedbackRequestsDir) {
|
|
118
|
+
ensureDirectory(feedbackRequestsDir);
|
|
119
|
+
return fs
|
|
120
|
+
.readdirSync(feedbackRequestsDir)
|
|
121
|
+
.filter((fileName) => fileName.endsWith(".json"))
|
|
122
|
+
.map((fileName) => path.join(feedbackRequestsDir, fileName))
|
|
123
|
+
.map((filePath) => ({ payload: readJsonOrNull(filePath), filePath }))
|
|
124
|
+
.filter(({ payload }) => payload && typeof payload === "object")
|
|
125
|
+
.map(({ payload, filePath }) => ({ ...payload, __filePath: filePath }))
|
|
126
|
+
.toSorted(
|
|
127
|
+
(a, b) => Date.parse(String(a.createdAt || "")) - Date.parse(String(b.createdAt || "")),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function filterRequests(requests, filters) {
|
|
132
|
+
return requests.filter((request) => {
|
|
133
|
+
if (filters.pending && request.status !== "pending") {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (filters.lane && String(request.lane) !== String(filters.lane)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (filters.wave !== null && Number(request.wave) !== Number(filters.wave)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (filters.agent && String(request.agentId) !== String(filters.agent)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatRequestRow(request) {
|
|
150
|
+
return [
|
|
151
|
+
truncate(request.id, 28).padEnd(28, " "),
|
|
152
|
+
truncate(request.status, 10).padEnd(10, " "),
|
|
153
|
+
truncate(request.lane, 12).padEnd(12, " "),
|
|
154
|
+
truncate(`w${request.wave}`, 6).padEnd(6, " "),
|
|
155
|
+
truncate(request.agentId, 10).padEnd(10, " "),
|
|
156
|
+
truncate(formatAgeFromTimestamp(Date.parse(String(request.updatedAt || ""))), 12).padEnd(
|
|
157
|
+
12,
|
|
158
|
+
" ",
|
|
159
|
+
),
|
|
160
|
+
truncate(request.question, 72),
|
|
161
|
+
].join(" ");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseFeedbackArgs(argv) {
|
|
165
|
+
const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
|
|
166
|
+
const subcommand = normalizedArgv[0];
|
|
167
|
+
if (
|
|
168
|
+
!subcommand ||
|
|
169
|
+
["ask", "respond", "list", "watch", "show"].every((value) => value !== subcommand)
|
|
170
|
+
) {
|
|
171
|
+
throw new Error("Expected subcommand: ask | respond | list | watch | show");
|
|
172
|
+
}
|
|
173
|
+
const args = normalizedArgv.slice(1);
|
|
174
|
+
const out = {
|
|
175
|
+
subcommand,
|
|
176
|
+
lane: DEFAULT_WAVE_LANE,
|
|
177
|
+
wave: null,
|
|
178
|
+
agent: null,
|
|
179
|
+
question: "",
|
|
180
|
+
context: "",
|
|
181
|
+
orchestratorId: "",
|
|
182
|
+
wait: false,
|
|
183
|
+
timeoutSeconds: DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
184
|
+
id: "",
|
|
185
|
+
response: "",
|
|
186
|
+
operator: "human-operator",
|
|
187
|
+
force: false,
|
|
188
|
+
pending: false,
|
|
189
|
+
json: false,
|
|
190
|
+
refreshMs: DEFAULT_WATCH_REFRESH_MS,
|
|
191
|
+
};
|
|
192
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
193
|
+
const arg = args[i];
|
|
194
|
+
if (arg === "--") {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (arg === "--lane") {
|
|
198
|
+
out.lane = sanitizeLaneName(args[++i]);
|
|
199
|
+
} else if (arg === "--wave") {
|
|
200
|
+
out.wave = parseNonNegativeInt(args[++i], "--wave");
|
|
201
|
+
} else if (arg === "--agent") {
|
|
202
|
+
out.agent = String(args[++i] || "").trim();
|
|
203
|
+
} else if (arg === "--question") {
|
|
204
|
+
out.question = String(args[++i] || "").trim();
|
|
205
|
+
} else if (arg === "--context") {
|
|
206
|
+
out.context = String(args[++i] || "").trim();
|
|
207
|
+
} else if (arg === "--orchestrator-id") {
|
|
208
|
+
out.orchestratorId = String(args[++i] || "").trim();
|
|
209
|
+
} else if (arg === "--wait") {
|
|
210
|
+
out.wait = true;
|
|
211
|
+
} else if (arg === "--timeout-seconds") {
|
|
212
|
+
out.timeoutSeconds = parsePositiveInt(args[++i], "--timeout-seconds");
|
|
213
|
+
} else if (arg === "--id") {
|
|
214
|
+
out.id = String(args[++i] || "").trim();
|
|
215
|
+
} else if (arg === "--response") {
|
|
216
|
+
out.response = String(args[++i] || "").trim();
|
|
217
|
+
} else if (arg === "--operator") {
|
|
218
|
+
out.operator = String(args[++i] || "").trim() || "human-operator";
|
|
219
|
+
} else if (arg === "--force") {
|
|
220
|
+
out.force = true;
|
|
221
|
+
} else if (arg === "--pending") {
|
|
222
|
+
out.pending = true;
|
|
223
|
+
} else if (arg === "--json") {
|
|
224
|
+
out.json = true;
|
|
225
|
+
} else if (arg === "--refresh-ms") {
|
|
226
|
+
out.refreshMs = parsePositiveInt(args[++i], "--refresh-ms");
|
|
227
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
228
|
+
return { help: true, options: out };
|
|
229
|
+
} else {
|
|
230
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { help: false, options: out };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function waitForAnswer(filePath, timeoutSeconds) {
|
|
237
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
238
|
+
while (Date.now() < deadline) {
|
|
239
|
+
const payload = readJsonOrNull(filePath);
|
|
240
|
+
if (payload?.status === "answered") {
|
|
241
|
+
return payload;
|
|
242
|
+
}
|
|
243
|
+
await sleep(500);
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Timed out waiting for response after ${timeoutSeconds}s`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function printHelp() {
|
|
249
|
+
console.log(`Usage:
|
|
250
|
+
pnpm exec wave-feedback ask --lane <lane> --wave <n> --agent <id> --question "<text>" [options]
|
|
251
|
+
pnpm exec wave-feedback respond --id <request-id> --response "<text>" [options]
|
|
252
|
+
pnpm exec wave-feedback list [--pending] [--lane <lane>] [--wave <n>] [--agent <id>] [--json]
|
|
253
|
+
pnpm exec wave-feedback watch [--pending] [--lane <lane>] [--wave <n>] [--agent <id>] [--refresh-ms <n>]
|
|
254
|
+
pnpm exec wave-feedback show --id <request-id>
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function runFeedbackCli(argv) {
|
|
259
|
+
const { help, options } = parseFeedbackArgs(argv);
|
|
260
|
+
if (help) {
|
|
261
|
+
printHelp();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const lanePaths = buildLanePaths(options.lane);
|
|
265
|
+
const requestsDir = lanePaths.feedbackRequestsDir;
|
|
266
|
+
const stateDir = lanePaths.feedbackStateDir;
|
|
267
|
+
|
|
268
|
+
if (options.subcommand === "ask") {
|
|
269
|
+
if (options.wave === null || !options.agent || !options.question) {
|
|
270
|
+
throw new Error("ask requires --wave, --agent, and --question");
|
|
271
|
+
}
|
|
272
|
+
const result = createFeedbackRequest({
|
|
273
|
+
feedbackStateDir: stateDir,
|
|
274
|
+
feedbackRequestsDir: requestsDir,
|
|
275
|
+
lane: options.lane,
|
|
276
|
+
wave: options.wave,
|
|
277
|
+
agentId: options.agent,
|
|
278
|
+
orchestratorId: options.orchestratorId,
|
|
279
|
+
question: options.question,
|
|
280
|
+
context: options.context,
|
|
281
|
+
});
|
|
282
|
+
console.log(`[wave-human-feedback] created ${result.requestId}`);
|
|
283
|
+
console.log(`file: ${path.relative(REPO_ROOT, result.filePath)}`);
|
|
284
|
+
if (options.wait) {
|
|
285
|
+
const answered = await waitForAnswer(result.filePath, options.timeoutSeconds);
|
|
286
|
+
console.log(
|
|
287
|
+
`[wave-human-feedback] answered by ${answered?.response?.operator || "human-operator"}`,
|
|
288
|
+
);
|
|
289
|
+
console.log(answered?.response?.text || "");
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (options.subcommand === "respond") {
|
|
295
|
+
if (!options.id || !options.response) {
|
|
296
|
+
throw new Error("respond requires --id and --response");
|
|
297
|
+
}
|
|
298
|
+
answerFeedbackRequest({
|
|
299
|
+
feedbackStateDir: stateDir,
|
|
300
|
+
feedbackRequestsDir: requestsDir,
|
|
301
|
+
requestId: options.id,
|
|
302
|
+
response: options.response,
|
|
303
|
+
operator: options.operator,
|
|
304
|
+
force: options.force,
|
|
305
|
+
});
|
|
306
|
+
console.log(`[wave-human-feedback] answered ${options.id}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.subcommand === "show") {
|
|
311
|
+
if (!options.id) {
|
|
312
|
+
throw new Error("show requires --id");
|
|
313
|
+
}
|
|
314
|
+
const filePath = requestFilePath(requestsDir, options.id);
|
|
315
|
+
const payload = readJsonOrNull(filePath);
|
|
316
|
+
if (!payload) {
|
|
317
|
+
throw new Error(`Request not found: ${options.id}`);
|
|
318
|
+
}
|
|
319
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const renderTable = () => {
|
|
324
|
+
const requests = filterRequests(readAllRequests(requestsDir), {
|
|
325
|
+
pending: options.pending,
|
|
326
|
+
lane: options.lane || null,
|
|
327
|
+
wave: options.wave,
|
|
328
|
+
agent: options.agent || null,
|
|
329
|
+
});
|
|
330
|
+
if (options.json) {
|
|
331
|
+
console.log(JSON.stringify(requests, null, 2));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.log(
|
|
335
|
+
[
|
|
336
|
+
"ID".padEnd(28),
|
|
337
|
+
"Status".padEnd(10),
|
|
338
|
+
"Lane".padEnd(12),
|
|
339
|
+
"Wave".padEnd(6),
|
|
340
|
+
"Agent".padEnd(10),
|
|
341
|
+
"Updated".padEnd(12),
|
|
342
|
+
"Question",
|
|
343
|
+
].join(" "),
|
|
344
|
+
);
|
|
345
|
+
for (const request of requests) {
|
|
346
|
+
console.log(
|
|
347
|
+
formatRequestRow({
|
|
348
|
+
...request,
|
|
349
|
+
question: compactSingleLine(request.question, 72),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (requests.length === 0) {
|
|
354
|
+
console.log("(none)");
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (options.subcommand === "list") {
|
|
359
|
+
renderTable();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (options.subcommand === "watch") {
|
|
364
|
+
while (true) {
|
|
365
|
+
if (process.stdout.isTTY) {
|
|
366
|
+
process.stdout.write("\u001bc");
|
|
367
|
+
}
|
|
368
|
+
renderTable();
|
|
369
|
+
await sleep(options.refreshMs);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|