devflow-kit 2.4.0 → 3.0.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 (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -4,13 +4,16 @@ argument-hint: "[ticket | issue-url | plan-doc]"
4
4
  output-dir: dist/commands
5
5
  ---
6
6
  @import { authoring_preamble } from "./_partials/_preamble.mds"
7
- @import { compliance_gate } from "./_partials/_compliance.mds"
7
+ @import { evidence_policy } from "./_partials/_evidence_policy.mds"
8
+ @import { docs_root } from "./_partials/_docs_root.mds"
8
9
  @import { agent_roster, agent_caveats } from "./_partials/_roster.mds"
9
10
  @import { gate1_postcode, gate2_acceptance, evaluator_panel, implement_bundle, review_pass, concurrency_doctrine, build_execution_doctrine, engine_output_schema, engine_invariants } from "./_partials/_engine.mds"
10
11
  @import { wave_loop, branch_merge_model, merge_doctrine, escalation_model } from "./_partials/_wave.mds"
11
12
  @import { acceptance_criteria_contract } from "./_partials/_plan_contract.mds"
12
-
13
- {authoring_preamble()}
13
+ @import { issue_ref_grammar, issue_capture_contract } from "./_partials/_tracker.mds"
14
+ @import "./_partials/_publication.mds" as pub
15
+ @import "./_partials/_compliance.mds" as compliance
16
+ {{authoring_preamble()}}
14
17
 
15
18
  ---
16
19
 
@@ -18,14 +21,14 @@ output-dir: dist/commands
18
21
 
19
22
  This command instructs you to construct and run a Claude Code dynamic Workflow that implements, reviews, and verifies one ticket or a full wave of tickets, reusing devflow's existing agents via `agentType`.
20
23
 
21
- {agent_roster()}
24
+ {{agent_roster()}}
22
25
 
23
- {agent_caveats()}
26
+ {{agent_caveats()}}
24
27
 
25
28
  ---
26
29
 
27
30
  **Requires:** ticket or task description; optional plan document and acceptance criteria from `/devflow:dynamic-plan`
28
- **Produces:** implemented and reviewed branch per ticket; wave run report at `.devflow/docs/waves/\{slug\}/\{ts\}/wave-report.md`
31
+ **Produces:** implemented and reviewed branch per ticket; wave run report at `{integration worktree root}/.devflow/docs/waves/{slug}/{ts}/wave-report.md`
29
32
 
30
33
  ---
31
34
 
@@ -35,8 +38,8 @@ Before authoring, verify:
35
38
 
36
39
  1. **Workflow tool available:** if the `Workflow` tool is not in your available tools, STOP and tell the user: "The Workflow tool is not available in this session. dynamic-build requires Claude Code's dynamic workflow runtime."
37
40
  2. **`agentType` support:** confirmed available (spike F5, 2026-06-11). If spawned agents return no results, check that devflow is installed (`devflow init` has been run).
38
- 3. **GitHub paths:** if the input is a GitHub issue URL, `gh` CLI must be authenticated. If not authenticated, note it and fall back to the issue text the user provided.
39
- 4. **No-remote path:** if the repo has no remote, skip GitHub-dependent steps (issue reading, PR creation) and proceed with local branch operations only.
41
+ 3. **Tracker paths:** an issue reference or URL in the input is read through the Git agent, which resolves the configured tracker and its access itself and reports `TRACEABILITY: DEGRADED ({reason})` when it cannot read one — then note it and fall back to the issue text the user provided. No tracker CLI is checked here.
42
+ 4. **No-remote path:** if the repo has no remote, skip the remote-dependent steps (the wave PR and its evidence) and proceed with local branch operations only; the Git agent reports DEGRADED for any tracker step it cannot reach.
40
43
 
41
44
  ---
42
45
 
@@ -44,12 +47,27 @@ Before authoring, verify:
44
47
 
45
48
  Before you write the workflow script:
46
49
 
47
- **0. Resolve compliance context**
50
+ **0. Resolve the evidence policy**
51
+
52
+ **Produces:** EVIDENCE_POLICY, ISSUE_REQUIRED, APPLY_CONVENTIONS, REQUIRE_NON_AUTHOR_APPROVAL
53
+
54
+ {{evidence_policy()}}
55
+
56
+ Author the resolved `ISSUE_REQUIRED` and `APPLY_CONVENTIONS` into the workflow script as constants — pass them as `issueRequired` and `applyConventions` when invoking the workflow — and pass both to the Git `setup-task` spawn.
57
+
58
+ **0b. Resolve the compliance lens**
59
+
60
+ **Produces:** COMPLIANCE_FRAMEWORKS
61
+
62
+ {{compliance.compliance_lens()}}
63
+
64
+ Pass it as `complianceFrameworks` when invoking the workflow; the engine hands it to every Code agent.
48
65
 
49
- {compliance_gate()}
50
- Reuse this result for every ticket in the wave.
66
+ **0c. Resolve the docs root**
51
67
 
52
- When authoring the workflow script, include the resolved value as a constant: `COMPLIANCE = COMPLIANCE_SKILL_INSTALLED ? "enabled" : "(none)"`. Pass `COMPLIANCE: \{COMPLIANCE\}` to Git agent spawns (setup-task and branch-creation). When compliance-gated (`COMPLIANCE_SKILL_INSTALLED === true`), branch names follow the `.devflow/conventions.md` Branch Naming convention if that file exists; degrade gracefully if absent.
68
+ {{docs_root()}}
69
+
70
+ `{integration worktree root}` is the toplevel of the checkout the integration branch is checked out in: `{worktree}` unless the wave runs in a linked worktree.
53
71
 
54
72
  **1. Apply decisions context**
55
73
 
@@ -62,7 +80,8 @@ Note the `budget` value from the Workflow tool context (or default to "medium" i
62
80
  **3. Detect mode: SINGLE or WAVE**
63
81
 
64
82
  - **SINGLE mode:** input is one ticket, one issue, one task description, or one plan document
65
- - **WAVE mode:** input is a set of GitHub issues (wave labels, milestone, issue list), or the user says "wave" / "all tickets in wave N"
83
+ - **WAVE mode:** input is a set of tracker issues (wave labels, milestone, issue list), or the user says "wave" / "all tickets in wave N"
84
+ - **A `/devflow:dynamic-tickets` ticket directory** (`{worktree}/.devflow/docs/tickets/{slug}/{ts}/`) is WAVE input: in each ticket file (every `.md` there but `tracking-issue.md`), the `**Issue:**` line directly after `**Depends on:**` is one raw `ISSUE_REFS` token, forwarded to the wave's pre-fetch verbatim — never rendered, normalised or re-derived. A ticket file with no `**Issue:**` line, or more than one, contributes no token; name it in the run summary as `not filed`.
66
85
 
67
86
  When ambiguous, ask the user before authoring: "Is this a single ticket or a wave of tickets?"
68
87
 
@@ -70,24 +89,41 @@ When ambiguous, ask the user before authoring: "Is this a single ticket or a wav
70
89
 
71
90
  Check for (in priority order):
72
91
  - A plan document passed as input (path or inline)
73
- - A GitHub issue body (fetch via `gh issue view <number>`)
92
+ - An issue body (fetched via the Git agent using `OPERATION: fetch-issue`)
74
93
  - The current working context (recent `/devflow:dynamic-plan` output)
75
94
  - An in-context task description
76
95
 
96
+ {{issue_capture_contract()}}
97
+
77
98
  Extract or note:
78
99
  - Implementation plan (for Code agent prompt and Evaluate agent)
79
- - Acceptance criteria and test plan (for Gate 2)
100
+ - Acceptance criteria (for Gate 2) — pass them as `criteria` when invoking the workflow
101
+ - The test plan (for Gate 2) — a plan's TP lines, passed only once they pass this check. Copy the plan's `## Test Plan` section byte for byte into a fresh `mktemp` file with the Write tool, never through an interpolated shell string, and run:
102
+
103
+ ```bash
104
+ node "$HOME/.devflow/scripts/verify-evidence.cjs" check tp <that file>; echo "exit=$?"
105
+ ```
106
+
107
+ Only `exit=0` passes it: pass the section's TP lines, as one string, as `testPlan` when invoking the workflow — a test plan alone still runs the Test agent. Any other result, or a plan with no `## Test Plan` section: omit `testPlan` and note `Test plan: missing or malformed` in the run summary. Nothing is repaired, and a test plan in any other shape — an older JSON one included — is never passed.
108
+
109
+ In WAVE mode, run this step once per ticket and author the results as `plans`, keyed by the ticket's reference: each ticket gets its own `plan`, `criteria` and checked `testPlan` — never one test plan for the whole wave. Keep each ticket's checked TP lines: step 3 after the workflow builds the wave test plan from them.
80
110
 
81
111
  If none found: build proceeds Gate-1-only (Gate 2 skipped with a note). Never refuse to build; never fabricate criteria.
82
112
 
83
113
  **5. Resolve tracking-issue number (optional)**
84
114
 
85
115
  Check, in priority order:
86
- - An explicit issue number or GitHub issue URL in the user's input (e.g., `#42`, `42`, or `https://github.com/…/issues/42`)
87
- - The tracking-issue reference in the ticket set's `tracking-issue.md` (written by `/devflow:dynamic-tickets` at `.devflow/docs/tickets/\{slug\}/\{ts\}/tracking-issue.md`)
116
+ - An explicit candidate issue reference or issue URL in the user's input (e.g. `#42`, `42`, or `https://github.com/…/issues/42`)
117
+ - The `**Issue:**` line directly after the H1 of the ticket set's `tracking-issue.md` (written by `/devflow:dynamic-tickets`' filing step; the file is at `{worktree}/.devflow/docs/tickets/{slug}/{ts}/tracking-issue.md`), as a raw token — only when the file holds exactly one `**Issue:**` line
88
118
  - Otherwise: none
89
119
 
90
- If a number is found, record it as the command-level `ISSUE_NUMBER` and pass it as `issueNumber: <number>` when invoking the workflow (the Code agent threads it through as `ISSUE_NUMBER`). If none is found, pass nothing — `ISSUE_NUMBER` defaults to `"(none)"` in the workflow script.
120
+ {{issue_ref_grammar()}}
121
+
122
+ If a number is found, record it as the command-level `ISSUE_NUMBER`:
123
+ - **SINGLE mode:** it is the ticket's own reference. Pass it as `issueNumber: <number>` when invoking the workflow; the engine hands it to setup-task as `ISSUE_INPUT`. If none is found, pass nothing.
124
+ - **WAVE mode:** it is the tracking issue. Only steps 2 and 3 after the workflow use it: step 2 for the wave report, step 3 for the wave block's tracking line. It never reaches a ticket: each ticket's engine gets that ticket's own reference as `issueInput`.
125
+
126
+ Never pass `ISSUE_PR_LINK` into the workflow. The engine binds each ticket's `ISSUE_NUMBER` and `ISSUE_PR_LINK` from that ticket's own setup-task Output (`### Handoff Values`), never from the token it passed in, and hands both to that ticket's Code agents. No `- **PR link line**:` captured ⇒ `ISSUE_PR_LINK` is `"(none)"` and the Code agent emits the `## Related Issues` heading with no reference.
91
127
 
92
128
  ---
93
129
 
@@ -105,22 +141,44 @@ export const meta = {
105
141
  // SINGLE mode: one ticket, one branch, full engine
106
142
 
107
143
  const TICKET = args.ticket || args[0] || "see task description";
108
- const BRANCH = args.branch || `ticket/${TICKET.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
109
144
  const PLAN = args.plan || null;
110
145
  const CRITERIA = args.criteria || null;
146
+ const TEST_PLAN = args.testPlan || null; // the plan's checked TP lines, one string (Pre-authoring step 4)
111
147
  const DECISIONS_CONTEXT = args.decisionsContext || ""; // injected before authoring
112
- const ISSUE_NUMBER = args.issueNumber || "(none)"; // resolved in Pre-authoring step 5
113
- const COMPLIANCE = args.compliance || "(none)"; // "enabled" when COMPLIANCE_SKILL_INSTALLED, else "(none)"
148
+ const ISSUE_INPUT = args.issueInput || args.issueNumber || "(none)"; // this ticket's OWN raw reference (Pre-authoring step 5; a wave passes issueInput) — setup-task's input, never a Code agent's
149
+ 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
150
+ const APPLY_CONVENTIONS = String(args.applyConventions) === "false" ? "false" : "true";
151
+ 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
114
152
 
115
- // Phase 1: Git setup — declare the operation; the agent owns the process
116
- await phase("setup", () =>
153
+ // Phase 1: Git setup — declare the operation; the agent owns the process, the branch name included
154
+ const setup = await phase("setup", () =>
117
155
  agent(`OPERATION: setup-task
118
156
  BASE_BRANCH: ${args.baseBranch || "HEAD"}
119
157
  TASK_DESCRIPTION: ${TICKET}
120
- COMPLIANCE: ${COMPLIANCE}
121
- ${ISSUE_NUMBER && ISSUE_NUMBER !== "(none)" ? "ISSUE_INPUT: " + ISSUE_NUMBER : ""}`.trim(), { agentType: "Git" })
158
+ ISSUE_REQUIRED: ${ISSUE_REQUIRED}
159
+ APPLY_CONVENTIONS: ${APPLY_CONVENTIONS}
160
+ ${ISSUE_INPUT !== "(none)" ? "ISSUE_INPUT: " + ISSUE_INPUT : ""}
161
+ Return: {"branch": "<the - **Branch name**: value under your ### Branch, or (none)>", "issueId": "<the - **Issue ID**: value under your ### Handoff Values, or (none)>", "prLinkLine": "<the - **PR link line**: value, or (none)>"}`, { agentType: "Git" })
122
162
  );
