@chllming/wave-orchestration 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +549 -0
  3. package/docs/agents/wave-deploy-verifier-role.md +34 -0
  4. package/docs/agents/wave-documentation-role.md +30 -0
  5. package/docs/agents/wave-evaluator-role.md +43 -0
  6. package/docs/agents/wave-infra-role.md +34 -0
  7. package/docs/agents/wave-integration-role.md +32 -0
  8. package/docs/agents/wave-launcher-role.md +37 -0
  9. package/docs/context7/bundles.json +91 -0
  10. package/docs/plans/component-cutover-matrix.json +112 -0
  11. package/docs/plans/component-cutover-matrix.md +49 -0
  12. package/docs/plans/context7-wave-orchestrator.md +130 -0
  13. package/docs/plans/current-state.md +44 -0
  14. package/docs/plans/master-plan.md +16 -0
  15. package/docs/plans/migration.md +23 -0
  16. package/docs/plans/wave-orchestrator.md +254 -0
  17. package/docs/plans/waves/wave-0.md +165 -0
  18. package/docs/reference/github-packages-setup.md +52 -0
  19. package/docs/reference/migration-0.2-to-0.5.md +622 -0
  20. package/docs/reference/npmjs-trusted-publishing.md +55 -0
  21. package/docs/reference/repository-guidance.md +18 -0
  22. package/docs/reference/runtime-config/README.md +85 -0
  23. package/docs/reference/runtime-config/claude.md +105 -0
  24. package/docs/reference/runtime-config/codex.md +81 -0
  25. package/docs/reference/runtime-config/opencode.md +93 -0
  26. package/docs/research/agent-context-sources.md +57 -0
  27. package/docs/roadmap.md +626 -0
  28. package/package.json +53 -0
  29. package/releases/manifest.json +101 -0
  30. package/scripts/context7-api-check.sh +21 -0
  31. package/scripts/context7-export-env.sh +52 -0
  32. package/scripts/research/agent-context-archive.mjs +472 -0
  33. package/scripts/research/generate-agent-context-indexes.mjs +85 -0
  34. package/scripts/research/import-agent-context-archive.mjs +793 -0
  35. package/scripts/research/manifests/harness-and-blackboard-2026-03-21.mjs +201 -0
  36. package/scripts/wave-autonomous.mjs +13 -0
  37. package/scripts/wave-cli-bootstrap.mjs +27 -0
  38. package/scripts/wave-dashboard.mjs +11 -0
  39. package/scripts/wave-human-feedback.mjs +11 -0
  40. package/scripts/wave-launcher.mjs +11 -0
  41. package/scripts/wave-local-executor.mjs +13 -0
  42. package/scripts/wave-orchestrator/agent-state.mjs +416 -0
  43. package/scripts/wave-orchestrator/autonomous.mjs +367 -0
  44. package/scripts/wave-orchestrator/clarification-triage.mjs +605 -0
  45. package/scripts/wave-orchestrator/config.mjs +848 -0
  46. package/scripts/wave-orchestrator/context7.mjs +464 -0
  47. package/scripts/wave-orchestrator/coord-cli.mjs +286 -0
  48. package/scripts/wave-orchestrator/coordination-store.mjs +987 -0
  49. package/scripts/wave-orchestrator/coordination.mjs +768 -0
  50. package/scripts/wave-orchestrator/dashboard-renderer.mjs +254 -0
  51. package/scripts/wave-orchestrator/dashboard-state.mjs +473 -0
  52. package/scripts/wave-orchestrator/dep-cli.mjs +219 -0
  53. package/scripts/wave-orchestrator/docs-queue.mjs +75 -0
  54. package/scripts/wave-orchestrator/executors.mjs +385 -0
  55. package/scripts/wave-orchestrator/feedback.mjs +372 -0
  56. package/scripts/wave-orchestrator/install.mjs +540 -0
  57. package/scripts/wave-orchestrator/launcher.mjs +3879 -0
  58. package/scripts/wave-orchestrator/ledger.mjs +332 -0
  59. package/scripts/wave-orchestrator/local-executor.mjs +263 -0
  60. package/scripts/wave-orchestrator/replay.mjs +246 -0
  61. package/scripts/wave-orchestrator/roots.mjs +10 -0
  62. package/scripts/wave-orchestrator/routing-state.mjs +542 -0
  63. package/scripts/wave-orchestrator/shared.mjs +405 -0
  64. package/scripts/wave-orchestrator/terminals.mjs +209 -0
  65. package/scripts/wave-orchestrator/traces.mjs +1094 -0
  66. package/scripts/wave-orchestrator/wave-files.mjs +1923 -0
  67. package/scripts/wave.mjs +103 -0
  68. package/wave.config.json +115 -0
