@chllming/wave-orchestration 0.8.8 → 0.9.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +23 -8
  3. package/docs/README.md +5 -3
  4. package/docs/concepts/context7-vs-skills.md +1 -1
  5. package/docs/concepts/operating-modes.md +1 -1
  6. package/docs/concepts/what-is-a-wave.md +1 -1
  7. package/docs/guides/author-and-run-waves.md +14 -1
  8. package/docs/guides/monorepo-projects.md +226 -0
  9. package/docs/guides/planner.md +9 -2
  10. package/docs/guides/{recommendations-0.8.8.md → recommendations-0.9.0.md} +7 -7
  11. package/docs/plans/current-state.md +8 -6
  12. package/docs/plans/end-state-architecture.md +1 -1
  13. package/docs/plans/examples/wave-example-design-handoff.md +3 -1
  14. package/docs/plans/examples/wave-example-live-proof.md +6 -1
  15. package/docs/plans/examples/wave-example-rollout-fidelity.md +2 -0
  16. package/docs/plans/migration.md +21 -18
  17. package/docs/plans/wave-orchestrator.md +4 -4
  18. package/docs/reference/cli-reference.md +55 -51
  19. package/docs/reference/coordination-and-closure.md +1 -1
  20. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  21. package/docs/reference/runtime-config/README.md +140 -12
  22. package/docs/reference/sample-waves.md +100 -5
  23. package/docs/reference/skills.md +1 -1
  24. package/docs/reference/wave-control.md +23 -5
  25. package/docs/roadmap.md +2 -2
  26. package/package.json +1 -1
  27. package/releases/manifest.json +37 -0
  28. package/scripts/wave-orchestrator/adhoc.mjs +49 -17
  29. package/scripts/wave-orchestrator/autonomous.mjs +49 -15
  30. package/scripts/wave-orchestrator/benchmark-external.mjs +23 -7
  31. package/scripts/wave-orchestrator/benchmark.mjs +33 -10
  32. package/scripts/wave-orchestrator/config.mjs +239 -24
  33. package/scripts/wave-orchestrator/control-cli.mjs +29 -23
  34. package/scripts/wave-orchestrator/coord-cli.mjs +22 -14
  35. package/scripts/wave-orchestrator/coordination-store.mjs +8 -0
  36. package/scripts/wave-orchestrator/dashboard-renderer.mjs +10 -3
  37. package/scripts/wave-orchestrator/dep-cli.mjs +47 -21
  38. package/scripts/wave-orchestrator/feedback.mjs +28 -11
  39. package/scripts/wave-orchestrator/human-input-resolution.mjs +5 -1
  40. package/scripts/wave-orchestrator/launcher.mjs +200 -112
  41. package/scripts/wave-orchestrator/planner.mjs +48 -27
  42. package/scripts/wave-orchestrator/project-profile.mjs +31 -8
  43. package/scripts/wave-orchestrator/proof-cli.mjs +18 -12
  44. package/scripts/wave-orchestrator/retry-cli.mjs +19 -13
  45. package/scripts/wave-orchestrator/shared.mjs +77 -14
  46. package/scripts/wave-orchestrator/traces.mjs +7 -0
  47. package/scripts/wave-orchestrator/wave-control-client.mjs +84 -16
  48. package/scripts/wave-orchestrator/wave-files.mjs +5 -1
  49. package/scripts/wave-orchestrator/wave-state-reducer.mjs +8 -1
@@ -25,6 +25,7 @@ import {
25
25
  buildLanePaths,
26
26
  compactSingleLine,
27
27
  ensureDirectory,
28
+ findAdhocRunRecord,
28
29
  parseNonNegativeInt,
29
30
  readJsonOrNull,
30
31
  REPO_ROOT,
@@ -205,8 +206,8 @@ async function withSuppressedNestedUpdateNotice(fn) {
205
206
  }
206
207
  }
207
208
 
