@chllming/wave-orchestration 0.8.9 → 0.9.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +135 -18
  3. package/docs/README.md +9 -3
  4. package/docs/architecture/README.md +1498 -0
  5. package/docs/concepts/context7-vs-skills.md +1 -1
  6. package/docs/concepts/operating-modes.md +3 -3
  7. package/docs/concepts/what-is-a-wave.md +1 -1
  8. package/docs/guides/author-and-run-waves.md +27 -4
  9. package/docs/guides/monorepo-projects.md +226 -0
  10. package/docs/guides/planner.md +10 -3
  11. package/docs/guides/{recommendations-0.8.9.md → recommendations-0.9.1.md} +8 -7
  12. package/docs/guides/sandboxed-environments.md +158 -0
  13. package/docs/guides/terminal-surfaces.md +14 -12
  14. package/docs/plans/current-state.md +11 -7
  15. package/docs/plans/end-state-architecture.md +3 -1
  16. package/docs/plans/examples/wave-example-design-handoff.md +3 -1
  17. package/docs/plans/examples/wave-example-live-proof.md +6 -1
  18. package/docs/plans/examples/wave-example-rollout-fidelity.md +2 -0
  19. package/docs/plans/migration.md +48 -18
  20. package/docs/plans/sandbox-end-state-architecture.md +153 -0
  21. package/docs/plans/wave-orchestrator.md +4 -4
  22. package/docs/reference/cli-reference.md +125 -57
  23. package/docs/reference/coordination-and-closure.md +1 -1
  24. package/docs/reference/github-packages-setup.md +1 -1
  25. package/docs/reference/migration-0.2-to-0.5.md +9 -7
  26. package/docs/reference/npmjs-token-publishing.md +53 -0
  27. package/docs/reference/npmjs-trusted-publishing.md +4 -50
  28. package/docs/reference/package-publishing-flow.md +272 -0
  29. package/docs/reference/runtime-config/README.md +140 -12
  30. package/docs/reference/sample-waves.md +100 -5
  31. package/docs/reference/skills.md +1 -1
  32. package/docs/reference/wave-control.md +23 -5
  33. package/docs/roadmap.md +43 -201
  34. package/package.json +1 -1
  35. package/releases/manifest.json +38 -0
  36. package/scripts/wave-orchestrator/adhoc.mjs +49 -17
  37. package/scripts/wave-orchestrator/agent-process-runner.mjs +344 -0
  38. package/scripts/wave-orchestrator/agent-state.mjs +0 -1
  39. package/scripts/wave-orchestrator/artifact-schemas.mjs +7 -0
  40. package/scripts/wave-orchestrator/autonomous.mjs +96 -29
  41. package/scripts/wave-orchestrator/benchmark-external.mjs +23 -7
  42. package/scripts/wave-orchestrator/benchmark.mjs +33 -10
  43. package/scripts/wave-orchestrator/closure-engine.mjs +138 -17
  44. package/scripts/wave-orchestrator/config.mjs +239 -24
  45. package/scripts/wave-orchestrator/control-cli.mjs +71 -28
  46. package/scripts/wave-orchestrator/coord-cli.mjs +22 -14
  47. package/scripts/wave-orchestrator/coordination-store.mjs +8 -0
  48. package/scripts/wave-orchestrator/dashboard-renderer.mjs +123 -44
  49. package/scripts/wave-orchestrator/dep-cli.mjs +47 -21
  50. package/scripts/wave-orchestrator/derived-state-engine.mjs +6 -3
  51. package/scripts/wave-orchestrator/feedback.mjs +28 -11
  52. package/scripts/wave-orchestrator/gate-engine.mjs +106 -38
  53. package/scripts/wave-orchestrator/human-input-resolution.mjs +5 -1
  54. package/scripts/wave-orchestrator/install.mjs +13 -0
  55. package/scripts/wave-orchestrator/launcher-progress.mjs +91 -0
  56. package/scripts/wave-orchestrator/launcher-runtime.mjs +179 -68
  57. package/scripts/wave-orchestrator/launcher.mjs +222 -53
  58. package/scripts/wave-orchestrator/ledger.mjs +7 -2
  59. package/scripts/wave-orchestrator/planner.mjs +48 -27
  60. package/scripts/wave-orchestrator/project-profile.mjs +31 -8
  61. package/scripts/wave-orchestrator/projection-writer.mjs +13 -1
  62. package/scripts/wave-orchestrator/proof-cli.mjs +18 -12
  63. package/scripts/wave-orchestrator/reducer-snapshot.mjs +6 -0
  64. package/scripts/wave-orchestrator/retry-cli.mjs +19 -13
  65. package/scripts/wave-orchestrator/retry-control.mjs +3 -3
  66. package/scripts/wave-orchestrator/retry-engine.mjs +93 -6
  67. package/scripts/wave-orchestrator/role-helpers.mjs +30 -0
  68. package/scripts/wave-orchestrator/session-supervisor.mjs +94 -85
  69. package/scripts/wave-orchestrator/shared.mjs +77 -14
  70. package/scripts/wave-orchestrator/supervisor-cli.mjs +1306 -0
  71. package/scripts/wave-orchestrator/terminals.mjs +12 -32
  72. package/scripts/wave-orchestrator/tmux-adapter.mjs +300 -0
  73. package/scripts/wave-orchestrator/wave-control-client.mjs +84 -16
  74. package/scripts/wave-orchestrator/wave-files.mjs +43 -6
  75. package/scripts/wave.mjs +13 -0