123
163
 
164
+ // The ticket's own Handoff Values, read from setup-task's Output — never derived from ISSUE_INPUT.
165
+ // An Issue ID is a bare number or a KEY-number, per the provider; any other value — "none", blank, prose — is no capture, so the stop fails closed.
166
+ const ISSUE_ID_SHAPE = /^(?:[1-9][0-9]{0,8}|[A-Z][A-Z0-9_]{0,9}-[1-9][0-9]{0,8})$/;
167
+ const ISSUE_NUMBER = ISSUE_ID_SHAPE.test(String(setup?.issueId ?? "")) ? setup.issueId : "(none)"; // the captured Issue ID; "(none)" ⇒ none was captured
168
+ const ISSUE_PR_LINK = setup?.prLinkLine || "(none)"; // "(none)" ⇒ Code emits the heading with no reference
169
+ // The branch setup-task created, as it reported it: every later phase and the merge use it. The engine never names one.
170
+ const BRANCH = typeof setup?.branch === "string" && setup.branch.trim() !== "" && setup.branch.trim() !== "(none)" ? setup.branch : "(none)";
171
+
172
+ // Ticket-link stop, before implement. The workflow cannot ask, so it never records an exception: it stops.
173
+ if (ISSUE_REQUIRED === "true" && ISSUE_NUMBER === "(none)") {
174
+ return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: "(none)", issuePrLink: ISSUE_PR_LINK, escalations: [{ type: "ticket-link-missing", description: "setup-task captured no Issue ID while issues are required — link or create this ticket's issue, then re-run" }] };
175
+ }
176
+
177
+ // Branch stop, before implement: with no branch there is nothing to build on or merge — a correctness stop, not a shape gate.
178
+ if (BRANCH === "(none)") {
179
+ return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK, escalations: [{ type: "branch-missing", description: "setup-task reported no branch name — check its Output, then re-run" }] };
180
+ }
181
+
124
182
  // Phase 2: Implement
