devflow-kit 2.4.0 → 2.5.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 (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -4,11 +4,13 @@ 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
8
  @import { agent_roster, agent_caveats } from "./_partials/_roster.mds"
9
9
  @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
10
  @import { wave_loop, branch_merge_model, merge_doctrine, escalation_model } from "./_partials/_wave.mds"
11
11
  @import { acceptance_criteria_contract } from "./_partials/_plan_contract.mds"
12
+ @import { issue_ref_grammar, issue_capture_contract } from "./_partials/_tracker.mds"
13
+ @import "./_partials/_publication.mds" as pub
12
14
 
13
15
  {authoring_preamble()}
14
16
 
@@ -35,8 +37,8 @@ Before authoring, verify:
35
37
 
36
38
  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
39
  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.
40
+ 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.
41
+ 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
42
 
41
43
  ---
42
44
 
@@ -44,12 +46,13 @@ Before authoring, verify:
44
46
 
45
47
  Before you write the workflow script:
46
48
 
47
- **0. Resolve compliance context**
49
+ **0. Resolve the evidence policy**
48
50
 
49
- {compliance_gate()}
50
- Reuse this result for every ticket in the wave.
51
+ **Produces:** EVIDENCE_POLICY, ISSUE_REQUIRED, APPLY_CONVENTIONS, REQUIRE_NON_AUTHOR_APPROVAL
51
52
 
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.
53
+ {evidence_policy()}
54
+
55
+ 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.
53
56
 
54
57
  **1. Apply decisions context**
55
58
 
@@ -62,7 +65,8 @@ Note the `budget` value from the Workflow tool context (or default to "medium" i
62
65
  **3. Detect mode: SINGLE or WAVE**
63
66
 
64
67
  - **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"
68
+ - **WAVE mode:** input is a set of tracker issues (wave labels, milestone, issue list), or the user says "wave" / "all tickets in wave N"
69
+ - **A `/devflow:dynamic-tickets` ticket directory** (`.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
70
 
67
71
  When ambiguous, ask the user before authoring: "Is this a single ticket or a wave of tickets?"
68
72
 
@@ -70,24 +74,41 @@ When ambiguous, ask the user before authoring: "Is this a single ticket or a wav
70
74
 
71
75
  Check for (in priority order):
72
76
  - A plan document passed as input (path or inline)
73
- - A GitHub issue body (fetch via `gh issue view <number>`)
77
+ - An issue body (fetched via the Git agent using `OPERATION: fetch-issue`)
74
78
  - The current working context (recent `/devflow:dynamic-plan` output)
75
79
  - An in-context task description
76
80
 
81
+ {issue_capture_contract()}
82
+
77
83
  Extract or note:
78
84
  - Implementation plan (for Code agent prompt and Evaluate agent)
79
- - Acceptance criteria and test plan (for Gate 2)
85
+ - Acceptance criteria (for Gate 2) — pass them as `criteria` when invoking the workflow
86
+ - 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:
87
+
88
+ ```bash
89
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" check tp <that file>; echo "exit=$?"
90
+ ```
91
+
92
+ 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.
93
+
94
+ 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
95
 
81
96
  If none found: build proceeds Gate-1-only (Gate 2 skipped with a note). Never refuse to build; never fabricate criteria.
82
97
 
83
98
  **5. Resolve tracking-issue number (optional)**
84
99
 
85
100
  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`)
101
+ - An explicit candidate issue reference or issue URL in the user's input (e.g. `#42`, `42`, or `https://github.com/…/issues/42`)
102
+ - 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 `.devflow/docs/tickets/\{slug\}/\{ts\}/tracking-issue.md`), as a raw token — only when the file holds exactly one `**Issue:**` line
88
103
  - Otherwise: none
89
104
 
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.
105
+ {issue_ref_grammar()}
106
+
107
+ If a number is found, record it as the command-level `ISSUE_NUMBER`:
108
+ - **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.
109
+ - **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`.
110
+
111
+ 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
112
 
92
113
  ---
93
114
 
@@ -105,22 +126,43 @@ export const meta = {
105
126
  // SINGLE mode: one ticket, one branch, full engine
106
127
 
107
128
  const TICKET = args.ticket || args[0] || "see task description";
108
- const BRANCH = args.branch || `ticket/${TICKET.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
109
129
  const PLAN = args.plan || null;
110
130
  const CRITERIA = args.criteria || null;
131
+ const TEST_PLAN = args.testPlan || null; // the plan's checked TP lines, one string (Pre-authoring step 4)
111
132
  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)"
133
+ 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
134
+ 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
135
+ const APPLY_CONVENTIONS = String(args.applyConventions) === "false" ? "false" : "true";
114
136
 
115
- // Phase 1: Git setup — declare the operation; the agent owns the process
116
- await phase("setup", () =>
137
+ // Phase 1: Git setup — declare the operation; the agent owns the process, the branch name included
138
+ const setup = await phase("setup", () =>
117
139
  agent(`OPERATION: setup-task
118
140
  BASE_BRANCH: ${args.baseBranch || "HEAD"}
119
141
  TASK_DESCRIPTION: ${TICKET}
120
- COMPLIANCE: ${COMPLIANCE}
121
- ${ISSUE_NUMBER && ISSUE_NUMBER !== "(none)" ? "ISSUE_INPUT: " + ISSUE_NUMBER : ""}`.trim(), { agentType: "Git" })
142
+ ISSUE_REQUIRED: ${ISSUE_REQUIRED}
143
+ APPLY_CONVENTIONS: ${APPLY_CONVENTIONS}
144
+ ${ISSUE_INPUT !== "(none)" ? "ISSUE_INPUT: " + ISSUE_INPUT : ""}
145
+ 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
146
  );
123
147
 
148
+ // The ticket's own Handoff Values, read from setup-task's Output — never derived from ISSUE_INPUT.
149
+ // 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.
150
+ const ISSUE_ID_SHAPE = /^(?:[1-9][0-9]{0,8}|[A-Z][A-Z0-9_]{0,9}-[1-9][0-9]{0,8})$/;
151
+ const ISSUE_NUMBER = ISSUE_ID_SHAPE.test(String(setup?.issueId ?? "")) ? setup.issueId : "(none)"; // the captured Issue ID; "(none)" ⇒ none was captured
152
+ const ISSUE_PR_LINK = setup?.prLinkLine || "(none)"; // "(none)" ⇒ Code emits the heading with no reference
153
+ // The branch setup-task created, as it reported it: every later phase and the merge use it. The engine never names one.
154
+ const BRANCH = typeof setup?.branch === "string" && setup.branch.trim() !== "" && setup.branch.trim() !== "(none)" ? setup.branch : "(none)";
155
+
156
+ // Ticket-link stop, before implement. The workflow cannot ask, so it never records an exception: it stops.
157
+ if (ISSUE_REQUIRED === "true" && ISSUE_NUMBER === "(none)") {
158
+ 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" }] };
159
+ }
160
+
161
+ // Branch stop, before implement: with no branch there is nothing to build on or merge — a correctness stop, not a shape gate.
162
+ if (BRANCH === "(none)") {
163
+ 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" }] };
164
+ }
165
+
124
166
  // Phase 2: Implement
125
167
  await phase("implement", () =>
126
168
  agent(`Implement the following ticket on branch ${BRANCH}:
@@ -133,6 +175,7 @@ Relevant architectural decisions (apply devflow:apply-decisions algorithm):
133
175
  ${DECISIONS_CONTEXT}
134
176
 
135
177
  ISSUE_NUMBER: ${ISSUE_NUMBER}
178
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
136
179
 
137
180
  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
181
 
@@ -155,6 +198,7 @@ Report: PASS or FAIL with details.`, { agentType: "Validate" });
155
198
  await agent(`Fix the validation failures on branch ${BRANCH}:
156
199
  ${validation.details}
157
200
  ISSUE_NUMBER: ${ISSUE_NUMBER}
201
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
158
202
  Commit fixes with conventional-commit message.`, { agentType: "Code" });
159
203
  const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH}. Report: PASS or FAIL.`, { agentType: "Validate" });
160
204
  if (recheck.verdict === "PASS") break;
@@ -175,8 +219,8 @@ Commit fixes with conventional-commit message.`, { agentType: "Code" });
175
219
 
176
220
  // Phase 4: Gate 2 — acceptance gate (once, before review pass)
177
221
  const gate2 = await phase("gate2", async () => {
178
- if (!PLAN && !CRITERIA) {
179
- return { evaluateVerdict: "SKIPPED", testVerdict: "SKIPPED", skipReasons: ["No plan and no criteria provided"] };
222
+ if (!PLAN && !CRITERIA && !TEST_PLAN) {
223
+ return { evaluateVerdict: "SKIPPED", testVerdict: "SKIPPED", skipReasons: ["No plan, no criteria and no test plan provided"] };
180
224
  }
181
225
 
182
226
  let evalVerdict = "SKIPPED";
@@ -197,23 +241,26 @@ Report: PASS or FAIL with rationale.`, { agentType: "Evaluate" }),
197
241
  await agent(`Fix the alignment issues identified by the Evaluate agent panel on branch ${BRANCH}:
198
242
  ${panel.filter(p => p.verdict === "FAIL").map(p => p.rationale).join("\n")}
199
243
  ISSUE_NUMBER: ${ISSUE_NUMBER}
244
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
200
245
  Self-verify your fix compiles (background-Bash + Monitor for any build >120s — see your "Long-running commands" discipline). Commit fixes.`, { agentType: "Code" });
201
246
  evalVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
202
247
  }
203
248
  }
204
249
 
205
250
  let testVerdict = "SKIPPED";
206
- if (CRITERIA) {
251
+ if (CRITERIA || TEST_PLAN) {
207
252
  const testResult = await agent(`Run scenario-based acceptance tests on branch ${BRANCH} against these criteria:
208
- ${CRITERIA}
253
+ ${CRITERIA || "(none)"}
254
+ TEST_PLAN: ${TEST_PLAN || "(none)"}
209
255
  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" });
256
+ Cover: functionality, API contracts, performance, and cover every TEST_PLAN scenario. Report: PASS or FAIL per scenario.`, { agentType: "Test" });
211
257
  testVerdict = testResult.verdict;
212
258
  if (testVerdict === "FAIL") {
213
259
  // fix-and-continue — no re-test, no inline Gate 1.
214
260
  await agent(`Fix the failing acceptance test scenarios on branch ${BRANCH}:
215
261
  ${testResult.failures}
216
262
  ISSUE_NUMBER: ${ISSUE_NUMBER}
263
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
217
264
  Self-verify your fix compiles and the scenarios pass (background-Bash + Monitor for any build/test >120s). Commit fixes.`, { agentType: "Code" });
218
265
  testVerdict = "FAIL-FIXED"; // issues found, fixes applied, not re-evaluated by design
219
266
  }
@@ -326,6 +373,7 @@ ${JSON.stringify(allFindings.map((f, i) => ({ index: i, description: f.descripti
326
373
  ${chunk.map(f => `- ${f.description} (${f.severity})`).join("\n")}
327
374
 
328
375
  ISSUE_NUMBER: ${ISSUE_NUMBER}
376
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
329
377
  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
378
  Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<description of any finding that could not be fixed>"]}`, { agentType: "Code" });
331
379
  chunkResults.push({ chunk, result: r });
@@ -375,6 +423,7 @@ Report: PASS or FAIL with details.`, { agentType: "Validate" });
375
423
  await agent(`Fix the final validation failures on branch ${BRANCH}:
376
424
  ${failureDetails}
377
425
  ISSUE_NUMBER: ${ISSUE_NUMBER}
426
+ ISSUE_PR_LINK: ${ISSUE_PR_LINK}
378
427
  Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code" });
379
428
  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
429
  if (recheck.verdict === "PASS") break;
@@ -395,15 +444,17 @@ Self-verify your fix compiles. Commit fixes with conventional-commit message.`,
395
444
  });
396
445
 
397
446
  // PASS requires survivingFindings.length === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED"
447
+ // and no FAIL-FIXED Gate 2 verdict: fixes applied but never re-run report UNVERIFIED, never PASS
398
448
  const coverageGaps = reviewResult.coverageGaps || [];
399
- const overallVerdict = (reviewResult.survivingFindings?.length || 0) === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED" ? "PASS" : "PARTIAL";
449
+ const gate2Unverified = [gate2.evaluateVerdict, gate2.testVerdict].includes("FAIL-FIXED");
450
+ const overallVerdict = (reviewResult.survivingFindings?.length || 0) === 0 && coverageGaps.length === 0 && gate1Final.verdict !== "ESCALATED" ? (gate2Unverified ? "UNVERIFIED" : "PASS") : "PARTIAL";
400
451
 
401
- // Phase 6: Report
402
- return phase("report", () =>
403
- agent(`Synthesize the build run for ticket ${TICKET} on branch ${BRANCH}:
452
+ // Phase 6: Report — the phase returns the engine result (engine_output_schema); its verdict is overallVerdict
453
+ return phase("report", async () => {
454
+ const report = await agent(`Synthesize the build run for ticket ${TICKET} on branch ${BRANCH}:
404
455
  - Implementation summary
405
456
  - Gate 1 (#1 post-implementation) result
406
- - Gate 2 result: ${JSON.stringify(gate2)}
457
+ - Gate 2 result: ${JSON.stringify(gate2)} (render FAIL-FIXED as "UNVERIFIED (fixes applied, not re-run)", never PASS)
407
458
  - Review: single pass (full branch diff)
408
459
  - Final Gate 1 (#2 post-fix): ${JSON.stringify(gate1Final)}
409
460
  - Findings disposition:
@@ -411,8 +462,18 @@ return phase("report", () =>
411
462
  - SURVIVING: ${reviewResult.survivingFindings?.length || 0} findings not addressed (fix Code agent failed or deferred): ${JSON.stringify(reviewResult.survivingFindings)}
412
463
  - Overall verdict: ${overallVerdict}
413
464
 
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
- );
465
+ 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" });
466
+ return {
467
+ ticket: TICKET, branch: BRANCH, verdict: overallVerdict, issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK,
468
+ survivingFindings: reviewResult.survivingFindings || [], fixedFindings: reviewResult.fixedFindings || [],
469
+ reviewCoverage: { failedFocuses: coverageGaps, complete: coverageGaps.length === 0 },
470
+ escalations: [
471
+ ...(gate1Final.verdict === "ESCALATED" ? [{ type: "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
472
+ ...coverageGaps.map(focus => ({ type: "review-coverage-incomplete", description: `review coverage incomplete: ${focus}` })),
473
+ ],
474
+ gate2, report,
475
+ };
476
+ });
416
477
  ```
417
478
 
418
479
  {concurrency_doctrine()}
@@ -451,12 +512,18 @@ When WAVE mode is detected, author a workflow that wraps the single-ticket engin
451
512
 
452
513
  **Wave workflow structure (author after the SINGLE engine blocks above):**
453
514
 
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.
515
+ 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
516
 
456
517
  Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
457
518
 
458
519
  ```js
459
520
  // Wave round loop: Design agent reader → per-ticket try/catch → cascade quarantine via next reader
521
+ // runSingleTicketEngine(args) is the SINGLE skeleton above as a function of its OWN args: the wave's
522
+ // args.issueNumber (the tracking issue, Pre-authoring step 5) is never in its scope.
523
+ const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
524
+ const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
525
+ // A row from an engine result: the fields step 3 after the workflow renders
526
+ 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
527
  const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
461
528
  const waveState = { quarantined: [], round: 0 };
462
529
  let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
@@ -477,7 +544,7 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
477
544
  { agentType: "Design" }
478
545
  );
479
546
 
480
- const ready = waveRead?.ready || [];
547
+ 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
548
  if (ready.length === 0) {
482
549
  if (reAskedThisDeadlock) break; // second empty read → declare deadlock with named blockers, break
483
550
  reAskedThisDeadlock = true; // re-ask once with vacuous-truth rule quoted verbatim
@@ -487,20 +554,31 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
487
554
 
488
555
  for (const ticketId of ready) {
489
556
  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" });
557
+ // ticketId is the ISSUE_REF the pre-fetch heading printed — this ticket's OWN reference: the engine's `ticket` (its TICKET)
558
+ // 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)
559
+ const engineResult = await runSingleTicketEngine({ ticket: ticketId, baseBranch: INTEGRATION_BRANCH, ...(plans[ticketId] || {}), decisionsContext: DECISIONS_CONTEXT, issueRequired: ISSUE_REQUIRED, applyConventions: APPLY_CONVENTIONS, issueInput: ticketId });
560
+ // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
561
+ // PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
562
+ if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
563
+ 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.
564
+ 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" });
565
+ results[ticketId] = rowOf(ticketId, engineResult, merge?.merged === true);
566
+ if (merge?.merged !== true) waveState.quarantined.push({ ticket: ticketId, reason: merge?.reason || "merge or post-merge build failed" });
494
567
  } else {
568
+ results[ticketId] = rowOf(ticketId, engineResult, false);
495
569
  waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
496
570
  }
497
571
  } catch (err) {
498
572
  // One ticket's crash/stall never kills the wave — quarantine it; cascade propagates to dependents via the next reader round
573
+ results[ticketId] = rowOf(ticketId, null, false);
499
574
  waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
500
575
  }
501
576
  remainingTickets = remainingTickets.filter(t => t !== ticketId);
502
577
  }
503
578
  }
579
+
580
+ // After the wave report is written: one row per wave ticket, in input order. No entry ⇒ never ran (cascade, deadlock, MAX_ROUNDS).
581
+ return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, issuePrLink: "(none)" }), quarantined: waveState.quarantined };
504
582
  ```
505
583
 
506
584
  ---
@@ -519,14 +597,122 @@ A workflow cannot pause mid-run. After the build/wave workflow returns, you (the
519
597
  WAVE_ID: \{WAVE_ID\}
520
598
  WORKTREE_PATH: \{integration worktree root, when the wave ran in a linked worktree; omit if cwd\}"
521
599
  ```
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.
600
+ 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
601
 
524
602
  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`."
603
+ 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.
604
+ - **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.
605
+ - **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
606
+ - Otherwise compose (a) and then (b).
607
+
608
+ **(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:
609
+
610
+ ```markdown
611
+ ## Related Issues
612
+ Refs {tracking ref}
613
+ {one related line per row that has one}
614
+
615
+ ## Wave Evidence
616
+ | T | Ticket | Verdict | Evaluate | Test | Surviving | Coverage |
617
+ |---|---|---|---|---|---|---|
618
+ | T{k} | {ticket} | {verdict} | {evaluate} | {test} | {surviving} | {coverage} |
619
+ ```
620
+
621
+ One row per `tickets` entry, `T1` … `Tn` in order. Each entry maps to exactly one row, by these rules:
622
+ - **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`.
623
+ - **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.
624
+ - **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)`.
625
+
626
+ **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.
627
+
628
+ Then check it:
629
+
630
+ ```bash
631
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" check wave <that file>; echo "exit=$?"
632
+ ```
633
+
634
+ 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.
635
+
636
+ **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.
637
+
638
+ **(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):
639
+
640
+ ```bash
641
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" check tp "{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"; echo "exit=$?"
642
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" render --plan "{integration worktree root}/.devflow/docs/evidence-wave-{slug}.md"; echo "exit=$?"
643
+ ```
644
+
645
+ 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)`.
646
+
647
+ **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.
648
+ 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.
649
+ - **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.
650
+ - A `ticket-link-missing` escalation carries its remedy: link or create that ticket's issue, then re-run. There is no per-ticket exception.
651
+ - **Headless** — `AskUserQuestion` is unavailable, or no answer comes — is a decline: nothing is created and nothing is pushed.
652
+ 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`."
653
+ 6. **Wave PR** — only after an explicit "open" in step 4. Spawn:
654
+ ```
655
+ Agent(subagent_type="Git"):
656
+ "OPERATION: ensure-pr-ready
657
+ WORKTREE_PATH: \{integration worktree root\}
658
+ PR_DESCRIPTION_GUIDANCE: \{counts only — the wave slug and the merged, quarantined and blocked counts; never an issue title or body\}
659
+ APPLY_CONVENTIONS: \{APPLY_CONVENTIONS\}
660
+ PR_WAVE_BLOCK: \{PR_WAVE_BLOCK verbatim\}
661
+ PR_TEST_PLAN_BLOCK: \{PR_TEST_PLAN_BLOCK verbatim, or (none)\}"
662
+ ```
663
+ 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
664
 
528
665
  Do NOT ask questions mid-workflow — that is impossible (F4). The workflow only WRITES the report; you read it and ask.
529
666
 
667
+ 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.
668
+
669
+ **(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 `.devflow/docs/evidence-wave-\{slug\}.md`:
670
+
671
+ ```
672
+ Agent(subagent_type="Test"):
673
+ "ORIGINAL_REQUEST: the merged tickets of wave/{slug}, as the wave test plan names them
674
+ FILES_CHANGED: {the files wave/{slug} changes against the - **Base**: branch step 6 reported}
675
+ TEST_PLAN: {the TP lines of the wave evidence file's ## Test Plan section}
676
+ WORKTREE_PATH: {integration worktree root}
677
+ Cover every TEST_PLAN line on the integration branch as it stands. Report PASS or FAIL with evidence."
678
+ ```
679
+
680
+ Nothing is fixed here, PASS or FAIL: the wave is done and its PR is open. Report the Test agent's Status.
681
+
682
+ **(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:
683
+
684
+ ```
685
+ - TP-<n> <PASS|FAIL|SKIP> sha:<head> by:test exit:<0-255>
686
+ ```
687
+
688
+ 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)`.
689
+
690
+ **(c) Push** the integration branch once — never force, no retry — so every claim's SHA is in the PR:
691
+
692
+ ```bash
693
+ git -C "{integration worktree root}" push origin HEAD; echo "exit=$?"
694
+ ```
695
+
696
+ Any result but `exit=0`, a rejected non-fast-forward push included ⇒ record `TRACEABILITY: DEGRADED (evidence push failed)` and refresh anyway.
697
+
698
+ **(d) Refresh.** Resolve the publication value for the integration worktree:
699
+
700
+ {pub.publication_gate()}
701
+
702
+ Then spawn:
703
+
704
+ ```
705
+ Agent(subagent_type="Git"):
706
+ "OPERATION: update-pr-evidence
707
+ PR_NUMBER: {n}
708
+ EVIDENCE_FILE: .devflow/docs/evidence-wave-{slug}.md
709
+ REVIEW_PUBLICATION: {REVIEW_PUBLICATION resolved above, or auto}
710
+ WORKTREE_PATH: {integration worktree root}
711
+ Update the wave PR's test-plan block and post its evidence comment."
712
+ ```
713
+
714
+ `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.
715
+
530
716
  ---
531
717
 
532
718
  ### Maintenance note
@@ -6,6 +6,7 @@ output-dir: dist/commands
6
6
  @import { authoring_preamble } from "./_partials/_preamble.mds"
7
7
  @import { agent_roster, agent_caveats } from "./_partials/_roster.mds"
8
8
  @import { acceptance_criteria_contract } from "./_partials/_plan_contract.mds"
9
+ @import { issue_ref_grammar, issue_capture_contract } from "./_partials/_tracker.mds"
9
10
 
10
11
  {authoring_preamble()}
11
12
 
@@ -21,7 +22,7 @@ This command instructs you to construct and run a Claude Code dynamic Workflow t
21
22
 
22
23
  ---
23
24
 
24
- **Requires:** ticket files directory or GitHub issue list; optional `~/.devflow/preference-profile.md`
25
+ **Requires:** ticket files directory or tracker issue list; optional `~/.devflow/preference-profile.md`
25
26
  **Produces:** per-ticket plan files + `DECISIONS-NEEDED.md` at `.devflow/docs/design/\{slug\}/\{ts\}/`
26
27
 
27
28
  ---
@@ -32,8 +33,8 @@ Before authoring, verify:
32
33
 
33
34
  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-plan requires Claude Code's dynamic workflow runtime."
34
35
  2. **`agentType` support:** confirmed available (spike F5, 2026-06-11).
35
- 3. **GitHub paths:** if the input is a list of GitHub issue URLs or numbers, `gh` CLI must be authenticated to read issue bodies. If not authenticated, fall back to reading ticket `.md` files from a local path.
36
- 4. **No-remote path:** if the repo has no remote, skip GitHub-dependent steps and read ticket files from the provided local path.
36
+ 3. **Tracker paths:** a list of issue references or URLs is read through the Git agent, which resolves the configured tracker and its access itself and reports `TRACEABILITY: DEGRADED (\{reason\})` when it cannot read an issue — then fall back to reading ticket `.md` files from a local path. No tracker CLI is checked here.
37
+ 4. **No-remote path:** if the repo has no remote, read ticket files from the provided local path; the Git agent reports DEGRADED for any issue it cannot reach.
37
38
 
38
39
  ---
39
40
 
@@ -56,11 +57,15 @@ If present, note its contents as `PREFERENCE_PROFILE`. This will be used to auto
56
57
 
57
58
  Determine the ticket source (in priority order):
58
59
  - A directory of ticket `.md` files (from `/devflow:dynamic-tickets` output)
59
- - A list of GitHub issue numbers/URLs
60
+ - A list of candidate issue references or issue URLs
60
61
  - Inline ticket descriptions passed as args
61
62
 
63
+ {issue_ref_grammar()}
64
+
62
65
  Read or note the tickets. The agents will read them in full; you need the list and any key constraints.
63
66
 
67
+ {issue_capture_contract()}
68
+
64
69
  ---
65
70
 
66
71
  ### CRITICAL (F4) — AskUserQuestion at the command boundary, NOT inside the workflow
@@ -68,9 +73,16 @@ Read or note the tickets. The agents will read them in full; you need the list a
68
73
  A workflow cannot pause mid-run. Open design decisions collected in `DECISIONS-NEEDED.md` are surfaced to the user via **AskUserQuestion AFTER the workflow returns** — at the command boundary. The workflow only WRITES the file; the command (you, the main model) reads it and asks.
69
74
 
70
75
  After the workflow completes:
71
- 1. Read the `decisionsNeededPath` returned by the workflow (e.g. `.devflow/docs/design/<slug>/<ts>/DECISIONS-NEEDED.md`). Always use the OUTDIR-scoped path the workflow wrote — never the flat `.devflow/docs/design/DECISIONS-NEEDED.md`.
72
- 2. First surface the **Auto-Resolved Decisions** section (decision → resolution → source) for audit, then surface ALL open **Decisions Needed** to the user in ONE batched `AskUserQuestion` (never one-at-a-time).
73
- 3. The user's answers feed into their own plan edits or a follow-up `/devflow:dynamic-plan` run.
76
+ 1. **Check each plan's test plan.** For every path in `planPaths`, copy that plan's `## Test Plan` section — the heading and its TP lines, up to the next `## ` heading — byte for byte into a fresh `mktemp` file with the Write tool, never through an interpolated shell string, and run:
77
+
78
+ ```bash
79
+ node "${DEVFLOW_DIR:-$HOME/.devflow}/scripts/verify-evidence.cjs" check tp <that file>; echo "exit=$?"
80
+ ```
81
+
82
+ Check the section, never the whole plan file: the plan's other `## ` headings are not evidence-file sections, so the script would parse the whole document as the plan. `exit=0` passes. Any other result names that plan `test plan malformed (<code>)` in your summary, `<code>` being the code the script printed on stderr; a plan with no `## Test Plan` section copies as an empty file, which the script refuses. Nothing is repaired: `/devflow:dynamic-build` passes no test plan from that plan.
83
+ 2. Read the `decisionsNeededPath` returned by the workflow (e.g. `.devflow/docs/design/<slug>/<ts>/DECISIONS-NEEDED.md`). Always use the OUTDIR-scoped path the workflow wrote — never the flat `.devflow/docs/design/DECISIONS-NEEDED.md`.
84
+ 3. First surface the **Auto-Resolved Decisions** section (decision → resolution → source) for audit, then surface ALL open **Decisions Needed** to the user in ONE batched `AskUserQuestion` (never one-at-a-time).
85
+ 4. The user's answers feed into their own plan edits or a follow-up `/devflow:dynamic-plan` run.
74
86
 
75
87
  State this explicitly in the workflow script as a comment: `// AskUserQuestion happens at the command boundary after this workflow returns — NOT here.`
76
88
 
@@ -92,6 +104,7 @@ export const meta = {
92
104
  const ticketSource = args.ticketSource || args[0] || "see task description";
93
105
  const DECISIONS_CONTEXT = args.decisionsContext || ""; // injected before authoring
94
106
  const PREFERENCE_PROFILE = args.preferenceProfile || ""; // injected before authoring
107
+ const TP_CONTRACT = args.tpContract || ""; // the "Test-plan line (TP)" contract below — its paragraph and four bullets — passed verbatim as tpContract when invoking the workflow, never pasted into this script (it holds backticks)
95
108
  const slug = args.slug || "wave";
96
109
  const ts = new Date().toISOString().slice(0,16).replace(/[-:T]/g, (c) => c === 'T' ? '_' : c === ':' ? '' : c);
97
110
  const OUTDIR = `.devflow/docs/design/${slug}/${ts}`;
@@ -100,7 +113,7 @@ const OUTDIR = `.devflow/docs/design/${slug}/${ts}`;
100
113
  const tickets = await phase("read-tickets", () =>
101
114
  agent(`Read all tickets from: ${ticketSource}
102
115
  For each ticket, extract: title, summary, wave, dependsOn, scope (in/out), acceptance criteria, open questions, and any existing implementation hints.
103
- If the source is a directory, read all .md files. If the source is GitHub issues, use gh issue view for each.
116
+ If the source is a directory, read all .md files. If the source is tracker issues, use the Git agent's fetch-issue or fetch-issues-batch operation.
104
117
  Return: array of ticket objects with all fields.`, { agentType: "Git" })
105
118
  );
106
119
 
@@ -130,15 +143,23 @@ Decisions context: ${DECISIONS_CONTEXT}
130
143
  Produce:
131
144
  1. List of improvements / gaps / edge cases / side-effects identified.
132
145
  2. Well-structured acceptance criteria (numbered, positive + negative, at least one negative per ticket). See the acceptance criteria contract below.
133
- 3. A test plan for the Test agent: for each criterion, scenario + setup + expected outcome + verification method.
146
+ 3. The test plan for the Test agent as TP lines, numbered from TP-1, at least one per criterion, each in exactly the shape of the test-plan line contract below. Map each scenario onto its line:
147
+ - the scenario, in plain words, is the line's scenario text — never a path, a reference, a mention or markup;
148
+ - the number of the criterion it covers is its (AC-m);
149
+ - its verification method is its method: a test committed to the suite is ci; a command run and read (a load test, a script) is local; a step performed and observed is manual;
150
+ - the paths it exercises, from the plan's affected files, are its files: globs.
151
+ Setup and expected outcome never go in a line: give them per TP in testScenarios.
134
152
  4. A list of genuine design decisions that require user input (not settled by the plan, the preference profile, or existing ADRs).
135
153
 
154
+ Test-plan line contract:
155
+ ${TP_CONTRACT}
156
+
136
157
  Acceptance criteria quality bar (apply strictly):
137
158
  - Vague criteria ("the feature should work correctly") are NOT acceptable — reject and rewrite.
138
159
  - Implementation-coupled criteria ("the function must call X") are NOT acceptable — test behavior, not implementation.
139
160
  - Untestable criteria are NOT acceptable.
140
161
 
141
- Return: { ticketTitle, improvements (array), acceptanceCriteria (array of numbered strings), testPlan (array of {scenario, setup, expectedOutcome, verificationMethod}), openDecisions (array) }.`, { agentType: "Evaluate" })
162
+ Return: { ticketTitle, improvements (array), acceptanceCriteria (array of numbered strings), testPlan (array of TP-line strings, TP-1 first), testScenarios (array of {tp, setup, outcome}), openDecisions (array) }.`, { agentType: "Evaluate" })
142
163
  ))
143
164
  );
144
165
 
@@ -190,7 +211,8 @@ For each ticket, write ${OUTDIR}/{ticket-slug}-plan.md containing:
190
211
  - ## Implementation Plan
191
212
  - The plan body (incorporate cross-plan amendments)
192
213
  - ## Acceptance Criteria (numbered, positive + negative)
193
- - ## Test Plan (per-criterion scenarios)
214
+ - ## Test Plan — the challenger's testPlan lines, verbatim, one per line, and nothing else: no prose, no blank line between them, no setup or outcome
215
+ - ## Test Scenarios — one line per TP, in TP order: TP-n: its setup, then its expected outcome, from testScenarios
194
216
  - ## Auto-Resolved Decisions (if any — list each as: decision → resolution → source)
195
217
 
196
218
  Then write ${OUTDIR}/DECISIONS-NEEDED.md:
@@ -228,14 +250,14 @@ The workflow returns:
228
250
 
229
251
  ```json
230
252
  {
231
- "planPaths": ["string — path to each per-ticket plan file"],
253
+ "planPaths": ["string — path to each per-ticket plan file; its ## Test Plan holds TP lines only, its ## Test Scenarios their setup and outcome"],
232
254
  "decisionsNeededPath": "string — path to DECISIONS-NEEDED.md",
233
255
  "decisionsNeededCount": "number — how many decisions need user input",
234
256
  "autoResolvedCount": "number — decisions auto-resolved by preference profile"
235
257
  }
236
258
  ```
237
259
 
238
- After the workflow returns: read `DECISIONS-NEEDED.md`. First briefly surface the **Auto-Resolved Decisions** section (decision → resolution → source) so silently-settled calls are visible and reversible, then surface ALL open **Decisions Needed** in ONE batched `AskUserQuestion` (never one-at-a-time). Do not ask questions mid-workflow — this is F4. If no preference profile was found, note in your summary: "no preference profile found — N decisions were surfaced that a profile might have auto-resolved; consider `/devflow:dynamic-profile`."
260
+ After the workflow returns: check each plan's test plan (step 1 of the F4 list above), then read `DECISIONS-NEEDED.md`. First briefly surface the **Auto-Resolved Decisions** section (decision → resolution → source) so silently-settled calls are visible and reversible, then surface ALL open **Decisions Needed** in ONE batched `AskUserQuestion` (never one-at-a-time). Do not ask questions mid-workflow — this is F4. If no preference profile was found, note in your summary: "no preference profile found — N decisions were surfaced that a profile might have auto-resolved; consider `/devflow:dynamic-profile`."
239
261
 
240
262
  ---
241
263