devflow-kit 3.5.0 → 3.7.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 (146) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/README.md +10 -6
  3. package/dist/agents/code.md +7 -11
  4. package/dist/agents/{diagnose.md → debug.md} +4 -4
  5. package/dist/agents/{code-fix.md → fix.md} +7 -8
  6. package/dist/agents/{scrutinize.md → harden.md} +6 -6
  7. package/dist/agents/{code-edit.md → refactor.md} +8 -9
  8. package/dist/agents/review.md +2 -0
  9. package/dist/cli/agents-view/render.js +1 -1
  10. package/dist/cli/commands/gates.js +276 -0
  11. package/dist/cli/commands/init-seed.js +9 -1
  12. package/dist/cli/commands/init.js +34 -11
  13. package/dist/cli/commands/learning.js +12 -12
  14. package/dist/cli/commands/legacy-hooks.js +1 -1
  15. package/dist/cli/commands/memory.js +18 -0
  16. package/dist/cli/commands/uninstall.js +111 -5
  17. package/dist/cli.js +2 -0
  18. package/dist/commands/bug-analysis.md +21 -17
  19. package/dist/commands/code-review.md +65 -66
  20. package/dist/commands/debug.md +17 -17
  21. package/dist/commands/dynamic-build.md +104 -116
  22. package/dist/commands/dynamic-plan.md +13 -11
  23. package/dist/commands/dynamic-profile.md +9 -7
  24. package/dist/commands/dynamic-tickets.md +13 -11
  25. package/dist/commands/explore.md +15 -15
  26. package/dist/commands/implement.md +80 -154
  27. package/dist/commands/plan.md +34 -66
  28. package/dist/commands/release.md +11 -7
  29. package/dist/commands/research.md +14 -10
  30. package/dist/commands/resolve.md +44 -88
  31. package/dist/commands/self-review.md +44 -44
  32. package/dist/core/agent-models.js +47 -11
  33. package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
  34. package/dist/core/echo-value.js +42 -0
  35. package/dist/core/evidence-policy.js +17 -5
  36. package/dist/core/gate-level.js +213 -0
  37. package/dist/core/learning-queue-cleanup.js +1 -1
  38. package/dist/core/manifest.js +26 -10
  39. package/dist/core/migrations.js +63 -44
  40. package/dist/core/model-discovery.js +7 -1
  41. package/dist/core/plugins.js +14 -12
  42. package/dist/core/project-paths.js +3 -3
  43. package/dist/core/tracker.js +5 -33
  44. package/dist/hud/codex-usage.js +247 -0
  45. package/dist/hud/components/codex-quota.js +37 -0
  46. package/dist/hud/components/usage-quota.js +1 -1
  47. package/dist/hud/config.js +1 -0
  48. package/dist/hud/index.js +16 -3
  49. package/dist/hud/render.js +3 -1
  50. package/dist/learning-off/agents/code.md +7 -11
  51. package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
  52. package/dist/learning-off/agents/{code-fix.md → fix.md} +7 -8
  53. package/dist/learning-off/agents/{scrutinize.md → harden.md} +6 -6
  54. package/dist/learning-off/agents/{code-edit.md → refactor.md} +8 -9
  55. package/dist/learning-off/agents/review.md +2 -0
  56. package/dist/learning-off/commands/bug-analysis.md +9 -9
  57. package/dist/learning-off/commands/code-review.md +54 -59
  58. package/dist/learning-off/commands/debug.md +6 -10
  59. package/dist/learning-off/commands/dynamic-build.md +98 -112
  60. package/dist/learning-off/commands/dynamic-plan.md +5 -5
  61. package/dist/learning-off/commands/dynamic-profile.md +1 -1
  62. package/dist/learning-off/commands/dynamic-tickets.md +5 -5
  63. package/dist/learning-off/commands/explore.md +4 -8
  64. package/dist/learning-off/commands/implement.md +68 -146
  65. package/dist/learning-off/commands/plan.md +23 -58
  66. package/dist/learning-off/commands/release.md +4 -2
  67. package/dist/learning-off/commands/research.md +2 -2
  68. package/dist/learning-off/commands/resolve.md +33 -81
  69. package/dist/learning-off/commands/self-review.md +32 -36
  70. package/dist/skills/git/references/pr/check-merge-readiness.md +2 -0
  71. package/dist/skills/git/references/pr/update-pr-evidence.md +4 -6
  72. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
  73. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
  74. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
  75. package/dist/targets/claude-code/installer.js +117 -84
  76. package/dist/targets/claude-code/learning-install.js +15 -3
  77. package/dist/targets/claude-code/tracker-servers.js +1 -1
  78. package/package.json +1 -1
  79. package/src/assets/agents/_partials/_code.mds +7 -8
  80. package/src/assets/agents/code.mds +4 -7
  81. package/src/assets/agents/{learning.md → curate.md} +8 -10
  82. package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
  83. package/src/assets/agents/evaluate.md +4 -4
  84. package/src/assets/agents/{code-fix.mds → fix.mds} +5 -5
  85. package/src/assets/agents/{scrutinize.mds → harden.mds} +6 -6
  86. package/src/assets/agents/{simplify.md → polish.md} +4 -4
  87. package/src/assets/agents/{code-edit.mds → refactor.mds} +6 -6
  88. package/src/assets/agents/review.mds +2 -0
  89. package/src/assets/agents/synthesize.md +2 -2
  90. package/src/assets/agents/test.md +2 -2
  91. package/src/assets/commands/_partials/_compliance.mds +1 -1
  92. package/src/assets/commands/_partials/_decisions.mds +26 -6
  93. package/src/assets/commands/_partials/_engine.mds +15 -15
  94. package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
  95. package/src/assets/commands/_partials/_knowledge.mds +14 -9
  96. package/src/assets/commands/_partials/_preamble.mds +4 -2
  97. package/src/assets/commands/_partials/_roster.mds +4 -4
  98. package/src/assets/commands/_partials/_settings.mds +2 -2
  99. package/src/assets/commands/_partials/_wave.mds +10 -17
  100. package/src/assets/commands/bug-analysis.mds +7 -7
  101. package/src/assets/commands/code-review.mds +51 -56
  102. package/src/assets/commands/debug.mds +2 -2
  103. package/src/assets/commands/dynamic-build.mds +67 -74
  104. package/src/assets/commands/implement.mds +65 -127
  105. package/src/assets/commands/plan.mds +21 -61
  106. package/src/assets/commands/release.mds +5 -1
  107. package/src/assets/commands/resolve.mds +28 -76
  108. package/src/assets/commands/self-review.mds +27 -27
  109. package/src/assets/mds/git/_ops_steps.mds +3 -3
  110. package/src/assets/mds/git/_pr.mds +6 -6
  111. package/src/assets/mds/tracker/_common.mds +23 -12
  112. package/src/assets/mds/tracker/_github.mds +2 -9
  113. package/src/assets/mds/tracker/_jira.mds +2 -15
  114. package/src/assets/mds/tracker/_linear.mds +2 -15
  115. package/src/assets/scripts/ci-wait.cjs +22 -4
  116. package/src/assets/scripts/claude-md-audit.cjs +66 -51
  117. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
  118. package/src/assets/scripts/hooks/background-memory-update +404 -95
  119. package/src/assets/scripts/hooks/capture-prompt +1 -1
  120. package/src/assets/scripts/hooks/capture-question +1 -1
  121. package/src/assets/scripts/hooks/capture-turn +1 -1
  122. package/src/assets/scripts/hooks/json-helper.cjs +13 -2
  123. package/src/assets/scripts/hooks/json-parse +18 -3
  124. package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
  125. package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -4
  126. package/src/assets/scripts/hooks/memory-permission +358 -0
  127. package/src/assets/scripts/hooks/preamble +1 -1
  128. package/src/assets/scripts/hooks/session-start-context +60 -47
  129. package/src/assets/scripts/hooks/session-start-memory +26 -5
  130. package/src/assets/scripts/lib/project-config.cjs +19 -4
  131. package/src/assets/scripts/resolve-settings.cjs +200 -32
  132. package/src/assets/scripts/verify-evidence.cjs +726 -9
  133. package/src/assets/skills/agent-references/SKILL.md +14 -6
  134. package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
  135. package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
  136. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
  137. package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
  138. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +1 -1
  139. package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
  140. package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
  141. package/src/assets/skills/apply-decisions/SKILL.md +6 -4
  142. package/src/assets/skills/docs-framework/SKILL.md +3 -3
  143. package/src/assets/skills/qa/SKILL.md +1 -1
  144. package/src/assets/skills/quality-gates/SKILL.md +7 -7
  145. package/src/assets/skills/quality-gates/references/report-template.md +7 -7
  146. /package/src/assets/skills/agent-references/references/{learning → curate}/maintain.md +0 -0