@@ -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
  }
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { loadWaveConfig } from "./config.mjs";
@@ -14,6 +13,7 @@ import {
14
13
  formatAgeFromTimestamp,
15
14
  formatElapsed,
16
15
  pad,
16
+ readJsonOrNull,
17
17
  sleep,
18
18
  truncate,
19
19
  } from "./shared.mjs";
@@ -21,6 +21,10 @@ import {
21
21
  createCurrentWaveDashboardTerminalEntry,
22
22
  createGlobalDashboardTerminalEntry,
23
23
  } from "./terminals.mjs";
24
+ import {
25
+ attachSession as attachTmuxSession,
26
+ hasSession as hasTmuxSession,
27
+ } from "./tmux-adapter.mjs";
24
28
 
25
29
  const DASHBOARD_ATTACH_TARGETS = ["current", "global"];
26
30
 
@@ -36,6 +40,7 @@ function normalizeDashboardAttachTarget(value) {
36
40
 
37
41
  export function parseDashboardArgs(argv) {
38
42
  const options = {
43
+ project: null,
39
44
  lane: DEFAULT_WAVE_LANE,
40
45
  dashboardFile: null,
41
46
  messageBoard: null,
@@ -50,6 +55,8 @@ export function parseDashboardArgs(argv) {
50
55
  }
51
56
  if (arg === "--watch") {
52
57
  options.watch = true;
58
+ } else if (arg === "--project") {
59
+ options.project = String(argv[++i] || "").trim() || null;
53
60
  } else if (arg === "--lane") {
54
61
  options.lane =
55
62
  String(argv[++i] || "")
@@ -75,55 +82,122 @@ export function parseDashboardArgs(argv) {
75
82
  return { help: false, options };
76
83
  }
77
84
 
78
- function tmuxSessionExists(socketName, sessionName) {
79
- const result = spawnSync("tmux", ["-L", socketName, "has-session", "-t", sessionName], {
80
- cwd: REPO_ROOT,
81
- encoding: "utf8",
82
- env: { ...process.env, TMUX: "" },
83
- });
84
- if (result.error) {
85
- throw new Error(`tmux session lookup failed: ${result.error.message}`);
86
- }
87
- if (result.status === 0) {
88
- return true;
89
- }
90
- const combined = `${String(result.stderr || "").toLowerCase()}\n${String(result.stdout || "").toLowerCase()}`;
91
- if (
92
- combined.includes("can't find session") ||
93
- combined.includes("no server running") ||
94
- combined.includes("error connecting")
95
- ) {
96
- return false;
97
- }
98
- throw new Error((result.stderr || result.stdout || "tmux has-session failed").trim());
99
- }
100
-
101
- function attachDashboardSession(lane, target) {
85
+ async function attachDashboardSession(project, lane, target) {
102
86
  const config = loadWaveConfig();
103
- const lanePaths = buildLanePaths(lane, { config });
87
+ const lanePaths = buildLanePaths(lane, {
88
+ config,
89
+ project: project || config.defaultProject,
90
+ });
104
91
  const entry =
105
92
  target === "global"
106
93
  ? createGlobalDashboardTerminalEntry(lanePaths, "current")
107
94
  : createCurrentWaveDashboardTerminalEntry(lanePaths);
108
- if (!tmuxSessionExists(lanePaths.tmuxSocketName, entry.sessionName)) {
109
- const dashboardsRel = path.relative(REPO_ROOT, path.dirname(lanePaths.globalDashboardPath));
110
- throw new Error(
111
- `No ${target} dashboard session is live for lane ${lanePaths.lane}. Launch a dashboarded run on that lane, then inspect ${dashboardsRel} if you need the last written dashboard state.`,
112
- );
95
+ if (!await hasTmuxSession(lanePaths.tmuxSocketName, entry.sessionName, { allowMissingBinary: false })) {
96
+ const fallback = resolveDashboardAttachFallback(lanePaths, target);
97
+ if (fallback) {
98
+ return fallback;
99
+ }
100
+ throw new Error(buildMissingDashboardAttachError(lanePaths, target));
101
+ }
102
+ try {
103
+ await attachTmuxSession(lanePaths.tmuxSocketName, entry.sessionName);
104
+ return null;
105
+ } catch (error) {
106
+ if (error?.tmuxMissingSession) {
107
+ const fallback = resolveDashboardAttachFallback(lanePaths, target);
108
+ if (fallback) {
109
+ return fallback;
110
+ }
111
+ throw new Error(buildMissingDashboardAttachError(lanePaths, target));
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ function buildMissingDashboardAttachError(lanePaths, target) {
118
+ const dashboardsRel = path.relative(REPO_ROOT, path.dirname(lanePaths.globalDashboardPath));
119
+ return `No ${target} dashboard session is live for lane ${lanePaths.lane}. Launch a dashboarded run on that lane, then inspect ${dashboardsRel} if you need the last written dashboard state.`;
120
+ }
121
+
122
+ function waveDashboardPathForNumber(lanePaths, waveNumber) {
123
+ if (!Number.isFinite(Number(waveNumber))) {
124
+ return null;
113
125
  }
114
- const result = spawnSync("tmux", ["-L", lanePaths.tmuxSocketName, "attach", "-t", entry.sessionName], {
115
- cwd: REPO_ROOT,
116
- stdio: "inherit",
117
- env: { ...process.env, TMUX: "" },
126
+ const candidate = path.join(lanePaths.dashboardsDir, `wave-${Number(waveNumber)}.json`);
127
+ return fs.existsSync(candidate) ? candidate : null;
128
+ }
129
+
130
+ function selectCurrentWaveFromGlobalDashboard(globalState) {
131
+ const waves = Array.isArray(globalState?.waves) ? globalState.waves : [];
132
+ const candidates = waves
133
+ .map((wave) => ({
134
+ waveNumber: Number.parseInt(String(wave?.wave ?? ""), 10),
135
+ status: String(wave?.status || "").trim().toLowerCase(),
136
+ updatedAt: Date.parse(
137
+ String(wave?.updatedAt || wave?.completedAt || wave?.startedAt || ""),
138
+ ),
139
+ }))
140
+ .filter((entry) => Number.isFinite(entry.waveNumber));
141
+ if (candidates.length === 0) {
142
+ return null;
143
+ }
144
+ candidates.sort((left, right) => {
145
+ const leftTerminal = TERMINAL_STATES.has(left.status);
146
+ const rightTerminal = TERMINAL_STATES.has(right.status);
147
+ if (leftTerminal !== rightTerminal) {
148
+ return leftTerminal ? 1 : -1;
149
+ }
150
+ const leftUpdatedAt = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
151
+ const rightUpdatedAt = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
152
+ if (leftUpdatedAt !== rightUpdatedAt) {
153
+ return rightUpdatedAt - leftUpdatedAt;
154
+ }
155
+ return right.waveNumber - left.waveNumber;
118
156
  });
119
- if (result.error) {
120
- throw new Error(`tmux attach failed: ${result.error.message}`);
157
+ return candidates[0].waveNumber;
158
+ }
159
+
160
+ export function resolveDashboardAttachFallback(lanePaths, target) {
161
+ if (target === "global") {
162
+ return fs.existsSync(lanePaths.globalDashboardPath)
163
+ ? { dashboardFile: lanePaths.globalDashboardPath }
164
+ : null;
121
165
  }
122
- if (result.status !== 0) {
123
- throw new Error(
124
- `tmux attach exited ${result.status} for lane ${lanePaths.lane} ${target} dashboard session ${entry.sessionName}.`,
125
- );
166
+ const globalState = readJsonOrNull(lanePaths.globalDashboardPath);
167
+ const preferredWaveNumber = selectCurrentWaveFromGlobalDashboard(globalState);
168
+ const preferredWavePath = waveDashboardPathForNumber(lanePaths, preferredWaveNumber);
169
+ if (preferredWavePath) {
170
+ return { dashboardFile: preferredWavePath };
126
171
  }
172
+ if (!fs.existsSync(lanePaths.dashboardsDir)) {
173
+ return fs.existsSync(lanePaths.globalDashboardPath)
174
+ ? { dashboardFile: lanePaths.globalDashboardPath }
175
+ : null;
176
+ }
177
+ const candidates = fs.readdirSync(lanePaths.dashboardsDir, { withFileTypes: true })
178
+ .filter((entry) => entry.isFile())
179
+ .map((entry) => ({
180
+ filePath: path.join(lanePaths.dashboardsDir, entry.name),
181
+ match: entry.name.match(/^wave-(\d+)\.json$/),
182
+ }))
183
+ .filter((entry) => entry.match)
184
+ .map((entry) => ({
185
+ dashboardFile: entry.filePath,
186
+ waveNumber: Number.parseInt(entry.match[1], 10),
187
+ mtimeMs: fs.statSync(entry.filePath).mtimeMs,
188
+ }))
189
+ .sort((left, right) => {
190
+ if (left.mtimeMs !== right.mtimeMs) {
191
+ return right.mtimeMs - left.mtimeMs;
192
+ }
193
+ return right.waveNumber - left.waveNumber;
194
+ });
195
+ if (candidates.length > 0) {
196
+ return { dashboardFile: candidates[0].dashboardFile };
197
+ }
198
+ return fs.existsSync(lanePaths.globalDashboardPath)
199
+ ? { dashboardFile: lanePaths.globalDashboardPath }
200
+ : null;
127
201
  }
128
202
 
129
203
  function readMessageBoardTail(messageBoardPath, maxLines = 24) {
@@ -449,11 +523,12 @@ export async function runDashboardCli(argv) {
449
523
  console.log(`Usage: pnpm exec wave dashboard --dashboard-file <path> [options]
450
524
 
451
525
  Options:
526
+ --project <id> Project id (default: config default)
452
527
  --lane <name> Wave lane name (default: ${DEFAULT_WAVE_LANE})
453
528
  --dashboard-file <path> Path to wave/global dashboard JSON
454
529
  --message-board <path> Optional message board path override
455
530
  --attach <current|global>
456
- Attach to the stable tmux-backed dashboard session for the lane
531
+ Attach to the stable dashboard session for the lane, or follow the last written dashboard file when no live session exists
457
532
  --watch Refresh continuously
458
533
  --refresh-ms <n> Refresh interval in ms (default: ${DEFAULT_REFRESH_MS})
459
534
  `);
@@ -461,8 +536,12 @@ Options:
461
536
  }
462
537
 
463
538
  if (options.attach) {
464
- attachDashboardSession(options.lane, options.attach);
465
- return;
539
+ const fallback = await attachDashboardSession(options.project, options.lane, options.attach);
540
+ if (!fallback) {
541
+ return;
542
+ }
543
+ options.dashboardFile = fallback.dashboardFile;
544
+ options.watch = true;
466
545
  }
467
546
 
468
547
  let terminalStateReachedAt = null;
@@ -15,10 +15,10 @@ import { parseWaveFiles } from "./wave-files.mjs";
15
15
 
16
16
  function printUsage() {
17
17
  console.log(`Usage:
18
- wave dep post --owner-lane <lane> --requester-lane <lane> --owner-wave <n> --requester-wave <n> --agent <id> --summary <text> [options]
19
- wave dep show --lane <lane> [--wave <n>] [--json]
20
- wave dep resolve --lane <lane> --id <id> --agent <id> [--detail <text>] [--status resolved|closed]
21
- wave dep render --lane <lane> [--wave <n>] [--json]
18
+ wave dep post --owner-project <id> --owner-lane <lane> --requester-project <id> --requester-lane <lane> --owner-wave <n> --requester-wave <n> --agent <id> --summary <text> [options]
19
+ wave dep show --project <id> --lane <lane> [--wave <n>] [--json]
20
+ wave dep resolve --project <id> --lane <lane> --id <id> --agent <id> [--detail <text>] [--status resolved|closed]
21
+ wave dep render --project <id> --lane <lane> [--wave <n>] [--json]
22
22
  `);
23
23
  }
24
24
 
@@ -26,6 +26,9 @@ function parseArgs(argv) {
26
26
  const args = argv[0] === "--" ? argv.slice(1) : argv;
27
27
  const subcommand = String(args[0] || "").trim().toLowerCase();
28
28
  const options = {
29
+ project: "",
30
+ ownerProject: "",
31
+ requesterProject: "",
29
32
  lane: "",
30
33
  ownerLane: "",
31
34
  requesterLane: "",
@@ -46,7 +49,13 @@ function parseArgs(argv) {
46
49
  };
47
50
  for (let index = 1; index < args.length; index += 1) {
48
51
  const arg = args[index];
49
- if (arg === "--lane") {
52
+ if (arg === "--project") {
53
+ options.project = String(args[++index] || "").trim();
54
+ } else if (arg === "--owner-project") {
55
+ options.ownerProject = String(args[++index] || "").trim();
56
+ } else if (arg === "--requester-project") {
57
+ options.requesterProject = String(args[++index] || "").trim();
58
+ } else if (arg === "--lane") {
50
59
  options.lane = String(args[++index] || "").trim();
51
60
  } else if (arg === "--owner-lane") {
52
61
  options.ownerLane = String(args[++index] || "").trim();
@@ -112,11 +121,15 @@ export async function runDependencyCli(argv) {
112
121
  return;
113
122
  }
114
123
  const { subcommand, options } = parseArgs(argv);
124
+ const baseProject =
125
+ options.project || options.ownerProject || options.requesterProject || undefined;
115
126
  const baseLane = options.lane || options.ownerLane || options.requesterLane || "main";
116
- const lanePaths = buildLanePaths(baseLane);
127
+ const lanePaths = buildLanePaths(baseLane, { project: baseProject });
117
128
  ensureDirectory(lanePaths.crossLaneDependenciesDir);
118
129
 
119
130
  if (subcommand === "post") {
131
+ const ownerProject = options.ownerProject || options.project || lanePaths.project;
132
+ const requesterProject = options.requesterProject || options.project || lanePaths.project;
120
133
  const ownerLane = options.ownerLane || options.lane;
121
134
  const requesterLane = options.requesterLane || lanePaths.lane;
122
135
  if (!ownerLane || !requesterLane || options.ownerWave === null || options.requesterWave === null) {
@@ -125,13 +138,17 @@ export async function runDependencyCli(argv) {
125
138
  if (!options.agent || !options.summary) {
126
139
  throw new Error("--agent and --summary are required");
127
140
  }
128
- const record = appendDependencyTicket(lanePaths.crossLaneDependenciesDir, ownerLane, {
141
+ const ownerLanePaths = buildLanePaths(ownerLane, { project: ownerProject });
142
+ ensureDirectory(ownerLanePaths.crossLaneDependenciesDir);
143
+ const record = appendDependencyTicket(ownerLanePaths.crossLaneDependenciesDir, ownerLane, {
129
144
  id: options.id || `dep-${Date.now().toString(36)}`,
130
145
  kind: "request",
131
146
  lane: ownerLane,
132
147
  wave: options.ownerWave,
148
+ ownerProject,
133
149
  ownerLane,
134
150
  ownerWave: options.ownerWave,
151
+ requesterProject,
135
152
  requesterLane,
136
153
  requesterWave: options.requesterWave,
137
154
  agentId: options.agent,
@@ -155,14 +172,15 @@ export async function runDependencyCli(argv) {
155
172
  if (!lane || !options.id || !options.agent) {
156
173
  throw new Error("--lane, --id, and --agent are required for resolve");
157
174
  }
158
- const filePath = dependencyFilePath(lanePaths, lane);
159
- const latest = materializeCoordinationState(readDependencyTickets(lanePaths.crossLaneDependenciesDir, lane)).byId.get(
175
+ const targetProject = options.project || options.ownerProject || lanePaths.project;
176
+ const targetLanePaths = buildLanePaths(lane, { project: targetProject });
177
+ const latest = materializeCoordinationState(readDependencyTickets(targetLanePaths.crossLaneDependenciesDir, lane)).byId.get(
160
178
  options.id,
161
179
  );
162
180
  if (!latest) {
163
181
  throw new Error(`Dependency ${options.id} not found for lane ${lane}`);
164
182
  }
165
- const record = appendDependencyTicket(lanePaths.crossLaneDependenciesDir, lane, {
183
+ const record = appendDependencyTicket(targetLanePaths.crossLaneDependenciesDir, lane, {
166
184
  ...latest,
167
185
  agentId: options.agent,
168
186
  status: options.status || "resolved",
@@ -175,17 +193,23 @@ export async function runDependencyCli(argv) {
175
193
 
176
194
  if (subcommand === "show") {
177
195
  const lane = options.lane || options.ownerLane || lanePaths.lane;
196
+ const targetProject = options.project || options.ownerProject || lanePaths.project;
197
+ const targetLanePaths = buildLanePaths(lane, { project: targetProject });
178
198
  const records =
179
199
  options.wave === null
180
- ? readAllDependencyTickets(lanePaths.crossLaneDependenciesDir).filter(
181
- (record) => record.ownerLane === lane || record.requesterLane === lane || record.lane === lane,
200
+ ? readAllDependencyTickets(targetLanePaths.crossLaneDependenciesDir).filter(
201
+ (record) =>
202
+ (record.ownerLane === lane || record.requesterLane === lane || record.lane === lane) &&
203
+ (!options.project ||
204
+ record.ownerProject === targetProject ||
205
+ record.requesterProject === targetProject),
182
206
  )
183
207
  : buildDependencySnapshot({
184
- dirPath: lanePaths.crossLaneDependenciesDir,
208
+ dirPath: targetLanePaths.crossLaneDependenciesDir,
185
209
  lane,
186
210
  waveNumber: options.wave,
187
- agents: loadWaveAgents(lanePaths, options.wave),
188
- capabilityRouting: lanePaths.capabilityRouting,
211
+ agents: loadWaveAgents(targetLanePaths, options.wave),
212
+ capabilityRouting: targetLanePaths.capabilityRouting,
189
213
  });
190
214
  if (options.json || options.wave !== null) {
191
215
  console.log(JSON.stringify(records, null, 2));
@@ -201,20 +225,22 @@ export async function runDependencyCli(argv) {
201
225
 
202
226
  if (subcommand === "render") {
203
227
  const lane = options.lane || options.ownerLane || lanePaths.lane;
228
+ const targetProject = options.project || options.ownerProject || lanePaths.project;
229
+ const targetLanePaths = buildLanePaths(lane, { project: targetProject });
204
230
  const snapshot = buildDependencySnapshot({
205
- dirPath: lanePaths.crossLaneDependenciesDir,
231
+ dirPath: targetLanePaths.crossLaneDependenciesDir,
206
232
  lane,
207
233
  waveNumber: options.wave ?? 0,
208
- agents: loadWaveAgents(lanePaths, options.wave ?? 0),
209
- capabilityRouting: lanePaths.capabilityRouting,
234
+ agents: loadWaveAgents(targetLanePaths, options.wave ?? 0),
235
+ capabilityRouting: targetLanePaths.capabilityRouting,
210
236
  });
211
- const markdownPath = dependencyMarkdownPath(lanePaths, lane);
212
- writeDependencySnapshot(path.join(lanePaths.crossLaneDependenciesDir, `${lane}.json`), snapshot, {
237
+ const markdownPath = dependencyMarkdownPath(targetLanePaths, lane);
238
+ writeDependencySnapshot(path.join(targetLanePaths.crossLaneDependenciesDir, `${lane}.json`), snapshot, {
213
239
  lane,
214
240
  wave: options.wave ?? 0,
215
241
  });
216
242
  writeTextAtomic(markdownPath, `${renderDependencySnapshotMarkdown(snapshot)}\n`);
217
- console.log(JSON.stringify({ markdownPath, jsonPath: path.join(lanePaths.crossLaneDependenciesDir, `${lane}.json`) }, null, 2));
243
+ console.log(JSON.stringify({ markdownPath, jsonPath: path.join(targetLanePaths.crossLaneDependenciesDir, `${lane}.json`) }, null, 2));
218
244
  return;
219
245
  }
220
246
 
@@ -26,7 +26,7 @@ import { deriveWaveLedger, readWaveLedger } from "./ledger.mjs";
26
26
  import { buildDocsQueue, readDocsQueue } from "./docs-queue.mjs";
27
27
  import { parseStructuredSignalsFromLog } from "./dashboard-state.mjs";
28
28
  import {
29
- isSecurityReviewAgent,
29
+ isSecurityReviewAgentForLane,
30
30
  resolveSecurityReviewReportPath,
31
31
  isContEvalImplementationOwningAgent,
32
32
  resolveWaveRoleBindings,
@@ -214,7 +214,9 @@ export function buildWaveSecuritySummary({
214
214
  summariesByAgentId = {},
215
215
  }) {
216
216
  const createdAt = toIsoTimestamp();
217
- const securityAgents = (wave.agents || []).filter((agent) => isSecurityReviewAgent(agent));
217
+ const securityAgents = (wave.agents || []).filter((agent) =>
218
+ isSecurityReviewAgentForLane(agent, lanePaths),
219
+ );
218
220
  if (securityAgents.length === 0) {
219
221
  return {
220
222
  wave: wave.wave,
@@ -377,7 +379,7 @@ function buildIntegrationEvidence({
377
379
  isContEvalImplementationOwningAgent(agent, {
378
380
  contEvalAgentId: roleBindings.contEvalAgentId,
379
381
  });
380
- if (isSecurityReviewAgent(agent)) {
382
+ if (isSecurityReviewAgentForLane(agent, lanePaths)) {
381
383
  continue;
382
384
  }
383
385
  if (agent.agentId === roleBindings.contEvalAgentId) {
@@ -710,6 +712,7 @@ export function buildWaveDerivedState({
710
712
  benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
711
713
  capabilityAssignments,
712
714
  dependencySnapshot,
715
+ securityRolePromptPath: lanePaths.securityRolePromptPath,
713
716
  });
714
717
  const inboxDir = waveInboxDir(lanePaths, wave.wave);
715
718
  const sharedSummary = compileSharedSummary({
@@ -6,10 +6,10 @@ import {
6
6
  DEFAULT_WAIT_TIMEOUT_SECONDS,
7
7
  DEFAULT_WATCH_REFRESH_MS,
8
8
  DEFAULT_WAVE_LANE,
9
- REPO_ROOT,
10
9
  buildLanePaths,
11
10
  compactSingleLine,
12
11
  ensureDirectory,
12
+ findAdhocRunRecord,
13
13
  formatAgeFromTimestamp,
14
14
  parseNonNegativeInt,
15
15
  parsePositiveInt,
@@ -41,9 +41,12 @@ function requestFilePath(feedbackRequestsDir, requestId) {
41
41
  return path.join(feedbackRequestsDir, `${requestId}.json`);
42
42
  }
43
43
 
44
- function resolveLaneForRun(runId, fallbackLane) {
45
- const resultPath = path.join(REPO_ROOT, ".wave", "adhoc", "runs", runId, "result.json");
46
- return readJsonOrNull(resultPath)?.lane || fallbackLane;
44
+ function resolveRunContext(runId, fallbackProject, fallbackLane) {
45
+ const record = findAdhocRunRecord(runId);
46
+ return {
47
+ project: record?.project || fallbackProject,
48
+ lane: record?.result?.lane || fallbackLane,
49
+ };
47
50
  }
48
51
 
49
52
  function buildRequestId({ lane, wave, agentId }) {
@@ -55,6 +58,7 @@ function buildRequestId({ lane, wave, agentId }) {
55
58
  export function createFeedbackRequest({
56
59
  feedbackStateDir,
57
60
  feedbackRequestsDir,
61
+ project = null,
58
62
  lane,
59
63
  wave,
60
64
  agentId,
@@ -69,6 +73,7 @@ export function createFeedbackRequest({
69
73
  const now = toIsoTimestamp();
70
74
  const payload = {
71
75
  id: requestId,
76
+ project: project || null,
72
77
  createdAt: now,
73
78
  updatedAt: now,
74
79
  lane,
@@ -88,7 +93,9 @@ export function createFeedbackRequest({
88
93
  });
89
94
  if (recordTelemetry) {
90
95
  try {
91
- const lanePaths = buildLanePaths(lane);
96
+ const lanePaths = buildLanePaths(lane, {
97
+ project: project || undefined,
98
+ });
92
99
  safeQueueWaveControlEvent(lanePaths, {
93
100
  category: "feedback",
94
101
  entityType: "human_input",
@@ -148,7 +155,9 @@ export function answerFeedbackRequest({
148
155
  });
149
156
  if (recordTelemetry) {
150
157
  try {
151
- const lanePaths = buildLanePaths(answeredPayload?.lane || DEFAULT_WAVE_LANE);
158
+ const lanePaths = buildLanePaths(answeredPayload?.lane || DEFAULT_WAVE_LANE, {
159
+ project: answeredPayload?.project || undefined,
160
+ });
152
161
  safeQueueWaveControlEvent(lanePaths, {
153
162
  category: "feedback",
154
163
  entityType: "human_input",
@@ -245,6 +254,7 @@ function parseFeedbackArgs(argv) {
245
254
  id: "",
246
255
  response: "",
247
256
  operator: "human-operator",
257
+ project: "",
248
258
  force: false,
249
259
  pending: false,
250
260
  json: false,
@@ -255,7 +265,9 @@ function parseFeedbackArgs(argv) {
255
265
  if (arg === "--") {
256
266
  continue;
257
267
  }
258
- if (arg === "--lane") {
268
+ if (arg === "--project") {
269
+ out.project = String(args[++i] || "").trim();
270
+ } else if (arg === "--lane") {
259
271
  out.lane = sanitizeLaneName(args[++i]);
260
272
  } else if (arg === "--run") {
261
273
  out.runId = sanitizeAdhocRunId(args[++i]);
@@ -310,10 +322,10 @@ async function waitForAnswer(filePath, timeoutSeconds) {
310
322
 
311
323
  function printHelp() {
312
324
  console.log(`Usage:
313
- pnpm exec wave-feedback ask --lane <lane> --wave <n> --agent <id> --question "<text>" [options]
325
+ pnpm exec wave-feedback ask --project <id> --lane <lane> --wave <n> --agent <id> --question "<text>" [options]
314
326
  pnpm exec wave-feedback respond --id <request-id> --response "<text>" [options]
315
- pnpm exec wave-feedback list [--pending] [--lane <lane>] [--wave <n>] [--agent <id>] [--json]
316
- pnpm exec wave-feedback watch [--pending] [--lane <lane>] [--wave <n>] [--agent <id>] [--refresh-ms <n>]
327
+ pnpm exec wave-feedback list [--pending] [--project <id>] [--lane <lane>] [--wave <n>] [--agent <id>] [--json]
328
+ pnpm exec wave-feedback watch [--pending] [--project <id>] [--lane <lane>] [--wave <n>] [--agent <id>] [--refresh-ms <n>]
317
329
  pnpm exec wave-feedback show --id <request-id>
318
330
  `);
319
331
  }
@@ -325,9 +337,12 @@ export async function runFeedbackCli(argv) {
325
337
  return;
326
338
  }
327
339
  if (options.runId) {
328
- options.lane = resolveLaneForRun(options.runId, options.lane);
340
+ const context = resolveRunContext(options.runId, options.project, options.lane);
341
+ options.project = context.project;
342
+ options.lane = context.lane;
329
343
  }
330
344
  const lanePaths = buildLanePaths(options.lane, {
345
+ project: options.project || undefined,
331
346
  adhocRunId: options.runId || null,
332
347
  });
333
348
  const requestsDir = lanePaths.feedbackRequestsDir;
@@ -340,6 +355,7 @@ export async function runFeedbackCli(argv) {
340
355
  const result = createFeedbackRequest({
341
356
  feedbackStateDir: stateDir,
342
357
  feedbackRequestsDir: requestsDir,
358
+ project: lanePaths.project,
343
359
  lane: options.lane,
344
360
  wave: options.wave,
345
361
  agentId: options.agent,
@@ -375,6 +391,7 @@ export async function runFeedbackCli(argv) {
375
391
  });
376
392
  if (answered?.lane && Number.isFinite(Number(answered.wave))) {
377
393
  answerHumanInputByRequest({
394
+ project: answered.project || options.project || null,
378
395
  lane: answered.lane,
379
396
  waveNumber: Number(answered.wave),
380
397
  requestId: options.id,