125
183
  await phase("implement", () =>
126
184
  agent(`Implement the following ticket on branch ${BRANCH}:
@@ -133,6 +191,8 @@ Relevant architectural decisions (apply devflow:apply-decisions algorithm):
133
191
  ${DECISIONS_CONTEXT}
134
192
 
135
193
  ISSUE_NUMBER: ${ISSUE_NUMBER}
194
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
195
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
136
196
 
137
197
  When you build or run tests to verify your work, use your "Long-running commands" discipline (background-Bash + Monitor poll) for anything that may run silent >120s, and prefer package-scoped commands.
138
198
 
@@ -155,6 +215,8 @@ Report: PASS or FAIL with details.`, { agentType: "Validate" });
155
215
  await agent(`Fix the validation failures on branch ${BRANCH}:
156
216
  ${validation.details}
157
217
  ISSUE_NUMBER: ${ISSUE_NUMBER}
218
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
219
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
158
220
  Commit fixes with conventional-commit message.`, { agentType: "Code" });
159
221
  const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH}. Report: PASS or FAIL.`, { agentType: "Validate" });
160
222
  if (recheck.verdict === "PASS") break;
@@ -175,8 +237,8 @@ Commit fixes with conventional-commit message.`, { agentType: "Code" });
175
237
 
176
238
  // Phase 4: Gate 2 — acceptance gate (once, before review pass)
