@chllming/wave-orchestration 0.8.4 → 0.8.5

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 (45) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +25 -12
  3. package/docs/README.md +2 -0
  4. package/docs/agents/wave-design-role.md +47 -0
  5. package/docs/concepts/what-is-a-wave.md +11 -7
  6. package/docs/guides/author-and-run-waves.md +24 -0
  7. package/docs/guides/planner.md +44 -0
  8. package/docs/plans/current-state.md +5 -1
  9. package/docs/plans/end-state-architecture.md +7 -2
  10. package/docs/plans/examples/wave-example-design-handoff.md +262 -0
  11. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  12. package/docs/plans/migration.md +208 -75
  13. package/docs/plans/wave-orchestrator.md +13 -3
  14. package/docs/reference/cli-reference.md +12 -0
  15. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  16. package/docs/reference/sample-waves.md +14 -7
  17. package/docs/reference/skills.md +10 -0
  18. package/package.json +1 -1
  19. package/releases/manifest.json +19 -0
  20. package/scripts/wave-orchestrator/agent-state.mjs +64 -0
  21. package/scripts/wave-orchestrator/config.mjs +5 -0
  22. package/scripts/wave-orchestrator/coordination.mjs +42 -1
  23. package/scripts/wave-orchestrator/gate-engine.mjs +106 -2
  24. package/scripts/wave-orchestrator/install.mjs +3 -0
  25. package/scripts/wave-orchestrator/launcher-runtime.mjs +7 -1
  26. package/scripts/wave-orchestrator/launcher.mjs +55 -1
  27. package/scripts/wave-orchestrator/ledger.mjs +56 -27
  28. package/scripts/wave-orchestrator/local-executor.mjs +37 -0
  29. package/scripts/wave-orchestrator/planner.mjs +24 -4
  30. package/scripts/wave-orchestrator/result-envelope.mjs +32 -1
  31. package/scripts/wave-orchestrator/retry-control.mjs +17 -2
  32. package/scripts/wave-orchestrator/retry-engine.mjs +85 -0
  33. package/scripts/wave-orchestrator/role-helpers.mjs +73 -1
  34. package/scripts/wave-orchestrator/shared.mjs +1 -0
  35. package/scripts/wave-orchestrator/skills.mjs +1 -0
  36. package/scripts/wave-orchestrator/task-entity.mjs +65 -45
  37. package/scripts/wave-orchestrator/wave-files.mjs +85 -1
  38. package/scripts/wave-orchestrator/wave-state-reducer.mjs +24 -7
  39. package/skills/README.md +7 -0
  40. package/skills/role-design/SKILL.md +50 -0
  41. package/skills/role-design/skill.json +36 -0
  42. package/skills/tui-design/SKILL.md +77 -0
  43. package/skills/tui-design/references/tui-design.md +259 -0
  44. package/skills/tui-design/skill.json +36 -0
  45. package/wave.config.json +15 -1
@@ -6,6 +6,7 @@ import {
6
6
  } from "./config.mjs";
