@chllming/wave-orchestration 0.8.9 → 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 (47) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +23 -7
  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.9.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 +19 -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 +24 -3
  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/wave-control-client.mjs +84 -16
  47. package/scripts/wave-orchestrator/wave-files.mjs +5 -1
@@ -17,6 +17,7 @@ const REPO_ROOT = WORKSPACE_ROOT;
17
17
 
18
18
  export const DEFAULT_WAVE_CONFIG_PATH = path.join(REPO_ROOT, "wave.config.json");
19
19
  export const DEFAULT_WAVE_LANE = "main";
20
+ export const DEFAULT_PROJECT_ID = "default";
20
21
  export const DEFAULT_CONT_QA_AGENT_ID = "A0";
21
22
  export const DEFAULT_CONT_EVAL_AGENT_ID = "E0";
22
23
  export const DEFAULT_INTEGRATION_AGENT_ID = "A8";
@@ -73,7 +74,8 @@ export const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full
73
74
  export const DEFAULT_CLAUDE_COMMAND = "claude";
74
75
  export const DEFAULT_OPENCODE_COMMAND = "opencode";
75
76
  export const DEFAULT_WAVE_CONTROL_AUTH_TOKEN_ENV_VAR = "WAVE_CONTROL_AUTH_TOKEN";
76
- export const DEFAULT_WAVE_CONTROL_REPORT_MODE = "metadata-plus-selected";
77
+ export const DEFAULT_WAVE_CONTROL_ENDPOINT = "https://wave-control.up.railway.app/api/v1";
78
+ export const DEFAULT_WAVE_CONTROL_REPORT_MODE = "metadata-only";
77
79
  export const DEFAULT_WAVE_CONTROL_REQUEST_TIMEOUT_MS = 5000;
78
80
  export const DEFAULT_WAVE_CONTROL_FLUSH_BATCH_SIZE = 25;
79
81
  export const DEFAULT_WAVE_CONTROL_MAX_PENDING_EVENTS = 1000;
@@ -137,6 +139,22 @@ function sanitizeLaneName(value) {
137
139
  return lane;
138
140
  }
139
141
 