@@ -0,0 +1,768 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ DEFAULT_WAVE_LANE,
5
+ LOCK_RETRY_INTERVAL_MS,
6
+ LOCK_STALE_MS,
7
+ LOCK_TIMEOUT_MS,
8
+ MESSAGEBOARD_PROMPT_MAX_CHARS,
9
+ ORCHESTRATOR_DETAIL_MAX_CHARS,
10
+ REPO_ROOT,
11
+ compactSingleLine,
12
+ ensureDirectory,
13
+ readJsonOrNull,
14
+ sleepSync,
15
+ toIsoTimestamp,
16
+ } from "./shared.mjs";
17
+
18
+ export const ENTRY_HEADER_REGEX = /^##\s+(.+?)\s+\|\s+Agent\s+([A-Za-z0-9.]+)\s*$/;
19
+ export const PLACEHOLDER_TIMESTAMP_REGEX = /\$\{(?:ts|TS)\}/;
20
+ export const AGENT_ID_REFERENCE_REGEX = /\b[A-Z]\d+(?:\.\d+)?\b/g;
21
+ export const ACTION_NONE_REGEX = /^(none|n\/a|na|-)\.?$/i;
22
+ export const RESOLUTION_REGEX =
23
+ /\b(resolved|unblocked|fixed|landed|completed|done|addressed|closed)\b/i;
24
+
25
+ function isProcessAlive(pid) {
26
+ if (!Number.isInteger(pid) || pid <= 0) {
27
+ return false;
28
+ }
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function readLockMetadata(lockPath) {
38
+ const payload = readJsonOrNull(lockPath);
39
+ let stat = null;
40
+ try {
41
+ stat = fs.statSync(lockPath);
42
+ } catch {
43
+ // no-op
44
+ }
45
+ const createdAtRaw = typeof payload?.createdAt === "string" ? payload.createdAt : null;
46
+ const createdAtMs = createdAtRaw ? Date.parse(createdAtRaw) : Number.NaN;
47
+ const pid = Number.parseInt(String(payload?.pid ?? ""), 10);
48
+ return {
49
+ pid: Number.isInteger(pid) ? pid : null,
50
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : (stat?.mtimeMs ?? Number.NaN),
51
+ };
52
+ }
53
+
54
+ function releaseLock(fd, lockPath) {
55
+ try {
56
+ fs.closeSync(fd);
57
+ } catch {
58
+ // no-op
59
+ }
60
+ fs.rmSync(lockPath, { force: true });
61
+ }
62
+
63
+ export function withFileLock(lockPath, fn, timeoutMs = LOCK_TIMEOUT_MS, options = {}) {
64
+ const staleMs = Math.max(
65
+ LOCK_STALE_MS,
66
+ Number.parseInt(String(options?.staleMs ?? LOCK_STALE_MS), 10) || LOCK_STALE_MS,
67
+ );
68
+ const deadline = Date.now() + timeoutMs;
69
+ while (true) {
70
+ try {
71
+ ensureDirectory(path.dirname(lockPath));
72
+ const fd = fs.openSync(lockPath, "wx");
73
+ fs.writeFileSync(
74
+ fd,
75
+ `${JSON.stringify({ pid: process.pid, createdAt: toIsoTimestamp() }, null, 2)}\n`,
76
+ "utf8",
77
+ );
78
+ let result;
79
+ try {
80
+ result = fn();
81
+ } catch (error) {
82
+ releaseLock(fd, lockPath);
83
+ throw error;
84
+ }
85
+ if (result && typeof result.then === "function") {
86
+ return result.finally(() => releaseLock(fd, lockPath));
87
+ }
88
+ releaseLock(fd, lockPath);
89
+ return result;
90
+ } catch (error) {
91
+ if (error?.code !== "EEXIST") {
92
+ throw error;
93
+ }
94
+ const metadata = readLockMetadata(lockPath);
95
+ const ageMs = Number.isFinite(metadata.createdAtMs) ? Date.now() - metadata.createdAtMs : 0;
96
+ if (
97
+ (metadata.pid !== null && !isProcessAlive(metadata.pid)) ||
98
+ (ageMs > 0 && ageMs >= staleMs)
99
+ ) {
100
+ fs.rmSync(lockPath, { force: true });
101
+ continue;
102
+ }
103
+ if (Date.now() >= deadline) {
104
+ throw new Error(`Timed out waiting for lock ${path.relative(REPO_ROOT, lockPath)}`, {
105
+ cause: error,
106
+ });
107
+ }
108
+ sleepSync(LOCK_RETRY_INTERVAL_MS);
109
+ }
110
+ }
111
+ }
112
+
113
+ export function buildMessageBoardTemplate({ wave, waveFile, agents }) {
114
+ const now = new Date().toISOString();
115
+ return [
116
+ `# Wave ${wave} Message Board`,
117
+ "",
118
+ `- Wave file: \`${waveFile}\``,
119
+ `- Agents: ${agents.map((agent) => agent.agentId).join(", ")}`,
120
+ `- Created: ${now}`,
121
+ "",
122
+ "## Roles",
123
+ "- Wave Orchestrator: creates wave files, initiates wave runs, and manages launch, retry, and completion flow.",
124
+ "- WAVE Executor (agent session): executes the assigned prompt end-to-end and coordinates on this board every turn.",
125
+ "",
126
+ "## Usage Rules",
127
+ "- YOU ARE IN WAVE MODE.",
128
+ "- On every turn, read the latest message board state before doing any work.",
129
+ "- On every turn, append an entry with progress, decisions, blockers, handoffs, or an explicit no-change heartbeat.",
130
+ "- Re-read it before major edits, before commit or push, and before your final report.",
131
+ "- Do not delete or rewrite another agent's entries; append only.",
132
+ "",
133
+ "## Communication Protocol",
134
+ "- Use real ISO-8601 timestamps only; never placeholders like `${ts}` or `${TS}`.",
135
+ "- If `Action requested (if any)` is not `None`, name the owner agent(s) and the exact unblock condition.",
136
+ "- If another agent requests action from you, acknowledge it in your next board turn.",
137
+ "- If requirements are ambiguous, ask for human clarification only when useful, then continue with the best reasonable assumption.",
138
+ "",
139
+ "## Entry Format",
140
+ "```",
141
+ "## <ISO-8601 timestamp> | Agent <ID>",
142
+ "- Change:",
143
+ "- Reason:",
144
+ "- Impact on other agents:",
145
+ "- Action requested (if any):",
146
+ "```",
147
+ "",
148
+ "## Entries",
149
+ "",
150
+ ].join("\n");
151
+ }
152
+
153
+ export function ensureWaveMessageBoard({ wave, waveFile, agents, messageBoardPath }) {
154
+ ensureDirectory(path.dirname(messageBoardPath));
155
+ if (!fs.existsSync(messageBoardPath)) {
156
+ fs.writeFileSync(
157
+ messageBoardPath,
158
+ `${buildMessageBoardTemplate({ wave, waveFile, agents })}\n`,
159
+ "utf8",
160
+ );
161
+ }
162
+ }
163
+
164
+ export function readMessageBoardSnapshot(messageBoardPath) {
165
+ if (!fs.existsSync(messageBoardPath)) {
166
+ return "(message board missing)";
167
+ }
168
+ const raw = fs.readFileSync(messageBoardPath, "utf8").trim();
169
+ if (!raw) {
170
+ return "(message board currently empty)";
171
+ }
172
+ if (raw.length <= MESSAGEBOARD_PROMPT_MAX_CHARS) {
173
+ return raw;
174
+ }
175
+ return [
176
+ `(snapshot truncated to last ${MESSAGEBOARD_PROMPT_MAX_CHARS} chars)`,
177
+ raw.slice(-MESSAGEBOARD_PROMPT_MAX_CHARS),
178
+ ].join("\n");
179
+ }
180
+
181
+ export function buildExecutionPrompt({
182
+ lane,
183
+ wave,
184
+ agent,
185
+ orchestratorId,
186
+ messageBoardPath,
187
+ messageBoardSnapshot,
188
+ sharedSummaryPath = null,
189
+ sharedSummaryText = "",
190
+ inboxPath = null,
191
+ inboxText = "",
192
+ context7 = null,
193
+ componentPromotions = null,
194
+ sharedPlanDocs = null,
195
+ evaluatorAgentId = "A0",
196
+ integrationAgentId = "A8",
197
+ documentationAgentId = "A9",
198
+ }) {
199
+ const relativeBoardPath = path.relative(REPO_ROOT, messageBoardPath);
200
+ const relativeSharedSummaryPath = sharedSummaryPath
201
+ ? path.relative(REPO_ROOT, sharedSummaryPath)
202
+ : null;
203
+ const relativeInboxPath = inboxPath ? path.relative(REPO_ROOT, inboxPath) : null;
204
+ const lanePlansDir = lane === DEFAULT_WAVE_LANE ? "docs/plans" : `docs/${lane}/plans`;
205
+ const resolvedSharedPlanDocs =
206
+ sharedPlanDocs && sharedPlanDocs.length > 0
207
+ ? sharedPlanDocs
208
+ : [
209
+ `${lanePlansDir}/master-plan.md`,
210
+ `${lanePlansDir}/current-state.md`,
211
+ `${lanePlansDir}/migration.md`,
212
+ ];
213
+ const sharedPlanDocList = resolvedSharedPlanDocs.map((docPath) => `\`${docPath}\``).join(", ");
214
+ const evaluatorRequirements =
215
+ agent.agentId === evaluatorAgentId
216
+ ? [
217
+ `- Because you are Agent ${evaluatorAgentId}, your evaluator report must end with exactly one standalone line in the form \`Verdict: PASS\`, \`Verdict: CONCERNS\`, or \`Verdict: BLOCKED\`.`,
218
+ "- Also emit one matching structured marker in your terminal output: `[wave-verdict] pass`, `[wave-verdict] concerns`, or `[wave-verdict] blocked`.",
219
+ "- Emit one final structured gate marker: `[wave-gate] architecture=<pass|concerns|blocked> integration=<pass|concerns|blocked> durability=<pass|concerns|blocked> live=<pass|concerns|blocked> docs=<pass|concerns|blocked> detail=<short-note>`.",
220
+ "- Only use `Verdict: PASS` when the wave is coherent enough to unblock the next wave.",
221
+ `- Do not declare PASS until the documentation gate is closed: impacted implementation-owned docs must exist, ${sharedPlanDocList} must reflect plan-affecting outcomes, and no unresolved architecture-versus-plans drift remains.`,
222
+ "- If shared-plan reconciliation is still active inside the wave, require the exact remaining doc delta and an explicit `closed` or `no-change` note from the documentation steward or named owner before finalizing. Do not treat ownership handoff alone as the blocker.",
223
+ "- Treat the last evaluator section and last structured gate marker as authoritative. Earlier concerns may remain in the append-only report history but do not control final completion if the closure sweep resolves them.",
224
+ ]
225
+ : [];
226
+ const docStewardRequirements =
227
+ agent.agentId === documentationAgentId
228
+ ? [
229
+ "- Emit one final structured closure marker: `[wave-doc-closure] state=<closed|no-change|delta> paths=<comma-separated-paths> detail=<short-note>`.",
230
+ "- If implementation work is still landing, any early closure note is provisional. Your final closure marker must reflect the post-implementation state seen during the closure sweep.",
231
+ ]
232
+ : [];
233
+ const implementationRequirements =
234
+ ![evaluatorAgentId, documentationAgentId].includes(agent.agentId)
235
+ ? [
236
+ "- Emit one final structured proof marker: `[wave-proof] completion=<contract|integrated|authoritative|live> durability=<none|ephemeral|durable> proof=<unit|integration|live> state=<met|gap> detail=<short-note>`.",
237
+ "- Emit one final structured documentation marker: `[wave-doc-delta] state=<none|owned|shared-plan> paths=<comma-separated-paths> detail=<short-note>`.",
238
+ ...(Array.isArray(agent.components) && agent.components.length > 0
239
+ ? [
240
+ "- Emit one final structured component marker per owned component: `[wave-component] component=<id> level=<level> state=<met|gap> detail=<short-note>`.",
241
+ ]
242
+ : []),
243
+ "- If you leave any material architecture, integration, durability, ops, or docs gap, emit `[wave-gap] kind=<architecture|integration|durability|ops|docs> detail=<short-note>` and make the gap explicit instead of implying completion.",
244
+ ]
245
+ : [];
246
+ const exitContractLines = agent.exitContract
247
+ ? [
248
+ "Exit contract for this run:",
249
+ `- completion: ${agent.exitContract.completion}`,
250
+ `- durability: ${agent.exitContract.durability}`,
251
+ `- proof: ${agent.exitContract.proof}`,
252
+ `- doc-impact: ${agent.exitContract.docImpact}`,
253
+ "- If your landed result is weaker than this contract, mark it as a gap. Do not present package-only, in-memory, or non-authoritative work as complete when the exit contract requires more.",
254
+ "",
255
+ ]
256
+ : [];
257
+ const askCommand = [
258
+ "pnpm exec wave-feedback ask",
259
+ `--lane ${lane}`,
260
+ `--wave ${wave}`,
261
+ `--agent ${agent.agentId}`,
262
+ `--orchestrator-id ${orchestratorId}`,
263
+ '--question "<specific clarification needed>"',
264
+ '--context "<what you tried, options, and impact>"',
265
+ "--timeout-seconds 30",
266
+ ].join(" ");
267
+ const coordinationCommand = [
268
+ "pnpm exec wave coord post",
269
+ `--lane ${lane}`,
270
+ `--wave ${wave}`,
271
+ `--agent ${agent.agentId}`,
272
+ '--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
273
+ '--summary "<one-line summary>"',
274
+ '--detail "<short detail>"',
275
+ ].join(" ");
276
+ const context7Selection = context7?.selection || agent?.context7Resolved || null;
277
+ const executorId = agent?.executorResolved?.id || "default";
278
+ const context7LibrarySummary =
279
+ context7Selection && Array.isArray(context7Selection.libraries) && context7Selection.libraries.length > 0
280
+ ? context7Selection.libraries
281
+ .map((library) => library.libraryName || library.libraryId || "unknown-library")
282
+ .join(", ")
283
+ : "none";
284
+ const context7PromptLines = context7Selection
285
+ ? context7Selection.bundleId === "none"
286
+ ? [
287
+ "Context7 scope for this run:",
288
+ "- No Context7 prefetch bundle is declared for this run.",
289
+ "- Repository docs and source remain the only planned authority for system truth.",
290
+ "- Do not broad-search third-party docs by default. If a direct Context7 tool is available in this session, use it only when an external dependency becomes truly necessary and keep the lookup narrowly scoped to that dependency.",
291
+ "",
292
+ ]
293
+ : [
294
+ "Context7 scope for this run:",
295
+ `- Bundle: ${context7Selection.bundleId}`,
296
+ `- Query focus: ${context7Selection.query || "(derived from assigned prompt)"}`,
297
+ `- Allowed external libraries: ${context7LibrarySummary}`,
298
+ "- Context7 is only for external library truth. It does not override repository architecture, contracts, ownership, or source files.",
299
+ "- If a direct Context7 tool is available in this session, use it only within the bundle and query scope listed here.",
300
+ ...(context7?.promptText
301
+ ? [
302
+ "",
303
+ "## External reference only (Context7, non-canonical)",
304
+ "",
305
+ "The following snippets are third-party documentation retrieved for this task.",
306
+ "They do not override repository architecture, contracts, or source files.",
307
+ "",
308
+ "```text",
309
+ context7.promptText,
310
+ "```",
311
+ "",
312
+ ]
313
+ : context7?.warning
314
+ ? [
315
+ `- Prefetched Context7 docs were not attached: ${context7.warning}`,
316
+ "",
317
+ ]
318
+ : [""]),
319
+ ]
320
+ : [];
321
+ const promotedComponentLines =
322
+ Array.isArray(componentPromotions) && componentPromotions.length > 0
323
+ ? [
324
+ "Component promotions for this wave:",
325
+ ...componentPromotions.map(
326
+ (promotion) => `- ${promotion.componentId}: ${promotion.targetLevel}`,
327
+ ),
328
+ "",
329
+ ]
330
+ : [];
331
+ const ownedComponentLines =
332
+ ![evaluatorAgentId, documentationAgentId].includes(agent.agentId) &&
333
+ Array.isArray(agent.components) &&
334
+ agent.components.length > 0
335
+ ? [
336
+ "Components you own in this wave:",
337
+ ...agent.components.map((componentId) => {
338
+ const targetLevel = agent.componentTargets?.[componentId] || null;
339
+ return targetLevel ? `- ${componentId}: ${targetLevel}` : `- ${componentId}`;
340
+ }),
341
+ "",
342
+ ]
343
+ : [];
344
+
345
+ return [
346
+ `Working directory: ${REPO_ROOT}`,
347
+ "",
348
+ "Role model for this run:",
349
+ "- Wave Orchestrator role: create wave files, initiate wave runs, and manage execution end-to-end.",
350
+ "- WAVE Executor role (you): deliver the assigned outcome end-to-end within your scope and coordinate through the Wave coordination log every turn.",
351
+ `- Evaluator agent id: ${evaluatorAgentId}`,
352
+ `- Integration steward agent id: ${integrationAgentId}`,
353
+ `- Documentation steward agent id: ${documentationAgentId}`,
354
+ `- Resolved executor: ${executorId}`,
355
+ "",
356
+ `You are the Wave executor running Wave ${wave} / Agent ${agent.agentId}: ${agent.title}.`,
357
+ "YOU ARE IN WAVE MODE.",
358
+ `Message board absolute path: ${messageBoardPath}`,
359
+ `Message board repo-relative path: ${relativeBoardPath}`,
360
+ "",
361
+ "Hard requirements for completeness:",
362
+ "- Follow repository instructions in AGENTS.md and CLAUDE.md if present.",
363
+ "- Read the compiled shared summary and your compiled inbox before taking action on every turn.",
364
+ "- Post a coordination record on every meaningful turn with progress, decisions, blockers, handoffs, evidence, or explicit acknowledgement.",
365
+ "- Re-read the generated board projection before major edits, before commit or push, and before your final report.",
366
+ "- If you change interfaces or contracts, include exact files and exact keys or fields affected.",
367
+ "- If your task touches persisted state, implement the required schema or migration work instead of leaving TODOs.",
368
+ "- If you are blocked on clarification, first emit a `clarification-request` coordination record with the concrete question, what you checked, and the impact.",
369
+ "- Use the human feedback CLI only when the orchestrator or policy resolution path cannot answer the question inside the wave loop.",
370
+ "- If a clarification is not answered immediately, continue with the best reasonable assumption and log that assumption as a coordination record.",
371
+ `- Human escalation command: \`${askCommand}\``,
372
+ `- Coordination command: \`${coordinationCommand}\``,
373
+ "- Run relevant tests, lint, and build checks for touched workspaces and fix failures caused by your changes.",
374
+ "- Emit explicit progress markers in your output: `[wave-phase] coding`, `[wave-phase] validating`, `[wave-phase] deploying`, `[wave-phase] finalizing`.",
375
+ "- During deployment checks, emit structured deployment markers: `[deploy-status] service=<service-name> state=<deploying|healthy|failed|rolledover> detail=<short-note>`.",
376
+ "- If your task touches machine validation, workload identity, node admission, deployment bootstrap, or approved machine actions, emit structured infra markers: `[infra-status] kind=<conformance|role-drift|dependency|identity|admission|action> target=<machine-or-surface> state=<checking|setup-required|setup-in-progress|conformant|drift|blocked|failed|action-required|action-approved|action-complete> detail=<short-note>`.",
377
+ ...evaluatorRequirements,
378
+ ...docStewardRequirements,
379
+ ...implementationRequirements,
380
+ `- Update docs impacted by your implementation. If your work changes status, sequencing, ownership, or explicit proof expectations, update the relevant docs. If shared plan docs need changes outside your owned files, post the exact doc paths and exact delta needed for ${sharedPlanDocList} as a coordination record instead of leaving documentation drift for later cleanup.`,
381
+ "- If the wave defines a documentation steward or other explicit owner for shared plan docs, coordinate those updates through that owner, notify them as soon as the delta is known, and stay engaged until they confirm `closed` or `no-change`. Do not treat the ownership boundary as the definition of done.",
382
+ "- In high-fanout waves, do not push to remote by default. A local Conventional Commit is okay when useful; push only when explicitly requested.",
383
+ "- Do not leave watch or dev servers running after completion.",
384
+ "",
385
+ ...(sharedSummaryPath
386
+ ? [
387
+ `Shared summary absolute path: ${sharedSummaryPath}`,
388
+ `Shared summary repo-relative path: ${relativeSharedSummaryPath}`,
389
+ ]
390
+ : []),
391
+ ...(inboxPath
392
+ ? [
393
+ `Agent inbox absolute path: ${inboxPath}`,
394
+ `Agent inbox repo-relative path: ${relativeInboxPath}`,
395
+ ]
396
+ : []),
397
+ "",
398
+ ...(sharedSummaryText
399
+ ? ["Current wave shared summary:", "```markdown", sharedSummaryText, "```", ""]
400
+ : []),
401
+ ...(inboxText
402
+ ? ["Current agent inbox:", "```markdown", inboxText, "```", ""]
403
+ : []),
404
+ ...exitContractLines,
405
+ ...promotedComponentLines,
406
+ ...ownedComponentLines,
407
+ ...context7PromptLines,
408
+ "Assigned implementation prompt:",
409
+ "```",
410
+ agent.prompt.trim(),
411
+ "```",
412
+ ].join("\n");
413
+ }
414
+
415
+ export function buildOrchestratorBoardTemplate(boardPath) {
416
+ const now = toIsoTimestamp();
417
+ return [
418
+ "# Orchestrator Coordination Board",
419
+ "",
420
+ `- Created: ${now}`,
421
+ `- Path: \`${path.relative(REPO_ROOT, boardPath)}\``,
422
+ "",
423
+ "## Purpose",
424
+ "- Coordinate multiple lane orchestrators running in parallel.",
425
+ "- Publish cross-lane blockers, handoffs, and dependency requests.",
426
+ "- Keep an append-only audit trail for start, retry, failure, and completion flow.",
427
+ "",
428
+ "## Coordination Rules",
429
+ "- Append-only: never edit prior entries.",
430
+ "- Use stable orchestrator IDs.",
431
+ "- If requesting cross-lane action, name the owner lane and explicit done condition.",
432
+ "- Post a follow-up entry when a requested action is resolved.",
433
+ "",
434
+ "## Entry Format",
435
+ "```",
436
+ "## <ISO-8601 timestamp> | Lane <lane> | Orchestrator <id>",
437
+ "- Event:",
438
+ "- Waves:",
439
+ "- Status:",
440
+ "- Details:",
441
+ "- Action requested (if any):",
442
+ "```",
443
+ "",
444
+ "## Entries",
445
+ "",
446
+ ].join("\n");
447
+ }
448
+
449
+ export function ensureOrchestratorBoard(boardPath) {
450
+ ensureDirectory(path.dirname(boardPath));
451
+ if (!fs.existsSync(boardPath)) {
452
+ fs.writeFileSync(boardPath, `${buildOrchestratorBoardTemplate(boardPath)}\n`, "utf8");
453
+ }
454
+ }
455
+
456
+ function formatWaveListForCoordination(waves) {
457
+ if (!Array.isArray(waves) || waves.length === 0) {
458
+ return "n/a";
459
+ }
460
+ return waves.map((wave) => String(wave)).join(", ");
461
+ }
462
+
463
+ function trimCoordinationDetail(value) {
464
+ const text = String(value || "")
465
+ .replace(/\s+/g, " ")
466
+ .trim();
467
+ if (!text) {
468
+ return "n/a";
469
+ }
470
+ return text.length <= ORCHESTRATOR_DETAIL_MAX_CHARS
471
+ ? text
472
+ : `${text.slice(0, ORCHESTRATOR_DETAIL_MAX_CHARS - 1)}…`;
473
+ }
474
+
475
+ function normalizeCoordinationAction(value) {
476
+ const text = String(value || "").trim();
477
+ if (!text || /^(none|n\/a|na|-)\.?$/i.test(text)) {
478
+ return "None.";
479
+ }
480
+ return text;
481
+ }
482
+
483
+ export function appendOrchestratorBoardEntry({
484
+ boardPath,
485
+ lane,
486
+ orchestratorId,
487
+ event,
488
+ waves,
489
+ status,
490
+ details,
491
+ actionRequested,
492
+ }) {
493
+ if (!boardPath) {
494
+ return;
495
+ }
496
+ ensureOrchestratorBoard(boardPath);
497
+ const entry = [
498
+ `## ${toIsoTimestamp()} | Lane ${lane} | Orchestrator ${orchestratorId}`,
499
+ `- Event: ${String(event || "update").trim()}.`,
500
+ `- Waves: ${formatWaveListForCoordination(waves)}.`,
501
+ `- Status: ${String(status || "info").trim()}.`,
502
+ `- Details: ${trimCoordinationDetail(details)}`,
503
+ `- Action requested (if any): ${normalizeCoordinationAction(actionRequested)}`,
504
+ "",
505
+ ].join("\n");
506
+ fs.appendFileSync(boardPath, `${entry}\n`, "utf8");
507
+ }
508
+
509
+ export function parseMessageBoardEntries(raw) {
510
+ const lines = raw.split(/\r?\n/);
511
+ const entriesHeaderIndex = lines.findIndex((line) => line.trim() === "## Entries");
512
+ const scopedLines = entriesHeaderIndex >= 0 ? lines.slice(entriesHeaderIndex + 1) : lines;
513
+ const blocks = [];
514
+ let current = null;
515
+ for (const line of scopedLines) {
516
+ if (line.startsWith("## ")) {
517
+ if (current) {
518
+ blocks.push(current);
519
+ }
520
+ current = { header: line.trimEnd(), lines: [] };
521
+ continue;
522
+ }
523
+ if (current) {
524
+ current.lines.push(line.trimEnd());
525
+ }
526
+ }
527
+ if (current) {
528
+ blocks.push(current);
529
+ }
530
+
531
+ return blocks.map((block, index) => {
532
+ const match = block.header.match(ENTRY_HEADER_REGEX);
533
+ let timestampMs = null;
534
+ let agentId = "unknown";
535
+ let headerMalformed = false;
536
+ let placeholderTimestamp = false;
537
+ let timestampRaw = null;
538
+
539
+ if (!match) {
540
+ headerMalformed = true;
541
+ placeholderTimestamp = PLACEHOLDER_TIMESTAMP_REGEX.test(block.header);
542
+ } else {
543
+ timestampRaw = String(match[1] || "").trim();
544
+ agentId = String(match[2] || "unknown").trim();
545
+ placeholderTimestamp = PLACEHOLDER_TIMESTAMP_REGEX.test(timestampRaw);
546
+ timestampMs = Date.parse(timestampRaw);
547
+ if (!Number.isFinite(timestampMs)) {
548
+ headerMalformed = true;
549
+ }
550
+ }
551
+
552
+ const fields = {
553
+ change: null,
554
+ reason: null,
555
+ impact: null,
556
+ action: null,
557
+ };
558
+ for (const line of block.lines) {
559
+ if (fields.change === null && line.startsWith("- Change:")) {
560
+ fields.change = line.slice("- Change:".length).trim();
561
+ } else if (fields.reason === null && line.startsWith("- Reason:")) {
562
+ fields.reason = line.slice("- Reason:".length).trim();
563
+ } else if (fields.impact === null && line.startsWith("- Impact on other agents:")) {
564
+ fields.impact = line.slice("- Impact on other agents:".length).trim();
565
+ } else if (fields.action === null && line.startsWith("- Action requested (if any):")) {
566
+ fields.action = line.slice("- Action requested (if any):".length).trim();
567
+ }
568
+ }
569
+
570
+ const missingRequiredFields = [];
571
+ if (fields.change === null) {
572
+ missingRequiredFields.push("Change");
573
+ }
574
+ if (fields.reason === null) {
575
+ missingRequiredFields.push("Reason");
576
+ }
577
+ if (fields.impact === null) {
578
+ missingRequiredFields.push("Impact on other agents");
579
+ }
580
+ if (fields.action === null) {
581
+ missingRequiredFields.push("Action requested (if any)");
582
+ }
583
+
584
+ const actionText = fields.action || "";
585
+ const actionable = Boolean(actionText) && !ACTION_NONE_REGEX.test(actionText);
586
+ const targetOwners = Array.from(
587
+ new Set((actionText.match(AGENT_ID_REFERENCE_REGEX) || []).map((item) => item.trim())),
588
+ );
589
+
590
+ return {
591
+ index,
592
+ header: block.header,
593
+ headerMalformed,
594
+ placeholderTimestamp,
595
+ timestampRaw,
596
+ timestampMs,
597
+ agentId,
598
+ actionable,
599
+ actionText,
600
+ targetOwners,
601
+ missingRequiredFields,
602
+ malformed: headerMalformed || missingRequiredFields.length > 0,
603
+ textForResolution: [block.header, ...block.lines].join("\n").toLowerCase(),
604
+ };
605
+ });
606
+ }
607
+
608
+ export function analyzeMessageBoardCommunication(messageBoardPath) {
609
+ const health = {
610
+ available: false,
611
+ reason: null,
612
+ totalEntries: 0,
613
+ actionableRequests: 0,
614
+ unresolvedRequests: 0,
615
+ unacknowledgedRequests: 0,
616
+ malformedEntries: 0,
617
+ placeholderTimestampEntries: 0,
618
+ lastAcknowledgementTimestamp: null,
619
+ oldestUnacknowledgedTimestamp: null,
620
+ };
621
+ if (!messageBoardPath) {
622
+ health.reason = "(message board path unavailable)";
623
+ return health;
624
+ }
625
+ if (!fs.existsSync(messageBoardPath)) {
626
+ health.reason = "(message board missing)";
627
+ return health;
628
+ }
629
+ const raw = fs.readFileSync(messageBoardPath, "utf8");
630
+ if (!raw.trim()) {
631
+ health.reason = "(message board currently empty)";
632
+ return health;
633
+ }
634
+ health.available = true;
635
+ const entries = parseMessageBoardEntries(raw);
636
+ health.totalEntries = entries.length;
637
+
638
+ for (const entry of entries) {
639
+ if (entry.malformed) {
640
+ health.malformedEntries += 1;
641
+ }
642
+ if (entry.placeholderTimestamp) {
643
+ health.placeholderTimestampEntries += 1;
644
+ }
645
+ }
646
+
647
+ const actionableEntries = entries.filter((entry) => entry.actionable);
648
+ health.actionableRequests = actionableEntries.length;
649
+
650
+ for (const request of actionableEntries) {
651
+ const ackOwners = request.targetOwners.length > 0 ? request.targetOwners : [request.agentId];
652
+ const laterEntries = entries.slice(request.index + 1);
653
+
654
+ let acknowledged = false;
655
+ for (const later of laterEntries) {
656
+ if (!ackOwners.includes(later.agentId)) {
657
+ continue;
658
+ }
659
+ acknowledged = true;
660
+ if (
661
+ Number.isFinite(later.timestampMs) &&
662
+ (!Number.isFinite(health.lastAcknowledgementTimestamp) ||
663
+ later.timestampMs > health.lastAcknowledgementTimestamp)
664
+ ) {
665
+ health.lastAcknowledgementTimestamp = later.timestampMs;
666
+ }
667
+ break;
668
+ }
669
+
670
+ if (!acknowledged && request.targetOwners.length > 0) {
671
+ health.unacknowledgedRequests += 1;
672
+ if (
673
+ Number.isFinite(request.timestampMs) &&
674
+ (!Number.isFinite(health.oldestUnacknowledgedTimestamp) ||
675
+ request.timestampMs < health.oldestUnacknowledgedTimestamp)
676
+ ) {
677
+ health.oldestUnacknowledgedTimestamp = request.timestampMs;
678
+ }
679
+ }
680
+
681
+ let resolved = false;
682
+ for (const later of laterEntries) {
683
+ const actorRelevant = later.agentId === request.agentId || ackOwners.includes(later.agentId);
684
+ if (actorRelevant && RESOLUTION_REGEX.test(later.textForResolution)) {
685
+ resolved = true;
686
+ break;
687
+ }
688
+ }
689
+ if (!resolved) {
690
+ health.unresolvedRequests += 1;
691
+ }
692
+ }
693
+
694
+ return health;
695
+ }
696
+
697
+ export function readWaveHumanFeedbackRequests({
698
+ feedbackRequestsDir,
699
+ lane,
700
+ waveNumber,
701
+ agentIds,
702
+ orchestratorId,
703
+ }) {
704
+ if (!fs.existsSync(feedbackRequestsDir)) {
705
+ return [];
706
+ }
707
+ const agentIdSet = new Set(agentIds || []);
708
+ const entries = [];
709
+ for (const fileName of fs
710
+ .readdirSync(feedbackRequestsDir)
711
+ .filter((name) => name.endsWith(".json"))) {
712
+ const payload = readJsonOrNull(path.join(feedbackRequestsDir, fileName));
713
+ if (!payload || typeof payload !== "object") {
714
+ continue;
715
+ }
716
+ if (
717
+ String(payload.lane || "")
718
+ .trim()
719
+ .toLowerCase() !== lane
720
+ ) {
721
+ continue;
722
+ }
723
+ const parsedWave = Number.parseInt(String(payload.wave ?? ""), 10);
724
+ if (!Number.isFinite(parsedWave) || parsedWave !== waveNumber) {
725
+ continue;
726
+ }
727
+ const agentId = String(payload.agentId || "").trim();
728
+ if (!agentId || (agentIdSet.size > 0 && !agentIdSet.has(agentId))) {
729
+ continue;
730
+ }
731
+ const requestOrchestratorId = String(payload.orchestratorId || "").trim();
732
+ if (requestOrchestratorId && orchestratorId && requestOrchestratorId !== orchestratorId) {
733
+ continue;
734
+ }
735
+ entries.push({
736
+ id: String(payload.id || path.basename(fileName, ".json")).trim(),
737
+ agentId,
738
+ status: String(payload.status || "pending")
739
+ .trim()
740
+ .toLowerCase(),
741
+ question: compactSingleLine(payload.question, 240),
742
+ context: compactSingleLine(payload.context, 240),
743
+ orchestratorId: requestOrchestratorId,
744
+ createdAt: String(payload.createdAt || ""),
745
+ updatedAt: String(payload.updatedAt || payload.createdAt || ""),
746
+ responseOperator: compactSingleLine(payload?.response?.operator || "", 64),
747
+ responseText: compactSingleLine(payload?.response?.text || "", 240),
748
+ });
749
+ }
750
+ entries.sort((a, b) => {
751
+ const aTs = Date.parse(a.createdAt);
752
+ const bTs = Date.parse(b.createdAt);
753
+ if (Number.isFinite(aTs) && Number.isFinite(bTs)) {
754
+ return aTs - bTs;
755
+ }
756
+ return a.id.localeCompare(b.id);
757
+ });
758
+ return entries;
759
+ }
760
+
761
+ export function feedbackStateSignature(request) {
762
+ return [
763
+ request.status || "",
764
+ request.updatedAt || "",
765
+ request.responseOperator || "",
766
+ request.responseText || "",
767
+ ].join("|");
768
+ }