7
7
  import {
8
8
  validateContEvalSummary,
9
+ validateDesignSummary,
9
10
  validateDocumentationClosureSummary,
10
11
  validateContQaSummary,
11
12
  validateImplementationSummary,
@@ -13,6 +14,8 @@ import {
13
14
  } from "./agent-state.mjs";
14
15
  import {
15
16
  isContEvalImplementationOwningAgent,
17
+ isDesignAgent,
18
+ isImplementationOwningDesignAgent,
16
19
  isSecurityReviewAgent,
17
20
  } from "./role-helpers.mjs";
18
21
  import { openClarificationLinkedRequests } from "./coordination-store.mjs";
@@ -54,6 +57,7 @@ export function buildSeedWaveLedger({
54
57
  }) {
55
58
  const tasks = [];
56
59
  for (const agent of wave.agents) {
60
+ const hybridDesignAgent = isImplementationOwningDesignAgent(agent);
57
61
  const kind =
58
62
  agent.agentId === contQaAgentId
59
63
  ? "cont-qa"
@@ -63,35 +67,46 @@ export function buildSeedWaveLedger({
63
67
  ? "integration"
64
68
  : agent.agentId === documentationAgentId
65
69
  ? "documentation"
70
+ : isDesignAgent(agent)
71
+ ? "design"
66
72
  : isSecurityReviewAgent(agent)
67
73
  ? "security"
68
- : "implementation";
69
- tasks.push({
70
- id: taskId(kind, agent.agentId),
71
- title: `${agent.agentId}: ${agent.title}`,
72
- owner: agent.agentId,
73
- kind,
74
- dependsOn: [],
75
- state: "planned",
76
- proofState: "pending",
77
- docState: "pending",
78
- infraState: "n/a",
79
- priority:
80
- kind === "implementation" ? "normal" : kind === "integration" ? "high" : "high",
81
- artifactRefs: agent.ownedPaths || [],
82
- runtime: agent.executorResolved
83
- ? {
84
- executorId: agent.executorResolved.id,
85
- role: agent.executorResolved.role,
86
- profile: agent.executorResolved.profile,
87
- selectedBy: agent.executorResolved.selectedBy,
88
- retryPolicy: agent.executorResolved.retryPolicy || null,
89
- allowFallbackOnRetry: agent.executorResolved.allowFallbackOnRetry !== false,
90
- fallbacks: agent.executorResolved.fallbacks || [],
91
- fallbackUsed: agent.executorResolved.fallbackUsed === true,
92
- }
93
- : null,
94
- });
74
+ : "implementation";
75
+ const runtime = agent.executorResolved
76
+ ? {
77
+ executorId: agent.executorResolved.id,
78
+ role: agent.executorResolved.role,
79
+ profile: agent.executorResolved.profile,
80
+ selectedBy: agent.executorResolved.selectedBy,
81
+ retryPolicy: agent.executorResolved.retryPolicy || null,
82
+ allowFallbackOnRetry: agent.executorResolved.allowFallbackOnRetry !== false,
83
+ fallbacks: agent.executorResolved.fallbacks || [],
84
+ fallbackUsed: agent.executorResolved.fallbackUsed === true,
85
+ }
86
+ : null;
87
+ const pushTask = (taskKind) => {
88
+ tasks.push({
89
+ id: taskId(taskKind, agent.agentId),
90
+ title: `${agent.agentId}: ${agent.title}`,
91
+ owner: agent.agentId,
92
+ kind: taskKind,
93
+ dependsOn: [],
94
+ state: "planned",
95
+ proofState: "pending",
96
+ docState: "pending",
97
+ infraState: "n/a",
98
+ priority:
99
+ taskKind === "implementation" ? "normal" : taskKind === "integration" ? "high" : "high",
100
+ artifactRefs: agent.ownedPaths || [],
101
+ runtime,
102
+ });
103
+ };
104
+ if (hybridDesignAgent && kind === "design") {
105
+ pushTask("design");
106
+ pushTask("implementation");
107
+ continue;
108
+ }
109
+ pushTask(kind);
95
110
  }
96
111
  for (const promotion of wave.componentPromotions || []) {
97
112
  tasks.push({
@@ -163,6 +178,11 @@ function derivePhase({
163
178
  return blockingHelperTasks.some((task) => task.state === "blocked") ? "blocked" : "running";
164
179
  }
165
180
  const implementationTasks = tasks.filter((task) => task.kind === "implementation");
181
+ const designTasks = tasks.filter((task) => task.kind === "design");
182
+ const allDesignDone = designTasks.every((task) => task.state === "done");
183
+ if (!allDesignDone && designTasks.length > 0) {
184
+ return "design";
185
+ }
166
186
  const allImplementationDone = implementationTasks.every((task) => task.state === "done");
167
187
  if (!allImplementationDone) {
168
188
  return "running";
@@ -221,6 +241,15 @@ export function deriveWaveLedger({
221
241
  docState: summary?.docDelta?.state || "pending",
222
242
  };
223
243
  }
244
+ if (task.kind === "design" && agent) {
245
+ const validation = validateDesignSummary(agent, summary);
246
+ return {
247
+ ...task,
248
+ state: taskStateFromValidation(validation),
249
+ proofState: validation.ok ? "met" : "gap",
250
+ docState: validation.ok ? "met" : "gap",
251
+ };
252
+ }
224
253
  if (task.kind === "documentation" && agent) {
225
254
  const validation = validateDocumentationClosureSummary(agent, summary);
226
255
  return {
@@ -173,6 +173,11 @@ function formatWaveEvalLine(evalMarker, detail) {
173
173
  return `[wave-eval] state=satisfied targets=${targetIds.length} benchmarks=${benchmarkIds.length} regressions=0${targetIdSegment}${benchmarkIdSegment} detail=${detail}`;
174
174
  }
175
175
 
176
+ function isDesignAgentPrompt(rawPrompt) {
177
+ const text = String(rawPrompt || "");
178
+ return /\[wave-design\]/i.test(text) || /\bwave design\b/i.test(text);
179
+ }
180
+
176
181
  export function resolveRepoOwnedDeliverablePath(relPath) {
177
182
  if (!relPath || path.isAbsolute(relPath)) {
178
183
  throw new Error(`Unsafe deliverable path: ${String(relPath || "")}`);
@@ -277,6 +282,8 @@ export function runLocalExecutorCli(argv) {
277
282
  const contQaAgent = agentId === contQaAgentId;
278
283
  const contEvalAgent = agentId === contEvalAgentId;
279
284
  const integrationAgent = agentId === integrationAgentId;
285
+ const designAgent = isDesignAgentPrompt(rawPrompt);
286
+ const implementationMarkersRequired = /\[wave-proof\]/i.test(rawPrompt);
280
287
  const ownedComponents = extractOwnedComponents(rawPrompt);
281
288
  const assignedPrompt = extractAssignedPrompt(rawPrompt);
282
289
  const ownedPaths = extractFileOwnershipPaths(assignedPrompt);
@@ -304,6 +311,21 @@ export function runLocalExecutorCli(argv) {
304
311
  console.log(
305
312
  "[wave-integration] state=ready-for-doc-closure claims=0 conflicts=0 blockers=0 detail=local-executor-no-deliverables",
306
313
  );
314
+ } else if (designAgent) {
315
+ console.log(
316
+ "[wave-design] state=ready-for-implementation decisions=1 assumptions=1 open_questions=0 detail=local-executor-no-deliverables",
317
+ );
318
+ if (implementationMarkersRequired) {
319
+ console.log(
320
+ "[wave-proof] completion=contract durability=none proof=unit state=met detail=local-executor-no-deliverables",
321
+ );
322
+ console.log("[wave-doc-delta] state=none detail=local-executor-no-deliverables");
323
+ for (const component of ownedComponents) {
324
+ console.log(
325
+ `[wave-component] component=${component.componentId} level=${component.level || "repo-landed"} state=met detail=local-executor-no-deliverables`,
326
+ );
327
+ }
328
+ }
307
329
  } else if (agentId === documentationAgentId) {
308
330
  console.log("[wave-doc-closure] state=no-change detail=local-executor-no-deliverables");
309
331
  } else if (agentId) {
@@ -348,6 +370,21 @@ export function runLocalExecutorCli(argv) {
348
370
  console.log(
349
371
  "[wave-integration] state=ready-for-doc-closure claims=0 conflicts=0 blockers=0 detail=local-executor-smoke",
350
372
  );
373
+ } else if (designAgent) {
374
+ console.log(
375
+ "[wave-design] state=ready-for-implementation decisions=2 assumptions=1 open_questions=0 detail=local-executor-smoke",
376
+ );
377
+ if (implementationMarkersRequired) {
378
+ console.log(
379
+ "[wave-proof] completion=contract durability=none proof=unit state=met detail=local-executor-smoke",
380
+ );
381
+ console.log("[wave-doc-delta] state=owned detail=local-executor-smoke");
382
+ for (const component of ownedComponents) {
383
+ console.log(
384
+ `[wave-component] component=${component.componentId} level=${component.level || "repo-landed"} state=met detail=local-executor-smoke`,
385
+ );
386
+ }
387
+ }
351
388
  } else if (agentId === documentationAgentId) {
352
389
  console.log("[wave-doc-closure] state=no-change detail=local-executor-smoke");
353
390
  } else if (agentId) {
@@ -317,6 +317,9 @@ function defaultTargetLevel(template) {
317
317
  }
318
318
 
319
319
  function defaultExecutorProfile(roleKind) {
320
+ if (roleKind === "design") {
321
+ return "design-pass";
322
+ }
320
323
  if (roleKind === "infra" || roleKind === "deploy" || roleKind === "research") {
321
324
  return "ops-triage";
322
325
  }
@@ -327,7 +330,7 @@ function defaultExecutorProfile(roleKind) {
327
330
  }
328
331
 
329
332
  function defaultExitContract(roleKind) {
330
- if (roleKind === "security") {
333
+ if (roleKind === "security" || roleKind === "design") {
331
334
  return null;
332
335
  }
333
336
  if (roleKind === "infra" || roleKind === "deploy") {
@@ -355,6 +358,9 @@ function defaultExitContract(roleKind) {
355
358
  }
356
359
 
357
360
  function buildDefaultValidationCommand(template, roleKind) {
361
+ if (roleKind === "design") {
362
+ return "Manual review of the design packet against the wave scope, constraints, and downstream ownership.";
363
+ }
358
364
  if (roleKind === "security") {
359
365
  return "Manual review of the changed security-sensitive surfaces plus required proofs.";
360
366
  }
@@ -368,6 +374,9 @@ function buildDefaultValidationCommand(template, roleKind) {
368
374
  }
369
375
 
370
376
  function buildDefaultOutputSummary(template, roleKind) {
377
+ if (roleKind === "design") {
378
+ return "Summarize the design packet, key decisions, assumptions, open questions, and exact implementation handoff.";
379
+ }
371
380
  if (roleKind === "security") {
372
381
  return "Summarize the threat model, findings, required approvals, requested fixes, and final security disposition.";
373
382
  }
@@ -381,6 +390,9 @@ function buildDefaultOutputSummary(template, roleKind) {
381
390
  }
382
391
 
383
392
  function buildDefaultPrimaryGoal(template, roleKind, title) {
393
+ if (roleKind === "design") {
394
+ return `Produce an implementation-ready design packet for the ${title.toLowerCase()} slice before coding starts.`;
395
+ }
384
396
  if (roleKind === "security") {
385
397
  return `Review the ${title.toLowerCase()} slice for security risks and route exact fixes before integration.`;
386
398
  }
@@ -1067,6 +1079,9 @@ function buildWorkerAgentSpec({
1067
1079
  if (roleKind === "research" && !capabilities.includes("research")) {
1068
1080
  capabilities.push("research");
1069
1081
  }
1082
+ if (roleKind === "design" && !capabilities.includes("design")) {
1083
+ capabilities.push("design");
1084
+ }
1070
1085
  return {
1071
1086
  agentId,
1072
1087
  title,
@@ -1075,6 +1090,8 @@ function buildWorkerAgentSpec({
1075
1090
  ? values.rolePromptPaths
1076
1091
  : roleKind === "security"
1077
1092
  ? [lanePaths.securityRolePromptPath]
1093
+ : roleKind === "design"
1094
+ ? [lanePaths.designRolePromptPath]
1078
1095
  : [],
1079
1096
  skills: values.skills || [],
1080
1097
  executor: {
@@ -1934,6 +1951,7 @@ function normalizePlannerContext7Bundle(bundle, bundleIndex) {
1934
1951
  function normalizePlannerWorkerAgent(rawAgent, context) {
1935
1952
  const agentId = cleanText(rawAgent?.agentId) || `A${context.index + 1}`;
1936
1953
  const roleKind = [
1954
+ "design",
1937
1955
  "implementation",
1938
1956
  "qa",
1939
1957
  "infra",
@@ -3067,12 +3085,12 @@ async function collectWorkerAgents({
3067
3085
  const title = cleanText(await prompt.ask(`Worker ${agentId} title`, defaults.title));
3068
3086
  const roleKind = await prompt.askChoice(
3069
3087
  `Worker ${agentId} role kind`,
3070
- ["implementation", "qa", "infra", "deploy", "research", "security"],
3088
+ ["design", "implementation", "qa", "infra", "deploy", "research", "security"],
3071
3089
  defaultRoleKind,
3072
3090
  );
3073
3091
  const executorProfile = await prompt.askChoice(
3074
3092
  `Worker ${agentId} executor profile`,
3075
- ["implement-fast", "deep-review", "eval-tuning", "docs-pass", "ops-triage", "security-review"],
3093
+ ["implement-fast", "design-pass", "deep-review", "eval-tuning", "docs-pass", "ops-triage", "security-review"],
3076
3094
  defaultExecutorProfile(roleKind),
3077
3095
  );
3078
3096
  const ownedPaths = normalizeRepoPathList(
@@ -3081,6 +3099,8 @@ async function collectWorkerAgents({
3081
3099
  `Worker ${agentId} owned paths (comma or | separated)`,
3082
3100
  roleKind === "security"
3083
3101
  ? `.tmp/${lane}-wave-launcher/security/wave-${waveNumber}-review.md`
3102
+ : roleKind === "design"
3103
+ ? `docs/plans/waves/design/wave-${waveNumber}-${agentId}.md`
3084
3104
  : template === "infra"
3085
3105
  ? "scripts/,docs/plans/"
3086
3106
  : template === "release"
@@ -3093,7 +3113,7 @@ async function collectWorkerAgents({
3093
3113
  const components = normalizeListText(
3094
3114
  await prompt.ask(
3095
3115
  `Worker ${agentId} component ids (comma or | separated)`,
3096
- roleKind === "security"
3116
+ roleKind === "security" || roleKind === "design"
3097
3117
  ? ""
3098
3118
  : componentPromotions.map((promotion) => promotion.componentId).join(", "),
3099
3119
  ),
@@ -7,6 +7,7 @@ import {
7
7
  } from "./shared.mjs";
8
8
 
9
9
  export const ENVELOPE_VALID_ROLES = [
10
+ "design",
10
11
  "implementation",
11
12
  "integration",
12
13
  "documentation",
@@ -24,6 +25,9 @@ function inferEnvelopeRole(agent, summary) {
24
25
  if (summary?.integration) {
25
26
  return "integration";
26
27
  }
28
+ if (summary?.design) {
29
+ return "design";
30
+ }
27
31
  if (summary?.docClosure) {
28
32
  return "documentation";
29
33
  }
@@ -205,7 +209,11 @@ export function buildAgentResultEnvelope(agent, summary, options = {}) {
205
209
  facts,
206
210
  };
207
211
 
208
- if (role === "implementation") {
212
+ if (
213
+ role === "implementation" ||
214
+ safeSummary.docDelta ||
215
+ Array.isArray(safeSummary.components)
216
+ ) {
209
217
  const docDelta = safeSummary.docDelta || {};
210
218
  const components = Array.isArray(safeSummary.components)
211
219
  ? safeSummary.components.map((component) => ({
@@ -225,6 +233,17 @@ export function buildAgentResultEnvelope(agent, summary, options = {}) {
225
233
  };
226
234
  }
227
235
 
236
+ if (role === "design") {
237
+ const design = safeSummary.design || {};
238
+ envelope.design = {
239
+ state: design.state || null,
240
+ decisions: design.decisions || 0,
241
+ assumptions: design.assumptions || 0,
242
+ openQuestions: design.openQuestions || 0,
243
+ detail: design.detail || null,
244
+ };
245
+ }
246
+
228
247
  if (role === "integration") {
229
248
  const integration = safeSummary.integration || {};
230
249
  envelope.integration = {
@@ -392,6 +411,16 @@ export function projectLegacySummaryFromEnvelope(envelope, options = {}) {
392
411
  : [];
393
412
  }
394
413
 
414
+ if (envelope.design) {
415
+ summary.design = {
416
+ state: envelope.design.state || null,
417
+ decisions: envelope.design.decisions || 0,
418
+ assumptions: envelope.design.assumptions || 0,
419
+ openQuestions: envelope.design.openQuestions || 0,
420
+ detail: envelope.design.detail || null,
421
+ };
422
+ }
423
+
395
424
  if (envelope.documentation?.docClosure) {
396
425
  summary.docClosure = {
397
426
  state: envelope.documentation.docClosure.state || "no-change",
@@ -466,6 +495,8 @@ export function projectLegacySummaryFromEnvelope(envelope, options = {}) {
466
495
 
467
496
  function requiredRoleSection(role) {
468
497
  switch (role) {
498
+ case "design":
499
+ return "design";
469
500
  case "implementation":
470
501
  return "implementation";
471
502
  case "integration":
@@ -10,7 +10,11 @@ import {
10
10
  readWaveControlPlaneState,
11
11
  syncWaveControlPlaneProjections,
12
12
  } from "./control-plane.mjs";
13
- import { isSecurityReviewAgent } from "./role-helpers.mjs";
13
+ import {
14
+ isDesignAgent,
15
+ isDocsOnlyDesignAgent,
16
+ isSecurityReviewAgent,
17
+ } from "./role-helpers.mjs";
14
18
  import { ensureDirectory, parseNonNegativeInt } from "./shared.mjs";
15
19
 
16
20
  function uniqueAgentIds(values) {
@@ -188,7 +192,18 @@ export function resolveRetryOverrideAgentIds(waveDefinition, lanePaths, override
188
192
  );
189
193
  if (resumePhase === "implementation") {
190
194
  return agents
191
- .filter((agent) => agent?.agentId && !closureAgentIds.has(agent.agentId) && !isSecurityReviewAgent(agent))
195
+ .filter(
196
+ (agent) =>
197
+ agent?.agentId &&
198
+ !closureAgentIds.has(agent.agentId) &&
199
+ !isSecurityReviewAgent(agent) &&
200
+ !isDocsOnlyDesignAgent(agent),
201
+ )
202
+ .map((agent) => agent.agentId);
203
+ }
204
+ if (resumePhase === "design") {
205
+ return agents
206
+ .filter((agent) => isDesignAgent(agent))
192
207
  .map((agent) => agent.agentId);
193
208
  }
194
209
  if (resumePhase === "integrating") {
@@ -20,6 +20,7 @@ import {
20
20
  } from "./shared.mjs";
21
21
  import {
22
22
  readAgentExecutionSummary,
23
+ validateDesignSummary,
23
24
  validateImplementationSummary,
24
25
  } from "./agent-state.mjs";
25
26
  import {
@@ -32,6 +33,9 @@ import {
32
33
  } from "./proof-registry.mjs";
33
34
  import { hashAgentPromptFingerprint } from "./context7.mjs";
34
35
  import {
36
+ isDocsOnlyDesignAgent,
37
+ isDesignAgent,
38
+ isImplementationOwningDesignAgent,
35
39
  isSecurityReviewAgent,
36
40
  resolveWaveRoleBindings,
37
41
  } from "./role-helpers.mjs";
@@ -315,6 +319,28 @@ export function hasReusableSuccessStatus(agent, statusPath, options = {}) {
315
319
  if (!basicReuseOk) {
316
320
  return false;
317
321
  }
322
+ if (isDocsOnlyDesignAgent(agent) || isImplementationOwningDesignAgent(agent)) {
323
+ const summary = readAgentExecutionSummary(statusPath, {
324
+ agent,
325
+ statusPath,
326
+ statusRecord,
327
+ logPath: options.logPath || null,
328
+ reportPath: options.reportPath || null,
329
+ });
330
+ if (!summary) {
331
+ return false;
332
+ }
333
+ const effectiveSummary = options.proofRegistry
334
+ ? augmentSummaryWithProofRegistry(agent, summary, options.proofRegistry)
335
+ : summary;
336
+ if (isDocsOnlyDesignAgent(agent)) {
337
+ return validateDesignSummary(agent, effectiveSummary).ok;
338
+ }
339
+ return (
340
+ validateDesignSummary(agent, effectiveSummary).ok &&
341
+ validateImplementationSummary(agent, effectiveSummary).ok
342
+ );
343
+ }
318
344
  const proofCentric =
319
345
  agentRequiresProofCentricValidation(agent) || waveRequiresProofCentricValidation(options.wave);
320
346
  if (!proofCentric) {
@@ -342,6 +368,37 @@ export function hasReusableSuccessStatus(agent, statusPath, options = {}) {
342
368
  return true;
343
369
  }
344
370
 
371
+ function hasReusableDesignPassStatus(agent, statusPath, options = {}) {
372
+ if (!isDesignAgent(agent)) {
373
+ return hasReusableSuccessStatus(agent, statusPath, options);
374
+ }
375
+ const statusRecord = readStatusRecordIfPresent(statusPath);
376
+ const basicReuseOk = Boolean(
377
+ statusRecord && statusRecord.code === 0 && statusRecord.promptHash === hashAgentPromptFingerprint(agent),
378
+ );
379
+ if (!basicReuseOk) {
380
+ return false;
381
+ }
382
+ const summary = readAgentExecutionSummary(statusPath, {
383
+ agent: {
384
+ ...agent,
385
+ exitContract: null,
386
+ components: [],
387
+ componentTargets: {},
388
+ deliverables: [],
389
+ proofArtifacts: [],
390
+ },
391
+ statusPath,
392
+ statusRecord,
393
+ logPath: options.logPath || null,
394
+ reportPath: options.reportPath || null,
395
+ });
396
+ if (!summary) {
397
+ return false;
398
+ }
399
+ return validateDesignSummary(agent, summary).ok;
400
+ }
401
+
345
402
  function isClosureAgentId(agent, lanePaths, waveDefinition = null) {
346
403
  return (
347
404
  resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents).closureAgentIds.includes(
@@ -377,6 +434,23 @@ export function selectInitialWaveRuns(agentRuns, lanePaths, waveDefinition = nul
377
434
  const implementationRuns = (agentRuns || []).filter(
378
435
  (run) => !isClosureAgentId(run?.agent, lanePaths, waveDefinition),
379
436
  );
437
+ const pendingDesignRuns = implementationRuns.filter(
438
+ (run) =>
439
+ isDesignAgent(run.agent) &&
440
+ !hasReusableDesignPassStatus(run.agent, run.statusPath, {
441
+ wave: waveDefinition,
442
+ logPath: run.logPath,
443
+ }),
444
+ );
445
+ if (pendingDesignRuns.length > 0) {
446
+ return pendingDesignRuns;
447
+ }
448
+ const implementationFanoutRuns = implementationRuns.filter(
449
+ (run) => !isDocsOnlyDesignAgent(run.agent),
450
+ );
451
+ if (implementationFanoutRuns.length > 0) {
452
+ return implementationFanoutRuns;
453
+ }
380
454
  return implementationRuns.length > 0 ? implementationRuns : agentRuns;
381
455
  }
382
456
 
@@ -573,6 +647,9 @@ function runsFromAgentIds(agentRuns, agentIds) {
573
647
 
574
648
  function resolveRunsForResumePhase(agentRuns, lanePaths, resumePhase, waveDefinition = null) {
575
649
  const roleBindings = resolveWaveRoleBindings(waveDefinition, lanePaths, waveDefinition?.agents);
650
+ if (resumePhase === "design") {
651
+ return (agentRuns || []).filter((run) => isDesignAgent(run.agent));
652
+ }
576
653
  if (resumePhase === "integrating") {
577
654
  return runsFromAgentIds(agentRuns, [roleBindings.integrationAgentId]);
578
655
  }
@@ -729,6 +806,12 @@ function resolveRelaunchRunsLegacy(agentRuns, failures, derivedState, lanePaths,
729
806
  barrier: null,
730
807
  };
731
808
  }
809
+ if (derivedState?.ledger?.phase === "design") {
810
+ return {
811
+ runs: agentRuns.filter((run) => isDesignAgent(run.agent)),
812
+ barrier: null,
813
+ };
814
+ }
732
815
  if (derivedState?.ledger?.phase === "security-review") {
733
816
  return {
734
817
  runs: agentRuns.filter((run) => isSecurityReviewAgent(run.agent)),
@@ -1018,6 +1101,8 @@ export function preflightWavesForExecutorAvailability(waves, lanePaths) {
1018
1101
 
1019
1102
  function phaseFromGate(gateName) {
1020
1103
  switch (gateName) {
1104
+ case "designGate":
1105
+ return "design";
1021
1106
  case "implementationGate":
1022
1107
  case "componentGate":
1023
1108
  case "helperAssignmentBarrier":
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DEFAULT_CONT_QA_AGENT_ID,
3
3
  DEFAULT_CONT_EVAL_AGENT_ID,
4
+ DEFAULT_DESIGN_ROLE_PROMPT_PATH,
4
5
  DEFAULT_DOCUMENTATION_AGENT_ID,
5
6
  DEFAULT_INTEGRATION_AGENT_ID,
6
7
  DEFAULT_SECURITY_ROLE_PROMPT_PATH,
@@ -37,6 +38,29 @@ export function isSecurityReportPath(relPath) {
37
38
  return /(?:^|\/).*security.*\.(?:md|txt)$/i.test(cleanPath(relPath));
38
39
  }
39
40
 
41
+ export function isDesignRolePromptPath(
42
+ relPath,
43
+ designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH,
44
+ ) {
45
+ const normalized = cleanPath(relPath);
46
+ const configured = cleanPath(designRolePromptPath);
47
+ return (
48
+ normalized === configured ||
49
+ normalized === DEFAULT_DESIGN_ROLE_PROMPT_PATH ||
50
+ normalized.endsWith("/wave-design-role.md")
51
+ );
52
+ }
53
+
54
+ export function isDesignReportPath(relPath) {
55
+ return /(?:^|\/).*(?:design|handoff|decision-lineage).*\.(?:md|txt)$/i.test(cleanPath(relPath));
56
+ }
57
+
58
+ function normalizedOwnedPaths(agent) {
59
+ return Array.isArray(agent?.ownedPaths)
60
+ ? agent.ownedPaths.map(cleanPath).filter(Boolean)
61
+ : [];
62
+ }
63
+
40
64
  export function isContEvalImplementationOwningAgent(
41
65
  agent,
42
66
  { contEvalAgentId = DEFAULT_CONT_EVAL_AGENT_ID } = {},
@@ -81,11 +105,59 @@ export function isSecurityReviewAgent(
81
105
  return capabilities.includes("security-review");
82
106
  }
83
107
 
108
+ export function isDesignAgent(
109
+ agent,
110
+ { designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH } = {},
111
+ ) {
112
+ if (!agent || typeof agent !== "object") {
113
+ return false;
114
+ }
115
+ const rolePromptPaths = Array.isArray(agent.rolePromptPaths) ? agent.rolePromptPaths : [];
116
+ if (
117
+ rolePromptPaths.some((rolePromptPath) =>
118
+ isDesignRolePromptPath(rolePromptPath, designRolePromptPath),
119
+ )
120
+ ) {
121
+ return true;
122
+ }
123
+ const capabilities = Array.isArray(agent.capabilities)
124
+ ? agent.capabilities.map((entry) => String(entry || "").trim().toLowerCase())
125
+ : [];
126
+ return capabilities.includes("design");
127
+ }
128
+
129
+ export function isImplementationOwningDesignAgent(
130
+ agent,
131
+ { designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH } = {},
132
+ ) {
133
+ if (!isDesignAgent(agent, { designRolePromptPath })) {
134
+ return false;
135
+ }
136
+ const ownedPaths = normalizedOwnedPaths(agent);
137
+ if (ownedPaths.length === 0) {
138
+ return false;
139
+ }
140
+ return ownedPaths.some((ownedPath) => !isDesignReportPath(ownedPath));
141
+ }
142
+
143
+ export function isDocsOnlyDesignAgent(
144
+ agent,
145
+ { designRolePromptPath = DEFAULT_DESIGN_ROLE_PROMPT_PATH } = {},
146
+ ) {
147
+ return isDesignAgent(agent, { designRolePromptPath }) &&
148
+ !isImplementationOwningDesignAgent(agent, { designRolePromptPath });
149
+ }
150
+
84
151
  export function resolveSecurityReviewReportPath(agent) {
85
- const ownedPaths = Array.isArray(agent?.ownedPaths) ? agent.ownedPaths.map(cleanPath).filter(Boolean) : [];
152
+ const ownedPaths = normalizedOwnedPaths(agent);
86
153
  return ownedPaths.find((ownedPath) => isSecurityReportPath(ownedPath)) || null;
87
154
  }
88
155
 
156
+ export function resolveDesignReportPath(agent) {
157
+ const ownedPaths = normalizedOwnedPaths(agent);
158
+ return ownedPaths.find((ownedPath) => isDesignReportPath(ownedPath)) || null;
159
+ }
160
+
89
161
  export function resolveWaveRoleBindings(wave = {}, lanePaths = {}, agents = wave?.agents || []) {
90
162
  const contQaAgentId =
91
163
  wave?.contQaAgentId || lanePaths?.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
@@ -234,6 +234,7 @@ export function buildLanePaths(laneInput = DEFAULT_WAVE_LANE, options = {}) {
234
234
  integrationRolePromptPath: laneProfile.roles.integrationRolePromptPath,
235
235
  documentationRolePromptPath: laneProfile.roles.documentationRolePromptPath,
236
236
  securityRolePromptPath: laneProfile.roles.securityRolePromptPath,
237
+ designRolePromptPath: laneProfile.roles.designRolePromptPath,
237
238
  requireDocumentationStewardFromWave:
238
239
  laneProfile.validation.requireDocumentationStewardFromWave,
239
240
  requireContext7DeclarationsFromWave:
@@ -9,6 +9,7 @@ export const DEFAULT_SKILLS_DIR = "skills";
9
9
  export const SKILL_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
10
10
  export const SUPPORTED_SKILL_RUNTIMES = ["codex", "claude", "opencode", "local"];
11
11
  export const SUPPORTED_SKILL_ROLES = [
12
+ "design",
12
13
  "implementation",
13
14
  "integration",
14
15
  "documentation",