142
+ export function sanitizeProjectId(value) {
143
+ const projectId = String(value || "")
144
+ .trim()
145
+ .toLowerCase()
146
+ .replace(/[^a-z0-9_-]+/g, "-")
147
+ .replace(/-+/g, "-")
148
+ .replace(/^-+|-+$/g, "");
149
+ if (!projectId) {
150
+ throw new Error("Project id is required");
151
+ }
152
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(projectId)) {
153
+ throw new Error(`Invalid project id: ${value}`);
154
+ }
155
+ return projectId;
156
+ }
157
+
140
158
  function readJsonOrNull(filePath) {
141
159
  try {
142
160
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
@@ -185,6 +203,26 @@ function normalizeOptionalString(value, fallback = null) {
185
203
  return normalized || fallback;
186
204
  }
187
205
 
206
+ function joinRepoPath(basePath, childPath) {
207
+ const base = String(basePath || "")
208
+ .replaceAll("\\", "/")
209
+ .replace(/^\.\/+/, "")
210
+ .replace(/\/+/g, "/")
211
+ .replace(/\/$/, "");
212
+ const child = String(childPath || "")
213
+ .replaceAll("\\", "/")
214
+ .replace(/^\.\/+/, "")
215
+ .replace(/\/+/g, "/")
216
+ .replace(/\/$/, "");
217
+ if (!base || base === ".") {
218
+ return child;
219
+ }
220
+ if (!child) {
221
+ return base;
222
+ }
223
+ return `${base}/${child}`.replace(/\/+/g, "/");
224
+ }
225
+
188
226
  function normalizeOptionalPositiveInt(value, label, fallback = null) {
189
227
  if (value === null || value === undefined || value === "") {
190
228
  return fallback;
@@ -282,11 +320,11 @@ function normalizeThreshold(value, fallback) {
282
320
  return parsed;
283
321
  }
284
322
 
285
- function defaultDocsDirForLane(lane, defaultLane, repoMode) {
323
+ function defaultDocsDirForLane(lane, defaultLane, repoMode, projectDocsDir = DEFAULT_DOCS_DIR) {
286
324
  if (repoMode === "single-repo" && lane === defaultLane) {
287
- return DEFAULT_DOCS_DIR;
325
+ return projectDocsDir;
288
326
  }
289
- return `${DEFAULT_DOCS_DIR}/${lane}`;
327
+ return joinRepoPath(projectDocsDir, lane);
290
328
  }
291
329
 
292
330
  function defaultPlansDir(docsDir) {
@@ -549,7 +587,7 @@ function normalizeWaveControl(rawWaveControl = {}, label = "waveControl") {
549
587
  reportMode !== "disabled" && normalizeOptionalBoolean(waveControl.enabled, true);
550
588
  return {
551
589
  enabled,
552
- endpoint: normalizeOptionalString(waveControl.endpoint, null),
590
+ endpoint: normalizeOptionalString(waveControl.endpoint, DEFAULT_WAVE_CONTROL_ENDPOINT),
553
591
  workspaceId: normalizeOptionalString(waveControl.workspaceId, null),
554
592
  projectId: normalizeOptionalString(waveControl.projectId, null),
555
593
  authTokenEnvVar:
@@ -931,6 +969,7 @@ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
931
969
  throw new Error(`Unsupported repoMode in ${path.relative(REPO_ROOT, configPath)}: ${repoMode}`);
932
970
  }
933
971
  const defaultLane = sanitizeLaneName(rawConfig.defaultLane || DEFAULT_WAVE_LANE);
972
+ const defaultProject = sanitizeProjectId(rawConfig.defaultProject || DEFAULT_PROJECT_ID);
934
973
  const paths = {
935
974
  docsDir: normalizeRepoRelativePath(rawConfig.paths?.docsDir || DEFAULT_DOCS_DIR, "paths.docsDir"),
936
975
  stateRoot: normalizeRepoRelativePath(
@@ -966,17 +1005,122 @@ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
966
1005
  };
967
1006
  const sharedPlanDocs =
968
1007
  normalizeOptionalPathArray(rawConfig.sharedPlanDocs, "sharedPlanDocs") || null;
969
- const lanes = Object.fromEntries(
1008
+ const legacyLanes = Object.fromEntries(
970
1009
  Object.entries(rawConfig.lanes || {}).map(([laneName, laneConfig]) => [
971
1010
  sanitizeLaneName(laneName),
972
1011
  laneConfig || {},
973
1012
  ]),
974
1013
  );
1014
+ const rawProjects =
1015
+ rawConfig.projects && typeof rawConfig.projects === "object" && !Array.isArray(rawConfig.projects)
1016
+ ? rawConfig.projects
1017
+ : null;
1018
+ const projects = Object.fromEntries(
1019
+ Object.entries(
1020
+ rawProjects || {
1021
+ [defaultProject]: {
1022
+ projectName: rawConfig.projectName,
1023
+ rootDir: ".",
1024
+ sharedPlanDocs: rawConfig.sharedPlanDocs,
1025
+ lanes: legacyLanes,
1026
+ },
1027
+ },
1028
+ ).map(([projectId, projectConfig]) => {
1029
+ const normalizedProjectId = sanitizeProjectId(projectId);
1030
+ const rawProject =
1031
+ projectConfig && typeof projectConfig === "object" && !Array.isArray(projectConfig)
1032
+ ? projectConfig
1033
+ : {};
1034
+ const rootDir = normalizeRepoRelativePath(
1035
+ rawProject.rootDir || ".",
1036
+ `projects.${projectId}.rootDir`,
1037
+ );
1038
+ const projectSharedPlanDocs =
1039
+ normalizeOptionalPathArray(
1040
+ rawProject.sharedPlanDocs,
1041
+ `projects.${projectId}.sharedPlanDocs`,
1042
+ ) || null;
1043
+ const projectDocsDir = normalizeRepoRelativePath(
1044
+ rawProject.paths?.docsDir || joinRepoPath(rootDir, rawConfig.paths?.docsDir || DEFAULT_DOCS_DIR),
1045
+ `projects.${projectId}.paths.docsDir`,
1046
+ );
1047
+ const projectPaths = {
1048
+ docsDir: projectDocsDir,
1049
+ stateRoot: normalizeRepoRelativePath(
1050
+ rawProject.paths?.stateRoot || rawConfig.paths?.stateRoot || DEFAULT_STATE_ROOT,
1051
+ `projects.${projectId}.paths.stateRoot`,
1052
+ ),
1053
+ orchestratorStateDir: normalizeRepoRelativePath(
1054
+ rawProject.paths?.orchestratorStateDir ||
1055
+ rawConfig.paths?.orchestratorStateDir ||
1056
+ DEFAULT_ORCHESTRATOR_STATE_DIR,
1057
+ `projects.${projectId}.paths.orchestratorStateDir`,
1058
+ ),
1059
+ terminalsPath: normalizeRepoRelativePath(
1060
+ rawProject.paths?.terminalsPath || rawConfig.paths?.terminalsPath || DEFAULT_TERMINALS_PATH,
1061
+ `projects.${projectId}.paths.terminalsPath`,
1062
+ ),
1063
+ context7BundleIndexPath: normalizeRepoRelativePath(
1064
+ rawProject.paths?.context7BundleIndexPath ||
1065
+ rawConfig.paths?.context7BundleIndexPath ||
1066
+ DEFAULT_CONTEXT7_BUNDLE_INDEX_PATH,
1067
+ `projects.${projectId}.paths.context7BundleIndexPath`,
1068
+ ),
1069
+ benchmarkCatalogPath: normalizeRepoRelativePath(
1070
+ rawProject.paths?.benchmarkCatalogPath ||
1071
+ rawConfig.paths?.benchmarkCatalogPath ||
1072
+ DEFAULT_BENCHMARK_CATALOG_PATH,
1073
+ `projects.${projectId}.paths.benchmarkCatalogPath`,
1074
+ ),
1075
+ componentCutoverMatrixDocPath: normalizeRepoRelativePath(
1076
+ rawProject.paths?.componentCutoverMatrixDocPath ||
1077
+ rawConfig.paths?.componentCutoverMatrixDocPath ||
1078
+ defaultComponentCutoverMatrixDocPath(defaultPlansDir(projectDocsDir)),
1079
+ `projects.${projectId}.paths.componentCutoverMatrixDocPath`,
1080
+ ),
1081
+ componentCutoverMatrixJsonPath: normalizeRepoRelativePath(
1082
+ rawProject.paths?.componentCutoverMatrixJsonPath ||
1083
+ rawConfig.paths?.componentCutoverMatrixJsonPath ||
1084
+ defaultComponentCutoverMatrixJsonPath(defaultPlansDir(projectDocsDir)),
1085
+ `projects.${projectId}.paths.componentCutoverMatrixJsonPath`,
1086
+ ),
1087
+ };
1088
+ const projectLanes = Object.fromEntries(
1089
+ Object.entries(rawProject.lanes || {}).map(([laneName, laneConfig]) => [
1090
+ sanitizeLaneName(laneName),
1091
+ laneConfig || {},
1092
+ ]),
1093
+ );
1094
+ return [
1095
+ normalizedProjectId,
1096
+ {
1097
+ projectId: normalizedProjectId,
1098
+ projectName: String(
1099
+ rawProject.projectName || rawConfig.projectName || normalizedProjectId,
1100
+ ).trim(),
1101
+ rootDir,
1102
+ sharedPlanDocs: projectSharedPlanDocs,
1103
+ paths: projectPaths,
1104
+ roles: rawProject.roles || {},
1105
+ validation: rawProject.validation || {},
1106
+ executors: rawProject.executors || {},
1107
+ planner: rawProject.planner || {},
1108
+ skills: rawProject.skills || {},
1109
+ capabilityRouting: rawProject.capabilityRouting || {},
1110
+ runtimePolicy: rawProject.runtimePolicy || {},
1111
+ waveControl: rawProject.waveControl || {},
1112
+ lanes: projectLanes,
1113
+ explicit: Boolean(rawProjects),
1114
+ },
1115
+ ];
1116
+ }),
1117
+ );
975
1118
  return {
976
1119
  version: Number.parseInt(String(rawConfig.version ?? "1"), 10) || 1,
977
1120
  projectName: String(rawConfig.projectName || "Wave Orchestrator").trim(),
978
1121
  repoMode,
979
1122
  defaultLane,
1123
+ defaultProject,
980
1124
  paths,
981
1125
  roles: normalizeRoles(rawConfig.roles),
982
1126
  validation: normalizeValidation(rawConfig.validation),
@@ -987,16 +1131,82 @@ export function loadWaveConfig(configPath = DEFAULT_WAVE_CONFIG_PATH) {
987
1131
  runtimePolicy: normalizeRuntimePolicy(rawConfig.runtimePolicy),
988
1132
  waveControl: normalizeWaveControl(rawConfig.waveControl, "waveControl"),
989
1133
  sharedPlanDocs,
990
- lanes,
1134
+ lanes: legacyLanes,
1135
+ projects,
991
1136
  configPath,
992
1137
  };
993
1138
  }
994
1139
 
995
- export function resolveLaneProfile(config, laneInput = config.defaultLane) {
1140
+ export function resolveProjectProfile(config, projectInput = config.defaultProject) {
1141
+ const projectId = sanitizeProjectId(projectInput || config.defaultProject || DEFAULT_PROJECT_ID);
1142
+ const projectConfig = config.projects?.[projectId];
1143
+ if (!projectConfig) {
1144
+ throw new Error(`Unknown project: ${projectInput}`);
1145
+ }
1146
+ const paths = {
1147
+ ...config.paths,
1148
+ ...(projectConfig.paths || {}),
1149
+ };
1150
+ return {
1151
+ projectId,
1152
+ projectName: projectConfig.projectName || config.projectName,
1153
+ rootDir: projectConfig.rootDir || ".",
1154
+ paths,
1155
+ sharedPlanDocs: projectConfig.sharedPlanDocs || null,
1156
+ roles: normalizeRoles({
1157
+ ...config.roles,
1158
+ ...(projectConfig.roles || {}),
1159
+ }),
1160
+ validation: normalizeValidation({
1161
+ ...config.validation,
1162
+ ...(projectConfig.validation || {}),
1163
+ }),
1164
+ executors: normalizeExecutors(
1165
+ mergeExecutors(config.executors, projectConfig.executors || {}),
1166
+ ),
1167
+ planner: normalizePlanner({
1168
+ ...config.planner,
1169
+ ...(projectConfig.planner || {}),
1170
+ }),
1171
+ skills: mergeSkillsConfig(
1172
+ config.skills || emptySkillsConfig(),
1173
+ normalizeLaneSkills(projectConfig.skills || {}, `${projectId}.skills`, {
1174
+ preserveOmittedDir: true,
1175
+ }),
1176
+ ),
1177
+ capabilityRouting: normalizeCapabilityRouting({
1178
+ ...config.capabilityRouting,
1179
+ ...(projectConfig.capabilityRouting || {}),
1180
+ }),
1181
+ runtimePolicy: normalizeRuntimePolicy({
1182
+ ...config.runtimePolicy,
1183
+ ...(projectConfig.runtimePolicy || {}),
1184
+ }),
1185
+ waveControl: normalizeWaveControl(
1186
+ {
1187
+ ...config.waveControl,
1188
+ projectId,
1189
+ ...(projectConfig.waveControl || {}),
1190
+ },
1191
+ `projects.${projectId}.waveControl`,
1192
+ ),
1193
+ lanes: projectConfig.lanes || {},
1194
+ explicit: projectConfig.explicit === true,
1195
+ };
1196
+ }
1197
+
1198
+ export function resolveLaneProfile(config, laneInput = config.defaultLane, projectInput = config.defaultProject) {
1199
+ const projectProfile = resolveProjectProfile(config, projectInput);
996
1200
  const lane = sanitizeLaneName(laneInput || config.defaultLane);
997
- const laneConfig = config.lanes[lane] || {};
1201
+ const laneConfig = projectProfile.lanes[lane] || config.lanes[lane] || {};
998
1202
  const docsDir = normalizeRepoRelativePath(
999
- laneConfig.docsDir || defaultDocsDirForLane(lane, config.defaultLane, config.repoMode),
1203
+ laneConfig.docsDir ||
1204
+ defaultDocsDirForLane(
1205
+ lane,
1206
+ config.defaultLane,
1207
+ config.repoMode,
1208
+ projectProfile.paths.docsDir,
1209
+ ),
1000
1210
  `${lane}.docsDir`,
1001
1211
  );
1002
1212
  const plansDir = normalizeRepoRelativePath(
@@ -1008,28 +1218,28 @@ export function resolveLaneProfile(config, laneInput = config.defaultLane) {
1008
1218
  `${lane}.wavesDir`,
1009
1219
  );
1010
1220
  const roles = normalizeRoles({
1011
- ...config.roles,
1221
+ ...projectProfile.roles,
1012
1222
  ...(laneConfig.roles || {}),
1013
1223
  });
1014
1224
  const validation = normalizeValidation({
1015
- ...config.validation,
1225
+ ...projectProfile.validation,
1016
1226
  ...(laneConfig.validation || {}),
1017
1227
  });
1018
1228
  const executors = normalizeExecutors(
1019
- mergeExecutors(config.executors, laneConfig.executors),
1229
+ mergeExecutors(projectProfile.executors, laneConfig.executors),
1020
1230
  );
1021
1231
  const skills = mergeSkillsConfig(
1022
- config.skills || emptySkillsConfig(),
1232
+ projectProfile.skills || emptySkillsConfig(),
1023
1233
  normalizeLaneSkills(laneConfig.skills, `${lane}.skills`, {
1024
1234
  preserveOmittedDir: true,
1025
1235
  }),
1026
1236
  );
1027
1237
  const capabilityRouting = normalizeCapabilityRouting({
1028
- ...config.capabilityRouting,
1238
+ ...projectProfile.capabilityRouting,
1029
1239
  ...(laneConfig.capabilityRouting || {}),
1030
1240
  });
1031
1241
  const runtimePolicy = normalizeRuntimePolicy({
1032
- ...config.runtimePolicy,
1242
+ ...projectProfile.runtimePolicy,
1033
1243
  ...(laneConfig.runtimePolicy || {}),
1034
1244
  ...(laneConfig.runtimeMixTargets ? { runtimeMixTargets: laneConfig.runtimeMixTargets } : {}),
1035
1245
  ...(laneConfig.defaultExecutorByRole
@@ -1041,18 +1251,23 @@ export function resolveLaneProfile(config, laneInput = config.defaultLane) {
1041
1251
  });
1042
1252
  const waveControl = normalizeWaveControl(
1043
1253
  {
1044
- ...config.waveControl,
1254
+ ...projectProfile.waveControl,
1045
1255
  ...(laneConfig.waveControl || {}),
1046
1256
  },
1047
1257
  `${lane}.waveControl`,
1048
1258
  );
1049
1259
  return {
1260
+ projectId: projectProfile.projectId,
1261
+ projectName: projectProfile.projectName,
1262
+ projectRootDir: projectProfile.rootDir,
1263
+ explicitProject: projectProfile.explicit === true,
1050
1264
  lane,
1051
1265
  docsDir,
1052
1266
  plansDir,
1053
1267
  wavesDir,
1054
1268
  sharedPlanDocs:
1055
1269
  normalizeOptionalPathArray(laneConfig.sharedPlanDocs, `${lane}.sharedPlanDocs`) ||
1270
+ projectProfile.sharedPlanDocs ||
1056
1271
  config.sharedPlanDocs ||
1057
1272
  defaultSharedPlanDocs(plansDir),
1058
1273
  roles,
@@ -1064,34 +1279,34 @@ export function resolveLaneProfile(config, laneInput = config.defaultLane) {
1064
1279
  waveControl,
1065
1280
  paths: {
1066
1281
  terminalsPath: normalizeRepoRelativePath(
1067
- laneConfig.terminalsPath || config.paths.terminalsPath,
1282
+ laneConfig.terminalsPath || projectProfile.paths.terminalsPath,
1068
1283
  `${lane}.terminalsPath`,
1069
1284
  ),
1070
1285
  stateRoot: normalizeRepoRelativePath(
1071
- laneConfig.stateRoot || config.paths.stateRoot,
1286
+ laneConfig.stateRoot || projectProfile.paths.stateRoot,
1072
1287
  `${lane}.stateRoot`,
1073
1288
  ),
1074
1289
  orchestratorStateDir: normalizeRepoRelativePath(
1075
- laneConfig.orchestratorStateDir || config.paths.orchestratorStateDir,
1290
+ laneConfig.orchestratorStateDir || projectProfile.paths.orchestratorStateDir,
1076
1291
  `${lane}.orchestratorStateDir`,
1077
1292
  ),
1078
1293
  context7BundleIndexPath: normalizeRepoRelativePath(
1079
- laneConfig.context7BundleIndexPath || config.paths.context7BundleIndexPath,
1294
+ laneConfig.context7BundleIndexPath || projectProfile.paths.context7BundleIndexPath,
1080
1295
  `${lane}.context7BundleIndexPath`,
1081
1296
  ),
1082
1297
  benchmarkCatalogPath: normalizeRepoRelativePath(
1083
- laneConfig.benchmarkCatalogPath || config.paths.benchmarkCatalogPath,
1298
+ laneConfig.benchmarkCatalogPath || projectProfile.paths.benchmarkCatalogPath,
1084
1299
  `${lane}.benchmarkCatalogPath`,
1085
1300
  ),
1086
1301
  componentCutoverMatrixDocPath: normalizeRepoRelativePath(
1087
1302
  laneConfig.componentCutoverMatrixDocPath ||
1088
- config.paths.componentCutoverMatrixDocPath ||
1303
+ projectProfile.paths.componentCutoverMatrixDocPath ||
1089
1304
  defaultComponentCutoverMatrixDocPath(plansDir),
1090
1305
  `${lane}.componentCutoverMatrixDocPath`,
1091
1306
  ),
1092
1307
  componentCutoverMatrixJsonPath: normalizeRepoRelativePath(
1093
1308
  laneConfig.componentCutoverMatrixJsonPath ||
1094
- config.paths.componentCutoverMatrixJsonPath ||
1309
+ projectProfile.paths.componentCutoverMatrixJsonPath ||
1095
1310
  defaultComponentCutoverMatrixJsonPath(plansDir),
1096
1311
  `${lane}.componentCutoverMatrixJsonPath`,
1097
1312
  ),
@@ -19,10 +19,9 @@ import {
19
19
  DEFAULT_COORDINATION_ACK_TIMEOUT_MS,
20
20
  DEFAULT_COORDINATION_RESOLUTION_STALE_MS,
21
21
  ensureDirectory,
22
+ findAdhocRunRecord,
22
23
  parseNonNegativeInt,
23
- readJsonOrNull,
24
24
  readStatusRecordIfPresent,
25
- REPO_ROOT,
26
25
  sanitizeAdhocRunId,
27
26
  sanitizeLaneName,
28
27
  toIsoTimestamp,
@@ -51,23 +50,23 @@ import {
51
50
 
52
51
  function printUsage() {
53
52
  console.log(`Usage:
54
- wave control status --lane <lane> --wave <n> [--agent <id>] [--json]
55
- wave control telemetry status --lane <lane> [--run <id>] [--json]
56
- wave control telemetry flush --lane <lane> [--run <id>] [--json]
53
+ wave control status --project <id> --lane <lane> --wave <n> [--agent <id>] [--json]
54
+ wave control telemetry status --project <id> --lane <lane> [--run <id>] [--json]
55
+ wave control telemetry flush --project <id> --lane <lane> [--run <id>] [--json]
57
56
 
58
- wave control task create --lane <lane> --wave <n> --agent <id> --kind <request|blocker|clarification|handoff|evidence|claim|decision|human-input> --summary <text> [options]
59
- wave control task list --lane <lane> --wave <n> [--agent <id>] [--json]
60
- wave control task get --lane <lane> --wave <n> --id <task-id> [--json]
61
- wave control task act <start|resolve|dismiss|cancel|reassign|answer|escalate|defer|mark-advisory|mark-stale|resolve-policy> --lane <lane> --wave <n> --id <task-id> [options]
57
+ wave control task create --project <id> --lane <lane> --wave <n> --agent <id> --kind <request|blocker|clarification|handoff|evidence|claim|decision|human-input> --summary <text> [options]
58
+ wave control task list --project <id> --lane <lane> --wave <n> [--agent <id>] [--json]
59
+ wave control task get --project <id> --lane <lane> --wave <n> --id <task-id> [--json]
60
+ wave control task act <start|resolve|dismiss|cancel|reassign|answer|escalate|defer|mark-advisory|mark-stale|resolve-policy> --project <id> --lane <lane> --wave <n> --id <task-id> [options]
62
61
 
63
- wave control rerun request --lane <lane> --wave <n> [--agent <id> ...] [--resume-cursor <cursor>] [--reuse-attempt <id> ...] [--reuse-proof <id> ...] [--reuse-derived-summaries <true|false>] [--invalidate-component <id> ...] [--clear-reuse <id> ...] [--preserve-reuse <id> ...] [--requested-by <name>] [--reason <text>] [--json]
64
- wave control rerun get --lane <lane> --wave <n> [--json]
65
- wave control rerun clear --lane <lane> --wave <n>
62
+ wave control rerun request --project <id> --lane <lane> --wave <n> [--agent <id> ...] [--resume-cursor <cursor>] [--reuse-attempt <id> ...] [--reuse-proof <id> ...] [--reuse-derived-summaries <true|false>] [--invalidate-component <id> ...] [--clear-reuse <id> ...] [--preserve-reuse <id> ...] [--requested-by <name>] [--reason <text>] [--json]
63
+ wave control rerun get --project <id> --lane <lane> --wave <n> [--json]
64
+ wave control rerun clear --project <id> --lane <lane> --wave <n>
66
65
 
67
- wave control proof register --lane <lane> --wave <n> --agent <id> --artifact <path> [--artifact <path> ...] [--component <id[:level]> ...] [--authoritative] [--satisfy-owned-components] [--completion <level>] [--durability <level>] [--proof-level <level>] [--doc-delta <state>] [--operator <name>] [--detail <text>] [--json]
68
- wave control proof get --lane <lane> --wave <n> [--agent <id>] [--id <bundle-id>] [--json]
69
- wave control proof supersede --lane <lane> --wave <n> --id <bundle-id> --agent <id> --artifact <path> [--artifact <path> ...] [options]
70
- wave control proof revoke --lane <lane> --wave <n> --id <bundle-id> [--operator <name>] [--detail <text>] [--json]
66
+ wave control proof register --project <id> --lane <lane> --wave <n> --agent <id> --artifact <path> [--artifact <path> ...] [--component <id[:level]> ...] [--authoritative] [--satisfy-owned-components] [--completion <level>] [--durability <level>] [--proof-level <level>] [--doc-delta <state>] [--operator <name>] [--detail <text>] [--json]
67
+ wave control proof get --project <id> --lane <lane> --wave <n> [--agent <id>] [--id <bundle-id>] [--json]
68
+ wave control proof supersede --project <id> --lane <lane> --wave <n> --id <bundle-id> --agent <id> --artifact <path> [--artifact <path> ...] [options]
69
+ wave control proof revoke --project <id> --lane <lane> --wave <n> --id <bundle-id> [--operator <name>] [--detail <text>] [--json]
71
70
  `);
72
71
  }
73
72
 
@@ -91,6 +90,7 @@ function parseArgs(argv) {
91
90
  const operation = String(args[1] || "").trim().toLowerCase();
92
91
  const action = String(args[2] || "").trim().toLowerCase();
93
92
  const options = {
93
+ project: "",
94
94
  lane: "main",
95
95
  wave: null,
96
96
  runId: "",
@@ -144,7 +144,9 @@ function parseArgs(argv) {
144
144
  if (i < startIndex) {
145
145
  continue;
146
146
  }
147
- if (arg === "--lane") {
147
+ if (arg === "--project") {
148
+ options.project = String(args[++i] || "").trim();
149
+ } else if (arg === "--lane") {
148
150
  options.lane = sanitizeLaneName(args[++i]);
149
151
  } else if (arg === "--run") {
150
152
  options.runId = sanitizeAdhocRunId(args[++i]);
@@ -228,11 +230,12 @@ function parseArgs(argv) {
228
230
  return { help: false, surface, operation, action, options };
229
231
  }
230
232
 
231
- function resolveLaneForRun(runId, fallbackLane) {
232
- return (
233
- readJsonOrNull(path.join(REPO_ROOT, ".wave", "adhoc", "runs", runId, "result.json"))?.lane ||
234
- fallbackLane
235
- );
233
+ function resolveRunContext(runId, fallbackProject, fallbackLane) {
234
+ const record = findAdhocRunRecord(runId);
235
+ return {
236
+ project: record?.project || fallbackProject,
237
+ lane: record?.result?.lane || fallbackLane,
238
+ };
236
239
  }
237
240
 
238
241
  function loadWave(lanePaths, waveNumber) {
@@ -955,9 +958,12 @@ export async function runControlCli(argv) {
955
958
  throw new Error("Expected control surface: status | telemetry | task | rerun | proof");
956
959
  }
957
960
  if (options.runId) {
958
- options.lane = resolveLaneForRun(options.runId, options.lane);
961
+ const context = resolveRunContext(options.runId, options.project, options.lane);
962
+ options.project = context.project;
963
+ options.lane = context.lane;
959
964
  }
960
965
  const lanePaths = buildLanePaths(options.lane, {
966
+ project: options.project || undefined,
961
967
  runVariant: options.dryRun ? "dry-run" : undefined,
962
968
  adhocRunId: options.runId || null,
963
969
  });
@@ -35,22 +35,21 @@ import { writeAssignmentSnapshot, writeDependencySnapshot } from "./artifact-sch
35
35
  import {
36
36
  buildLanePaths,
37
37
  ensureDirectory,
38
+ findAdhocRunRecord,
38
39
  parseNonNegativeInt,
39
- readJsonOrNull,
40
- REPO_ROOT,
41
40
  sanitizeAdhocRunId,
42
41
  } from "./shared.mjs";
43
42
  import { parseWaveFiles } from "./wave-files.mjs";
44
43
 
45
44
  function printUsage() {
46
45
  console.log(`Usage:
47
- wave coord post --lane <lane> --wave <n> --agent <id> --kind <kind> --summary <text> [--dry-run] [options]
48
- wave coord show --lane <lane> --wave <n> [--dry-run] [--json]
49
- wave coord render --lane <lane> --wave <n> [--dry-run]
50
- wave coord inbox --lane <lane> --wave <n> --agent <id> [--dry-run]
51
- wave coord explain --lane <lane> --wave <n> [--agent <id>] [--json]
52
- wave coord act <resolve|dismiss|reroute|reassign|escalate|answer-human> --lane <lane> --wave <n> [options]
53
- wave coord <subcommand> --run <id> [--wave 0] ...
46
+ wave coord post --project <id> --lane <lane> --wave <n> --agent <id> --kind <kind> --summary <text> [--dry-run] [options]
47
+ wave coord show --project <id> --lane <lane> --wave <n> [--dry-run] [--json]
48
+ wave coord render --project <id> --lane <lane> --wave <n> [--dry-run]
49
+ wave coord inbox --project <id> --lane <lane> --wave <n> --agent <id> [--dry-run]
50
+ wave coord explain --project <id> --lane <lane> --wave <n> [--agent <id>] [--json]
51
+ wave coord act <resolve|dismiss|reroute|reassign|escalate|answer-human> --project <id> --lane <lane> --wave <n> [options]
52
+ wave coord <subcommand> --run <id> [--project <id>] [--wave 0] ...
54
53
  `);
55
54
  }
56
55
 
@@ -58,6 +57,7 @@ function parseArgs(argv) {
58
57
  const args = argv[0] === "--" ? argv.slice(1) : argv;
59
58
  const subcommand = String(args[0] || "").trim().toLowerCase();
60
59
  const options = {
60
+ project: "",
61
61
  lane: "main",
62
62
  wave: null,
63
63
  runId: "",
@@ -85,7 +85,9 @@ function parseArgs(argv) {
85
85
  }
86
86
  for (let i = startIndex; i < args.length; i += 1) {
87
87
  const arg = args[i];
88
- if (arg === "--lane") {
88
+ if (arg === "--project") {
89
+ options.project = String(args[++i] || "").trim();
90
+ } else if (arg === "--lane") {
89
91
  options.lane = String(args[++i] || "").trim();
90
92
  } else if (arg === "--run") {
91
93
  options.runId = sanitizeAdhocRunId(args[++i]);
@@ -131,9 +133,12 @@ function parseArgs(argv) {
131
133
  return { subcommand, options };
132
134
  }
133
135
 
134
- function resolveLaneForRun(runId, fallbackLane) {
135
- const resultPath = path.join(REPO_ROOT, ".wave", "adhoc", "runs", runId, "result.json");
136
- return readJsonOrNull(resultPath)?.lane || fallbackLane;
136
+ function resolveRunContext(runId, fallbackProject, fallbackLane) {
137
+ const record = findAdhocRunRecord(runId);
138
+ return {
139
+ project: record?.project || fallbackProject,
140
+ lane: record?.result?.lane || fallbackLane,
141
+ };
137
142
  }
138
143
 
139
144
  function loadWave(lanePaths, waveNumber) {
@@ -329,9 +334,12 @@ export async function runCoordinationCli(argv) {
329
334
  }
330
335
  const { subcommand, options } = parseArgs(argv);
331
336
  if (options.runId) {
332
- options.lane = resolveLaneForRun(options.runId, options.lane);
337
+ const context = resolveRunContext(options.runId, options.project, options.lane);
338
+ options.project = context.project;
339
+ options.lane = context.lane;
333
340
  }
334
341
  const lanePaths = buildLanePaths(options.lane, {
342
+ project: options.project || undefined,
335
343
  runVariant: options.dryRun ? "dry-run" : undefined,
336
344
  adhocRunId: options.runId || null,
337
345
  });
@@ -242,8 +242,11 @@ export function normalizeCoordinationRecord(rawRecord, defaults = {}) {
242
242
  : Number.parseInt(String(rawRecord.attempt), 10),
243
243
  source: normalizeString(rawRecord.source ?? defaults.source, "launcher"),
244
244
  executorId: normalizeString(rawRecord.executorId ?? defaults.executorId, ""),
245
+ project: normalizeString(rawRecord.project ?? defaults.project, ""),
245
246
  requesterLane: normalizeString(rawRecord.requesterLane ?? defaults.requesterLane, ""),
246
247
  ownerLane: normalizeString(rawRecord.ownerLane ?? defaults.ownerLane, ""),
248
+ requesterProject: normalizeString(rawRecord.requesterProject ?? defaults.requesterProject, ""),
249
+ ownerProject: normalizeString(rawRecord.ownerProject ?? defaults.ownerProject, ""),
247
250
  requesterWave:
248
251
  rawRecord.requesterWave === null || rawRecord.requesterWave === undefined || rawRecord.requesterWave === ""
249
252
  ? defaults.requesterWave ?? null
@@ -264,8 +267,10 @@ export function appendCoordinationRecord(filePath, rawRecord, defaults = {}) {
264
267
  ensureDirectory(path.dirname(filePath));
265
268
  fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf8");
266
269
  const runIdHint = normalizeString(rawRecord?.runId ?? defaults.runId, "");
270
+ const projectHint = normalizeString(rawRecord?.project ?? defaults.project, "");
267
271
  try {
268
272
  const lanePaths = buildLanePaths(record.lane, {
273
+ ...(projectHint ? { project: projectHint } : {}),
269
274
  ...(runIdHint ? { adhocRunId: runIdHint } : {}),
270
275
  });
271
276
  if (lanePaths?.waveControl?.captureCoordinationRecords !== false) {
@@ -301,8 +306,11 @@ export function appendCoordinationRecord(filePath, rawRecord, defaults = {}) {
301
306
  closureCondition: record.closureCondition,
302
307
  required: record.required,
303
308
  executorId: record.executorId || null,
309
+ project: record.project || null,
304
310
  requesterLane: record.requesterLane || null,
305
311
  ownerLane: record.ownerLane || null,
312
+ requesterProject: record.requesterProject || null,
313
+ ownerProject: record.ownerProject || null,
306
314
  },
307
315
  });
308
316
  }
@@ -36,6 +36,7 @@ function normalizeDashboardAttachTarget(value) {
36
36
 
37
37
  export function parseDashboardArgs(argv) {
38
38
  const options = {
39
+ project: null,
39
40
  lane: DEFAULT_WAVE_LANE,
40
41
  dashboardFile: null,
41
42
  messageBoard: null,
@@ -50,6 +51,8 @@ export function parseDashboardArgs(argv) {
50
51
  }
51
52
  if (arg === "--watch") {
52
53
  options.watch = true;
54
+ } else if (arg === "--project") {
55
+ options.project = String(argv[++i] || "").trim() || null;
53
56
  } else if (arg === "--lane") {
54
57
  options.lane =
55
58
  String(argv[++i] || "")
@@ -98,9 +101,12 @@ function tmuxSessionExists(socketName, sessionName) {
98
101
  throw new Error((result.stderr || result.stdout || "tmux has-session failed").trim());
99
102
  }
100
103
 
101
- function attachDashboardSession(lane, target) {
104
+ function attachDashboardSession(project, lane, target) {
102
105
  const config = loadWaveConfig();
103
- const lanePaths = buildLanePaths(lane, { config });
106
+ const lanePaths = buildLanePaths(lane, {
107
+ config,
108
+ project: project || config.defaultProject,
109
+ });
104
110
  const entry =
105
111
  target === "global"
106
112
  ? createGlobalDashboardTerminalEntry(lanePaths, "current")
@@ -449,6 +455,7 @@ export async function runDashboardCli(argv) {
449
455
  console.log(`Usage: pnpm exec wave dashboard --dashboard-file <path> [options]
450
456
 
451
457
  Options:
458
+ --project <id> Project id (default: config default)
452
459
  --lane <name> Wave lane name (default: ${DEFAULT_WAVE_LANE})
453
460
  --dashboard-file <path> Path to wave/global dashboard JSON
454
461
  --message-board <path> Optional message board path override
@@ -461,7 +468,7 @@ Options:
461
468
  }
462
469
 
463
470
  if (options.attach) {
464
- attachDashboardSession(options.lane, options.attach);
471
+ attachDashboardSession(options.project, options.lane, options.attach);
465
472
  return;
466
473
  }
467
474