devflow-kit 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/cli/commands/learning.js +25 -5
  4. package/dist/commands/code-review.md +1 -1
  5. package/dist/commands/debug.md +1 -1
  6. package/dist/commands/dynamic-build.md +141 -94
  7. package/dist/commands/dynamic-plan.md +1 -1
  8. package/dist/commands/explore.md +1 -1
  9. package/dist/commands/implement.md +133 -133
  10. package/dist/commands/resolve.md +27 -16
  11. package/dist/commands/self-review.md +16 -11
  12. package/dist/core/agent-models.js +10 -7
  13. package/dist/core/learning-tuning-config.js +8 -0
  14. package/dist/core/plugins.js +7 -2
  15. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
  16. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
  17. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
  18. package/package.json +1 -1
  19. package/src/assets/agents/code.md +71 -18
  20. package/src/assets/agents/design.md +10 -0
  21. package/src/assets/agents/diagnose.md +16 -11
  22. package/src/assets/agents/evaluate.md +14 -25
  23. package/src/assets/agents/knowledge.md +5 -3
  24. package/src/assets/agents/learning.md +4 -6
  25. package/src/assets/agents/research.md +19 -0
  26. package/src/assets/agents/review.md +10 -0
  27. package/src/assets/agents/scrutinize.md +34 -10
  28. package/src/assets/agents/simplify.md +20 -0
  29. package/src/assets/agents/skim.md +4 -2
  30. package/src/assets/agents/synthesize.md +12 -0
  31. package/src/assets/agents/test.md +2 -2
  32. package/src/assets/agents/triage.md +7 -1
  33. package/src/assets/agents/validate.md +7 -2
  34. package/src/assets/commands/_partials/_engine.mds +21 -24
  35. package/src/assets/commands/_partials/_knowledge.mds +1 -1
  36. package/src/assets/commands/_partials/_plan_contract.mds +1 -1
  37. package/src/assets/commands/_partials/_wave.mds +8 -6
  38. package/src/assets/commands/code-review.mds +1 -1
  39. package/src/assets/commands/dynamic-build.mds +111 -63
  40. package/src/assets/commands/implement.mds +132 -132
  41. package/src/assets/commands/resolve.mds +26 -15
  42. package/src/assets/commands/self-review.mds +15 -10
  43. package/src/assets/mds/tracker/_common.mds +1 -1
  44. package/src/assets/scripts/ci-wait.cjs +636 -0
  45. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -1
  46. package/src/assets/scripts/hooks/background-memory-update +290 -9
  47. package/src/assets/scripts/release-trace.cjs +27 -10
  48. package/src/assets/skills/accessibility/SKILL.md +1 -1
  49. package/src/assets/skills/apply-decisions/SKILL.md +12 -82
  50. package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
  51. package/src/assets/skills/architecture/SKILL.md +1 -1
  52. package/src/assets/skills/boundary-validation/SKILL.md +1 -1
  53. package/src/assets/skills/complexity/SKILL.md +1 -1
  54. package/src/assets/skills/compliance/SKILL.md +1 -1
  55. package/src/assets/skills/consistency/SKILL.md +1 -1
  56. package/src/assets/skills/database/SKILL.md +1 -1
  57. package/src/assets/skills/dependencies/SKILL.md +1 -1
  58. package/src/assets/skills/dependency-research/SKILL.md +3 -6
  59. package/src/assets/skills/design-review/SKILL.md +1 -1
  60. package/src/assets/skills/docs-framework/SKILL.md +1 -1
  61. package/src/assets/skills/documentation/SKILL.md +1 -1
  62. package/src/assets/skills/gap-analysis/SKILL.md +1 -1
  63. package/src/assets/skills/git/SKILL.md +1 -1
  64. package/src/assets/skills/go/SKILL.md +1 -1
  65. package/src/assets/skills/java/SKILL.md +1 -1
  66. package/src/assets/skills/patterns/SKILL.md +1 -1
  67. package/src/assets/skills/performance/SKILL.md +1 -1
  68. package/src/assets/skills/python/SKILL.md +1 -1
  69. package/src/assets/skills/qa/SKILL.md +1 -3
  70. package/src/assets/skills/quality-gates/SKILL.md +9 -12
  71. package/src/assets/skills/quality-gates/references/report-template.md +20 -20
  72. package/src/assets/skills/react/SKILL.md +1 -1
  73. package/src/assets/skills/regression/SKILL.md +1 -1
  74. package/src/assets/skills/reliability/SKILL.md +1 -1
  75. package/src/assets/skills/research-codebase/SKILL.md +1 -1
  76. package/src/assets/skills/research-competitor/SKILL.md +1 -1
  77. package/src/assets/skills/research-external/SKILL.md +1 -1
  78. package/src/assets/skills/research-technology/SKILL.md +1 -1
  79. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  80. package/src/assets/skills/rust/SKILL.md +1 -1
  81. package/src/assets/skills/security/SKILL.md +1 -1
  82. package/src/assets/skills/software-design/SKILL.md +1 -1
  83. package/src/assets/skills/test-driven-development/SKILL.md +9 -29
  84. package/src/assets/skills/testing/SKILL.md +1 -1
  85. package/src/assets/skills/typescript/SKILL.md +1 -1
  86. package/src/assets/skills/ui-design/SKILL.md +1 -1
  87. package/src/assets/skills/worktree-support/SKILL.md +3 -55
  88. package/src/assets/skills/worktree-support/references/discovery.md +48 -0
  89. package/src/assets/skills/worktree-support/references/roots.md +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to Devflow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.3.0] - 2026-10-09