177
239
  const gate2 = await phase("gate2", async () => {
178
- if (!PLAN && !CRITERIA) {
179
- return { evaluateVerdict: "SKIPPED", testVerdict: "SKIPPED", skipReasons: ["No plan and no criteria provided"] };
240
+ if (!PLAN && !CRITERIA && !TEST_PLAN) {
241
+ return { evaluateVerdict: "SKIPPED", testVerdict: "SKIPPED", skipReasons: ["No plan, no criteria and no test plan provided"] };
180
242
  }
181
243
 
182
244
  let evalVerdict = "SKIPPED";
@@ -197,23 +259,28 @@ Report: PASS or FAIL with rationale.`, { agentType: "Evaluate" }),
197
259
  await agent(`Fix the alignment issues identified by the Evaluate agent panel on branch ${BRANCH}:
198
260
  ${panel.filter(p => p.verdict === "FAIL").map(p => p.rationale).join("\n")}
199
261
  ISSUE_NUMBER: ${ISSUE_NUMBER}
262
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
263
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
200
264
  Self-verify your fix compiles (background-Bash + Monitor for any build >120s — see your "Long-running commands" discipline). Commit fixes.`, { agentType: "Code" });
201
265
  evalVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
202
266
  }
203
267
  }
204
268
 
205
269
  let testVerdict = "SKIPPED";
206
- if (CRITERIA) {
270
+ if (CRITERIA || TEST_PLAN) {
207
271
  const testResult = await agent(`Run scenario-based acceptance tests on branch ${BRANCH} against these criteria:
208
- ${CRITERIA}
272
+ ${CRITERIA || "(none)"}
273
+ TEST_PLAN: ${TEST_PLAN || "(none)"}
209
274
  For any test/build command that may run silent >120s, use the background-Bash + Monitor poll procedure (your "Long-running commands" discipline) so you never trip the 180s watchdog.
210
- Cover: functionality, API contracts, performance. Report: PASS or FAIL per scenario.`, { agentType: "Test" });
275
+ Cover: functionality, API contracts, performance, and cover every TEST_PLAN scenario. Report: PASS or FAIL per scenario.`, { agentType: "Test" });
211
276
  testVerdict = testResult.verdict;
212
277
  if (testVerdict === "FAIL") {
213
278
  // fix-and-continue — no re-test, no inline Gate 1.
214
279
  await agent(`Fix the failing acceptance test scenarios on branch ${BRANCH}:
215
280
  ${testResult.failures}
216
281
  ISSUE_NUMBER: ${ISSUE_NUMBER}
282
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
283
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
217
284
  Self-verify your fix compiles and the scenarios pass (background-Bash + Monitor for any build/test >120s). Commit fixes.`, { agentType: "Code" });
218
285
  testVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
219
286
  }
@@ -326,6 +393,8 @@ ${JSON.stringify(allFindings.map((f, i) => ({ index: i, description: f.descripti
326
393
  ${chunk.map(f => `- ${f.description} (${f.severity})`).join("\n")}
327
394
 
328
395
  ISSUE_NUMBER: ${ISSUE_NUMBER}
396
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
397
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
329
398
  Fix all findings in this batch. Self-verify your fix compiles (background-Bash + Monitor for any build >120s — see your "Long-running commands" discipline). Commit with conventional-commit message.
330
399
  Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<description of any finding that could not be fixed>"]}`, { agentType: "Code" });
331
400
  chunkResults.push({ chunk, result: r });
@@ -375,6 +444,8 @@ Report: PASS or FAIL with details.`, { agentType: "Validate" });
375
444
  await agent(`Fix the final validation failures on branch ${BRANCH}:
376
445
  ${failureDetails}
377
446
  ISSUE_NUMBER: ${ISSUE_NUMBER}
447
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
448
+ COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
378
449
  Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code" });
379
450
  const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (background+Monitor for long commands). Report: PASS or FAIL.`, { agentType: "Validate" });
380
451
  if (recheck.verdict === "PASS") break;
@@ -395,15 +466,17 @@ Self-verify your fix compiles. Commit fixes with conventional-commit message.`,
395
466
  });
396
467
 
397
468
  // PASS requires survivingFindings.length === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED"
469
+ // and no FAIL-FIXED Gate 2 verdict: fixes applied but never re-run report UNVERIFIED, never PASS
398
470
  const coverageGaps = reviewResult.coverageGaps || [];
399
- const overallVerdict = (reviewResult.survivingFindings?.length || 0) === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED" ? "PASS" : "PARTIAL";
471
+ const gate2Unverified = [gate2.evaluateVerdict, gate2.testVerdict].includes("FAIL-FIXED");
472
+ const overallVerdict = (reviewResult.survivingFindings?.length || 0) === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED" ? (gate2Unverified ? "UNVERIFIED" : "PASS") : "PARTIAL";
400
473
 
401
- // Phase 6: Report
402
- return phase("report", () =>
403
- agent(`Synthesize the build run for ticket ${TICKET} on branch ${BRANCH}:
474
+ // Phase 6: Report — the phase returns the engine result (engine_output_schema); its verdict is overallVerdict
475
+ return phase("report", async () => {
476
+ const report = await agent(`Synthesize the build run for ticket ${TICKET} on branch ${BRANCH}:
404
477
  - Implementation summary
405
478
  - Gate 1 (#1 post-implementation) result
406
- - Gate 2 result: ${JSON.stringify(gate2)}
479
+ - Gate 2 result: ${JSON.stringify(gate2)} (render FAIL-FIXED as "UNVERIFIED (fixes applied, not re-run)", never PASS)
407
480
  - Review: single pass (full branch diff)
408
481
  - Final Gate 1 (#2 post-fix): ${JSON.stringify(gate1Final)}
409
482
  - Findings disposition:
@@ -411,29 +484,39 @@ return phase("report", () =>
411
484
  - SURVIVING: ${reviewResult.survivingFindings?.length || 0} findings not addressed (fix Code agent failed or deferred): ${JSON.stringify(reviewResult.survivingFindings)}
412
485
  - Overall verdict: ${overallVerdict}
413
486
 
414
- Write a concise report. Present only surviving findings as outstanding — never present FIXED findings as outstanding. The branch is ready for user review — do NOT merge to main.`, { agentType: "Synthesize" })
415
- );
487
+ Write a concise report. Present only surviving findings as outstanding — never present FIXED findings as outstanding. The branch is ready for user review — do NOT merge to main.`, { agentType: "Synthesize" });
488
+ return {
489
+ ticket: TICKET, branch: BRANCH, verdict: overallVerdict, issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK,
490
+ survivingFindings: reviewResult.survivingFindings || [], fixedFindings: reviewResult.fixedFindings || [],
491
+ reviewCoverage: { failedFocuses: coverageGaps, complete: coverageGaps.length === 0 },
492
+ escalations: [
493
+ ...(gate1Final.verdict === "ESCALATED" ? [{ type: "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
494
+ ...coverageGaps.map(focus => ({ type: "review-coverage-incomplete", description: `review coverage incomplete: ${focus}` })),
495
+ ],
496
+ gate2, report,
497
+ };
498
+ });
416
499
  ```
417
500
 
418
- {concurrency_doctrine()}
501
+ {{concurrency_doctrine()}}
419
502
 
420
- {build_execution_doctrine()}
503
+ {{build_execution_doctrine()}}
421
504
 
422
- {implement_bundle()}
505
+ {{implement_bundle()}}
423
506
 
424
- {gate1_postcode()}
507
+ {{gate1_postcode()}}
425
508
 
426
- {gate2_acceptance()}
509
+ {{gate2_acceptance()}}
427
510
 
428
- {evaluator_panel()}
511
+ {{evaluator_panel()}}
429
512
 
430
- {acceptance_criteria_contract()}
513
+ {{acceptance_criteria_contract()}}
431
514
 
432
- {review_pass()}
515
+ {{review_pass()}}
433
516
 
434
- {engine_invariants()}
517
+ {{engine_invariants()}}
435
518
 
436
- {engine_output_schema()}
519
+ {{engine_output_schema()}}
437
520
 
438
521
  ---
439
522
 
@@ -441,22 +524,28 @@ Write a concise report. Present only surviving findings as outstanding — never
441
524
 
442
525
  When WAVE mode is detected, author a workflow that wraps the single-ticket engine with the wave loop:
443
526
 
444
- {wave_loop()}
527
+ {{wave_loop()}}
445
528
 
446
- {branch_merge_model()}
529
+ {{branch_merge_model()}}
447
530
 
448
- {merge_doctrine()}
531
+ {{merge_doctrine()}}
449
532
 
450
- {escalation_model()}
533
+ {{escalation_model()}}
451
534
 
452
535
  **Wave workflow structure (author after the SINGLE engine blocks above):**
453
536
 
454
- 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). Per-ticket branches are `ticket/<slug>`. 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.
537
+ 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.
455
538
 
456
539
  Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
457
540
 
458
541
  ```js
459
542
  // Wave round loop: Design agent reader → per-ticket try/catch → cascade quarantine via next reader
543
+ // runSingleTicketEngine(args) is the SINGLE skeleton above as a function of its OWN args: the wave's
544
+ // args.issueNumber (the tracking issue, Pre-authoring step 5) is never in its scope.
545
+ const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
546
+ const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
547
+ // A row from an engine result: the fields step 3 after the workflow renders
548
+ 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 });
460
549
  const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
461
550
  const waveState = { quarantined: [], round: 0 };
462
551
  let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
@@ -477,7 +566,7 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
477
566
  { agentType: "Design" }
478
567
  );
479
568
 
480
- const ready = waveRead?.ready || [];
569
+ const ready = (waveRead?.ready || []).filter(t => remainingTickets.includes(t)); // only the pre-fetch's own refs: a ready ID the reader invents never reaches an engine
481
570
  if (ready.length === 0) {
482
571
  if (reAskedThisDeadlock) break; // second empty read → declare deadlock with named blockers, break
483
572
  reAskedThisDeadlock = true; // re-ask once with vacuous-truth rule quoted verbatim
@@ -487,20 +576,31 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
487
576
 
488
577
  for (const ticketId of ready) {
489
578
  try {
490
- const engineResult = await runSingleTicketEngine({ ticketId, integrationBranch: INTEGRATION_BRANCH, plans, decisionsContext: DECISIONS_CONTEXT, issueNumber: ISSUE_NUMBER });
491
- // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias)
492
- if ((engineResult.verdict || engineResult.overallVerdict) === "PASS") {
493
- await agent(`Merge ticket/${ticketId} to ${INTEGRATION_BRANCH}. Include ticket ID ${ticketId} in the merge commit message. Run Validate agent (build + test) after merge.`, { agentType: "Git" });
579
+ // ticketId is the ISSUE_REF the pre-fetch heading printed — this ticket's OWN reference: the engine's `ticket` (its TICKET)
580
+ // 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)
581
+ 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 });
582
+ // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
583
+ // PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
584
+ if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
585
+ 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.
586
+ 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" });
587
+ results[ticketId] = rowOf(ticketId, engineResult, merge?.merged === true);
588
+ if (merge?.merged !== true) waveState.quarantined.push({ ticket: ticketId, reason: merge?.reason || "merge or post-merge build failed" });
494
589
  } else {
590
+ results[ticketId] = rowOf(ticketId, engineResult, false);
495
591
  waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
496
592
  }
497
593
  } catch (err) {
498
594
  // One ticket's crash/stall never kills the wave — quarantine it; cascade propagates to dependents via the next reader round
595
+ results[ticketId] = rowOf(ticketId, null, false);
499
596
  waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
500
597
  }
501
598
  remainingTickets = remainingTickets.filter(t => t !== ticketId);
502
599
  }
503
600
  }
601
+
602
+ // After the wave report is written: one row per wave ticket, in input order. No entry ⇒ never ran (cascade, deadlock, MAX_ROUNDS).
603
+ return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, issuePrLink: "(none)" }), quarantined: waveState.quarantined };
504
604
  ```
505
605
 
506
606
  ---
@@ -509,24 +609,132 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
509
609
 
510
610
  A workflow cannot pause mid-run. After the build/wave workflow returns, you (the main model) surface anything that needs a human decision — all at once:
511
611
 
512
- 1. Read the wave report (`.devflow/docs/waves/\{slug\}/\{ts\}/wave-report.md`) for its **escalations** (quarantined/blocked tickets and why), and read any `DECISIONS-NEEDED.md` left by a prior `/devflow:dynamic-plan` run for this initiative.
513
- 2. **Post wave-report as tracking-issue comment** (WAVE mode only — skip this step entirely in SINGLE mode; SINGLE runs produce no wave report): In WAVE mode, if a tracking-issue number was resolved in Pre-authoring step 5 (`ISSUE_NUMBER` is not `(none)`) AND `.devflow/docs/waves/\{slug\}/\{ts\}/wave-report.md` exists, define `WAVE_ID` as the timestamped wave directory slug (the `\{ts\}` component, e.g. `2026-08-20_1730`), then spawn:
612
+ 1. Read the wave report (`{integration worktree root}/.devflow/docs/waves/{slug}/{ts}/wave-report.md`) for its **escalations** (quarantined/blocked tickets and why), and read any `DECISIONS-NEEDED.md` left by a prior `/devflow:dynamic-plan` run for this initiative.
613
+ 2. **Post wave-report as tracking-issue comment** (WAVE mode only — skip this step entirely in SINGLE mode; SINGLE runs produce no wave report): In WAVE mode, if a tracking-issue number was resolved in Pre-authoring step 5 (`ISSUE_NUMBER` is not `(none)`) AND `{integration worktree root}/.devflow/docs/waves/{slug}/{ts}/wave-report.md` exists, define `WAVE_ID` as the timestamped wave directory slug (the `{ts}` component, e.g. `2026-08-20_1730`), then spawn:
514
614
  ```
515
615
  Agent(subagent_type="Git"):
516
616
  "OPERATION: post-wave-report
517
- TRACKING_ISSUE: \{ISSUE_NUMBER\}
518
- WAVE_REPORT_PATH: .devflow/docs/waves/\{slug\}/\{ts\}/wave-report.md
519
- WAVE_ID: \{WAVE_ID\}
520
- WORKTREE_PATH: \{integration worktree root, when the wave ran in a linked worktree; omit if cwd\}"
617
+ TRACKING_ISSUE: {ISSUE_NUMBER}
618
+ WAVE_REPORT_PATH: .devflow/docs/waves/{slug}/{ts}/wave-report.md
619
+ WAVE_ID: {WAVE_ID}
620
+ WORKTREE_PATH: {integration worktree root}"
521
621
  ```
522
- The Git agent deduplicates via marker `<!-- devflow:wave-report wave:\{WAVE_ID\} -->` — skips if already present. On API failure it degrades gracefully (`TRACEABILITY: DEGRADED (\{reason\})`) and continues — never blocks the post-wave step. This comment is the evidence surface for the PR-less integration-branch path; no other PR machinery is invented.
622
+ The Git agent deduplicates via its own marker — it skips if a report for this `WAVE_ID` is already posted. The marker's format belongs to the operation; this caller passes `WAVE_ID` and never restates the literal. On API failure it degrades gracefully (`TRACEABILITY: DEGRADED ({reason})`) and continues — never blocks the post-wave step. This comment is the evidence surface for the PR-less integration-branch path; no other PR machinery is invented.
523
623
 
524
624
  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.
525
- 3. Surface ALL of them — escalations AND open decisions — to the user in ONE batched `AskUserQuestion` (never one-at-a-time). `_wave.mds`'s escalation model already quarantines-and-continues; this batches the surfacing so the user answers everything in a single pass.
526
- 4. If `~/.devflow/preference-profile.md` was absent, note in your summary: "no preference profile found — N decisions surfaced that a profile might have auto-resolved; consider `/devflow:dynamic-profile`."
625
+ 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.
626
+ - **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.
627
+ - **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
628
+ - Otherwise compose (a) and then (b).
629
+
630
+ **(a) The wave block.** Write it with the Write tool, byte for byte, to a fresh `mktemp` file — never through an interpolated shell string — in exactly this shape: the two headings with one blank line between them, the tracking line and then the related lines in row order under the first, and the header and separator verbatim directly under the second:
631
+
632
+ ```markdown
633
+ ## Related Issues
634
+ Refs {tracking ref}
635
+ {one related line per row that has one}
636
+
637
+ ## Wave Evidence
638
+ | T | Ticket | Verdict | Evaluate | Test | Surviving | Coverage |
639
+ |---|---|---|---|---|---|---|
640
+ | T{k} | {ticket} | {verdict} | {evaluate} | {test} | {surviving} | {coverage} |
641
+ ```
642
+
643
+ One row per `tickets` entry, `T1` … `Tn` in order. Each entry maps to exactly one row, by these rules:
644
+ - **Verdict** — one of `PASS | UNVERIFIED | QUARANTINED | BLOCKED`. `ran: false` (cascade, deadlock, MAX_ROUNDS) ⇒ `BLOCKED`. `merged: true` with `verdict` `PASS` ⇒ `PASS`. `merged: true` with `verdict` `UNVERIFIED` ⇒ `UNVERIFIED`: the row stays flagged, and its TP lines join the wave test plan. Anything else that ran — PARTIAL, FAIL, ESCALATED, no verdict, an engine crash, a failed merge or a red post-merge build — ⇒ `QUARANTINED`.
645
+ - **Evaluate, Test** — the entry's `evaluateVerdict` and `testVerdict` when it is `PASS`, `FAIL`, `FAIL-FIXED` or `SKIPPED`, else `—`. **Surviving** — its `surviving` count when it is 0–999, else `—`. **Coverage** — `complete` or `incomplete` from `coverageComplete`, else `—`. A `BLOCKED` row is `—` in all four.
646
+ - **Ticket and related line** — the entry's captured `issuePrLink` is the only source of a closing reference. When it is not `(none)`: a `PASS` or `UNVERIFIED` row's related line is `issuePrLink` verbatim; a `QUARANTINED` row's is `issuePrLink` with a leading `Closes ` replaced by `Refs `; and the Ticket cell is the reference that line names (its text after `Closes ` or `Refs `). When it is `(none)`: no related line, and the Ticket cell is `(none)` on a `PASS` or `UNVERIFIED` row; on any other row it is the entry's `ticket` when that is a `#N` or `KEY-N` reference, else `(none)`.
647
+
648
+ **Tracking line** — only when Pre-authoring step 5 resolved a tracking issue whose token is, as a whole, a `#N` or `KEY-N` reference: `Refs ` and that token, verbatim, as the first line under `## Related Issues`. Never `Closes` — the tracking issue outlives the wave. Any other token (a bare number, a URL), or none ⇒ no tracking line: nothing is composed from a number.
649
+
650
+ Then check it:
651
+
652
+ ```bash
653
+ node "$HOME/.devflow/scripts/verify-evidence.cjs" check wave <that file>; echo "exit=$?"
654
+ ```
655
+
656
+ Only `exit=0` admits the file's text, verbatim, as `PR_WAVE_BLOCK`. Any other result: no wave PR — record `Wave PR: not opened (wave block refused: <the code on stderr>)` and go to step 4.
657
+
658
+ **Required link** — only when `EVIDENCE_POLICY` is `required`: a `PASS` or `UNVERIFIED` row whose Ticket is `(none)` — a merged ticket whose setup-task captured an Issue ID but no link line, so the wave PR would close nothing for it — ⇒ `Wave PR: BLOCKED (no ticket link for T<k>, …)`, with no wave PR question and the remedy "link or create those tickets' issues, then re-run"; there is no exception. Under `standard` such a row stays as the Ticket rule above renders it.
659
+
660
+ **(b) The wave test plan.** Take the checked TP lines each merged row's ticket was given in Pre-authoring step 4, in row order; renumber them `TP-1`, `TP-2`, … and prefix each scenario with its row's `T<k>: `. A line is never shortened or reworded: one whose prefixed scenario would pass 200 characters leaves its ticket with no usable test plan. Write `## Test Plan` and those lines, and nothing else, to `"{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"` with the Write tool, then run (the path double-quoted; one rewrite from the same lines after a refusal, never a second):
661
+
662
+ ```bash
663
+ node "$HOME/.devflow/scripts/verify-evidence.cjs" check tp "{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"; echo "exit=$?"
664
+ node "$HOME/.devflow/scripts/verify-evidence.cjs" render --plan "{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"; echo "exit=$?"
665
+ ```
666
+
667
+ Only when both exit 0 is `PR_TEST_PLAN_BLOCK` the render's stdout, byte for byte without its `exit=` line; in every other case it is `(none)`.
668
+
669
+ **Required plan** — only when `EVIDENCE_POLICY` is `required`: a merged ticket with no usable test plan ⇒ `Wave PR: BLOCKED (no test plan for T<k>, …)`, more than 200 lines in all ⇒ `Wave PR: BLOCKED (test plan over 200 lines)`, and a plan that did not check and render ⇒ `Wave PR: BLOCKED (test plan malformed)` — each with no wave PR question and the remedy "run `/devflow:dynamic-plan` for those tickets and re-run, or ship them through `/implement`"; no exception is offered here. Otherwise the block is optional: a ticket with no usable test plan is left out of it and named in the summary, and `(none)` is passed when no line remains.
670
+ 4. Surface ALL of them — escalations AND open decisions — to the user in ONE batched `AskUserQuestion` (never one-at-a-time). `_wave.mds`'s escalation model already quarantines-and-continues; this batches the surfacing so the user answers everything in a single pass.
671
+ - **The wave PR question.** Only when step 3 composed `PR_WAVE_BLOCK`, the batch gains exactly one question: "Open the wave PR from wave/{slug}? It links {n} merged tickets ({u} UNVERIFIED) and references {q} quarantined." It has exactly two options: open it, or don't. The counts come from the checked block's rows: `{n}` PASS and UNVERIFIED, `{u}` UNVERIFIED, `{q}` QUARANTINED.
672
+ - A `ticket-link-missing` escalation carries its remedy: link or create that ticket's issue, then re-run. There is no per-ticket exception.
673
+ - **Headless** — `AskUserQuestion` is unavailable, or no answer comes — is a decline: nothing is created and nothing is pushed.
674
+ 5. If `~/.devflow/preference-profile.md` was absent, note in your summary: "no preference profile found — N decisions surfaced that a profile might have auto-resolved; consider `/devflow:dynamic-profile`."
675
+ 6. **Wave PR** — only after an explicit "open" in step 4. Spawn:
676
+ ```
677
+ Agent(subagent_type="Git"):
678
+ "OPERATION: ensure-pr-ready
679
+ WORKTREE_PATH: {integration worktree root}
680
+ PR_DESCRIPTION_GUIDANCE: {counts only — the wave slug and the merged, quarantined and blocked counts; never an issue title or body}
681
+ APPLY_CONVENTIONS: {APPLY_CONVENTIONS}
682
+ PR_WAVE_BLOCK: {PR_WAVE_BLOCK verbatim}
683
+ PR_TEST_PLAN_BLOCK: {PR_TEST_PLAN_BLOCK verbatim, or (none)}"
684
+ ```
685
+ The Git agent pastes each block only behind its own check. Report its `**PR**` line and any `TRACEABILITY: DEGRADED ({reason})` lines. The wave PR is opened here and nowhere else; it is never merged, and main is never touched — the user merges.
527
686
 
528
687
  Do NOT ask questions mid-workflow — that is impossible (F4). The workflow only WRITES the report; you read it and ask.
529
688
 
689
+ 7. **Wave PR evidence** — only when step 6 reported the wave PR, after it; it never blocks, and every outcome below goes into the run summary. Take `{n}` from step 6's `- **PR**: #{n}` line when `{n}` matches `^[1-9][0-9]{0,9}$` as a whole; no such line ⇒ record `TRACEABILITY: DEGRADED (wave PR number not captured)` and skip this step. `PR_TEST_PLAN_BLOCK` `(none)` ⇒ record `Wave evidence: skipped (no wave test plan)` and skip it.
690
+
691
+ **(a) Test the wave.** Spawn one Test agent on the integration worktree with the wave test plan — the TP lines of the `## Test Plan` section step 3(b) wrote to `{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md`:
692
+
693
+ ```
694
+ Agent(subagent_type="Test"):
695
+ "ORIGINAL_REQUEST: the merged tickets of wave/{slug}, as the wave test plan names them
696
+ FILES_CHANGED: {the files wave/{slug} changes against the - **Base**: branch step 6 reported}
697
+ TEST_PLAN: {the TP lines of the wave evidence file's ## Test Plan section}
698
+ WORKTREE_PATH: {integration worktree root}
699
+ Cover every TEST_PLAN line on the integration branch as it stands. Report PASS or FAIL with evidence."
700
+ ```
701
+
702
+ Nothing is fixed here, PASS or FAIL: the wave is done and its PR is open. Report the Test agent's Status.
703
+
704
+ **(b) Claims.** Append its TP claims, PASS or FAIL alike, to the `## Claims` section of `"{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"` — the file's last section, created when absent. Append only: never edit or remove a claim. Each line is `/implement`'s TP claim, keyed to the 40-hex `HEAD:` the Test agent's report shows:
705
+
706
+ ```
707
+ - TP-<n> <PASS|FAIL|SKIP> sha:<head> by:test exit:<0-255>
708
+ ```
709
+
710
+ One line per `### Test Plan Evidence` row whose TP is in the wave test plan, with the row's outcome; the line ends at `by:test` when the row's Exit is not a number from 0 to 255. A report whose `HEAD:` is not a single 40-hex SHA — a reported before/after change included — gets no claim: record `Wave evidence: no claims (HEAD not one SHA)`.
711
+
712
+ **(c) Push** the integration branch once — never force, no retry — so every claim's SHA is in the PR:
713
+
714
+ ```bash
715
+ git -C "{integration worktree root}" push origin HEAD; echo "exit=$?"
716
+ ```
717
+
718
+ Any result but `exit=0`, a rejected non-fast-forward push included ⇒ record `TRACEABILITY: DEGRADED (evidence push failed)` and refresh anyway.
719
+
720
+ **(d) Refresh.** Resolve the publication value for the integration worktree:
721
+
722
+ {{pub.publication_gate()}}
723
+
724
+ Then spawn:
725
+
726
+ ```
727
+ Agent(subagent_type="Git"):
728
+ "OPERATION: update-pr-evidence
729
+ PR_NUMBER: {n}
730
+ EVIDENCE_FILE: .devflow/docs/evidence-wave-{slug}.md
731
+ REVIEW_PUBLICATION: {REVIEW_PUBLICATION resolved above, or auto}
732
+ WORKTREE_PATH: {integration worktree root}
733
+ Update the wave PR's test-plan block and post its evidence comment."
734
+ ```
735
+
736
+ `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.
737
+
530
738
  ---
531
739
 
532
740
  ### Maintenance note