@@ -35,17 +35,17 @@ Run a comprehensive code review of the current branch by spawning parallel revie
35
35
  - **Exactly one listed:** load neither the skill nor the reference — that worktree is the only candidate, and Step 0c's Git agent validates its branch
36
36
  2. **If `--path` flag provided:** use only that worktree, skip discovery
37
37
  **`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop.
38
- 3. **If only 1 reviewable worktree** (the common case): proceed as single-worktree flow — zero behavior change
38
+ 3. **If only 1 reviewable worktree**: proceed as single-worktree flow — zero behavior change
39
39
  4. **If multiple reviewable worktrees:** report "Found N worktrees with reviewable branches: {list with paths and branches}" and proceed with multi-worktree flow
40
40
 
41
- #### Step 0b: Resolve the compliance lens
41
+ #### Step 0b: Resolve the compliance lens and gate level
42
42
 
43
- **Produces:** COMPLIANCE_ACTIVE, COMPLIANCE_FRAMEWORKS
43
+ **Produces:** COMPLIANCE_ACTIVE, COMPLIANCE_FRAMEWORKS, GATE_LEVEL
44
44
 
45
45
  {{settings.settings_resolve()}}
46
46
 
47
47
  {{compliance_gate()}}
48
- Keep each worktree's values for every downstream phase of that worktree.
48
+ Keep each worktree's values for every downstream phase.
49
49
 
50
50
  #### Step 0b-ii: Resolve the evidence policy
51
51
 
@@ -87,7 +87,7 @@ Validate branch, commit if needed, push, create PR if needed.
87
87
  Return: branch, base_branch, branch-slug, PR#"
88
88
  ```
89
89
 
90
- In multi-worktree mode, spawn all pre-flight agents **in a single message** (parallel).
90
+ In multi-worktree mode, spawn all pre-flight agents **in a single message**.
91
91
 
92
92
  **If BLOCKED:** In single-worktree mode, stop and report. In multi-worktree mode, report the failure but continue with other worktrees.
93
93
 
@@ -127,7 +127,7 @@ For each worktree, perform a single pass over timestamped directories:
127
127
  2. Iterate once: accumulate CYCLE_NUMBER count for each directory containing `resolution-summary.md`; capture the first (most-recent) such directory as PRIOR_DIR.
128
128
  3. If CYCLE_NUMBER = 0: set PRIOR_RESOLUTIONS=(none), CYCLE_NUMBER=1, proceed.
129
129
  4. Otherwise: set CYCLE_NUMBER = count + 1. Read `{PRIOR_DIR}/resolution-summary.md` as PRIOR_RESOLUTIONS.
130
- 5. If `--full`: still load PRIOR_RESOLUTIONS (valuable for Review agent cross-cycle awareness).
130
+ 5. If `--full`: still load PRIOR_RESOLUTIONS.
131
131
 
132
132
  #### Step 0e-ii: Convergence Assessment
133
133
 
@@ -169,7 +169,7 @@ For each reviewable worktree, call:
169
169
  ### Phase 1: Analyze Changed Files
170
170
 
171
171
  **Produces:** REVIEW_FOCUS_LIST, DIFF_CLASS, DIFF_FILES
172
- **Requires:** DIFF_RANGE, REVIEW_DIR, COMPLIANCE_ACTIVE (from Step 0b)
172
+ **Requires:** DIFF_RANGE, REVIEW_DIR, COMPLIANCE_ACTIVE, GATE_LEVEL (from Step 0b)
173
173
 
174
174
  Per worktree, in order. Each `git` command here writes to a file and prints nothing.
175
175
 
@@ -185,7 +185,7 @@ git -C "{WORKTREE_PATH}" diff --name-only --no-renames {DIFF_RANGE} > "{REVIEW_D
185
185
  wc -l < "{REVIEW_DIR}/paths.txt"
