devflow-kit 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/cli/commands/learning.js +25 -5
  4. package/dist/commands/code-review.md +1 -1
  5. package/dist/commands/debug.md +1 -1
  6. package/dist/commands/dynamic-build.md +141 -94
  7. package/dist/commands/dynamic-plan.md +1 -1
  8. package/dist/commands/explore.md +1 -1
  9. package/dist/commands/implement.md +133 -133
  10. package/dist/commands/resolve.md +27 -16
  11. package/dist/commands/self-review.md +16 -11
  12. package/dist/core/agent-models.js +10 -7
  13. package/dist/core/learning-tuning-config.js +8 -0
  14. package/dist/core/plugins.js +7 -2
  15. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
  16. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
  17. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
  18. package/package.json +1 -1
  19. package/src/assets/agents/code.md +71 -18
  20. package/src/assets/agents/design.md +10 -0
  21. package/src/assets/agents/diagnose.md +16 -11
  22. package/src/assets/agents/evaluate.md +14 -25
  23. package/src/assets/agents/knowledge.md +5 -3
  24. package/src/assets/agents/learning.md +4 -6
  25. package/src/assets/agents/research.md +19 -0
  26. package/src/assets/agents/review.md +10 -0
  27. package/src/assets/agents/scrutinize.md +34 -10
  28. package/src/assets/agents/simplify.md +20 -0
  29. package/src/assets/agents/skim.md +4 -2
  30. package/src/assets/agents/synthesize.md +12 -0
  31. package/src/assets/agents/test.md +2 -2
  32. package/src/assets/agents/triage.md +7 -1
  33. package/src/assets/agents/validate.md +7 -2
  34. package/src/assets/commands/_partials/_engine.mds +21 -24
  35. package/src/assets/commands/_partials/_knowledge.mds +1 -1
  36. package/src/assets/commands/_partials/_plan_contract.mds +1 -1
  37. package/src/assets/commands/_partials/_wave.mds +8 -6
  38. package/src/assets/commands/code-review.mds +1 -1
  39. package/src/assets/commands/dynamic-build.mds +111 -63
  40. package/src/assets/commands/implement.mds +132 -132
  41. package/src/assets/commands/resolve.mds +26 -15
  42. package/src/assets/commands/self-review.mds +15 -10
  43. package/src/assets/mds/tracker/_common.mds +1 -1
  44. package/src/assets/scripts/ci-wait.cjs +636 -0
  45. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -1
  46. package/src/assets/scripts/hooks/background-memory-update +290 -9
  47. package/src/assets/scripts/release-trace.cjs +27 -10
  48. package/src/assets/skills/accessibility/SKILL.md +1 -1
  49. package/src/assets/skills/apply-decisions/SKILL.md +12 -82
  50. package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
  51. package/src/assets/skills/architecture/SKILL.md +1 -1
  52. package/src/assets/skills/boundary-validation/SKILL.md +1 -1
  53. package/src/assets/skills/complexity/SKILL.md +1 -1
  54. package/src/assets/skills/compliance/SKILL.md +1 -1
  55. package/src/assets/skills/consistency/SKILL.md +1 -1
  56. package/src/assets/skills/database/SKILL.md +1 -1
  57. package/src/assets/skills/dependencies/SKILL.md +1 -1
  58. package/src/assets/skills/dependency-research/SKILL.md +3 -6
  59. package/src/assets/skills/design-review/SKILL.md +1 -1
  60. package/src/assets/skills/docs-framework/SKILL.md +1 -1
  61. package/src/assets/skills/documentation/SKILL.md +1 -1
  62. package/src/assets/skills/gap-analysis/SKILL.md +1 -1
  63. package/src/assets/skills/git/SKILL.md +1 -1
  64. package/src/assets/skills/go/SKILL.md +1 -1
  65. package/src/assets/skills/java/SKILL.md +1 -1
  66. package/src/assets/skills/patterns/SKILL.md +1 -1
  67. package/src/assets/skills/performance/SKILL.md +1 -1
  68. package/src/assets/skills/python/SKILL.md +1 -1
  69. package/src/assets/skills/qa/SKILL.md +1 -3
  70. package/src/assets/skills/quality-gates/SKILL.md +9 -12
  71. package/src/assets/skills/quality-gates/references/report-template.md +20 -20
  72. package/src/assets/skills/react/SKILL.md +1 -1
  73. package/src/assets/skills/regression/SKILL.md +1 -1
  74. package/src/assets/skills/reliability/SKILL.md +1 -1
  75. package/src/assets/skills/research-codebase/SKILL.md +1 -1
  76. package/src/assets/skills/research-competitor/SKILL.md +1 -1
  77. package/src/assets/skills/research-external/SKILL.md +1 -1
  78. package/src/assets/skills/research-technology/SKILL.md +1 -1
  79. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  80. package/src/assets/skills/rust/SKILL.md +1 -1
  81. package/src/assets/skills/security/SKILL.md +1 -1
  82. package/src/assets/skills/software-design/SKILL.md +1 -1
  83. package/src/assets/skills/test-driven-development/SKILL.md +9 -29
  84. package/src/assets/skills/testing/SKILL.md +1 -1
  85. package/src/assets/skills/typescript/SKILL.md +1 -1
  86. package/src/assets/skills/ui-design/SKILL.md +1 -1
  87. package/src/assets/skills/worktree-support/SKILL.md +3 -55
  88. package/src/assets/skills/worktree-support/references/discovery.md +48 -0
  89. package/src/assets/skills/worktree-support/references/roots.md +2 -2