9
+
10
+ ### Changed
11
+
12
+ - **Agents ship chosen tiers, efforts and tool sets** ([#422](https://github.com/dean0x/devflow/issues/422)). Fifteen agents now carry a deliberate model, effort, tool policy and `omitClaudeMd` setting, held by a guard to one table. Skim moves from `sonnet` to `haiku`, the only model change. Every agent except Learning, Git and Tracker now ships an effort and no longer follows your session's effort: `high` for Code, Design, Review and Triage, `medium` for the rest. `devflow agents --set <agent> --effort inherit` restores the old behaviour for one agent. Review ships `opus` at `high` effort: an A/B run of the `/code-review` focus agents over two seeded PRs found every seeded bug at `high` that it found at `xhigh`, for about 43% lower cost. Learning keeps its model, its absent effort and its CLAUDE.md context: the cheaper candidates fell short of the capture bar, and without that context it stops retiring entries as Encoded. Code, Research, Scrutinize and Simplify deny one shared list of orchestration and host-UI tools (Scrutinize and Simplify add `Skill`) and keep their MCP and web access; every other agent holds an allowlist. Synthesize and Skim omit CLAUDE.md. Preloads leave where the body never reads them: `testing` from Validate and Test, `software-design` from Evaluate, the five pattern skills from Diagnose, which now loads its focus's skills on demand, and `apply-decisions` from Learning. `devflow learning --configure` marks as recommended the model the Learning agent ships, read from its frontmatter, so a change of the shipped tier moves the recommendation with it.
13
+ - **The gates run in a new order, and a script waits on CI** ([#421](https://github.com/dean0x/devflow/issues/421)). `/implement` now runs Simplify, Scrutinize, Evaluate, then one Validate over the final HEAD, then Test, then pushes and gates CI; a changed-only Validate follows only when a QA fix committed. The alignment and QA loops no longer spawn a Validate of their own, and `/self-review` validates whenever HEAD moved, so a Simplify-only commit is no longer skipped. `ci-wait.cjs` is a head-bound foreground CI wait that replaces re-spawning the Git agent once a minute: it binds the verdict to the pushed head, waits inside node and prints one line. One wait is capped at 570 seconds, and each CI gate allows at most 3 waits and 2 fixes, spawning Code only on a failing verdict. Scrutinize reports one status vocabulary, PASS, FIXED or BLOCKED (READY is gone), and each gate duty has one owner: Naming and Consistency belong to Simplify. In the dynamic-build engine both Gate 1 passes end with Validate. The Git agent now merges locally and validates nothing; the workflow runs a post-merge Validate after every merge, counts the merge as kept only on PASS, and undoes a red merge with `reset --keep`; a refused undo halts the wave and names the red HEAD, and a first Gate 1 that ends ESCALATED or BLOCKED returns early. `/resolve` waits on CI through the same script.
14
+ - **Skills cost less** ([#423](https://github.com/dean0x/devflow/issues/423)). Code preloads six skills (`git`, `testing`, `test-driven-development`, `worktree-support`, `apply-feature-knowledge` and `apply-decisions`) where it preloaded ten: 25,578 bytes of skill text, down from 53,450. `software-design`, `patterns`, `boundary-validation` and `dependency-research` load on demand from a table with one row per operating mode. Every Code spawn now opens its prompt with `OPERATION: <mode>`, including two new modes, `ci-fix` (fix the failing CI checks the gate names) and `edit` (a mechanical change that adds no behaviour), and the orchestrator charter's direct Code delegations do the same. `apply-decisions` reads an entry by finding its heading with `command grep -nF` and then Reading only that section by offset, instead of opening the whole file. `worktree-support` keeps its discovery algorithm in a reference that `/code-review` and `/resolve` read when they need it, rather than in every agent's preload. Skill descriptions are capped by who reads them, at 90 characters for the agent-internal skills and 200 for the user-trigger ones, and 38 shrank. The Git agent's loaded sets are 1,704 characters smaller.
15
+ - **The memory worker reads `agents.memory` and runs lean** ([#424](https://github.com/dean0x/devflow/issues/424)). `background-memory-update` takes its model and effort from `agents.memory` (`devflow agents --set memory ...`), checking each field on its own and falling back to the shipped default for one it cannot use. A bounded `claude --version` probe picks the argv. On Claude Code 2.1.286 or later the worker runs with `--safe-mode`, `--tools Write`, an empty strict MCP config, `--max-turns 3` and an explicit `--effort`, with any inherited `CLAUDE_CODE_EFFORT_LEVEL` cleared so the effort it names is the one that applies; below that, or when the probe fails, it runs a legacy argv that still limits the worker to Write and keeps CLAUDE.md and auto-memory out, but passes no effort, safe mode, MCP isolation or turn cap. A staged file that cannot be removed stops the run before any model call, which is what makes a Write-only tool set safe.
16
+
17
+ ### Fixed
18
+
19
+ - **Only closing keywords ship an issue in a release.** `Closes`, `Fixes` and `Resolves` ship an issue; a `Refs #N` commit mentions one without closing it, yet it reached the release's Shipped Issues, its back-links and its milestone. It no longer does. The release trace map still counts `Refs` as traced, because that map asks only whether a commit names an issue at all.
20
+
21
+ ---
22
+
8
23
  ## [3.2.0] - 2026-10-08
9
24
 
10
25
  ### Added
@@ -1517,6 +1532,7 @@ devflow init
1517
1532
  ---
1518
1533
 
1519
1534
  [Unreleased]: https://github.com/dean0x/devflow/compare/v2.0.0...HEAD
1535
+ [3.3.0]: https://github.com/dean0x/devflow/compare/v3.2.0...v3.3.0
1520
1536
  [3.2.0]: https://github.com/dean0x/devflow/compare/v3.1.0...v3.2.0
1521
1537
  [3.1.0]: https://github.com/dean0x/devflow/compare/v3.0.1...v3.1.0
1522
1538
  [3.0.1]: https://github.com/dean0x/devflow/compare/v3.0.0...v3.0.1
package/README.md CHANGED
@@ -30,9 +30,9 @@ you: /implement (hand it the plan)
30
30
 
31
31
  Git branch feat/42-rate-limit-upload
32
32
  Code implements the plan — your learned decisions, pitfalls, and feature knowledge preloaded
33
- Validate build ✓ typecheck ✓ lint ✓ tests ✓
34
33
  Simplify · Scrutinize cleanup pass, then 9-pillar quality gate
35
34
  Evaluate implementation matches the original request ✓
35
+ Validate build ✓ typecheck ✓ lint ✓ tests ✓
36
36
  Test 5/5 QA scenarios pass → PR opened
37
37
 
38
38
  you: /code-review
@@ -4,6 +4,7 @@ import * as path from 'path';
4
4
  import * as p from '@clack/prompts';
5
5
  import color from 'picocolors';
6
6
  import { getLearningDir, getLearningTuningConfigPath, } from '../../core/project-paths.js';
7
+ import { loadShippedAgentDefaults } from '../../core/agent-models.js';
7
8
  import { readMachineFeature, writeMachineFeature } from '../../core/feature-switch.js';
8
9
  import { loadSettingsModule, narrowedSwitchLabel, personalConfigTrackedWarning } from '../../core/evidence-policy.js';
9
10
  import { getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
@@ -273,15 +274,34 @@ export const CONFIGURE_SCOPE_OPTIONS = [
273
274
  hint: 'All projects (~/.devflow/learning.json); a devflow agents Learning mapping takes precedence over it',
274
275
  },
275
276
  ];
277
+ /** The models `devflow learning --configure` offers, each with what it is good for. */
278
+ const LEARNING_MODEL_CHOICES = [
279
+ { value: 'opus', label: 'Opus', note: 'highest quality for detection + curation judgment' },
280
+ { value: 'sonnet', label: 'Sonnet', note: 'good balance of quality and speed' },
281
+ { value: 'haiku', label: 'Haiku', note: 'fastest, lowest cost' },
282
+ ];
283
+ /**
284
+ * D-LEARNING-SHIPPED-ROW: the model offered as "Recommended" is the one the Learning
285
+ * agent ships with, the `model:` line of its frontmatter. It is read from there and
286
+ * written nowhere else, so a change of the shipped tier moves the recommendation with
287
+ * it and this list never has to be edited. A shipped model outside the three offered
288
+ * (or none) recommends nothing.
289
+ */
290
+ export function learningModelOptions(shippedModel) {
291
+ return LEARNING_MODEL_CHOICES.map(choice => ({
292
+ value: choice.value,
293
+ label: choice.label,
294
+ hint: choice.value === shippedModel
295
+ ? `Recommended — ${choice.note}`
296
+ : choice.note.charAt(0).toUpperCase() + choice.note.slice(1),
297
+ }));
298
+ }
276
299
  async function handleConfigure() {
277
300
  p.intro(color.bgCyan(color.black(' Learning Configuration ')));
301
+ const shippedModel = (await loadShippedAgentDefaults()).learning?.model;
278
302
  const model = await p.select({
279
303
  message: 'Model for decision detection',
280
- options: [
281
- { value: 'opus', label: 'Opus', hint: 'Recommended — highest quality for detection + curation judgment' },
282
- { value: 'sonnet', label: 'Sonnet', hint: 'Good balance of quality and speed' },
283
- { value: 'haiku', label: 'Haiku', hint: 'Fastest, lowest cost' },
284
- ],
304
+ options: learningModelOptions(shippedModel),
285
305
  });
286
306
  if (p.isCancel(model)) {
287
307
  p.cancel('Configuration cancelled.');
@@ -24,7 +24,7 @@ Run a comprehensive code review of the current branch by spawning parallel revie
24
24
 
25
25
  1. **Discover reviewable worktrees** using the `devflow:worktree-support` skill discovery algorithm:
26
26
  - Run `git worktree list --porcelain` → parse, filter (skip protected/detached/mid-rebase), dedup by branch, sort by recent commit
27
- - See the `devflow:worktree-support` skill for the full 7-step algorithm and canonical protected branch list
27
+ - Invoke the `devflow:worktree-support` skill, then Read its `references/discovery.md` from the skill's base directory for the full 7-step algorithm; the canonical protected branch list stays in the skill
28
28
  2. **If `--path` flag provided:** use only that worktree, skip discovery
29
29
  **`--path` validation**: Before proceeding, verify the path exists as a directory and appears in `git worktree list` output. If not: report error and stop.
30
30
  3. **If only 1 reviewable worktree** (the common case): proceed as single-worktree flow — zero behavior change
@@ -244,7 +244,7 @@ If the settings line says `KNOWLEDGE=off`, skip write-back entirely. The machine
244
244
  **Step 2 — Evaluate whether write-back is warranted:**
245
245
 
246
246
  Only proceed if **at least one** of these is true:
247
- - This workflow changed files in a directory that is documented by an existing feature knowledge base (a documented area changed).
247
+ - This workflow changed files in a directory that is documented by an existing feature knowledge base (a documented area changed). Knowledge bases are written through at that point, never on a background schedule.
248
248
  - This workflow surfaced durable, cross-cutting knowledge about a codebase area that would help future agents working in the same area — patterns, anti-patterns, integration points, gotchas not visible from a single file read.
249
249
 
250
250
  **Never spawn unconditionally.** If neither condition is met, skip write-back silently.
@@ -322,7 +322,8 @@ if (BRANCH === "(none)") {
322
322
 
323
323
  // Phase 2: Implement
324
324
  await phase("implement", () =>
325
- agent(`Implement the following ticket on branch ${BRANCH}:
325
+ agent(`OPERATION: implement
326
+ Implement the following ticket on branch ${BRANCH}:
326
327
 
327
328
  ${TICKET}
328
329
 
@@ -343,39 +344,51 @@ After implementing, commit your changes with a conventional-commit message and r
343
344
  - Any open questions or blockers`, { agentType: "Code" })
344
345
  );
345
346
 
346
- // Phase 3: Gate 1 — post-code pipeline
347
+ // Phase 3: Gate 1 #1 — post-code pipeline: Simplify agent → Scrutinize agent → Validate agent.
348
+ // Validate runs last and unconditionally, so its one full run covers every commit the pass made (see gate1_postcode()).
347
349
  const gate1 = await phase("gate1", async () => {
348
- // Gate 1 #1 — runs once, immediately after the initial implementation.
350
+ await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
351
+
352
+ const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
353
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
354
+ // Anything but an explicit PASS or FIXED is a stop: a Scrutinize that returned no status, or one this skeleton does not know, has not accepted the code.
355
+ if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
356
+ return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Scrutinize agent returned no recognised status: counted as BLOCKED" };
357
+ }
358
+
349
359
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH}.
350
360
  Follow your Running commands block for every build and test command.
351
- Report: PASS or FAIL with details.`, { agentType: "Validate" });
361
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
352
362
 
353
- if (validation.verdict === "FAIL") {
354
- // Up to 2 fix attempts
363
+ // Anything but an explicit PASS is a failure: a Validate that returned no verdict has not validated the branch.
364
+ if (validation?.verdict !== "PASS") {
365
+ let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
366
+ // Up to 2 fix attempts, each followed by a Validate re-run
355
367
  for (let attempt = 1; attempt <= 2; attempt++) {
356
- await agent(`Fix the validation failures on branch ${BRANCH}:
357
- ${validation.details}
368
+ await agent(`OPERATION: validation-fix
369
+ Fix the validation failures on branch ${BRANCH}:
370
+ ${failureDetails}
358
371
  ISSUE_NUMBER: ${ISSUE_NUMBER}
359
372
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
360
373
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
361
374
  Commit fixes with conventional-commit message.`, { agentType: "Code" });
362
- const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH}. Report: PASS or FAIL.`, { agentType: "Validate" });
363
- if (recheck.verdict === "PASS") break;
364
- if (attempt === 2) return { verdict: "ESCALATED", reason: "Validation exhausted after 2 Code agent fix attempts" };
375
+ const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} after fix attempt ${attempt} (follow your Running commands block).
376
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
377
+ if (recheck?.verdict === "PASS") break;
378
+ failureDetails = recheck?.details || failureDetails;
379
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Validation exhausted after 2 Code agent fix attempts" };
365
380
  }
366
381
  }
367
382
 
368
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
369
-
370
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
371
-
372
- if (scrutiny.codeChanged) {
373
- await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes). Report: PASS or FAIL.`, { agentType: "Validate" });
374
- }
375
-
376
383
  return { verdict: "PASS" };
377
384
  });
378
385
 
386
+ // Gate 1 #1 stop, before Gate 2: an exhausted Validate or a BLOCKED Scrutinize leaves a broken or unfinished build,
387
+ // so Gate 2 and the review pass never run on it — a correctness stop, like the ticket-link and branch stops.
388
+ if (gate1.verdict === "ESCALATED") {
389
+ return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK, escalations: [{ type: gate1.type, description: gate1.reason }] };
390
+ }
391
+
379
392
  // Phase 4: Gate 2 — acceptance gate (once, before review pass)
380
393
  const gate2 = await phase("gate2", async () => {
381
394
  if (!PLAN && !CRITERIA && !TEST_PLAN) {
@@ -384,21 +397,19 @@ const gate2 = await phase("gate2", async () => {
384
397
 
385
398
  let evalVerdict = "SKIPPED";
386
399
  if (PLAN) {
387
- const panel = await parallel([
388
- () => agent(`Acceptance-criteria evaluator: does the implementation on branch ${BRANCH} satisfy each numbered criterion, INCLUDING negative criteria?
400
+ const evaluation = await agent(`Acceptance and scope check on branch ${BRANCH}. Answer both questions:
401
+ 1. Does the implementation satisfy each numbered criterion, INCLUDING negative criteria (what it must NOT do)?
402
+ 2. Did the Code agent introduce any unplanned changes, smuggled anti-features, or drift from the plan's intent?
389
403
  Plan: ${PLAN}
390
404
  Criteria: ${CRITERIA || "see plan"}
391
- Report: PASS or FAIL per criterion with rationale.`, { agentType: "Evaluate" }),
392
- () => agent(`Scope/intent-drift evaluator: did the Code agent on branch ${BRANCH} introduce any unplanned changes, smuggled anti-features, or drift from the plan's intent?
393
- Plan: ${PLAN}
394
- Report: PASS or FAIL with rationale.`, { agentType: "Evaluate" }),
395
- ]);
396
- evalVerdict = panel.every(p => p.verdict === "PASS") ? "PASS" : "FAIL";
405
+ Return: {"verdict": "PASS" | "FAIL", "rationale": "..."} — FAIL if either answer is no, naming the failing criterion or the drift.`, { agentType: "Evaluate" });
406
+ evalVerdict = evaluation?.verdict === "PASS" ? "PASS" : "FAIL";
397
407
  if (evalVerdict === "FAIL") {
398
408
  // fix-and-continue — no re-evaluate, no inline Gate 1. The final Gate 1 (#2)
399
409
  // after the review pass is the build gate before the branch is handed back.
400
- await agent(`Fix the alignment issues identified by the Evaluate agent panel on branch ${BRANCH}:
401
- ${panel.filter(p => p.verdict === "FAIL").map(p => p.rationale).join("\n")}
410
+ await agent(`OPERATION: alignment-fix
411
+ Fix the alignment issues identified by the Evaluate agent on branch ${BRANCH}:
412
+ ${evaluation?.rationale || "see the Evaluate agent's report"}
402
413
  ISSUE_NUMBER: ${ISSUE_NUMBER}
403
414
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
404
415
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
@@ -417,7 +428,8 @@ Cover: functionality, API contracts, performance, and cover every TEST_PLAN scen
417
428
  testVerdict = testResult.verdict;
418
429
  if (testVerdict === "FAIL") {
419
430
  // fix-and-continue — no re-test, no inline Gate 1.
420
- await agent(`Fix the failing acceptance test scenarios on branch ${BRANCH}:
431
+ await agent(`OPERATION: qa-fix
432
+ Fix the failing acceptance test scenarios on branch ${BRANCH}:
421
433
  ${testResult.failures}
422
434
  ISSUE_NUMBER: ${ISSUE_NUMBER}
423
435
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
@@ -444,7 +456,7 @@ const reviewResult = await phase("review", async () => {
444
456
  const chunkSize = 5;
445
457
  const reviewThunks = reviewFocuses.map(focus =>
446
458
  () => agent(`${focus.charAt(0).toUpperCase() + focus.slice(1)} review. ${reviewScope}
447
- Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
459
+ Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
448
460
  );
449
461
 
450
462
  let rawReviews = [];
@@ -461,7 +473,7 @@ Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "fil
461
473
  const focus = reviewFocuses[i];
462
474
  if (!r || r.reviewed !== true) {
463
475
  // Dead Review agent: retry once sequentially
464
- const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
476
+ const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
465
477
  if (!retry || retry.reviewed !== true) {
466
478
  coverageGaps.push(focus);
467
479
  log(`review coverage incomplete: ${focus}`);
@@ -526,13 +538,18 @@ ${JSON.stringify(allFindings.map((f, i) => ({ index: i, description: f.descripti
526
538
  // Distinct-file groups are paced in chunks of FIX_CHUNK — same pacing bar as the Review spawn
527
539
  // path — to avoid launching all Code agents at once on a large branch (reliability: explicit bound).
528
540
  const FIX_CHUNK = 5;
529
- const fixThunks = Object.entries(fileGroups).map(([fileKey, findings]) => async () => {
541
+ // issue-fix SCOPE per finding: CRITICAL or HIGH takes the Careful protocol (failing regression test first); anything else is Standard.
542
+ const CAREFUL_SEVERITIES = new Set(["critical", "high"]);
543
+ const fixThunks = Object.values(fileGroups).map(findings => async () => {
530
544
  const chunkResults = [];
531
545
  for (let i = 0; i < findings.length; i += MAX_BATCH) {
532
546
  const chunk = findings.slice(i, i + MAX_BATCH);
533
- const r = await agent(`Fix the following confirmed review findings on branch ${BRANCH}${fileKey.startsWith("__nofile") ? "" : ` in ${fileKey}`}:
534
- ${chunk.map(f => `- ${f.description} (${f.severity})`).join("\n")}
535
-
547
+ const r = await agent(`OPERATION: issue-fix
548
+ Fix the following confirmed review findings on branch ${BRANCH}:
549
+ ISSUES:
550
+ ${chunk.map((f, n) => `${n + 1}. ${f.file ? `${f.file}${f.line ? `:${f.line}` : ""} — ` : ""}${f.description} (${f.severity})`).join("\n")}
551
+ SCOPE: ${chunk.map((f, n) => `${n + 1}=${CAREFUL_SEVERITIES.has(String(f.severity).toLowerCase()) ? "Careful" : "Standard"}`).join(", ")}
552
+ PUSH: false
536
553
  ISSUE_NUMBER: ${ISSUE_NUMBER}
537
554
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
538
555
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
@@ -571,38 +588,40 @@ Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<
571
588
  return { survivingFindings: notAddressed, fixedFindings, coverageGaps };
572
589
  });
573
590
 
574
- // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (full Validate agent → Simplify agent → Scrutinize agent).
591
+ // Phase 5.5: Gate 1 #2 — FINAL post-fix gate (Simplify agent → Scrutinize agent → full Validate agent, Validate last and unconditional).
575
592
  // Runs ONCE, after the review pass. Inside the pass the Code agent self-verified its own fixes;
576
593
  // this is the build gate before the branch is handed back. See gate1_postcode() cadence.
577
594
  const gate1Final = await phase("gate1-final", async () => {
595
+ await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
596
+
597
+ const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
598
+ Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
599
+ if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
600
+ return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Final Gate 1 Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Final Gate 1 Scrutinize agent returned no recognised status: counted as BLOCKED" };
601
+ }
602
+
578
603
  const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH} (final gate after all fixing).
579
604
  Follow your Running commands block for every build and test command.
580
- Report: PASS or FAIL with details.`, { agentType: "Validate" });
605
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
581
606
 
582
- if (validation.verdict === "FAIL") {
583
- let failureDetails = validation.details;
607
+ if (validation?.verdict !== "PASS") {
608
+ let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
584
609
  for (let attempt = 1; attempt <= 2; attempt++) {
585
- await agent(`Fix the final validation failures on branch ${BRANCH}:
610
+ await agent(`OPERATION: validation-fix
611
+ Fix the final validation failures on branch ${BRANCH}:
586
612
  ${failureDetails}
587
613
  ISSUE_NUMBER: ${ISSUE_NUMBER}
588
614
  ISSUE_PR_LINK: ${ISSUE_PR_LINK}
589
615
  COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
590
616
  Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code" });
591
- const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (follow your Running commands block). Report: PASS or FAIL.`, { agentType: "Validate" });
592
- if (recheck.verdict === "PASS") break;
593
- failureDetails = recheck.details || failureDetails;
594
- if (attempt === 2) return { verdict: "ESCALATED", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
617
+ const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (final gate, after fix attempt ${attempt}; follow your Running commands block).
618
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
619
+ if (recheck?.verdict === "PASS") break;
620
+ failureDetails = recheck?.details || failureDetails;
621
+ if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
595
622
  }
596
623
  }
597
624
 
598
- await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
599
-
600
- const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
601
-
602
- if (scrutiny.codeChanged) {
603
- await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes; follow your Running commands block). Report: PASS or FAIL.`, { agentType: "Validate" });
604
- }
605
-
606
625
  return { verdict: "PASS" };
607
626
  });
608
627
 
@@ -631,7 +650,7 @@ Write a concise report. Present only surviving findings as outstanding — never
631
650
  survivingFindings: reviewResult.survivingFindings || [], fixedFindings: reviewResult.fixedFindings || [],
632
651
  reviewCoverage: { failedFocuses: coverageGaps, complete: coverageGaps.length === 0 },
633
652
  escalations: [
634
- ...(gate1Final.verdict === "ESCALATED" ? [{ type: "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
653
+ ...(gate1Final.verdict === "ESCALATED" ? [{ type: gate1Final.type || "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
635
654
  ...coverageGaps.map(focus => ({ type: "review-coverage-incomplete", description: `review coverage incomplete: ${focus}` })),
636
655
  ],
637
656
  gate2, report,
@@ -679,12 +698,12 @@ Run builds, typechecks, lints and tests in the foreground, each with an explicit
679
698
  The standard implementation unit for one ticket. Run in order:
680
699
 
681
700
  ```
682
- Code(agentType:"Code", prompt: full task + plan + DECISIONS_CONTEXT + handoff if sequential)
701
+ Code(agentType:"Code", prompt: "OPERATION: implement" first line, then full task + plan + DECISIONS_CONTEXT + handoff if sequential)
683
702
  → gate1_postcode()
684
703
  → gate2_acceptance() ← Gate 2 runs HERE — before the review pass, not after
685
704
  ```
686
705
 
687
- The Code agent prompt must include: task description, implementation plan (if one exists), relevant DECISIONS_CONTEXT (the index you loaded before authoring), the compliance lens (`COMPLIANCE_FRAMEWORKS` — every Code prompt carries it, fix prompts included), and any PRIOR_PHASE_SUMMARY / HANDOFF_FILE for sequential multi-phase tickets.
706
+ Every Code agent prompt opens with `OPERATION: <mode>` as its first line (`implement`, `issue-fix`, `validation-fix`, `alignment-fix` or `qa-fix`). It must include: task description, implementation plan (if one exists), relevant DECISIONS_CONTEXT (the index you loaded before authoring), the compliance lens (`COMPLIANCE_FRAMEWORKS` — every Code prompt carries it, fix prompts included), and any PRIOR_PHASE_SUMMARY / HANDOFF_FILE for sequential multi-phase tickets.
688
707
 
689
708
  Gate 2 runs at implementation acceptance — this matches devflow's deliberate placement: "evaluation is part of implementation acceptance, not post-review" (§6.1).
690
709
 
@@ -692,21 +711,21 @@ Gate 2 runs at implementation acceptance — this matches devflow's deliberate p
692
711
 
693
712
  ORDER IS LOAD-BEARING. Run exactly in this sequence:
694
713
 
695
- 1. **Validate agent** — build / typecheck / lint / test
714
+ 1. **Simplify agent** — reduce complexity, remove duplication
715
+ 2. **Scrutinize agent** — 9-pillar self-review (deep structural analysis); returns `{"status": "PASS" | "FIXED" | "BLOCKED"}`
716
+ - BLOCKED (a P0 issue cannot be fixed in scope), or a missing or unrecognised status, which counts as BLOCKED → stop the pass and escalate as `scrutiny-blocked`; Validate does not run on code Scrutinize could not accept
717
+ 3. **Validate agent** — build / typecheck / lint / test; returns `{"verdict": "PASS" | "FAIL", "details": "..."}`. It runs LAST and UNCONDITIONALLY: one full run over the HEAD that Simplify and Scrutinize left, so it covers their commits whether or not Scrutinize changed code
696
718
  - Build/test commands follow `build_execution_doctrine()`: the Validate agent's Running commands block, foreground under an explicit Bash timeout.
697
- - FAIL → Code agent fix (max 2 retries) → re-run Validate agent
698
- - If still FAIL after 2 retries → escalate (do not loop endlessly)
699
- 2. **Simplify agent** — reduce complexity, remove duplication
700
- 3. **Scrutinize agent** — 9-pillar self-review (deep structural analysis)
701
- - If Scrutinize agent changed code → re-run Validate agent (verify the Scrutinize agent's edits compile/pass)
719
+ - FAIL → Code agent fix (max 2 retries), each followed by a Validate agent re-run
720
+ - If still FAIL after 2 retries → escalate as `validation-exhausted` (do not loop endlessly)
702
721
 
703
722
  **Gate 1 contains NO Evaluate agent and NO Test agent.** Those are Gate 2 only.
704
723
 
705
724
  Depth scales to change size + budget: a trivial one-line fix warrants a lighter pass; a multi-file refactor warrants the full depth.
706
725
 
707
726
  **Cadence — Gate 1 runs at exactly TWO points per ticket:**
708
- 1. **Gate 1 #1** — immediately after the initial Code agent implementation (inside `implement_bundle()`).
709
- 2. **Gate 1 #2** — the FINAL gate, after ALL Gate-2 fixes AND the entire review pass have completed.
727
+ 1. **Gate 1 #1** — immediately after the initial Code agent implementation (inside `implement_bundle()`). An escalated result — Validate exhausted, or Scrutinize BLOCKED — returns early: the ticket's engine returns `ESCALATED` with one escalation, and Gate 2 and the review pass never run on a broken or unfinished build.
728
+ 2. **Gate 1 #2** — the FINAL gate, after ALL Gate-2 fixes AND the entire review pass have completed. Scrutinize BLOCKED here is `ESCALATED` as well.
710
729
 
711
730
  It does NOT run inside the review pass, nor after each individual Gate-2 / review / QA fix. At those points the fixing Code agent self-verifies its OWN build compiles (see `review_pass()` and the Code agent's Running commands block). The final Gate 1 #2 is the invariant that all written code passes before merge.
712
731
 
@@ -716,30 +735,27 @@ Gate 2 fires ONCE: after the implement-bundle and BEFORE the review pass. It doe
716
735
 
717
736
  Gate 2 inputs are produced by `/devflow:dynamic-plan`'s plan-challenge step — the acceptance criteria and test plan written for the Evaluate agent and Test agent.
718
737
 
719
- **Evaluate agent panel** (only if a plan exists):
720
- - Run `evaluator_panel()` — see that block for the panel composition
721
- - If any critical lens returns MISALIGNED: fix-and-continue — the demanded fixes are applied by a Code agent that self-verifies its own build (batched per the review-pass batching doctrine if numerous). The recorded verdict becomes `FAIL-FIXED` (issues found, fixes applied, not re-evaluated by design); Gate 2 then proceeds. In SINGLE mode the run reports it as `UNVERIFIED`, never PASS.
738
+ **Evaluate agent** (only if a plan exists):
739
+ - Run `evaluator_panel()` — see that block for the one spawn and the two lenses it names
740
+ - If the Evaluate agent returns FAIL (either lens failed): fix-and-continue — the demanded fixes are applied by a Code agent that self-verifies its own build (batched per the review-pass batching doctrine if numerous). The recorded verdict becomes `FAIL-FIXED` (issues found, fixes applied, not re-evaluated by design); Gate 2 then proceeds. In SINGLE mode the run reports it as `UNVERIFIED`, never PASS.
722
741
 
723
742
  **Test agent** (only if acceptance criteria or a test plan exist):
724
743
  - Scenario-based acceptance tests covering functionality, API contracts, performance
725
744
  - FAIL → fix-and-continue — a Code agent applies the demanded fixes and self-verifies its own build. The recorded verdict becomes `FAIL-FIXED`; Gate 2 then proceeds. In SINGLE mode the run reports it as `UNVERIFIED`, never PASS.
726
745
 
727
746
  **When Gate 2 inputs are absent:**
728
- - No plan → skip Evaluate agent panel silently (note in output: "Gate 2 Evaluate agent skipped — no plan available")
747
+ - No plan → skip the Evaluate agent silently (note in output: "Gate 2 Evaluate agent skipped — no plan available")
729
748
  - No acceptance criteria and no test plan → skip Test agent silently (note in output: "Gate 2 Test agent skipped — no criteria available")
730
749
  - Build proceeds Gate-1-only. Never refuse to build; never force-generate fake criteria. Trust the user.
731
750
 
732
- ### Evaluate agent panel (§12 — diverse-lens verification)
751
+ ### Evaluate agent spawn (one agent, two lenses)
733
752
 
734
- Run 2–3 Evaluate agents with DISTINCT lenses. Diversity not redundancy — each agent asks a different question:
753
+ Run ONE Evaluate agent whose prompt names both lenses, so a ticket pays for one spawn and one read of the plan:
735
754
 
736
- 1. **Acceptance-criteria evaluator** — "Does the implementation satisfy each numbered acceptance criterion, INCLUDING the negative criteria (what it must NOT do)?"
737
- 2. **Scope / intent-drift evaluator** — "Did the Code agent smuggle in unplanned changes, deviate from the plan's intent, or introduce anti-features?"
738
- 3. **Cross-ticket-consistency evaluator** — (wave-only; skip for single-ticket runs) "Does this ticket honor the API contracts and invariants that other wave tickets depend on?"
755
+ 1. **Acceptance criteria** — "Does the implementation satisfy each numbered acceptance criterion, INCLUDING the negative criteria (what it must NOT do)?"
756
+ 2. **Scope / intent drift** — "Did the Code agent smuggle in unplanned changes, deviate from the plan's intent, or introduce anti-features?"
739
757
 
740
- Gate = **all-critical-must-pass**: a single critical FAIL from any lens blocks acceptance.
741
-
742
- Keep the panel to 2–3 agents. Do not spawn redundant agents asking the same question — that is cost with no diversity benefit.
758
+ It returns `{"verdict": "PASS" | "FAIL", "rationale": "..."}`: FAIL when either lens fails, the rationale naming the failing criterion or the drift. A FAIL on either lens blocks acceptance. A wave ticket runs this same skeleton through `runSingleTicketEngine`, so it gets the same single spawn.
743
759
 
744
760
  ### Acceptance criteria + test plan contract
745
761
 
@@ -784,7 +800,7 @@ Every line of a `## Test Plan` section, or of a PR's test-plan block, follows th
784
800
 
785
801
  #### Consumption by Gate 2
786
802
 
787
- The Evaluate agent panel receives: the per-ticket plan + the numbered acceptance criteria (positive and negative).
803
+ The Evaluate agent receives: the per-ticket plan + the numbered acceptance criteria (positive and negative).
788
804
 
789
805
  The Test agent receives: the test plan's TP lines, once `check tp` has admitted them.
790
806
 
@@ -846,7 +862,7 @@ If survivors remain: batch the confirmed findings for fixing: group findings by
846
862
 
847
863
  1. **Code is written ONLY by Code agents.** No other agent type writes code — not Review agent, not Evaluate agent.
848
864
  2. **Findings are verified before any fix is written.** The adversarial verification step is not optional; unverified findings are not passed to the Code agent.
849
- 3. **All written code passes Gate 1.** No code merge, commit, or handoff before Validate agent + Simplify agent + Scrutinize agent (in that order).
865
+ 3. **All written code passes Gate 1.** No code merge, commit, or handoff before Simplify agent + Scrutinize agent + Validate agent (in that order — Validate last, so its one run covers the commits of the two before it).
850
866
  4. **Gate 2 runs once, at implementation acceptance.** It does not re-run after review-fixes.
851
867
  5. **NEVER auto-merge to main or master.** All merges target the integration branch. The user merges to main themselves.
852
868
  6. **No unauthorized tracker or remote side-effects.** Sub-agents NEVER create issues/PRs on the tracker, comment on them, or push beyond the ticket-authorized branch unless the ticket, plan, or user explicitly authorizes that exact action. This applies to whatever tracker is resolved, not to one vendor. Proposed follow-ups go in the run report.
@@ -888,7 +904,7 @@ Each ticket engine run returns a structured result. The Synthesize agent or the
888
904
  },
889
905
  "escalations": [
890
906
  {
891
- "type": "merge-conflict | gate2-fail | validation-exhausted | ambiguous-resolution | review-coverage-incomplete | dependency-blocked | engine-crash | ticket-link-missing | branch-missing",
907
+ "type": "merge-conflict | gate2-fail | validation-exhausted | scrutiny-blocked | ambiguous-resolution | review-coverage-incomplete | dependency-blocked | engine-crash | ticket-link-missing | branch-missing",
892
908
  "description": "string"
893
909
  }
894
910
  ],
@@ -947,8 +963,8 @@ For each ready ticket (sequentially by default; parallel only past the §7.1 bar
947
963
  - Branch setup: the engine's setup-task creates the ticket's branch off integration HEAD at ready-time (so it already contains merged deps); every later phase, and the merge, uses the branch setup-task created, and a setup-task that reports none stops the ticket before implementing
948
964
  - Run the single-ticket engine inside a try/catch — one ticket's crash/stall never kills the wave; catch the exception, quarantine that ticket, and continue with the remaining ready set
949
965
  - The engine gets the ticket's own reference, the one the pre-fetch printed, as its setup-task input — never the wave's tracking issue
950
- - On engine PASS or UNVERIFIED: merge to integration branch, run Validate agent (build + test)
951
- - Merge FAIL (build red after merge): quarantine ticket, mark as escalated, continue
966
+ - On engine PASS or UNVERIFIED: merge to the integration branch locally through Git (no push, no build or test), then the workflow spawns the Validate agent (build + test) over the merge. The merge counts as kept only when that Validate returns PASS
967
+ - Validate FAIL or no PASS (build red after merge): the workflow spawns Git to undo the merge, quarantines the ticket, marks it as escalated, and continues. If the undo is refused, the wave stops taking merges and rounds
952
968
  - On any other verdict (PARTIAL, FAIL, ESCALATED) or none: quarantine ticket, do not block independent siblings
953
969
 
954
970
  **Cascade quarantine:** when a ticket is quarantined for any reason (Gate-1 exhausted, engine crash/stall, build-red after merge, review coverage incomplete after retry), the quarantine cascades to its direct and transitive dependents — each is marked blocked with the named reason, naming the blocker by its `{ISSUE_REF}` (e.g. "blocked: depends on {ISSUE_REF} which failed Gate-1"). Independent siblings are never affected. The quarantined list is injected into every subsequent Design agent reader prompt so the reader never schedules dependents of failed tickets.
@@ -975,7 +991,9 @@ MAX_ROUNDS = LLM judgment based on ticket count (heuristic: ticket_count * 2 + 5
975
991
 
976
992
  **Parallel independent tickets:** each gets its own `git worktree add` + durable branch managed by the Git agent. Use explicit `git worktree add` — NOT the Workflow tool's ephemeral `isolation:'worktree'`. The branch must persist across implement → review → resolve → merge stages; ephemeral worktrees are gone when the agent call ends.
977
993
 
978
- **Post-merge validation:** after EVERY merge into the integration branch, run Validate agent (build + test). A red build immediately after merge is the cheapest possible conflict detector. Red → quarantine the merged result + escalate.
994
+ **Post-merge validation:** after EVERY merge into the integration branch, the workflow itself spawns the Validate agent (build + test) over the integration branch; Git runs no build or test. A red build immediately after merge is the cheapest possible conflict detector. It always runs — the merge reports `treeEqual` (whether the merge commit's tree equals the ticket branch head's tree) and the wave row records it, but a true value never skips the Validate.
995
+
996
+ **Undo of a red merge:** on FAIL or no PASS verdict the workflow spawns Git to undo the merge. The undo runs only when no remote branch contains the merge commit and the integration HEAD still equals it, as `git reset --keep {mergeSha}^1` on the integration branch — never `--hard`, never a push. It returns `{"undone": true}` or `{"undone": false, "reason": "..."}`. Undone → the row reads `merged: false` and the ticket is quarantined ("post-merge build red; merge undone"). Not undone → the wave stops taking merges and rounds, and its report names the red integration HEAD.
979
997
 
980
998
  **Commit discipline:** Git agent creates atomic commits per logical change, conventional-commit format, on the ticket branch before merge.
981
999
 
@@ -986,13 +1004,13 @@ Two parallel sibling tickets can produce real git conflicts. The resolution is *
986
1004
  **Resolution procedure:**
987
1005
 
988
1006
  1. Git agent detects the conflict and reports the conflicting files + sections
989
- 2. Spawn a Code agent with FULL intent context:
1007
+ 2. Spawn a Code agent whose prompt opens with `OPERATION: implement` and carries `COMPLIANCE_FRAMEWORKS`, with FULL intent context:
990
1008
  - Both ticket descriptions and plans
991
1009
  - The conflicting diff sections (both sides)
992
1010
  - Relevant ADRs from DECISIONS_CONTEXT (loaded by main model before authoring)
993
1011
  3. Code agent resolves to PRESERVE BOTH INTENTS — the resolution must honor what both tickets were trying to achieve
994
1012
  4. If the correct resolution is NOT UNAMBIGUOUS from the intent context: **do NOT guess** → quarantine + escalate
995
- 5. After any resolution: Validate agent (build + test) immediately
1013
+ 5. After any resolution: the post-merge Validate covers it — the workflow spawns it after the merge, and no Validate is requested from Git
996
1014
 
997
1015
  **Conservative-or-escalate is absolute.** An LLM silently guessing a wrong merge is the highest-danger failure mode in the whole design. When in doubt: quarantine + surface in report. The user re-runs (resume) with the escalated context.
998
1016
 
@@ -1007,7 +1025,7 @@ A workflow cannot pause mid-run (F4). "Escalate" means: quarantine-and-continue
1007
1025
  - Ticket engine FAIL after max retries (Gate 1 exhausted)
1008
1026
  - Gate 2 FAIL after max retries (Evaluate agent/Test agent not satisfied)
1009
1027
  - Circular dependency detected (all remaining tickets blocked on each other)
1010
- - Build red after merge (Validate agent fails post-merge)
1028
+ - Build red after merge (the post-merge Validate agent fails: the merge is undone and the ticket quarantined; a refused undo halts the wave)
1011
1029
  - Review coverage incomplete after retry (a focus area failed to produce a live Review agent result after the retry)
1012
1030
  - Ticket engine crash/stall (unrecoverable exception or watchdog kill — quarantine cascades to dependents)
1013
1031
  - No ticket link while issues are required (the engine stops before implementing)
@@ -1025,7 +1043,7 @@ A workflow cannot pause mid-run (F4). "Escalate" means: quarantine-and-continue
1025
1043
 
1026
1044
  **Wave workflow structure (author after the SINGLE engine blocks above):**
1027
1045
 
1028
- 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.
1046
+ The wave workflow uses the same phases as SINGLE but wraps them in a wave loop. The integration branch is `wave/<initiative>` — the initiative slug, referenced below as `{slug}` — (or the user's current branch). Each ticket works on the branch setup-task created. The Git agent manages worktrees for parallel-eligible tickets. After every merge the workflow spawns the Validate agent (build + test) and undoes a red merge. Escalations accumulate in a list; the final report lists all of them.
1029
1047
 
1030
1048
  Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
1031
1049
 
@@ -1036,11 +1054,12 @@ Wave skeleton — compact reference (see `wave_loop()` doctrine for full semanti
1036
1054
  const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
1037
1055
  const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
1038
1056
  // A row from an engine result: the fields step 3 after the workflow renders
1039
- 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 });
1057
+ const rowOf = (ticketId, r, merged, treeEqual = null) => ({ ticket: ticketId, ran: true, verdict: r?.verdict || r?.overallVerdict || null, merged, treeEqual, issuePrLink: r?.issuePrLink || "(none)", evaluateVerdict: r?.gate2?.evaluateVerdict, testVerdict: r?.gate2?.testVerdict, surviving: r?.survivingFindings?.length, coverageComplete: r?.reviewCoverage?.complete });
1040
1058
  const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
1041
- const waveState = { quarantined: [], round: 0 };
1059
+ const MERGE_SHA = /^[0-9a-f]{40}$/; // a merge commit as git prints it: the only Git-reported value that reaches a later prompt
1060
+ const waveState = { quarantined: [], round: 0, halted: null };
1042
1061
  let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
1043
- while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS) {
1062
+ while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS && waveState.halted === null) {
1044
1063
  waveState.round++;
1045
1064
 
1046
1065
  // Design agent reader (opus) applies vacuous-truth rule: a ticket with no unmet dependencies is ALWAYS ready.
@@ -1073,10 +1092,36 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
1073
1092
  // Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
1074
1093
  // PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
1075
1094
  if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
1076
- 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.
1077
- 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" });
1078
- results[ticketId] = rowOf(ticketId, engineResult, merge?.merged === true);
1079
- if (merge?.merged !== true) waveState.quarantined.push({ ticket: ticketId, reason: merge?.reason || "merge or post-merge build failed" });
1095
+ // Git merges locally and validates nothing: the workflow owns the post-merge Validate (D-WAVE-MERGE-VALIDATE).
1096
+ const merge = await agent(`Merge ${engineResult.branch} to ${INTEGRATION_BRANCH} locally. Include ticket ID ${ticketId} in the merge commit message. Do not push, and run no build or test.
1097
+ Return: {"merged": true, "mergeSha": "<40-hex merge commit>", "treeEqual": <true when mergeSha^{tree} equals the tree of ${engineResult.branch}'s head>} — or {"merged": false, "reason": "<why>"} when no merge is kept.`, { agentType: "Git" });
1098
+ let kept = false;
1099
+ let reason = merge?.reason || "merge failed";
1100
+ if (merge?.merged === true && !MERGE_SHA.test(String(merge.mergeSha))) {
1101
+ // A merge may sit on the integration branch with no SHA to validate or undo it by: stop taking merges.
1102
+ waveState.halted = { ticket: ticketId, mergeSha: null, reason: "merge reported without a 40-hex mergeSha; the integration branch may hold an unvalidated merge" };
1103
+ reason = waveState.halted.reason;
1104
+ } else if (merge?.merged === true) {
1105
+ // A merge counts as kept only when this Validate returns PASS. It always runs: treeEqual is recorded, never a reason to skip.
1106
+ const post = await agent(`Run build, typecheck, lint, and tests on ${INTEGRATION_BRANCH} after merging ${engineResult.branch} (merge commit ${merge.mergeSha}).
1107
+ Follow your Running commands block for every build and test command.
1108
+ Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
1109
+ if (post?.verdict === "PASS") {
1110
+ kept = true;
1111
+ } else {
1112
+ const undo = await agent(`Undo the merge ${merge.mergeSha} on ${INTEGRATION_BRANCH}, only when no remote branch contains ${merge.mergeSha} and the HEAD of ${INTEGRATION_BRANCH} still equals ${merge.mergeSha}. On the integration branch run exactly this one command and no other that changes a branch or a remote: git reset --keep ${merge.mergeSha}^1
1113
+ Return: {"undone": true} — or {"undone": false, "reason": "<why>"} when you did not undo it.`, { agentType: "Git" });
1114
+ if (undo?.undone === true) {
1115
+ reason = "post-merge build red; merge undone";
1116
+ } else {
1117
+ // A merge that stays red would poison every later ticket: stop taking merges and rounds, and name the red HEAD.
1118
+ reason = `post-merge build red; merge not undone (${undo?.reason || "no reason given"})`;
1119
+ waveState.halted = { ticket: ticketId, mergeSha: merge.mergeSha, reason };
1120
+ }
1121
+ }
1122
+ }
1123
+ results[ticketId] = rowOf(ticketId, engineResult, kept, merge?.merged === true && typeof merge.treeEqual === "boolean" ? merge.treeEqual : null);
1124
+ if (!kept) waveState.quarantined.push({ ticket: ticketId, reason });
1080
1125
  } else {
1081
1126
  results[ticketId] = rowOf(ticketId, engineResult, false);
1082
1127
  waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
@@ -1087,11 +1132,12 @@ Return: {"merged": true} — or {"merged": false, "reason": "<why>"} when the me
1087
1132
  waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
1088
1133
  }
1089
1134
  remainingTickets = remainingTickets.filter(t => t !== ticketId);
1135
+ if (waveState.halted !== null) break; // a red integration HEAD takes no further merge in this round
1090
1136
  }
1091
1137
  }
1092
1138
 
1093
1139
  // After the wave report is written: one row per wave ticket, in input order. No entry ⇒ never ran (cascade, deadlock, MAX_ROUNDS).
1094
- return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, issuePrLink: "(none)" }), quarantined: waveState.quarantined };
1140
+ return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, treeEqual: null, issuePrLink: "(none)" }), quarantined: waveState.quarantined, halted: waveState.halted };
1095
1141
  ```
1096
1142
 
1097
1143
  ---
@@ -1115,6 +1161,7 @@ A workflow cannot pause mid-run. After the build/wave workflow returns, you (the
1115
1161
  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.
1116
1162
  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.
1117
1163
  - **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.
1164
+ - **Halted** (the workflow returned a non-null `halted`: a post-merge build it could not undo, or a merge reported with no SHA): the integration HEAD may be red. Record `Wave PR: skipped (integration HEAD red, wave halted at <halted.ticket>)`, name `halted.mergeSha` (or `none reported`) and `halted.reason` as the red integration HEAD in the run summary, and go to step 4 with no wave PR.
1118
1165
  - **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
1119
1166
  - Otherwise compose (a) and then (b).
1120
1167