186
186
  ```
187
187
 
188
- Read `paths.txt` once when the count is about 40 or fewer, and in ranges of about 40 lines (Read with offset and limit) when it is more. `--no-renames` lists a moved file under its old path and its new one, so a move cannot take a file out of its own class.
188
+ Read `paths.txt` once when the count is about 40 or fewer, and in ranges of about 40 lines (Read with offset and limit) when it is more.
189
189
 
190
190
  Set `DIFF_CLASS` by these rules, in order; the first that decides wins:
191
191
 
@@ -202,7 +202,7 @@ Each reduced class runs only its focus set, every member reading `diff.patch`:
202
202
  - `tests-only`: testing, reliability, consistency, security
203
203
  - `lockfile-only`: dependencies, security
204
204
 
205
- A mixed non-code diff runs the union of the sets of the classes it holds. Language and conditional triggers inside a reduced class are suppressed: they add no focus, and Step 1.2 applies to the `code` class only.
205
+ A mixed non-code diff runs the union of the sets of the classes it holds. Language and conditional triggers inside a reduced class are suppressed.
206
206
 
207
207
  #### Step 1.2: Choose the focuses
208
208
 
@@ -226,13 +226,27 @@ When `DIFF_CLASS` is `code`, read the file types from the paths in `paths.txt` t
226
226
  | Docs or significant code | documentation |
227
227
  | COMPLIANCE_ACTIVE AND diff touches regulated surface | compliance |
228
228
 
229
- If `COMPLIANCE_ACTIVE` AND the diff touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention): add `compliance` to REVIEW_FOCUS_LIST for this worktree.
229
+ If `COMPLIANCE_ACTIVE` AND the diff touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention): on a `code` diff, add `compliance`.
230
230
 
231
231
  **Language focus stamp.** The eight language focuses — `typescript`, `react`, `accessibility`, `ui-design`, `go`, `java`, `python`, `rust` — ship with optional plugins, and the installer stamps the ones installed on this machine onto this line, rewriting it on every install and on `uninstall --plugin`:
232
232
 
233
233
  Installed language focuses: (none)
234
234
 
235
- A language focus is spawned only when its file-type condition above fires AND its name appears in that stamped line. A focus missing from the line is never spawned, even when its trigger fires. The eight core focuses are unconditional and are never stamp-gated.
235
+ A language focus is spawned only when its file-type condition above fires AND its name appears in that stamped line. A focus missing from the line is never spawned, even when its trigger fires. On a `code` diff, REVIEW_FOCUS_LIST starts with the eight core focuses. Step 1.2b may narrow them; they are never stamp-gated.
236
+
237
+ #### Step 1.2b: Narrow by gate level
238
+
239
+ **Produces:** REVIEW_FOCUS_LIST, GATE_LEVEL
240
+ **Requires:** REVIEW_FOCUS_LIST, DIFF_CLASS, CHANGED_PATHS, GATE_LEVEL
241
+
242
+ **Gate level.** Narrow REVIEW_FOCUS_LIST, for every class, by this worktree's `GATE_LEVEL`. `full` keeps it and reads nothing.
243
+
244
+ At `standard` or `lean`, once per run, run `d="${CLAUDE_CONFIG_DIR:-}"; case "$d" in /*) ;; *) d="$HOME/.claude" ;; esac; printf '%s\n' "$d"` in Bash and Read `<that directory>/skills/devflow:agent-references/references/commands/code-review-levels.md` with the Read tool.
245
+
246
+ - Apply each class's row to that class's own set, then take the union of the results. A row only removes focuses.
247
+ - If the Read fails, or a row is missing or malformed, set `GATE_LEVEL` to `full` for every worktree of this run and say `level reference unreadable: full sets applied`.
248
+ - Security runs at every level and in every class.
249
+ - Compliance keeps the trigger above at every level.
236
250
 
237
251
  #### Step 1.3: Write the diff files
238
252
 
@@ -277,7 +291,7 @@ The core, database, dependencies, documentation and compliance focuses, and ever
277
291
 
278
292
  {{decisions_load()}}
279
293
  <!-- learning:on -->
280
- This produces a compact index of active ADR/PF entries. Pass `DECISIONS_CONTEXT` to all Review agents. Review agents use `devflow:apply-decisions` to Read full entry bodies on demand.
294
+ This produces a compact index of active ADR/PF entries. Pass `DECISIONS_CONTEXT` to all Review agents as one shared slice, cut over the diff's files. Review agents use `devflow:apply-decisions` to Read full entry bodies on demand.
281
295
 
282
296
  <!-- learning:end -->
283
297
  {{knowledge_load()}}
@@ -297,9 +311,9 @@ Pass `FEATURE_KNOWLEDGE` to all Review agents.
297
311
  **Requires:** DIFF_RANGE, REVIEW_DIR, TIMESTAMP, FEATURE_KNOWLEDGE, PR_DESCRIPTION, PRIOR_RESOLUTIONS, REVIEW_FOCUS_LIST, DIFF_CLASS, DIFF_FILES
298
312
  <!-- learning:end -->
299
313
 
300
- Spawn Review agents **in a single message**, for the focus set `DIFF_CLASS` selects (max 20 per worktree):
314
+ Spawn Review agents **in a single message**, one per focus in REVIEW_FOCUS_LIST (max 20 per worktree):
301
315
 
302
- - `code`: the 8 core reviews on the full `diff.patch`, plus the language and conditional focuses Phase 1 added.
316
+ - `code`: the 8 core reviews on the full `diff.patch` (lean keeps 3), plus the language and conditional focuses Phase 1 added.
303
317
  - `docs-only`, `tests-only`, `lockfile-only`: only the reduced set Phase 1 names, each reading `diff.patch`; a mixed non-code diff runs the union. No language or conditional focus is spawned, whatever its trigger.
304
318
 
305
319
  | Focus | Runs | Pattern Skill | DIFF_FILE |
@@ -325,7 +339,7 @@ Spawn Review agents **in a single message**, for the focus set `DIFF_CLASS` sele
325
339
  | documentation | conditional | devflow:documentation | diff.patch |
326
340
  | compliance | diff-driven | devflow:compliance | diff.patch |
327
341
 
328
- Review agent count: `code` runs 8 core, +1-12 added by Phase 1 (max 20 total per worktree); a reduced class or a mix runs 2-6.
342
+ Review agent count: at `full`, `code` runs 8 core +1-12; `standard` runs up to 2 fewer; lean `code` runs 3-4; a reduced class or a mix runs 2-6.
329
343
 
330
344
  {{compliance_focus_inject()}}
331
345
 
@@ -353,16 +367,16 @@ Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE — feature-specifi
353
367
  IMPORTANT: Write report to {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}/{focus}.md using Write tool"
354
368
  ```
355
369
 
356
- In multi-worktree mode, process worktrees **sequentially** (one worktree at a time). Complete Phases 1-4 for each worktree before starting the next. This prevents agent overload — spawning 8-20 Review agents per worktree across multiple worktrees simultaneously overwhelms the system.
370
+ In multi-worktree mode, process worktrees **sequentially**: complete Phases 1-4 for one worktree before starting the next, so Review spawns never pile up.
357
371
 
358
372
  ### Phase 3: Synthesis → Review Comment (Sequential)
359
373
 
360
374
  **Produces:** REVIEW_SUMMARY, PR_COMMENT
361
- **Requires:** REVIEW_FOCUS_OUTPUTS, REVIEW_DIR, PR_INFO
375
+ **Requires:** REVIEW_FOCUS_OUTPUTS, REVIEW_DIR, PR_INFO, GATE_LEVEL, REVIEW_FOCUS_LIST
362
376
 
363
377
  **WAIT** for Phase 2. For each worktree, run steps 3a and 3b in sequence:
364
378
 
365
- **Step 3a — Synthesize agent** per worktree (wait for completion before 3b):
379
+ **Step 3a — Synthesize agent** per worktree:
366
380
  ```
367
381
  Agent(subagent_type="Synthesize", run_in_background=false):
368
382
  "Mode: review
@@ -370,6 +384,8 @@ WORKTREE_PATH: {worktree_path} (omit if cwd)
370
384
  REVIEW_BASE_DIR: {worktree_path}/.devflow/docs/reviews/{branch-slug}/{timestamp}
371
385
  TIMESTAMP: {timestamp}
372
386
  CYCLE_NUMBER: {cycle_number}
387
+ GATE_LEVEL: {GATE_LEVEL}
388
+ FOCUSES: {REVIEW_FOCUS_LIST}
373
389
  PRIOR_RESOLUTIONS: <prior-resolution-summary>{prior_resolutions}</prior-resolution-summary>
374
390
  Include Convergence Status section in review-summary.md.
375
391
  Aggregate findings, determine merge recommendation.
@@ -402,8 +418,6 @@ Per worktree, after successful completion:
402
418
  - Publication status: FULL (private repo) | FULL (config override) | STUB (public repository) | OFF (publication disabled by config) | STUB (visibility undeterminable) | STUB (evidence policy) (from Git agent)
403
419
  - Artifact paths
404
420
 
405
- In multi-worktree mode, report results per worktree.
406
-
407
421
  ## Architecture
408
422
 
409
423
  ```
@@ -411,29 +425,27 @@ In multi-worktree mode, report results per worktree.
411
425
  │
412
426
  ├─ Phase 0: Worktree Discovery & Pre-flight
413
427
  │ ├─ Step 0a: git worktree list → filter reviewable
414
- │ ├─ Step 0b: Resolve the compliance lens
428
+ │ ├─ Step 0b: Resolve the compliance lens and gate level
415
429
  │ ├─ Step 0c: Git agent (ensure-pr-ready) per worktree [parallel]
416
430
  │ ├─ Step 0d: Incremental detection + timestamp setup per worktree
417
431
  │ ├─ Step 0e-i: Load prior resolution-summary.md
418
- │ ├─ Step 0e-ii: Convergence assessment (warn if FP ratio > 70%)
432
+ │ ├─ Step 0e-ii: Convergence assessment (warn only)
419
433
  │ └─ Step 0f: Resolve REVIEW_PUBLICATION per worktree (publication gate)
420
434
  │
421
435
  ├─ Per worktree (SEQUENTIAL — one worktree at a time):
422
436
  │ │
423
437
  │ ├─ Phase 1: Analyze changed files
424
438
  │ │ ├─ Classify the diff (paths.txt → DIFF_CLASS)
425
- │ │ ├─ Choose the focuses (code class: file types + the language stamp)
439
+ │ │ ├─ Choose the focuses (code: file types + the language stamp; all: the gate level)
426
440
  │ │ └─ Write diff.patch and diff-{focus}.patch, by redirect
427
441
  │ │
428
- │ ├─ Phase 2: Reviews (PARALLEL within worktree)
429
- │ │ ├─ code: Review agents security, architecture, performance, complexity,
430
- │ │ │ consistency, regression, testing, reliability (diff.patch)
431
- │ │ ├─ code: + language (diff-{focus}.patch) and conditional Review agents
442
+ │ ├─ Phase 2: Reviews (PARALLEL within worktree), one per REVIEW_FOCUS_LIST focus
443
+ │ │ ├─ code: core (diff.patch), language (diff-{focus}.patch), conditional
432
444
  │ │ └─ docs-only / tests-only / lockfile-only: the reduced set (diff.patch)
433
445
  │ │
434
446
  │ ├─ Phase 3: Synthesis → Review Comment (SEQUENTIAL within worktree)
435
447
  │ │ ├─ Step 3a: Synthesize agent (mode: review, writes review-summary.md)
436
- │ │ └─ Step 3b: Git agent (post-review-summary, D7 marker-dedup, REVIEW_PUBLICATION)
448
+ │ │ └─ Step 3b: Git agent (post-review-summary)
437
449
  │ │
438
450
  │ └─ Phase 4: Write .last-review-head + display results
439
451
  ```
@@ -442,44 +454,27 @@ In multi-worktree mode, report results per worktree.
442
454
 
443
455
  | Case | Handling |
444
456
  |------|----------|
445
- | No new commits since last review | Skip review, report: "No new commits since last review. Use --full for a full re-review." |
446
- | Rebase invalidates `.last-review-head` SHA | `git cat-file -t` check fails → fallback to full diff |
447
- | Same-minute review collision | `mkdir` fails → retry with seconds appended (`YYYY-MM-DD_HHMMSS`) |
448
- | Worktree in detached HEAD | Filtered out (no branch name → not reviewable) |
449
- | Worktree mid-rebase or mid-merge | Filtered out by status check |
450
- | Two worktrees on same branch | Deduplicate by branch — review once, use first worktree's path |
451
- | Worktree on protected branch | Filtered out (not reviewable) |
452
- | Worktree pre-flight fails | Report failure, continue with other worktrees |
453
- | `--full` in multi-worktree mode | Applies to all worktrees (global modifier) |
454
- | Many worktrees (5+) | Report count and proceed — user manages their worktree count |
457
+ | `--full` in multi-worktree mode | Applies to all worktrees (global modifier). It sets the diff range, never the gate level |
455
458
  | Review comment already posted | Git agent matches its own marker on the `{cycle, timestamp}` pair → skip silently (D7); a re-review in the same cycle (different timestamp) posts its own comment. The marker's format belongs to the operation; this caller passes the pair and never restates the literal |
456
- | First review (no prior resolution) | PRIOR_RESOLUTIONS=(none), no convergence check |
457
- | fp_ratio denominator = 0 | fp_ratio = 0, no warning |
458
- | `--full` flag | Bypass incremental detection (Step 0d), still load PRIOR_RESOLUTIONS for cross-cycle awareness |
459
- | Parsing failure on resolution-summary.md | fp_ratio = 0, convergence tracking degraded (see Step 0e-ii) |
460
- | Concurrent sessions | Advisory only, each session computes independently |
461
- | Docs-only diff | `docs-only`: documentation, consistency, security on `diff.patch`; no language or conditional focus |
462
- | Tests-only diff | `tests-only`: testing, reliability, consistency, security on `diff.patch`; no language or conditional focus |
463
- | Lockfile-only diff | `lockfile-only`: dependencies, security on `diff.patch` |
464
- | Mixed non-code diff (docs with tests, tests with a lockfile) | The union of the matching sets, on `diff.patch` |
465
- | A path matching no class pattern, or an empty path list | `code` (fail-safe): the 8 core focuses on the full diff |
466
- | A file moved across classes | `--no-renames` lists the old and the new path, so a code path makes the diff `code` |
467
- | Path listing above about 40 lines | The count is printed; `paths.txt` is read in ranges of about 40 lines |
459
+ | Docs-only diff | Its Step 1.1 set, narrowed by level |
460
+ | Tests-only diff | Its Step 1.1 set, narrowed by level |
461
+ | Lockfile-only diff | Its Step 1.1 set, narrowed by level |
462
+ | Mixed non-code diff | The union of the matching sets |
463
+ | A path matching no class pattern, or an empty path list | `code` (fail-safe) |
464
+ | A file moved across classes | `--no-renames` lists both paths, so a code path makes the diff `code` |
465
+ | Path listing above about 40 lines | Read in ranges (Step 1.1) |
468
466
  | Language files changed, plugin not installed | The focus is absent from the stamped line: not spawned |
469
467
  | Public repo (`auto` mode) | Git agent probes visibility, posts counts-only STUB comment; full report stays local |
470
468
  | `reviewPublication: off` | Git agent skips posting the review comment (OFF) — except that, only when `EVIDENCE_POLICY` is `required`, Step 0f passes `stub` and a counts-only STUB comment posts |
471
469
 
472
470
  ## Backwards Compatibility
473
471
 
474
- - **Single worktree**: Auto-discovery finds only one worktree → proceeds exactly as before. Zero behavior change.
475
- - **Legacy flat layout**: If `{worktree}/.devflow/docs/reviews/{branch-slug}/` contains flat `*.md` files (no timestamped subdirectories), new runs create timestamped subdirectories. Old flat files remain untouched.
472
+ - **Gate level**: `full` runs the sets named here; the default `standard` and `lean` narrow them. `devflow gates --set full` keeps them.
476
473
 
477
474
  ## Principles
478
475
 
479
476
  1. **Orchestration only** - Command spawns agents, doesn't do git/review work itself. The exceptions are the read-only Phase 0 probes (`git worktree list`, `git -C ... branch --show-current`, `git -C ... cat-file -t`), the bounded Phase 1 path listing (a redirect into `paths.txt`, a `wc -l` count and ranged reads) and the redirect-only patch writes (`diff.patch`, `diff-{focus}.patch`). Redirect-only writes print nothing, so no diff content reaches this thread
480
477
  2. **Parallel, not background** - Multiple agents in one message, but `run_in_background=false` so phases complete before proceeding
481
478
  3. **Git agent for git work** - All git operations go through Git agent, except the exceptions named in Principle 1; redirect-only writes print nothing
482
- 4. **Clear ownership** - Each agent owns its output completely
483
- 5. **Honest reporting** - Display agent outputs directly
484
- 6. **Incremental by default** - Only review new changes unless `--full` specified
485
- 7. **Auto-discover worktrees** - One command handles all reviewable branches
479
+ 4. **Honest reporting** - Display agent outputs directly
480
+ 5. **Incremental by default** - Only review new changes unless `--full` specified
@@ -202,7 +202,7 @@ Produce the final report:
202
202
 
203
203
  Ask user via AskUserQuestion: "Want me to implement this fix?"
204
204
 
205
- - **YES** → implement the fix through the Code agent, never in this thread, then spawn `Agent(subagent_type="Simplify")` on the changed files. /debug loads no feature knowledge, so the fix gets none:
205
+ - **YES** → implement the fix through the Code agent, never in this thread, then spawn `Agent(subagent_type="Polish")` on the changed files. /debug loads no feature knowledge, so the fix gets none:
206
206
 
207
207
  ```
208
208
  Agent(subagent_type="Code"):
@@ -249,7 +249,7 @@ Ask user via AskUserQuestion: "Want me to implement this fix?"
249
249
  ├─ Phase 6: Root cause report with confidence level
250
250
  │
251
251
  ├─ Phase 7: Offer Fix
252
- │ └─ AskUserQuestion → Code agent (fix) + Simplify agent, or done
252
+ │ └─ AskUserQuestion → Code agent (fix) + Polish agent, or done
253
253
  │
254
254
  └─ Feature Knowledge Write-Back (Conditional)
255
255
  └─ Knowledge agent (if investigation surfaced durable cross-cutting knowledge)
@@ -67,7 +67,7 @@ Author the resolved `ISSUE_REQUIRED` and `APPLY_CONVENTIONS` into the workflow s
67
67
 
68
68
  {{compliance.compliance_lens()}}
69
69
 
70
- Pass it as `complianceFrameworks` when invoking the workflow; the engine builds `COMPLIANCE_DIRECTIVE` from it and hands that to every Code and Code-fix agent.
70
+ Pass it as `complianceFrameworks` when invoking the workflow; the engine builds `COMPLIANCE_DIRECTIVE` from it and hands that to every Code and Fix agent.
71
71
 
72
72
  **0c. Resolve the docs root**
73
73
 
@@ -165,7 +165,7 @@ const ISSUE_INPUT = args.issueInput || args.issueNumber || "(none)"; // this ti
165
165
  const ISSUE_REQUIRED = String(args.issueRequired) === "false" ? "false" : "true"; // Pre-authoring step 0; only an explicit false turns it off — absent or unrecognised fails closed, like the resolver
166
166
  const APPLY_CONVENTIONS = String(args.applyConventions) === "false" ? "false" : "true";
167
167
  const COMPLIANCE_FRAMEWORKS = /^(?:off|none|[a-z][a-z0-9-]{0,15}(?:,[a-z][a-z0-9-]{0,15}){0,7})$/.test(String(args.complianceFrameworks)) ? String(args.complianceFrameworks) : "off"; // Pre-authoring step 0b; any other shape is no lens
168
- const COMPLIANCE_DIRECTIVE = COMPLIANCE_FRAMEWORKS === "off" ? "" : `When the task touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention), invoke Skill(skill="devflow:compliance") and load references/{id}.md only for the ids in ${COMPLIANCE_FRAMEWORKS} (none: generic controls only); never fabricate guidance for a framework you were not given.`; // D-COMPLIANCE-INJECT: empty when the lens is off, so a Code or Code-fix spawn then carries nothing about compliance; otherwise the one line _compliance.mds renders as compliance_directive(), with the validated ids in place of <ids>
168
+ const COMPLIANCE_DIRECTIVE = COMPLIANCE_FRAMEWORKS === "off" ? "" : `When the task touches regulated surface (data models, auth flows, logging/observability, payments, IaC, retention), invoke Skill(skill="devflow:compliance") and load references/{id}.md only for the ids in ${COMPLIANCE_FRAMEWORKS} (none: generic controls only); never fabricate guidance for a framework you were not given.`; // D-COMPLIANCE-INJECT: empty when the lens is off, so a Code or Fix spawn then carries nothing about compliance; otherwise the one line _compliance.mds renders as compliance_directive(), with the validated ids in place of <ids>
169
169
 
170
170
  // Phase 1: Git setup — declare the operation; the agent owns the process, the branch name included
171
171
  const setup = await phase("setup", () =>
@@ -220,16 +220,16 @@ After implementing, commit your changes with a conventional-commit message and r
220
220
  - Any open questions or blockers`, { agentType: "Code" })
221
221
  );
222
222
 
223
- // Phase 3: Gate 1 #1 — post-code pipeline: Simplify agent → Scrutinize agent → Validate agent.
223
+ // Phase 3: Gate 1 #1 — post-code pipeline: Polish agent → Harden agent → Validate agent.
224
224
  // Validate runs last and unconditionally, so its one full run covers every commit the pass made (see gate1_postcode()).
225
225
  const gate1 = await phase("gate1", async () => {
226
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
226
+ await agent(`Polish and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Polish" });
227
227
 
228
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
229
- Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
230
- // 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.
231
- if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
232
- 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" };
228
+ const harden = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
229
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Harden" });
230
+ // Anything but an explicit PASS or FIXED is a stop: a Harden that returned no status, or one this skeleton does not know, has not accepted the code.
231
+ if (!["PASS", "FIXED"].includes(harden?.status)) {
232
+ return { verdict: "ESCALATED", type: "harden-blocked", reason: harden?.status === "BLOCKED" ? "Harden agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Harden agent returned no recognised status: counted as BLOCKED" };
233
233
  }
234
234
 
235
235
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH}.
@@ -248,19 +248,19 @@ ${failureDetails}
248
248
  DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
249
249
  <!-- learning:end -->
250
250
  ${COMPLIANCE_DIRECTIVE}
251
- Commit fixes with conventional-commit message.`, { agentType: "Code-fix" });
251
+ Commit fixes with conventional-commit message.`, { agentType: "Fix" });
252
252
  const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} after fix attempt ${attempt} (follow your Running commands block).
253
253
  Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
254
254
  if (recheck?.verdict === "PASS") break;
255
255
  failureDetails = recheck?.details || failureDetails;
256
- if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Validation exhausted after 2 Code agent fix attempts" };
256
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Validation exhausted after 2 Fix agent attempts" };
257
257
  }
258
258
  }
259
259
 
260
260
  return { verdict: "PASS" };
261
261
  });
262
262
 
263
- // Gate 1 #1 stop, before Gate 2: an exhausted Validate or a BLOCKED Scrutinize leaves a broken or unfinished build,
263
+ // Gate 1 #1 stop, before Gate 2: an exhausted Validate or a BLOCKED Harden leaves a broken or unfinished build,
264
264
  // so Gate 2 and the review pass never run on it — a correctness stop, like the ticket-link and branch stops.
265
265
  if (gate1.verdict === "ESCALATED") {
266
266
  return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK, escalations: [{ type: gate1.type, description: gate1.reason }] };
@@ -291,7 +291,7 @@ ${evaluation?.rationale || "see the Evaluate agent's report"}
291
291
  DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
292
292
  <!-- learning:end -->
293
293
  ${COMPLIANCE_DIRECTIVE}
294
- Self-verify your fix compiles, following your Running commands block. Commit fixes.`, { agentType: "Code-fix" });
294
+ Self-verify your fix compiles, following your Running commands block. Commit fixes.`, { agentType: "Fix" });
295
295
  evalVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
296
296
  }
297
297
  }
@@ -313,7 +313,7 @@ ${testResult.failures}
313
313
  DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
314
314
  <!-- learning:end -->
315
315
  ${COMPLIANCE_DIRECTIVE}
316
- Self-verify your fix compiles and the scenarios pass, following your Running commands block. Commit fixes.`, { agentType: "Code-fix" });
316
+ Self-verify your fix compiles and the scenarios pass, following your Running commands block. Commit fixes.`, { agentType: "Fix" });
317
317
  testVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
318
318
  }
319
319
  }
@@ -468,16 +468,16 @@ Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<
468
468
  return { survivingFindings: notAddressed, fixedFindings, coverageGaps };
469
469
  });
470
470
 
471
- // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (Simplify agent → Scrutinize agent → full Validate agent, Validate last and unconditional).
471
+ // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (Polish agent → Harden agent → full Validate agent, Validate last and unconditional).
472
472
  // Runs ONCE, after the review pass. Inside the pass the Code agent self-verified its own fixes;
473
473
  // this is the build gate before the branch is handed back. See gate1_postcode() cadence.
474
474
  const gate1Final = await phase("gate1-final", async () => {
475
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
475
+ await agent(`Polish and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Polish" });
476
476
 
477
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
478
- Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
479
- if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
480
- 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" };
477
+ const harden = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
478
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Harden" });
479
+ if (!["PASS", "FIXED"].includes(harden?.status)) {
480
+ return { verdict: "ESCALATED", type: "harden-blocked", reason: harden?.status === "BLOCKED" ? "Final Gate 1 Harden agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Final Gate 1 Harden agent returned no recognised status: counted as BLOCKED" };
481
481
  }
482
482
 
483
483
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH} (final gate after all fixing).
@@ -494,12 +494,12 @@ ${failureDetails}
494
494
  DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
495
495
  <!-- learning:end -->
496
496
  ${COMPLIANCE_DIRECTIVE}
497
- Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code-fix" });
497
+ Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Fix" });
498
498
  const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (final gate, after fix attempt ${attempt}; follow your Running commands block).
499
499
  Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
500
500
  if (recheck?.verdict === "PASS") break;
501
501
  failureDetails = recheck?.details || failureDetails;
502
- if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
502
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Final Gate 1 validation exhausted after 2 Fix agent attempts" };
503
503
  }
504
504
  }
505
505
 
@@ -575,22 +575,49 @@ When WAVE mode is detected, author a workflow that wraps the single-ticket engin
575
575
 
576
576
  **Wave workflow structure (author after the SINGLE engine blocks above):**
577
577
 
578
- 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.
578
+ 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).
579
579
 
580
580
  Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
581
581
 
582
582
  ```js
583
- // Wave round loop: Design agent reader → per-ticket try/catch → cascade quarantine via next reader
584
583
  // runSingleTicketEngine(args) is the SINGLE skeleton above as a function of its OWN args: the wave's
585
584
  // args.issueNumber (the tracking issue, Pre-authoring step 5) is never in its scope.
586
585
  const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
587
- const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
586
+ const results = {}; // ticketId → its row of the workflow's return
588
587
  // A row from an engine result: the fields step 3 after the workflow renders
589
588
  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 });
590
589
  const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
591
- const MERGE_SHA = /^[0-9a-f]{40}$/; // a merge commit as git prints it: the only Git-reported value that reaches a later prompt
590
+ const MERGE_SHA = /^[0-9a-f]{40}$/; // a SHA as git prints it; with the checked branch, the only Git-reported values that reach a merge, Validate or undo prompt
591
+ const BRANCH_NAME = /^(?![-#]|(?:@|(?:FETCH_|ORIG_|MERGE_)?HEAD)$)(?!.*\.\.)[A-Za-z0-9._/+@,#-]{1,200}$/; // implement's ref allowlist; no leading - or #, no revision name, no ..: the name enters git lines
592
+ // One ticket's merge, post-merge Validate and undo (D-WAVE-MERGE-VALIDATE: Git validates nothing).
593
+ const mergeValidateUndo = async (ticketId, engineResult) => {
594
+ if (typeof engineResult.branch !== "string" || !BRANCH_NAME.test(engineResult.branch)) return { kept: false, reason: "ticket branch name refused: not a plain git ref name", halted: null, treeEqual: null };
595
+ 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.
596
+ Check out ${INTEGRATION_BRANCH}, run git rev-parse ${INTEGRATION_BRANCH} as baseSha, run git rev-parse --verify --quiet refs/heads/${engineResult.branch} (on a non-zero exit return merged false, reason "no such local branch"), then run git merge --no-ff refs/heads/${engineResult.branch}. If the merge stops after it began (git rev-parse -q --verify MERGE_HEAD succeeds), run git merge --abort; if that abort fails, return merged true with no mergeSha. A merge that did not begin returns merged false with the reason.
597
+ Return: {"merged": true, "mergeSha": "<40-hex merge commit>", "treeEqual": <true when mergeSha^{tree} equals the tree of ${engineResult.branch}'s head>, "baseSha": "<40-hex integration HEAD before the merge>"} — or {"merged": false, "reason": "<why>"} when no merge is kept.`, { agentType: "Git" });
598
+ const treeEqual = merge?.merged === true && typeof merge.treeEqual === "boolean" ? merge.treeEqual : null;
599
+ const done = (kept, reason, halted = null) => ({ kept, reason, halted, treeEqual });
600
+ if (merge?.merged !== true) return done(false, merge?.reason || "merge failed");
601
+ const badSha = ![merge.mergeSha, merge.baseSha].every(v => MERGE_SHA.test(String(v)));
602
+ if (badSha || merge.mergeSha === merge.baseSha) {
603
+ // No usable SHA, or no commit added: stop taking merges. Fixed reasons; no rejected value is copied.
604
+ const reason = badSha ? "merge reported without a 40-hex mergeSha or baseSha; the integration branch may hold an unvalidated merge" : "merge added no commit, so the wave stopped: an unchanged HEAD cannot be checked. Check that the ticket branch holds its commits, then run the wave again.";
605
+ return done(false, reason, { ticket: ticketId, mergeSha: null, baseSha: null, reason, kind: badSha ? "bad-sha" : "unchanged" });
606
+ }
607
+ const post = await agent(`Run build, typecheck, lint, and tests on ${INTEGRATION_BRANCH} after merging ${engineResult.branch} (merge commit ${merge.mergeSha}).
608
+ Follow your Running commands block for every build and test command.
609
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
610
+ if (post?.verdict === "PASS") {
611
+ return done(true, "");
612
+ }
613
+ const undo = await agent(`Undo the merge ${merge.mergeSha} on ${INTEGRATION_BRANCH}, only when no remote branch contains ${merge.mergeSha}. Run these read-only checks in order; a non-zero exit or a different value means not undone: git symbolic-ref --short HEAD must print ${INTEGRATION_BRANCH}; git rev-parse refs/heads/${INTEGRATION_BRANCH} must print ${merge.mergeSha}; git rev-parse ${merge.mergeSha}^1 must print ${merge.baseSha}. Then run exactly this one command and no other that changes a branch or a remote: git reset --keep ${merge.baseSha}
614
+ Return: {"undone": true, "head": "<40-hex from git rev-parse refs/heads/${INTEGRATION_BRANCH}>"} — or {"undone": false, "reason": "<why>"} when you did not undo it.`, { agentType: "Git" });
615
+ if (undo?.undone === true && undo.head === merge.baseSha) return done(false, "post-merge build red; merge undone");
616
+ const reason = `post-merge build red; merge not undone (${undo?.undone === true ? "undo reported a head other than the recorded pre-merge SHA" : undo?.reason || "no reason given"})`;
617
+ return done(false, reason, { ticket: ticketId, mergeSha: merge.mergeSha, baseSha: merge.baseSha, reason, kind: "red" });
618
+ };
592
619
  const waveState = { quarantined: [], round: 0, halted: null };
593
- let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
620
+ let reAskedThisDeadlock = false;
594
621
  while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS && waveState.halted === null) {
595
622
  waveState.round++;
596
623
 
@@ -614,56 +641,28 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
614
641
  reAskedThisDeadlock = true; // re-ask once with vacuous-truth rule quoted verbatim
615
642
  continue;
616
643
  }
617
- reAskedThisDeadlock = false; // reset on a non-empty ready set
644
+ reAskedThisDeadlock = false;
618
645
 
619
646
  for (const ticketId of ready) {
620
647
  try {
621
- // ticketId is the ISSUE_REF the pre-fetch heading printed — this ticket's OWN reference: the engine's `ticket` (its TICKET)
622
- // and its setup-task ISSUE_INPUT; the engine returns the branch that setup-task created, merged below; plans[ticketId] is its own plan, criteria and checked testPlan (Pre-authoring step 4)
648
+ // ticketId is this ticket's OWN ISSUE_REF (pre-fetch heading): the engine's `ticket` and setup-task ISSUE_INPUT; the engine returns the branch setup-task created, merged below; plans[ticketId] is its own plan (Pre-authoring step 4)
623
649
  <!-- learning:on -->
624
650
  const engineResult = await runSingleTicketEngine({ ticket: ticketId, baseBranch: INTEGRATION_BRANCH, ...(plans[ticketId] || {}), decisionsContext: DECISIONS_CONTEXT, issueRequired: ISSUE_REQUIRED, applyConventions: APPLY_CONVENTIONS, complianceFrameworks: COMPLIANCE_FRAMEWORKS, issueInput: ticketId });
625
651
  <!-- learning:off -->
626
652
  const engineResult = await runSingleTicketEngine({ ticket: ticketId, baseBranch: INTEGRATION_BRANCH, ...(plans[ticketId] || {}), issueRequired: ISSUE_REQUIRED, applyConventions: APPLY_CONVENTIONS, complianceFrameworks: COMPLIANCE_FRAMEWORKS, issueInput: ticketId });
627
653
  <!-- learning:end -->
628
- // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
629
- // PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
654
+ // PASS and UNVERIFIED merge (verdict, or overallVerdict as the SINGLE alias); PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
630
655
  if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
631
- // Git merges locally and validates nothing: the workflow owns the post-merge Validate (D-WAVE-MERGE-VALIDATE).
632
- 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.
633
- 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" });
634
- let kept = false;
635
- let reason = merge?.reason || "merge failed";
636
- if (merge?.merged === true && !MERGE_SHA.test(String(merge.mergeSha))) {
637
- // A merge may sit on the integration branch with no SHA to validate or undo it by: stop taking merges.
638
- waveState.halted = { ticket: ticketId, mergeSha: null, reason: "merge reported without a 40-hex mergeSha; the integration branch may hold an unvalidated merge" };
639
- reason = waveState.halted.reason;
640
- } else if (merge?.merged === true) {
641
- // A merge counts as kept only when this Validate returns PASS. It always runs: treeEqual is recorded, never a reason to skip.
642
- const post = await agent(`Run build, typecheck, lint, and tests on ${INTEGRATION_BRANCH} after merging ${engineResult.branch} (merge commit ${merge.mergeSha}).
643
- Follow your Running commands block for every build and test command.
644
- Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
645
- if (post?.verdict === "PASS") {
646
- kept = true;
647
- } else {
648
- 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
649
- Return: {"undone": true} — or {"undone": false, "reason": "<why>"} when you did not undo it.`, { agentType: "Git" });
650
- if (undo?.undone === true) {
651
- reason = "post-merge build red; merge undone";
652
- } else {
653
- // A merge that stays red would poison every later ticket: stop taking merges and rounds, and name the red HEAD.
654
- reason = `post-merge build red; merge not undone (${undo?.reason || "no reason given"})`;
655
- waveState.halted = { ticket: ticketId, mergeSha: merge.mergeSha, reason };
656
- }
657
- }
658
- }
659
- results[ticketId] = rowOf(ticketId, engineResult, kept, merge?.merged === true && typeof merge.treeEqual === "boolean" ? merge.treeEqual : null);
656
+ const { kept, reason, halted, treeEqual } = await mergeValidateUndo(ticketId, engineResult);
657
+ if (halted !== null) waveState.halted = halted;
658
+ results[ticketId] = rowOf(ticketId, engineResult, kept, treeEqual);
660
659
  if (!kept) waveState.quarantined.push({ ticket: ticketId, reason });
661
660
  } else {
662
661
  results[ticketId] = rowOf(ticketId, engineResult, false);
663
662
  waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
664
663
  }
665
664
  } catch (err) {
666
- // One ticket's crash/stall never kills the wave — quarantine it; cascade propagates to dependents via the next reader round
665
+ // Quarantine the ticket: the cascade reaches its dependents via the next reader round
667
666
  results[ticketId] = rowOf(ticketId, null, false);
668
667
  waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
669
668
  }
@@ -697,7 +696,7 @@ A workflow cannot pause mid-run. After the build/wave workflow returns, you (the
697
696
  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.
698
697
  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.
699
698
  - **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.
700
- - **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.
699
+ - **Halted** (a non-null `halted`, whose `kind` is `red`, `bad-sha` or `unchanged`): record `Wave PR: skipped (<text>, wave halted at <halted.ticket>)` and go to step 4 with no wave PR. `<text>` is fixed by `kind`, and no other `halted` field is shown. `red` (a post-merge build it could not undo): `integration HEAD red at <halted.mergeSha>`. `bad-sha` (a merge without a usable SHA): `integration branch may hold an unvalidated merge`. `unchanged` (a merge that added no commit): `merge added no commit`, with the remedy to check that the ticket branch holds its commits, then run the wave again.
701
700
  - **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
702
701
  - Otherwise compose (a) and then (b).
703
702
 
@@ -795,22 +794,16 @@ git -C "{integration worktree root}" push origin HEAD; echo "exit=$?"
795
794
 
796
795
  {{pub.publication_gate()}}
797
796
 
798
- Then spawn:
797
+ Then run the script inline — it is the whole `update-pr-evidence` operation, with no Git agent spawn — through the Bash tool with `timeout: 180000`:
799
798
 
800
- ```
801
- Agent(subagent_type="Git"):
802
- "OPERATION: update-pr-evidence
803
- PR_NUMBER: {n}
804
- EVIDENCE_FILE: .devflow/docs/evidence-wave-{slug}.md
805
- REVIEW_PUBLICATION: {REVIEW_PUBLICATION resolved above, or auto}
806
- WORKTREE_PATH: {integration worktree root}
807
- Update the wave PR's test-plan block and post its evidence comment."
799
+ ```bash
800
+ cd "{integration worktree root}" && node "$HOME/.devflow/scripts/verify-evidence.cjs" publish --pr {n} --evidence ".devflow/docs/evidence-wave-{slug}.md" --publication {REVIEW_PUBLICATION resolved above, or auto}; echo "exit=$?"
808
801
  ```
809
802
 
810
- `update-pr-evidence` decides what each publication value means for the evidence comment. Report its `## PR Evidence` block — its `EVIDENCE` line and its `**Body**:` / `**Comment**:` line — or its `TRACEABILITY: DEGRADED ({reason})` line; a spawn that returns neither ⇒ `TRACEABILITY: DEGRADED (evidence refresh failed)`. Whatever it returns, the run ends here.
803
+ `{n}` is the number admitted above, and the publication value is `auto`, `full`, `off` or `stub`. `publish` decides what each publication value means for the evidence comment. Report its `## PR Evidence` block — its `EVIDENCE` line and its `**Body**:` / `**Comment**:` line — with any `TRACEABILITY: DEGRADED ({reason})` lines after it, or its `TRACEABILITY: DEGRADED (evidence unavailable)` line; empty stdout or any other exit ⇒ `TRACEABILITY: DEGRADED (evidence refresh failed)`. Whatever it returns, the run ends here.
811
804
 
812
805
  ---
813
806
 
814
807
  ### Maintenance note
815
808
 
816
- This recipe encodes the current `/implement` + `/code-review` + `/resolve` orchestration shape as of the authoring date (2026-06-12). When those base commands change their orchestration, update this recipe to match. No tooling detects drift — by design (the LLM-vs-plumbing Iron Rule). The reminder lives in the design doc §16.
809
+ This recipe mirrors the `/implement` + `/code-review` + `/resolve` orchestration as of 2026-06-12. When those commands change, update it. No tooling detects drift (the LLM-vs-plumbing Iron Rule).