@@ -187,7 +187,8 @@ if (BRANCH === "(none)") {
187
187
 
188
188
  // Phase 2: Implement
189
189
  await phase("implement", () =>
190
- agent(`Implement the following ticket on branch ${BRANCH}:
190
+ agent(`OPERATION: implement
191
+ Implement the following ticket on branch ${BRANCH}:
191
192
 
192
193
  ${TICKET}
193
194
 
@@ -208,39 +209,51 @@ After implementing, commit your changes with a conventional-commit message and r
208
209
  - Any open questions or blockers`, { agentType: "Code" })
209
210
  );
210
211
 
211
- // Phase 3: Gate 1 — post-code pipeline
212
+ // Phase 3: Gate 1 #1 — post-code pipeline: Simplify agent → Scrutinize agent → Validate agent.
213
+ // Validate runs last and unconditionally, so its one full run covers every commit the pass made (see gate1_postcode()).
212
214
  const gate1 = await phase("gate1", async () => {
213
- // Gate 1 #1 — runs once, immediately after the initial implementation.
215
+ await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
216
+
217
+ const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
218
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
219
+ // Anything but an explicit PASS or FIXED is a stop: a Scrutinize that returned no status, or one this skeleton does not know, has not accepted the code.
220
+ if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
221
+ return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Scrutinize agent returned no recognised status: counted as BLOCKED" };
222
+ }
223
+
214
224
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH}.
215
225
  Follow your Running commands block for every build and test command.
216
- Report: PASS or FAIL with details.`, { agentType: "Validate" });
226
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
217
227
 
218
- if (validation.verdict === "FAIL") {
219
- // Up to 2 fix attempts
228
+ // Anything but an explicit PASS is a failure: a Validate that returned no verdict has not validated the branch.
229
+ if (validation?.verdict !== "PASS") {
230
+ let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
231
+ // Up to 2 fix attempts, each followed by a Validate re-run
220
232
  for (let attempt = 1; attempt <= 2; attempt++) {
221
- await agent(`Fix the validation failures on branch ${BRANCH}:
222
- ${validation.details}
233
+ await agent(`OPERATION: validation-fix
234
+ Fix the validation failures on branch ${BRANCH}:
235
+ ${failureDetails}
223
236
  ISSUE_NUMBER: ${ISSUE_NUMBER}
224
237
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
225
238
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
226
239
  Commit fixes with conventional-commit message.`, { agentType: "Code" });
227
- const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH}. Report: PASS or FAIL.`, { agentType: "Validate" });
228
- if (recheck.verdict === "PASS") break;
229
- if (attempt === 2) return { verdict: "ESCALATED", reason: "Validation exhausted after 2 Code agent fix attempts" };
240
+ const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} after fix attempt ${attempt} (follow your Running commands block).
241
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
242
+ if (recheck?.verdict === "PASS") break;
243
+ failureDetails = recheck?.details || failureDetails;
244
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Validation exhausted after 2 Code agent fix attempts" };
230
245
  }
231
246
  }
232
247
 
233
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
234
-
235
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
236
-
237
- if (scrutiny.codeChanged) {
238
- await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes). Report: PASS or FAIL.`, { agentType: "Validate" });
239
- }
240
-
241
248
  return { verdict: "PASS" };
242
249
  });
243
250
 
251
+ // Gate 1 #1 stop, before Gate 2: an exhausted Validate or a BLOCKED Scrutinize leaves a broken or unfinished build,
252
+ // so Gate 2 and the review pass never run on it — a correctness stop, like the ticket-link and branch stops.
253
+ if (gate1.verdict === "ESCALATED") {
254
+ return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK, escalations: [{ type: gate1.type, description: gate1.reason }] };
255
+ }
256
+
244
257
  // Phase 4: Gate 2 — acceptance gate (once, before review pass)
245
258
  const gate2 = await phase("gate2", async () => {
246
259
  if (!PLAN && !CRITERIA && !TEST_PLAN) {
@@ -249,21 +262,19 @@ const gate2 = await phase("gate2", async () => {
249
262
 
250
263
  let evalVerdict = "SKIPPED";
251
264
  if (PLAN) {
252
- const panel = await parallel([
253
- () => agent(`Acceptance-criteria evaluator: does the implementation on branch ${BRANCH} satisfy each numbered criterion, INCLUDING negative criteria?
265
+ const evaluation = await agent(`Acceptance and scope check on branch ${BRANCH}. Answer both questions:
266
+ 1. Does the implementation satisfy each numbered criterion, INCLUDING negative criteria (what it must NOT do)?
267
+ 2. Did the Code agent introduce any unplanned changes, smuggled anti-features, or drift from the plan's intent?
254
268
  Plan: ${PLAN}
255
269
  Criteria: ${CRITERIA || "see plan"}
256
- Report: PASS or FAIL per criterion with rationale.`, { agentType: "Evaluate" }),
257
- () => agent(`Scope/intent-drift evaluator: did the Code agent on branch ${BRANCH} introduce any unplanned changes, smuggled anti-features, or drift from the plan's intent?
258
- Plan: ${PLAN}
259
- Report: PASS or FAIL with rationale.`, { agentType: "Evaluate" }),
260
- ]);
261
- evalVerdict = panel.every(p => p.verdict === "PASS") ? "PASS" : "FAIL";
270
+ Return: {"verdict": "PASS" | "FAIL", "rationale": "..."} — FAIL if either answer is no, naming the failing criterion or the drift.`, { agentType: "Evaluate" });
271
+ evalVerdict = evaluation?.verdict === "PASS" ? "PASS" : "FAIL";
262
272
  if (evalVerdict === "FAIL") {
263
273
  // fix-and-continue — no re-evaluate, no inline Gate 1. The final Gate 1 (#2)
264
274
  // after the review pass is the build gate before the branch is handed back.
265
- await agent(`Fix the alignment issues identified by the Evaluate agent panel on branch ${BRANCH}:
266
- ${panel.filter(p => p.verdict === "FAIL").map(p => p.rationale).join("\n")}
275
+ await agent(`OPERATION: alignment-fix
276
+ Fix the alignment issues identified by the Evaluate agent on branch ${BRANCH}:
277
+ ${evaluation?.rationale || "see the Evaluate agent's report"}
267
278
  ISSUE_NUMBER: ${ISSUE_NUMBER}
268
279
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
269
280
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
@@ -282,7 +293,8 @@ Cover: functionality, API contracts, performance, and cover every TEST_PLAN scen
282
293
  testVerdict = testResult.verdict;
283
294
  if (testVerdict === "FAIL") {
284
295
  // fix-and-continue — no re-test, no inline Gate 1.
285
- await agent(`Fix the failing acceptance test scenarios on branch ${BRANCH}:
296
+ await agent(`OPERATION: qa-fix
297
+ Fix the failing acceptance test scenarios on branch ${BRANCH}:
286
298
  ${testResult.failures}
287
299
  ISSUE_NUMBER: ${ISSUE_NUMBER}
288
300
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
@@ -309,7 +321,7 @@ const reviewResult = await phase("review", async () => {
309
321
  const chunkSize = 5;
310
322
  const reviewThunks = reviewFocuses.map(focus =>
311
323
  () => agent(`${focus.charAt(0).toUpperCase() + focus.slice(1)} review. ${reviewScope}
312
- Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
324
+ Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
313
325
  );
314
326
 
315
327
  let rawReviews = [];
@@ -326,7 +338,7 @@ Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "fil
326
338
  const focus = reviewFocuses[i];
327
339
  if (!r || r.reviewed !== true) {
328
340
  // Dead Review agent: retry once sequentially
329
- const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
341
+ const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
330
342
  if (!retry || retry.reviewed !== true) {
331
343
  coverageGaps.push(focus);
332
344
  log(`review coverage incomplete: ${focus}`);
@@ -391,13 +403,18 @@ ${JSON.stringify(allFindings.map((f, i) => ({ index: i, description: f.descripti
391
403
  // Distinct-file groups are paced in chunks of FIX_CHUNK — same pacing bar as the Review spawn
392
404
  // path — to avoid launching all Code agents at once on a large branch (reliability: explicit bound).
393
405
  const FIX_CHUNK = 5;
394
- const fixThunks = Object.entries(fileGroups).map(([fileKey, findings]) => async () => {
406
+ // issue-fix SCOPE per finding: CRITICAL or HIGH takes the Careful protocol (failing regression test first); anything else is Standard.
407
+ const CAREFUL_SEVERITIES = new Set(["critical", "high"]);
408
+ const fixThunks = Object.values(fileGroups).map(findings => async () => {
395
409
  const chunkResults = [];
396
410
  for (let i = 0; i < findings.length; i += MAX_BATCH) {
397
411
  const chunk = findings.slice(i, i + MAX_BATCH);
398
- const r = await agent(`Fix the following confirmed review findings on branch ${BRANCH}${fileKey.startsWith("__nofile") ? "" : ` in ${fileKey}`}:
399
- ${chunk.map(f => `- ${f.description} (${f.severity})`).join("\n")}
400
-
412
+ const r = await agent(`OPERATION: issue-fix
413
+ Fix the following confirmed review findings on branch ${BRANCH}:
414
+ ISSUES:
415
+ ${chunk.map((f, n) => `${n + 1}. ${f.file ? `${f.file}${f.line ? `:${f.line}` : ""} — ` : ""}${f.description} (${f.severity})`).join("\n")}
416
+ SCOPE: ${chunk.map((f, n) => `${n + 1}=${CAREFUL_SEVERITIES.has(String(f.severity).toLowerCase()) ? "Careful" : "Standard"}`).join(", ")}
417
+ PUSH: false
401
418
  ISSUE_NUMBER: ${ISSUE_NUMBER}
402
419
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
403
420
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
@@ -436,38 +453,40 @@ Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<
436
453
  return { survivingFindings: notAddressed, fixedFindings, coverageGaps };
437
454
  });
438
455
 
439
- // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (full Validate agent → Simplify agent → Scrutinize agent).
456
+ // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (Simplify agent → Scrutinize agent → full Validate agent, Validate last and unconditional).
440
457
  // Runs ONCE, after the review pass. Inside the pass the Code agent self-verified its own fixes;
441
458
  // this is the build gate before the branch is handed back. See gate1_postcode() cadence.
442
459
  const gate1Final = await phase("gate1-final", async () => {
460
+ await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
461
+
462
+ const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
463
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
464
+ if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
465
+ return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Final Gate 1 Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Final Gate 1 Scrutinize agent returned no recognised status: counted as BLOCKED" };
466
+ }
467
+
443
468
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH} (final gate after all fixing).
444
469
  Follow your Running commands block for every build and test command.
445
- Report: PASS or FAIL with details.`, { agentType: "Validate" });
470
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
446
471
 
447
- if (validation.verdict === "FAIL") {
448
- let failureDetails = validation.details;
472
+ if (validation?.verdict !== "PASS") {
473
+ let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
449
474
  for (let attempt = 1; attempt <= 2; attempt++) {
450
- await agent(`Fix the final validation failures on branch ${BRANCH}:
475
+ await agent(`OPERATION: validation-fix
476
+ Fix the final validation failures on branch ${BRANCH}:
451
477
  ${failureDetails}
452
478
  ISSUE_NUMBER: ${ISSUE_NUMBER}
453
479
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
454
480
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
455
481
  Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code" });
456
- const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (follow your Running commands block). Report: PASS or FAIL.`, { agentType: "Validate" });
457
- if (recheck.verdict === "PASS") break;
458
- failureDetails = recheck.details || failureDetails;
459
- if (attempt === 2) return { verdict: "ESCALATED", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
482
+ const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (final gate, after fix attempt ${attempt}; follow your Running commands block).
483
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
484
+ if (recheck?.verdict === "PASS") break;
485
+ failureDetails = recheck?.details || failureDetails;
486
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
460
487
  }
461
488
  }
462
489
 
463
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
464
-
465
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
466
-
467
- if (scrutiny.codeChanged) {
468
- await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes; follow your Running commands block). Report: PASS or FAIL.`, { agentType: "Validate" });
469
- }
470
-
471
490
  return { verdict: "PASS" };
472
491
  });
473
492
 
@@ -496,7 +515,7 @@ Write a concise report. Present only surviving findings as outstanding — never
496
515
  survivingFindings: reviewResult.survivingFindings || [], fixedFindings: reviewResult.fixedFindings || [],
497
516
  reviewCoverage: { failedFocuses: coverageGaps, complete: coverageGaps.length === 0 },
498
517
  escalations: [
499
- ...(gate1Final.verdict === "ESCALATED" ? [{ type: "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
518
+ ...(gate1Final.verdict === "ESCALATED" ? [{ type: gate1Final.type || "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
500
519
  ...coverageGaps.map(focus => ({ type: "review-coverage-incomplete", description: `review coverage incomplete: ${focus}` })),
501
520
  ],
502
521
  gate2, report,
@@ -540,7 +559,7 @@ When WAVE mode is detected, author a workflow that wraps the single-ticket engin
540
559
 
541
560
  **Wave workflow structure (author after the SINGLE engine blocks above):**
542
561
 
543
- The wave workflow uses the same phases as SINGLE but wraps them in a wave loop. The integration branch is `wave/<initiative>` — the initiative slug, referenced below as `{slug}` — (or the user's current branch). Each ticket works on the branch setup-task created. The Git agent manages worktrees for parallel-eligible tickets. After every merge: Validate agent (build + test). Escalations accumulate in a list; the final report lists all of them.
562
+ The wave workflow uses the same phases as SINGLE but wraps them in a wave loop. The integration branch is `wave/<initiative>` — the initiative slug, referenced below as `{slug}` — (or the user's current branch). Each ticket works on the branch setup-task created. The Git agent manages worktrees for parallel-eligible tickets. After every merge the workflow spawns the Validate agent (build + test) and undoes a red merge. Escalations accumulate in a list; the final report lists all of them.
544
563
 
545
564
  Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
546
565
 
@@ -551,11 +570,12 @@ Wave skeleton — compact reference (see `wave_loop()` doctrine for full semanti
551
570
  const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
552
571
  const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
553
572
  // A row from an engine result: the fields step 3 after the workflow renders
554
- const rowOf = (ticketId, r, merged) => ({ ticket: ticketId, ran: true, verdict: r?.verdict || r?.overallVerdict || null, merged, issuePrLink: r?.issuePrLink || "(none)", evaluateVerdict: r?.gate2?.evaluateVerdict, testVerdict: r?.gate2?.testVerdict, surviving: r?.survivingFindings?.length, coverageComplete: r?.reviewCoverage?.complete });
573
+ const rowOf = (ticketId, r, merged, treeEqual = null) => ({ ticket: ticketId, ran: true, verdict: r?.verdict || r?.overallVerdict || null, merged, treeEqual, issuePrLink: r?.issuePrLink || "(none)", evaluateVerdict: r?.gate2?.evaluateVerdict, testVerdict: r?.gate2?.testVerdict, surviving: r?.survivingFindings?.length, coverageComplete: r?.reviewCoverage?.complete });
555
574
  const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
556
- const waveState = { quarantined: [], round: 0 };
575
+ const MERGE_SHA = /^[0-9a-f]{40}$/; // a merge commit as git prints it: the only Git-reported value that reaches a later prompt
576
+ const waveState = { quarantined: [], round: 0, halted: null };
557
577
  let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
558
- while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS) {
578
+ while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS && waveState.halted === null) {
559
579
  waveState.round++;
560
580
 
561
581
  // Design agent reader (opus) applies vacuous-truth rule: a ticket with no unmet dependencies is ALWAYS ready.
@@ -588,10 +608,36 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
588
608
  // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
589
609
  // PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
590
610
  if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
591
- const merge = await agent(`Merge ${engineResult.branch} to ${INTEGRATION_BRANCH}. Include ticket ID ${ticketId} in the merge commit message. Run Validate agent (build + test) after merge.
592
- Return: {"merged": true} — or {"merged": false, "reason": "<why>"} when the merge or the post-merge build failed and the merge was not kept.`, { agentType: "Git" });
593
- results[ticketId] = rowOf(ticketId, engineResult, merge?.merged === true);
594
- if (merge?.merged !== true) waveState.quarantined.push({ ticket: ticketId, reason: merge?.reason || "merge or post-merge build failed" });
611
+ // Git merges locally and validates nothing: the workflow owns the post-merge Validate (D-WAVE-MERGE-VALIDATE).
612
+ const merge = await agent(`Merge ${engineResult.branch} to ${INTEGRATION_BRANCH} locally. Include ticket ID ${ticketId} in the merge commit message. Do not push, and run no build or test.
613
+ Return: {"merged": true, "mergeSha": "<40-hex merge commit>", "treeEqual": <true when mergeSha^{tree} equals the tree of ${engineResult.branch}'s head>} — or {"merged": false, "reason": "<why>"} when no merge is kept.`, { agentType: "Git" });
614
+ let kept = false;
615
+ let reason = merge?.reason || "merge failed";
616
+ if (merge?.merged === true && !MERGE_SHA.test(String(merge.mergeSha))) {
617
+ // A merge may sit on the integration branch with no SHA to validate or undo it by: stop taking merges.
618
+ waveState.halted = { ticket: ticketId, mergeSha: null, reason: "merge reported without a 40-hex mergeSha; the integration branch may hold an unvalidated merge" };
619
+ reason = waveState.halted.reason;
620
+ } else if (merge?.merged === true) {
621
+ // A merge counts as kept only when this Validate returns PASS. It always runs: treeEqual is recorded, never a reason to skip.
622
+ const post = await agent(`Run build, typecheck, lint, and tests on ${INTEGRATION_BRANCH} after merging ${engineResult.branch} (merge commit ${merge.mergeSha}).
623
+ Follow your Running commands block for every build and test command.
624
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
625
+ if (post?.verdict === "PASS") {
626
+ kept = true;
627
+ } else {
628
+ const undo = await agent(`Undo the merge ${merge.mergeSha} on ${INTEGRATION_BRANCH}, only when no remote branch contains ${merge.mergeSha} and the HEAD of ${INTEGRATION_BRANCH} still equals ${merge.mergeSha}. On the integration branch run exactly this one command and no other that changes a branch or a remote: git reset --keep ${merge.mergeSha}^1
629
+ Return: {"undone": true} — or {"undone": false, "reason": "<why>"} when you did not undo it.`, { agentType: "Git" });
630
+ if (undo?.undone === true) {
631
+ reason = "post-merge build red; merge undone";
632
+ } else {
633
+ // A merge that stays red would poison every later ticket: stop taking merges and rounds, and name the red HEAD.
634
+ reason = `post-merge build red; merge not undone (${undo?.reason || "no reason given"})`;
635
+ waveState.halted = { ticket: ticketId, mergeSha: merge.mergeSha, reason };
636
+ }
637
+ }
638
+ }
639
+ results[ticketId] = rowOf(ticketId, engineResult, kept, merge?.merged === true && typeof merge.treeEqual === "boolean" ? merge.treeEqual : null);
640
+ if (!kept) waveState.quarantined.push({ ticket: ticketId, reason });
595
641
  } else {
596
642
  results[ticketId] = rowOf(ticketId, engineResult, false);
597
643
  waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
@@ -602,11 +648,12 @@ Return: {"merged": true} — or {"merged": false, "reason": "<why>"} when the me
602
648
  waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
603
649
  }
604
650
  remainingTickets = remainingTickets.filter(t => t !== ticketId);
651
+ if (waveState.halted !== null) break; // a red integration HEAD takes no further merge in this round
605
652
  }
606
653
  }
607
654
 
608
655
  // After the wave report is written: one row per wave ticket, in input order. No entry ⇒ never ran (cascade, deadlock, MAX_ROUNDS).
609
- return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, issuePrLink: "(none)" }), quarantined: waveState.quarantined };
656
+ return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, treeEqual: null, issuePrLink: "(none)" }), quarantined: waveState.quarantined, halted: waveState.halted };
610
657
  ```
611
658
 
612
659
  ---
@@ -630,6 +677,7 @@ A workflow cannot pause mid-run. After the build/wave workflow returns, you (the
630
677
  In WAVE mode, if no tracking-issue number was resolved in Pre-authoring step 5: state `TRACEABILITY: DEGRADED (no tracking issue for this run)` in the run summary and skip — never skip silently.
631
678
  3. **Compose the wave PR inputs** (WAVE mode only — skip this step entirely in SINGLE mode). The workflow returned `tickets`: one entry per wave ticket, in its input order, each `{ticket, ran, verdict, merged, issuePrLink, evaluateVerdict, testVerdict, surviving, coverageComplete}`. Every value in it is agent-reported and treated as untrusted: nothing below is repaired, and no reference is ever composed from a number.
632
679
  - **Branch check.** Run `git -C "{integration worktree root}" branch --show-current`. Only a name matching `^wave/[a-z0-9][a-z0-9-]{0,59}$` opens a wave PR, and its part after `wave/` is the `{slug}` below. Any other name: record `TRACEABILITY: DEGRADED (not a wave branch)` and go to step 4 with no wave PR.
680
+ - **Halted** (the workflow returned a non-null `halted`: a post-merge build it could not undo, or a merge reported with no SHA): the integration HEAD may be red. Record `Wave PR: skipped (integration HEAD red, wave halted at <halted.ticket>)`, name `halted.mergeSha` (or `none reported`) and `halted.reason` as the red integration HEAD in the run summary, and go to step 4 with no wave PR.
633
681
  - **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
634
682
  - Otherwise compose (a) and then (b).
635
683