208
- function readEffectiveProjectProfile(config) {
209
- return readProjectProfile({ config }) || buildDefaultProjectProfile(config);
209
+ function readEffectiveProjectProfile(config, project) {
210
+ return readProjectProfile({ config, project }) || buildDefaultProjectProfile(config);
210
211
  }
211
212
 
212
213
  function detectKeyword(text, keywords) {
@@ -451,6 +452,10 @@ function buildCommonRequiredContext() {
451
452
  );
452
453
  }
453
454
 
455
+ function adhocReportPath(lanePaths, filename) {
456
+ return repoRelativePath(path.join(lanePaths.adhocRunDir, "reports", filename));
457
+ }
458
+
454
459
  function buildDocumentationOwnedPaths({ lanePaths, waveNumber, mode }) {
455
460
  const canonicalPaths = requiredDocumentationStewardPathsForWave(waveNumber, {
456
461
  laneProfile: lanePaths.laneProfile,
@@ -459,7 +464,7 @@ function buildDocumentationOwnedPaths({ lanePaths, waveNumber, mode }) {
459
464
  return canonicalPaths;
460
465
  }
461
466
  return uniqueStrings([
462
- `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-doc-closure.md`,
467
+ adhocReportPath(lanePaths, `wave-${waveNumber}-doc-closure.md`),
463
468
  ...canonicalPaths,
464
469
  ]);
465
470
  }
@@ -476,11 +481,11 @@ function buildSpecialAgents({
476
481
  const contQaReportPath =
477
482
  mode === "roadmap"
478
483
  ? `docs/plans/waves/reviews/wave-${waveNumber}-cont-qa.md`
479
- : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-cont-qa.md`;
484
+ : adhocReportPath(lanePaths, `wave-${waveNumber}-cont-qa.md`);
480
485
  const contEvalReportPath =
481
486
  mode === "roadmap"
482
487
  ? `docs/plans/waves/reviews/wave-${waveNumber}-cont-eval.md`
483
- : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-cont-eval.md`;
488
+ : adhocReportPath(lanePaths, `wave-${waveNumber}-cont-eval.md`);
484
489
  const documentationOwnedPaths = buildDocumentationOwnedPaths({
485
490
  lanePaths,
486
491
  waveNumber,
@@ -489,7 +494,7 @@ function buildSpecialAgents({
489
494
  const securityReportPath =
490
495
  mode === "roadmap"
491
496
  ? relativeStatePath(path.join(lanePaths.securityDir, `wave-${waveNumber}-review.md`))
492
- : `.wave/adhoc/runs/${lanePaths.runId}/reports/wave-${waveNumber}-security-review.md`;
497
+ : adhocReportPath(lanePaths, `wave-${waveNumber}-security-review.md`);
493
498
  const integrationOwnedPaths = [
494
499
  relativeStatePath(path.join(lanePaths.integrationDir, `wave-${waveNumber}.md`)),
495
500
  relativeStatePath(path.join(lanePaths.integrationDir, `wave-${waveNumber}.json`)),
@@ -727,6 +732,7 @@ function buildAdhocRequest({ runId, lanePaths, profile, tasks, launcherArgs = []
727
732
  schemaVersion: ADHOC_SCHEMA_VERSION,
728
733
  runKind: "adhoc",
729
734
  runId,
735
+ project: lanePaths.project,
730
736
  lane: lanePaths.lane,
731
737
  createdAt: toIsoTimestamp(),
732
738
  oversightMode: profile.defaultOversightMode,
@@ -846,6 +852,7 @@ function buildResultRecord(lanePaths, request, spec, status, extra = {}) {
846
852
  schemaVersion: ADHOC_SCHEMA_VERSION,
847
853
  runKind: "adhoc",
848
854
  runId: request.runId,
855
+ project: request.project || lanePaths.project,
849
856
  lane: request.lane,
850
857
  title: spec.title,
851
858
  status,
@@ -1009,6 +1016,7 @@ function parseArgs(argv) {
1009
1016
  const args = Array.isArray(argv) ? argv.slice() : [];
1010
1017
  const subcommand = cleanText(args.shift()).toLowerCase();
1011
1018
  const options = {
1019
+ project: "",
1012
1020
  lane: "",
1013
1021
  runId: "",
1014
1022
  wave: null,
@@ -1022,6 +1030,8 @@ function parseArgs(argv) {
1022
1030
  const arg = args.shift();
1023
1031
  if (arg === "--task") {
1024
1032
  options.tasks.push(cleanText(args.shift()));
1033
+ } else if (arg === "--project") {
1034
+ options.project = cleanText(args.shift());
1025
1035
  } else if (arg === "--lane") {
1026
1036
  options.lane = cleanText(args.shift());
1027
1037
  } else if (arg === "--run") {
@@ -1074,24 +1084,25 @@ function parseArgs(argv) {
1074
1084
 
1075
1085
  function printUsage() {
1076
1086
  console.log(`Usage:
1077
- wave adhoc plan --task <text> [--task <text>] [--lane <lane>] [--json]
1078
- wave adhoc run --task <text> [--task <text>] [--lane <lane>] [--yes] [--json] [launcher options]
1079
- wave adhoc list [--lane <lane>] [--json]
1087
+ wave adhoc plan --task <text> [--task <text>] [--project <id>] [--lane <lane>] [--json]
1088
+ wave adhoc run --task <text> [--task <text>] [--project <id>] [--lane <lane>] [--yes] [--json] [launcher options]
1089
+ wave adhoc list [--project <id>] [--lane <lane>] [--json]
1080
1090
  wave adhoc show --run <id> [--json]
1081
- wave adhoc promote --run <id> --wave <n> [--force] [--json]
1091
+ wave adhoc promote --run <id> [--project <id>] --wave <n> [--force] [--json]
1082
1092
  `);
1083
1093
  }
1084
1094
 
1085
1095
  function resolveLaneForOptions(config, options) {
1086
- const profile = readEffectiveProjectProfile(config);
1096
+ const profile = readEffectiveProjectProfile(config, options.project || config.defaultProject);
1087
1097
  return cleanText(options.lane) || profile.plannerDefaults?.lane || config.defaultLane;
1088
1098
  }
1089
1099
 
1090
1100
  function createStoredRun({ config, options }) {
1091
- const profile = readEffectiveProjectProfile(config);
1101
+ const projectId = options.project || config.defaultProject;
1102
+ const profile = readEffectiveProjectProfile(config, projectId);
1092
1103
  const lane = resolveLaneForOptions(config, options);
1093
1104
  const runId = buildAdhocRunId();
1094
- const lanePaths = buildLanePaths(lane, { config, adhocRunId: runId });
1105
+ const lanePaths = buildLanePaths(lane, { config, project: projectId, adhocRunId: runId });
1095
1106
  const request = buildAdhocRequest({
1096
1107
  runId,
1097
1108
  lanePaths,
@@ -1114,7 +1125,17 @@ function createStoredRun({ config, options }) {
1114
1125
  }
1115
1126
 
1116
1127
  function readStoredRun(runId) {
1117
- const lanePaths = buildLanePaths(DEFAULT_LANE_PLACEHOLDER, { adhocRunId: runId });
1128
+ const record = findAdhocRunRecord(runId);
1129
+ if (!record) {
1130
+ throw new Error(`Ad-hoc run not found: ${runId}`);
1131
+ }
1132
+ const lanePaths = buildLanePaths(
1133
+ record.result?.lane || DEFAULT_LANE_PLACEHOLDER,
1134
+ {
1135
+ project: record.project,
1136
+ adhocRunId: runId,
1137
+ },
1138
+ );
1118
1139
  const request = readJsonOrNull(lanePaths.adhocRequestPath);
1119
1140
  const spec = readJsonOrNull(lanePaths.adhocSpecPath);
1120
1141
  const result = readJsonOrNull(lanePaths.adhocResultPath);
@@ -1166,6 +1187,7 @@ export async function runAdhocCli(argv) {
1166
1187
  }
1167
1188
  const launchLanePaths = buildLanePaths(stored.lanePaths.lane, {
1168
1189
  config,
1190
+ project: stored.request.project || stored.lanePaths.project,
1169
1191
  adhocRunId: stored.lanePaths.runId,
1170
1192
  runVariant: options.launcherArgs.includes("--dry-run") ? "dry-run" : undefined,
1171
1193
  });
@@ -1178,6 +1200,8 @@ export async function runAdhocCli(argv) {
1178
1200
  try {
1179
1201
  await withSuppressedNestedUpdateNotice(() =>
1180
1202
  runLauncherCli([
1203
+ "--project",
1204
+ stored.request.project || stored.lanePaths.project,
1181
1205
  "--lane",
1182
1206
  stored.lanePaths.lane,
1183
1207
  "--adhoc-run",
@@ -1219,9 +1243,10 @@ export async function runAdhocCli(argv) {
1219
1243
 
1220
1244
  if (subcommand === "list") {
1221
1245
  const lane = cleanText(options.lane);
1222
- const lanePaths = buildLanePaths(lane || config.defaultLane, { config });
1246
+ const projectId = options.project || config.defaultProject;
1247
+ const lanePaths = buildLanePaths(lane || config.defaultLane, { config, project: projectId });
1223
1248
  const runs = collectStoredRuns(lanePaths.adhocIndexPath).filter((run) =>
1224
- lane ? run.lane === lane : true,
1249
+ (lane ? run.lane === lane : true) && (options.project ? run.project === projectId : true),
1225
1250
  );
1226
1251
  if (options.json) {
1227
1252
  console.log(JSON.stringify(runs, null, 2));
@@ -1242,6 +1267,7 @@ export async function runAdhocCli(argv) {
1242
1267
  const { lanePaths, request, spec, result } = readStoredRun(options.runId);
1243
1268
  const payload = {
1244
1269
  runId: options.runId,
1270
+ project: result.project || request.project || lanePaths.project,
1245
1271
  lane: result.lane,
1246
1272
  status: result.status,
1247
1273
  title: result.title,
@@ -1263,6 +1289,7 @@ export async function runAdhocCli(argv) {
1263
1289
  return;
1264
1290
  }
1265
1291
  console.log(`[wave:adhoc] run=${payload.runId}`);
1292
+ console.log(`[wave:adhoc] project=${payload.project}`);
1266
1293
  console.log(`[wave:adhoc] lane=${payload.lane}`);
1267
1294
  console.log(`[wave:adhoc] status=${payload.status}`);
1268
1295
  console.log(`[wave:adhoc] title=${payload.title}`);
@@ -1290,8 +1317,13 @@ export async function runAdhocCli(argv) {
1290
1317
  throw new Error("--wave <n> is required for `wave adhoc promote`.");
1291
1318
  }
1292
1319
  const stored = readStoredRun(options.runId);
1320
+ const projectId =
1321
+ cleanText(options.project) ||
1322
+ cleanText(stored.result.project) ||
1323
+ cleanText(stored.request.project) ||
1324
+ config.defaultProject;
1293
1325
  const lane = cleanText(options.lane) || stored.result.lane || config.defaultLane;
1294
- const lanePaths = buildLanePaths(lane, { config });
1326
+ const lanePaths = buildLanePaths(lane, { config, project: projectId });
1295
1327
  const wavePath = path.join(lanePaths.wavesDir, `wave-${options.wave}.md`);
1296
1328
  const specPath = path.join(lanePaths.wavesDir, "specs", `wave-${options.wave}.json`);
1297
1329
  if (!options.force && (fs.existsSync(wavePath) || fs.existsSync(specPath))) {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import {
5
5
  DEFAULT_CODEX_SANDBOX_MODE,
6
6
  DEFAULT_EXECUTOR_MODE,
7
+ loadWaveConfig,
7
8
  normalizeCodexSandboxMode,
8
9
  normalizeExecutorMode,
9
10
  SUPPORTED_EXECUTOR_MODES,
@@ -42,6 +43,7 @@ function printUsage() {
42
43
  console.log(`Usage: pnpm exec wave autonomous [options]
43
44
 
44
45
  Options:
46
+ --project <id> Project id
45
47
  --lane <name> Lane name (default: ${DEFAULT_WAVE_LANE})
46
48
  --timeout-minutes <n> Per-wave timeout passed to launcher (default: ${DEFAULT_TIMEOUT_MINUTES})
47
49
  --max-retries-per-wave <n> Per-wave relaunches inside launcher (default: ${DEFAULT_MAX_RETRIES_PER_WAVE})
@@ -64,7 +66,9 @@ Options:
64
66
  }
65
67
 
66
68
  export function parseArgs(argv) {
69
+ const config = loadWaveConfig();
67
70
  const options = {
71
+ project: config.defaultProject,
68
72
  lane: DEFAULT_WAVE_LANE,
69
73
  timeoutMinutes: DEFAULT_TIMEOUT_MINUTES,
70
74
  maxRetriesPerWave: DEFAULT_MAX_RETRIES_PER_WAVE,
@@ -92,6 +96,8 @@ export function parseArgs(argv) {
92
96
  }
93
97
  if (arg === "--lane") {
94
98
  options.lane = sanitizeLaneName(argv[++i]);
99
+ } else if (arg === "--project") {
100
+ options.project = String(argv[++i] || "").trim();
95
101
  } else if (arg === "--timeout-minutes") {
96
102
  options.timeoutMinutes = parsePositiveInt(argv[++i], "--timeout-minutes");
97
103
  } else if (arg === "--max-retries-per-wave") {
@@ -132,7 +138,10 @@ export function parseArgs(argv) {
132
138
  }
133
139
  }
134
140
  if (!executorProvided) {
135
- options.executorMode = buildLanePaths(options.lane).executors.default;
141
+ options.executorMode = buildLanePaths(options.lane, {
142
+ config,
143
+ project: options.project,
144
+ }).executors.default;
136
145
  }
137
146
  options.orchestratorId ||= `${options.lane}-autonomous`;
138
147
  if (options.executorMode === "local") {
@@ -146,8 +155,8 @@ export function parseArgs(argv) {
146
155
  return { help: false, options };
147
156
  }
148
157
 
149
- function getWaveNumbers(lane) {
150
- const lanePaths = buildLanePaths(lane);
158
+ function getWaveNumbers(lane, project) {
159
+ const lanePaths = buildLanePaths(lane, { project });
151
160
  if (!fs.existsSync(lanePaths.wavesDir)) {
152
161
  throw new Error(`Waves directory not found: ${path.relative(REPO_ROOT, lanePaths.wavesDir)}`);
153
162
  }
@@ -184,27 +193,52 @@ function runCommand(args, envOverrides = {}) {
184
193
  return Number.isInteger(result.status) ? result.status : 1;
185
194
  }
186
195
 
187
- function reconcile(lane) {
196
+ function reconcile(lane, project) {
188
197
  return runCommand(
189
- [path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--reconcile-status"],
198
+ [
199
+ path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"),
200
+ "--project",
201
+ project,
202
+ "--lane",
203
+ lane,
204
+ "--reconcile-status",
205
+ ],
190
206
  { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" },
191
207
  );
192
208
  }
193
209
 
194
- function dryRun(lane) {
210
+ function dryRun(lane, project) {
195
211
  return runCommand(
196
- [path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--dry-run", "--no-dashboard"],
212
+ [
213
+ path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"),
214
+ "--project",
215
+ project,
216
+ "--lane",
217
+ lane,
218
+ "--dry-run",
219
+ "--no-dashboard",
220
+ ],
197
221
  { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" },
198
222
  );
199
223
  }
200
224
 
201
- function listPendingFeedback(lane) {
202
- return runCommand([path.join(PACKAGE_ROOT, "scripts", "wave-human-feedback.mjs"), "list", "--lane", lane, "--pending"]);
225
+ function listPendingFeedback(lane, project) {
226
+ return runCommand([
227
+ path.join(PACKAGE_ROOT, "scripts", "wave-human-feedback.mjs"),
228
+ "list",
229
+ "--project",
230
+ project,
231
+ "--lane",
232
+ lane,
233
+ "--pending",
234
+ ]);
203
235
  }
204
236
 
205
237
  function launchSingleWave(params) {
206
238
  const args = [
207
239
  path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"),
240
+ "--project",
241
+ params.project,
208
242
  "--lane",
209
243
  params.lane,
210
244
  "--start-wave",
@@ -359,27 +393,27 @@ export async function runAutonomousCli(argv) {
359
393
  }
360
394
  await maybeAnnouncePackageUpdate();
361
395
  const options = parsed.options;
362
- const allWaves = getWaveNumbers(options.lane);
396
+ const allWaves = getWaveNumbers(options.lane, options.project);
363
397
  console.log(`[autonomous] lane=${options.lane} orchestrator=${options.orchestratorId}`);
364
398
  console.log(`[autonomous] executor=${options.executorMode}`);
365
399
  console.log(`[autonomous] codex_sandbox=${options.codexSandboxMode}`);
366
400
  console.log(`[autonomous] waves=${allWaves.join(", ")}`);
367
401
 
368
- const dryRunStatus = dryRun(options.lane);
402
+ const dryRunStatus = dryRun(options.lane, options.project);
369
403
  if (dryRunStatus !== 0) {
370
404
  throw new Error(`[autonomous] dry-run preflight failed with status=${dryRunStatus}`);
371
405
  }
372
- const feedbackListStatus = listPendingFeedback(options.lane);
406
+ const feedbackListStatus = listPendingFeedback(options.lane, options.project);
373
407
  if (feedbackListStatus !== 0) {
374
408
  throw new Error(`[autonomous] feedback preflight failed with status=${feedbackListStatus}`);
375
409
  }
376
- const reconcileStatus = reconcile(options.lane);
410
+ const reconcileStatus = reconcile(options.lane, options.project);
377
411
  if (reconcileStatus !== 0) {
378
412
  throw new Error(`[autonomous] initial reconcile failed with status=${reconcileStatus}`);
379
413
  }
380
414
 
381
415
  let launchedCount = 0;
382
- const lanePaths = buildLanePaths(options.lane);
416
+ const lanePaths = buildLanePaths(options.lane, { project: options.project });
383
417
  while (true) {
384
418
  const completed = readRunState(lanePaths.defaultRunStatePath).completedWaves;
385
419
  const wave = nextIncompleteWave(allWaves, completed);
@@ -405,7 +439,7 @@ export async function runAutonomousCli(argv) {
405
439
  wave,
406
440
  attempt,
407
441
  });
408
- reconcile(options.lane);
442
+ reconcile(options.lane, options.project);
409
443
  if (status === 0) {
410
444
  launchedCount += 1;
411
445
  success = true;
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import {
5
- DEFAULT_WAVE_LANE,
6
5
  REPO_ROOT,
7
6
  buildLanePaths,
8
7
  ensureDirectory,
@@ -107,9 +106,11 @@ function escapeMarkdownCell(value) {
107
106
  .replace(/\|/g, "\\|");
108
107
  }
109
108
 
110
- function benchmarkTelemetryLanePaths() {
109
+ function benchmarkTelemetryLanePaths(options = {}) {
111
110
  try {
112
- return buildLanePaths(DEFAULT_WAVE_LANE);
111
+ return buildLanePaths(options.lane || undefined, {
112
+ project: options.project || undefined,
113
+ });
113
114
  } catch {
114
115
  return null;
115
116
  }
@@ -119,6 +120,15 @@ function benchmarkRunId(output) {
119
120
  return `bench-${output.adapter.id}-${output.manifest.id}-${String(output.generatedAt || toIsoTimestamp()).replace(/[-:.TZ]/g, "").slice(0, 14)}`;
120
121
  }
121
122
 
123
+ function flushBenchmarkTelemetryBestEffort(lanePaths) {
124
+ return flushWaveControlQueue(lanePaths).catch((error) => {
125
+ console.warn(
126
+ `[wave:benchmark] telemetry flush skipped: ${error instanceof Error ? error.message : String(error)}`,
127
+ );
128
+ return null;
129
+ });
130
+ }
131
+
122
132
  function reviewValidityForResult(result, output) {
123
133
  if (result.success && output.comparisonReady) {
124
134
  return "comparison-valid";
@@ -179,8 +189,8 @@ function externalTaskArtifacts(result) {
179
189
  return artifacts;
180
190
  }
181
191
 
182
- function publishExternalBenchmarkTelemetry({ output, outputDir, failureReview }) {
183
- const lanePaths = benchmarkTelemetryLanePaths();
192
+ function publishExternalBenchmarkTelemetry({ output, outputDir, failureReview, project, lane }) {
193
+ const lanePaths = benchmarkTelemetryLanePaths({ project, lane });
184
194
  if (!lanePaths || lanePaths.waveControl?.captureBenchmarkRuns === false) {
185
195
  return null;
186
196
  }
@@ -338,7 +348,7 @@ function publishExternalBenchmarkTelemetry({ output, outputDir, failureReview })
338
348
  },
339
349
  });
340
350
  }
341
- void flushWaveControlQueue(lanePaths);
351
+ void flushBenchmarkTelemetryBestEffort(lanePaths);
342
352
  return benchmarkRunIdValue;
343
353
  }
344
354
 
@@ -1376,7 +1386,13 @@ export function runExternalBenchmarkPilot(options = {}) {
1376
1386
  writeTextAtomic(path.join(outputDir, "results.md"), `${renderExternalResultsMarkdown(output)}\n`);
1377
1387
  writeJsonAtomic(path.join(outputDir, "failure-review.json"), failureReview);
1378
1388
  writeTextAtomic(path.join(outputDir, "failure-review.md"), `${renderExternalFailureReviewMarkdown(failureReview)}\n`);
1379
- publishExternalBenchmarkTelemetry({ output, outputDir, failureReview });
1389
+ publishExternalBenchmarkTelemetry({
1390
+ output,
1391
+ outputDir,
1392
+ failureReview,
1393
+ project: options.project,
1394
+ lane: options.lane,
1395
+ });
1380
1396
  return {
1381
1397
  ...output,
1382
1398
  outputDir: path.relative(REPO_ROOT, outputDir).replaceAll(path.sep, "/"),
@@ -7,7 +7,6 @@ import {
7
7
  import { buildRequestAssignments } from "./routing-state.mjs";
8
8
  import { loadBenchmarkCases, loadExternalBenchmarkAdapters } from "./benchmark-cases.mjs";
9
9
  import {
10
- DEFAULT_WAVE_LANE,
11
10
  REPO_ROOT,
12
11
  buildLanePaths,
13
12
  ensureDirectory,
@@ -44,9 +43,11 @@ function normalizeId(value, label) {
44
43
  return normalized;
45
44
  }
46
45
 
47
- function benchmarkTelemetryLanePaths() {
46
+ function benchmarkTelemetryLanePaths(options = {}) {
48
47
  try {
49
- return buildLanePaths(DEFAULT_WAVE_LANE);
48
+ return buildLanePaths(options.lane || undefined, {
49
+ project: options.project || undefined,
50
+ });
50
51
  } catch {
51
52
  return null;
52
53
  }
@@ -56,8 +57,17 @@ function localBenchmarkRunId(output) {
56
57
  return `bench-local-${String(output.generatedAt || toIsoTimestamp()).replace(/[-:.TZ]/g, "").slice(0, 14)}`;
57
58
  }
58
59
 
59
- function publishLocalBenchmarkTelemetry({ output, outputDir }) {
60
- const lanePaths = benchmarkTelemetryLanePaths();
60
+ function flushBenchmarkTelemetryBestEffort(lanePaths) {
61
+ return flushWaveControlQueue(lanePaths).catch((error) => {
62
+ console.warn(
63
+ `[wave:benchmark] telemetry flush skipped: ${error instanceof Error ? error.message : String(error)}`,
64
+ );
65
+ return null;
66
+ });
67
+ }
68
+
69
+ function publishLocalBenchmarkTelemetry({ output, outputDir, project, lane }) {
70
+ const lanePaths = benchmarkTelemetryLanePaths({ project, lane });
61
71
  if (!lanePaths || lanePaths.waveControl?.captureBenchmarkRuns === false) {
62
72
  return null;
63
73
  }
@@ -140,7 +150,7 @@ function publishLocalBenchmarkTelemetry({ output, outputDir }) {
140
150
  });
141
151
  }
142
152
  }
143
- void flushWaveControlQueue(lanePaths);
153
+ void flushBenchmarkTelemetryBestEffort(lanePaths);
144
154
  return benchmarkRunIdValue;
145
155
  }
146
156
 
@@ -718,7 +728,12 @@ export function runBenchmarkSuite(options = {}) {
718
728
  ensureDirectory(outputDir);
719
729
  writeJsonAtomic(path.join(outputDir, "results.json"), output);
720
730
  writeTextAtomic(path.join(outputDir, "results.md"), `${renderMarkdownReport(output)}\n`);
721
- publishLocalBenchmarkTelemetry({ output, outputDir });
731
+ publishLocalBenchmarkTelemetry({
732
+ output,
733
+ outputDir,
734
+ project: options.project,
735
+ lane: options.lane,
736
+ });
722
737
  output.outputDir = path.relative(REPO_ROOT, outputDir).replaceAll(path.sep, "/");
723
738
  }
724
739
  return output;
@@ -728,12 +743,12 @@ function printUsage() {
728
743
  console.log(`Usage:
729
744
  wave benchmark list [--json]
730
745
  wave benchmark show --case <id> [--json]
731
- wave benchmark run [--case <id>] [--family <id>] [--benchmark <id>] [--arm <id>] [--output-dir <path>] [--json]
746
+ wave benchmark run [--project <id>] [--lane <lane>] [--case <id>] [--family <id>] [--benchmark <id>] [--arm <id>] [--output-dir <path>] [--json]
732
747
  wave benchmark adapters [--json]
733
748
  wave benchmark external-list [--json]
734
749
  wave benchmark external-show --adapter <id> [--json]
735
- wave benchmark external-pilots [--json]
736
- wave benchmark external-run --adapter <id> [--manifest <path>] [--task <id>] [--arm <id>] [--dry-run] [--command-config <path>] [run options] [--json]
750
+ wave benchmark external-pilots [--project <id>] [--lane <lane>] [--json]
751
+ wave benchmark external-run --adapter <id> [--project <id>] [--lane <lane>] [--manifest <path>] [--task <id>] [--arm <id>] [--dry-run] [--command-config <path>] [run options] [--json]
737
752
  `);
738
753
  }
739
754
 
@@ -741,6 +756,8 @@ function parseArgs(argv) {
741
756
  const args = Array.isArray(argv) ? argv.slice() : [];
742
757
  const subcommand = cleanText(args.shift()).toLowerCase();
743
758
  const options = {
759
+ project: "",
760
+ lane: "",
744
761
  json: false,
745
762
  caseIds: [],
746
763
  familyIds: [],
@@ -768,6 +785,10 @@ function parseArgs(argv) {
768
785
  const arg = args[index];
769
786
  if (arg === "--json") {
770
787
  options.json = true;
788
+ } else if (arg === "--project") {
789
+ options.project = cleanText(args[++index]);
790
+ } else if (arg === "--lane") {
791
+ options.lane = cleanText(args[++index]);
771
792
  } else if (arg === "--case") {
772
793
  options.caseIds.push(args[++index]);
773
794
  } else if (arg === "--family") {
@@ -929,6 +950,8 @@ export async function runBenchmarkCli(argv) {
929
950
  throw new Error("wave benchmark external-run requires --adapter <id>");
930
951
  }
931
952
  const output = runExternalBenchmarkPilot({
953
+ project: options.project || undefined,
954
+ lane: options.lane || undefined,
932
955
  adapterId: options.adapterId,
933
956
  manifestPath: options.manifestPath || undefined,
934
957
  taskIds: options.taskIds,