muonroi-cli 1.8.5 → 1.9.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 (206) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  3. package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
  4. package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
  7. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
  9. package/dist/src/agent-harness/mock-model.d.ts +10 -0
  10. package/dist/src/agent-harness/mock-model.js +6 -2
  11. package/dist/src/chat/chat-keychain.d.ts +7 -12
  12. package/dist/src/chat/chat-keychain.js +19 -86
  13. package/dist/src/cli/keys.d.ts +8 -45
  14. package/dist/src/cli/keys.js +18 -324
  15. package/dist/src/council/clarifier.d.ts +4 -2
  16. package/dist/src/council/clarifier.js +158 -36
  17. package/dist/src/council/debate-planner.js +3 -2
  18. package/dist/src/council/debate.js +59 -5
  19. package/dist/src/council/index.d.ts +23 -0
  20. package/dist/src/council/index.js +37 -2
  21. package/dist/src/council/llm.d.ts +62 -0
  22. package/dist/src/council/llm.js +123 -23
  23. package/dist/src/council/panel-select.js +13 -3
  24. package/dist/src/council/planner.js +17 -0
  25. package/dist/src/council/preflight.d.ts +10 -0
  26. package/dist/src/council/preflight.js +36 -0
  27. package/dist/src/council/prompts.d.ts +9 -1
  28. package/dist/src/council/prompts.js +23 -6
  29. package/dist/src/council/types.d.ts +10 -0
  30. package/dist/src/ee/auth.d.ts +19 -0
  31. package/dist/src/ee/auth.js +39 -0
  32. package/dist/src/ee/client.js +28 -3
  33. package/dist/src/ee/ee-onboarding.js +6 -26
  34. package/dist/src/flow/compaction/compress.d.ts +2 -2
  35. package/dist/src/flow/compaction/compress.js +21 -8
  36. package/dist/src/flow/compaction/extract.d.ts +3 -3
  37. package/dist/src/flow/compaction/extract.js +6 -6
  38. package/dist/src/flow/compaction/index.d.ts +2 -1
  39. package/dist/src/flow/compaction/index.js +29 -3
  40. package/dist/src/flow/compaction/progress.d.ts +35 -0
  41. package/dist/src/flow/compaction/progress.js +35 -0
  42. package/dist/src/generated/version.d.ts +1 -1
  43. package/dist/src/generated/version.js +1 -1
  44. package/dist/src/gsd/flags.d.ts +11 -0
  45. package/dist/src/gsd/flags.js +19 -0
  46. package/dist/src/gsd/plan-council.js +104 -72
  47. package/dist/src/gsd/verdict-schema.d.ts +1 -1
  48. package/dist/src/headless/council-answers.js +4 -0
  49. package/dist/src/index.js +129 -260
  50. package/dist/src/lsp/builtins.js +3 -1
  51. package/dist/src/lsp/manager.d.ts +5 -1
  52. package/dist/src/lsp/manager.js +249 -3
  53. package/dist/src/lsp/npm-cache.d.ts +11 -1
  54. package/dist/src/lsp/npm-cache.js +17 -1
  55. package/dist/src/lsp/runtime.d.ts +6 -1
  56. package/dist/src/lsp/runtime.js +17 -1
  57. package/dist/src/lsp/types.d.ts +83 -1
  58. package/dist/src/lsp/types.js +10 -0
  59. package/dist/src/mcp/client-pool.js +43 -15
  60. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  61. package/dist/src/mcp/lsp-tools.js +93 -2
  62. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  63. package/dist/src/mcp/mcp-keychain.js +9 -49
  64. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  65. package/dist/src/mcp/setup-guide-text.js +22 -2
  66. package/dist/src/mcp/tools-server.d.ts +10 -0
  67. package/dist/src/mcp/tools-server.js +10 -2
  68. package/dist/src/models/catalog.json +19 -19
  69. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  70. package/dist/src/orchestrator/ask-user.js +65 -0
  71. package/dist/src/orchestrator/compaction.d.ts +2 -3
  72. package/dist/src/orchestrator/compaction.js +8 -8
  73. package/dist/src/orchestrator/council-manager.js +9 -8
  74. package/dist/src/orchestrator/council-request.d.ts +49 -0
  75. package/dist/src/orchestrator/council-request.js +62 -0
  76. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  77. package/dist/src/orchestrator/interactive-pause.js +36 -0
  78. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  79. package/dist/src/orchestrator/message-processor.js +26 -8
  80. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  81. package/dist/src/orchestrator/orchestrator.js +204 -50
  82. package/dist/src/orchestrator/preprocessor.js +2 -2
  83. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  84. package/dist/src/orchestrator/safety-askcard.js +5 -2
  85. package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
  86. package/dist/src/orchestrator/safety-intercept.js +7 -0
  87. package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
  88. package/dist/src/orchestrator/stall-watchdog.js +24 -3
  89. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  90. package/dist/src/orchestrator/stream-runner.js +54 -21
  91. package/dist/src/orchestrator/tool-engine.d.ts +19 -0
  92. package/dist/src/orchestrator/tool-engine.js +241 -25
  93. package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
  94. package/dist/src/orchestrator/turn-watchdog.js +38 -9
  95. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  96. package/dist/src/pil/agent-operating-contract.js +6 -4
  97. package/dist/src/pil/discovery.d.ts +1 -1
  98. package/dist/src/pil/discovery.js +2 -2
  99. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  100. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  101. package/dist/src/pil/llm-classify.d.ts +78 -3
  102. package/dist/src/pil/llm-classify.js +351 -111
  103. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  104. package/dist/src/pil/native-capabilities-workbook.js +7 -0
  105. package/dist/src/pil/pipeline.js +2 -0
  106. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  107. package/dist/src/pil/repo-grounding-probe.js +136 -0
  108. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  109. package/dist/src/pil/repo-structure-hints.js +45 -0
  110. package/dist/src/product-loop/artifact-io.js +4 -0
  111. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  112. package/dist/src/product-loop/criteria-seed.js +200 -0
  113. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  114. package/dist/src/product-loop/discovery-interview.js +37 -18
  115. package/dist/src/product-loop/discovery-recommender.js +2 -1
  116. package/dist/src/product-loop/discovery-schema.js +14 -1
  117. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  118. package/dist/src/product-loop/discovery-triage.js +109 -0
  119. package/dist/src/product-loop/gather.js +150 -2
  120. package/dist/src/product-loop/index.js +7 -0
  121. package/dist/src/product-loop/loop-driver.js +21 -8
  122. package/dist/src/product-loop/phase-plan.d.ts +16 -0
  123. package/dist/src/product-loop/phase-plan.js +42 -4
  124. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  125. package/dist/src/product-loop/phase-rituals.js +8 -3
  126. package/dist/src/product-loop/phase-runner.js +30 -11
  127. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  128. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  129. package/dist/src/product-loop/sprint-runner.d.ts +62 -0
  130. package/dist/src/product-loop/sprint-runner.js +309 -8
  131. package/dist/src/product-loop/types.d.ts +25 -0
  132. package/dist/src/providers/anthropic.d.ts +9 -8
  133. package/dist/src/providers/anthropic.js +13 -47
  134. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  135. package/dist/src/providers/auth/grok-oauth.js +30 -5
  136. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  137. package/dist/src/providers/auth/openai-oauth.js +14 -0
  138. package/dist/src/providers/auth/token-store.d.ts +9 -9
  139. package/dist/src/providers/auth/token-store.js +8 -70
  140. package/dist/src/providers/auth/types.d.ts +8 -0
  141. package/dist/src/providers/env-store.d.ts +17 -0
  142. package/dist/src/providers/env-store.js +228 -0
  143. package/dist/src/providers/keychain.d.ts +21 -17
  144. package/dist/src/providers/keychain.js +124 -135
  145. package/dist/src/providers/runtime.d.ts +24 -9
  146. package/dist/src/providers/runtime.js +48 -37
  147. package/dist/src/providers/strategies/thinking-mode.js +9 -1
  148. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  149. package/dist/src/providers/warm.d.ts +65 -0
  150. package/dist/src/providers/warm.js +145 -0
  151. package/dist/src/self-qa/agentic-loop.js +3 -2
  152. package/dist/src/storage/transcript.js +56 -2
  153. package/dist/src/tools/git-safety.d.ts +19 -0
  154. package/dist/src/tools/git-safety.js +168 -0
  155. package/dist/src/tools/native-tools.d.ts +1 -1
  156. package/dist/src/tools/native-tools.js +76 -1
  157. package/dist/src/tools/registry.d.ts +17 -0
  158. package/dist/src/tools/registry.js +116 -1
  159. package/dist/src/types/index.d.ts +29 -1
  160. package/dist/src/ui/app.js +67 -4
  161. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  162. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  163. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  164. package/dist/src/ui/components/compact-progress-card.js +42 -0
  165. package/dist/src/ui/components/council-phase-timeline.js +17 -2
  166. package/dist/src/ui/components/council-question-card.js +1 -0
  167. package/dist/src/ui/components/message-view.d.ts +15 -0
  168. package/dist/src/ui/components/message-view.js +50 -1
  169. package/dist/src/ui/components/tool-group.d.ts +15 -3
  170. package/dist/src/ui/components/tool-group.js +69 -11
  171. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  172. package/dist/src/ui/containers/modals-layer.js +2 -2
  173. package/dist/src/ui/council-harness-event.d.ts +57 -0
  174. package/dist/src/ui/council-harness-event.js +46 -0
  175. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  176. package/dist/src/ui/heartbeat-debug.js +45 -0
  177. package/dist/src/ui/modals/api-key-modal.js +1 -1
  178. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  179. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  180. package/dist/src/ui/slash/ee.js +81 -0
  181. package/dist/src/ui/slash/menu-items.js +11 -2
  182. package/dist/src/ui/use-app-logic.js +354 -224
  183. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  184. package/dist/src/ui/utils/agent-activities.js +96 -0
  185. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  186. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  187. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  188. package/dist/src/ui/utils/tool-summary.js +91 -0
  189. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  190. package/dist/src/utils/event-loop-monitor.js +107 -0
  191. package/dist/src/utils/llm-deadline.d.ts +14 -0
  192. package/dist/src/utils/llm-deadline.js +19 -0
  193. package/dist/src/utils/loop-profiler.d.ts +102 -0
  194. package/dist/src/utils/loop-profiler.js +202 -0
  195. package/dist/src/utils/settings.d.ts +27 -0
  196. package/dist/src/utils/settings.js +38 -2
  197. package/dist/src/utils/side-question.d.ts +1 -2
  198. package/dist/src/utils/side-question.js +2 -2
  199. package/dist/src/verify/entrypoint.js +51 -16
  200. package/dist/src/verify/orchestrator.d.ts +1 -1
  201. package/dist/src/verify/orchestrator.js +20 -3
  202. package/package.json +1 -2
  203. package/dist/src/cli/bw-vault.d.ts +0 -55
  204. package/dist/src/cli/bw-vault.js +0 -133
  205. package/dist/src/mcp/ee-tools.d.ts +0 -46
  206. package/dist/src/mcp/ee-tools.js +0 -194
@@ -0,0 +1,144 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
3
+ /**
4
+ * Run an isolated sub-agent with a wall-clock backstop. The review/fix agents
5
+ * were bare `await`s — a provider that hangs post-stream would wedge the whole
6
+ * sprint (same class as the impl stall, run mrhc43f0fb9b). On timeout the race
7
+ * rejects; we convert it into a failure ToolResult so the existing `.success`
8
+ * paths handle it (review → leave gate; fix → stop the loop) instead of hanging.
9
+ */
10
+ async function runIsolatedGuarded(run, req, label) {
11
+ try {
12
+ return await withDeadlineRace(() => run(req), getIsolatedTaskDeadlineMs(), label);
13
+ }
14
+ catch (err) {
15
+ return { success: false, output: "", error: err instanceof Error ? err.message : String(err) };
16
+ }
17
+ }
18
+ function currentDiff(cwd) {
19
+ try {
20
+ const r = spawnSync("git", ["diff", "HEAD"], {
21
+ cwd,
22
+ encoding: "utf8",
23
+ timeout: 20000,
24
+ maxBuffer: 20 * 1024 * 1024,
25
+ });
26
+ return (r.stdout ?? "").trim();
27
+ }
28
+ catch {
29
+ return "";
30
+ }
31
+ }
32
+ function parseReview(output) {
33
+ const m = output.match(/\{[\s\S]*\}/);
34
+ if (!m)
35
+ return null;
36
+ try {
37
+ return JSON.parse(m[0]);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function normalizeDeviations(dev) {
44
+ if (!Array.isArray(dev))
45
+ return [];
46
+ return dev
47
+ .map((d) => {
48
+ if (typeof d === "string")
49
+ return d.trim();
50
+ const where = d.where ? `[${d.where}] ` : "";
51
+ const issue = d.issue ?? "";
52
+ const fix = d.fix ? ` → FIX: ${d.fix}` : "";
53
+ return `${where}${issue}${fix}`.trim();
54
+ })
55
+ .filter((s) => s.length > 0);
56
+ }
57
+ export async function* runPlanAdherenceReview(args) {
58
+ const maxRounds = Math.max(1, Math.min(4, args.maxRounds ?? 2));
59
+ const plan = args.planSynthesis.trim();
60
+ if (!plan)
61
+ return { rounds: 0, adherent: true, deviations: [] };
62
+ const getDiff = args.diffProvider ?? currentDiff;
63
+ let diff = getDiff(args.cwd);
64
+ if (!diff) {
65
+ yield { type: "content", content: `\n> [adherence] No diff to review for sprint ${args.sprintN}; skipping.\n` };
66
+ return { rounds: 0, adherent: true, deviations: [] };
67
+ }
68
+ let lastDeviations = [];
69
+ for (let round = 1; round <= maxRounds; round++) {
70
+ const reviewPrompt = `You are a SENIOR code reviewer. Judge whether the implementation faithfully ` +
71
+ `follows the APPROVED PLAN below — both its file_edits (right files, right ` +
72
+ `approach: e.g. pass-through vs re-implementation, correct operation/API) and ` +
73
+ `its acceptance_criteria. Be strict and specific.\n\n` +
74
+ `=== APPROVED PLAN ===\n${plan.slice(0, 9000)}\n\n` +
75
+ `=== ACTUAL GIT DIFF ===\n${diff.slice(0, 12000)}\n\n` +
76
+ `Return ONLY JSON: {"adherent": boolean, "deviations": [{"where":"<file/symbol>",` +
77
+ `"issue":"<what diverges from the plan>","fix":"<concrete instruction to conform>"}]}. ` +
78
+ `adherent=true ONLY if there are no material deviations.`;
79
+ const review = await runIsolatedGuarded(args.runIsolatedTask, {
80
+ agent: "general",
81
+ description: `Sprint ${args.sprintN} plan-adherence review (round ${round})`,
82
+ prompt: reviewPrompt,
83
+ modelId: args.reviewModelId,
84
+ maxToolRounds: 12,
85
+ }, `adherence-review-s${args.sprintN}-r${round}`);
86
+ const parsed = review.success ? parseReview(review.output ?? "") : null;
87
+ if (!parsed) {
88
+ yield {
89
+ type: "content",
90
+ content: `\n> [adherence] Reviewer produced no parseable verdict (round ${round}); leaving verify+criteria as the gate.\n`,
91
+ };
92
+ return { rounds: round, adherent: true, deviations: [] };
93
+ }
94
+ lastDeviations = normalizeDeviations(parsed.deviations);
95
+ const adherent = parsed.adherent === true || lastDeviations.length === 0;
96
+ if (adherent) {
97
+ yield {
98
+ type: "content",
99
+ content: `\n> [adherence] Round ${round}: reviewer (${args.reviewModelId}) confirms the implementation follows the plan.\n`,
100
+ };
101
+ return { rounds: round, adherent: true, deviations: [] };
102
+ }
103
+ yield {
104
+ type: "content",
105
+ content: `\n> [adherence] Round ${round}: ${lastDeviations.length} deviation(s) from plan:\n` +
106
+ lastDeviations.map((d) => ` - ${d}`).join("\n") +
107
+ "\n",
108
+ };
109
+ if (round === maxRounds) {
110
+ yield {
111
+ type: "content",
112
+ content: `\n> [adherence] Max rounds reached; deviations remain for the verify+criteria gate to catch.\n`,
113
+ };
114
+ return { rounds: round, adherent: false, deviations: lastDeviations };
115
+ }
116
+ // Hand the fix to the lower-tier agent.
117
+ const fixPrompt = `The implementation deviates from the APPROVED PLAN. A senior reviewer found ` +
118
+ `these deviations — fix EACH one by editing the code so it conforms to the plan. ` +
119
+ `Apply edits directly; do not narrate or re-plan.\n\n` +
120
+ `Deviations:\n${lastDeviations.map((d, i) => `${i + 1}. ${d}`).join("\n")}\n\n` +
121
+ `=== APPROVED PLAN (for reference) ===\n${plan.slice(0, 6000)}\n`;
122
+ yield {
123
+ type: "content",
124
+ content: `\n> [adherence] Dispatching fix task to ${args.fixModelId} (round ${round})…\n`,
125
+ };
126
+ const fix = await runIsolatedGuarded(args.runIsolatedTask, {
127
+ agent: "general",
128
+ description: `Sprint ${args.sprintN} plan-adherence fix (round ${round})`,
129
+ prompt: fixPrompt,
130
+ modelId: args.fixModelId,
131
+ }, `adherence-fix-s${args.sprintN}-r${round}`);
132
+ if (!fix.success) {
133
+ yield {
134
+ type: "content",
135
+ content: `\n> [adherence] Fix task failed (round ${round}): ${fix.error ?? "unknown"}; stopping the loop.\n`,
136
+ };
137
+ return { rounds: round, adherent: false, deviations: lastDeviations };
138
+ }
139
+ // Re-read the diff for the next review round.
140
+ diff = getDiff(args.cwd);
141
+ }
142
+ return { rounds: maxRounds, adherent: false, deviations: lastDeviations };
143
+ }
144
+ //# sourceMappingURL=plan-adherence-review.js.map
@@ -97,6 +97,68 @@ export declare const IMPL_EXECUTION_DIRECTIVE: string;
97
97
  * orchestrator promise may leak in the background, but the run recovers.
98
98
  */
99
99
  export declare function withImplIdleWatchdog(gen: AsyncGenerator<StreamChunk, void, unknown>, idleMs: number, sprintN: number, totalMs?: number): AsyncGenerator<StreamChunk, void, unknown>;
100
+ /**
101
+ * Wall-clock deadline for the ISOLATED implementation path.
102
+ *
103
+ * The isolated path (`ctx.runIsolatedTask`) returns a single Promise, not a
104
+ * stream, so `withImplIdleWatchdog` (which guards the streamed non-isolated
105
+ * path) cannot wrap it. Its only protection was the sub-agent's INTERNAL
106
+ * per-chunk stall-watchdog — which does NOT fire once the sub-agent's LLM stream
107
+ * has finished but its orchestrator turn hangs on the JS side afterwards (the
108
+ * exact "wrote N files then went silent" wedge documented on
109
+ * `withImplIdleWatchdog`). Observed live 2026-07-12 (run mrhc43f0fb9b): the
110
+ * isolated impl wrote 2 files, emitted its final `llm-done`, then wedged for 30+
111
+ * min with zero events and an idle process — because this `await` had no outer
112
+ * ceiling.
113
+ *
114
+ * This races the isolated task against a hard total-elapsed deadline. On
115
+ * timeout it rejects so the caller's existing try/catch converts the wedge into
116
+ * a visible phaseError (the sprint surfaces + can recover), mirroring what
117
+ * `withImplIdleWatchdog` / `runVerifyWithWatchdog` do for the other stages. The
118
+ * suspended sub-agent promise may leak in the background, but the run recovers.
119
+ * `totalMs <= 0` disables the guard (returns the task unchanged).
120
+ */
121
+ /**
122
+ * Extract why an isolated implementation task failed, from its ToolResult.
123
+ *
124
+ * `output` is checked because StreamRunner reports EVERY sub-agent failure
125
+ * there — "Task failed: …" (stream-runner.ts:1061), "[Cancelled]" (:982), a
126
+ * provider stall (:988), an unknown-agent message (:265) — and never assigns
127
+ * `error`; grep stream-runner.ts for `error:` and there are no hits. Reading
128
+ * `error` alone made `result.error?.trim()` permanently undefined, so every
129
+ * distinct failure collapsed into the contentless fallback and two /ideal runs
130
+ * halted 1s into implementation with the cause already erased.
131
+ *
132
+ * `error` still wins when a caller does populate it — ToolResult declares the
133
+ * field, so a future non-StreamRunner producer may be more specific.
134
+ */
135
+ export declare function resolveImplFailureReason(result: {
136
+ output?: string;
137
+ error?: string;
138
+ }): string;
139
+ /**
140
+ * Persist an implementation-stage failure to `interaction_logs`.
141
+ *
142
+ * The implementation stage is where /ideal either ships code or does not, so its
143
+ * exception is the single most valuable line in a post-mortem — yet run
144
+ * mrn9yfle9801 halted with only `halt_card_open {trigger:"loop_throw"}` on
145
+ * record and the message itself unrecoverable: stderr belongs to the TUI child
146
+ * (the harness never captures it) and the council path writes no `messages`
147
+ * rows. `elapsedMs` is what separates the two indistinguishable causes — an
148
+ * immediate `!result.success` from a `withIsolatedImplDeadline` watchdog trip.
149
+ *
150
+ * Never throws: a broken audit trail must not take down the sprint it is
151
+ * describing.
152
+ */
153
+ export declare function logSprintImplError(ctx: DriverContext, info: {
154
+ sprintN: number;
155
+ message: string;
156
+ stack?: string;
157
+ implModelId?: string;
158
+ elapsedMs: number;
159
+ isolated: boolean;
160
+ }): void;
161
+ export declare function withIsolatedImplDeadline<T>(task: Promise<T>, totalMs: number, sprintN: number): Promise<T>;
100
162
  export { computeFailureSignature, loadVerifyFailureSignatures, pushFailureToEE, recordVerifyFailureAndMaybePush, saveVerifyFailureSignatures, type VerifyFailureRecord, type VerifyFailureSignatures, } from "./verify-failure-tracking.js";
101
163
  export interface RunSprintArgs {
102
164
  sprintN: number;
@@ -26,6 +26,7 @@ import { readFile, writeFile } from "node:fs/promises";
26
26
  import * as path from "node:path";
27
27
  import { prependDecisionsLock, readDecisionsLock } from "../council/decisions-lock.js";
28
28
  import { runCouncil } from "../council/index.js";
29
+ import { resolveLeaderModel } from "../council/leader.js";
29
30
  import { phaseDone, phaseError, phaseStart } from "../council/phase-events.js";
30
31
  import { fireAndForgetWorkflowEvent } from "../ee/workflow-event.js";
31
32
  import { readArtifact, writeArtifact } from "../flow/artifact-io.js";
@@ -33,21 +34,24 @@ import { renderResumeDigest, writeSprintOutcome, writeSprintVerify } from "../fl
33
34
  import { isContextRailEnabled } from "../gsd/flags.js";
34
35
  import { SPRINT_EXECUTION_MARKER } from "../pil/layer6-output.js";
35
36
  import { detectProviderForModel } from "../providers/runtime.js";
36
- import { logUIInteraction } from "../storage/index.js";
37
+ import { logInteraction, logUIInteraction } from "../storage/index.js";
37
38
  import { commitToProduct, release } from "../usage/ledger.js";
38
39
  import { CapBreachError } from "../usage/types.js";
40
+ import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
39
41
  import { runVerifyOrchestration } from "../verify/orchestrator.js";
40
42
  import { appendIteration, readCriteria } from "./artifact-io.js";
41
43
  import { formatUnverifiedForSprintContext, readLedger } from "./assumption-ledger.js";
42
44
  import { readBacklog } from "./backlog-store.js";
43
45
  import { CB1_costProjection, CB2_oscillation, CB3_verifyBlank } from "./circuit-breakers.js";
44
46
  import { reserveForProduct } from "./cost-scoper.js";
47
+ import { extractAcceptanceCriteria, judgeCriteriaAgainstVerify, planQualityIssues, seedCriteriaFromPlan, } from "./criteria-seed.js";
45
48
  import { formatProjectContextForPrompt } from "./discovery-context-format.js";
46
49
  import { readProjectContext } from "./discovery-persistence.js";
47
50
  import { evaluateDoneGate } from "./done-gate.js";
48
51
  import { buildContinueFeedback } from "./feedback-routing.js";
49
52
  import { idealTrace } from "./ideal-trace.js";
50
53
  import { postSprintBoundary } from "./phase-tracker-bridge.js";
54
+ import { runPlanAdherenceReview } from "./plan-adherence-review.js";
51
55
  import { computeProgressSnapshot, renderSnapshotMarkdown } from "./progress-snapshot.js";
52
56
  import { appendRoleMemory } from "./role-memory.js";
53
57
  import { loadVerifyFailureSignatures, recordVerifyFailureAndMaybePush } from "./verify-failure-tracking.js";
@@ -254,6 +258,97 @@ export async function* withImplIdleWatchdog(gen, idleMs, sprintN, totalMs = getI
254
258
  clearTimeout(totalTimer);
255
259
  }
256
260
  }
261
+ /**
262
+ * Wall-clock deadline for the ISOLATED implementation path.
263
+ *
264
+ * The isolated path (`ctx.runIsolatedTask`) returns a single Promise, not a
265
+ * stream, so `withImplIdleWatchdog` (which guards the streamed non-isolated
266
+ * path) cannot wrap it. Its only protection was the sub-agent's INTERNAL
267
+ * per-chunk stall-watchdog — which does NOT fire once the sub-agent's LLM stream
268
+ * has finished but its orchestrator turn hangs on the JS side afterwards (the
269
+ * exact "wrote N files then went silent" wedge documented on
270
+ * `withImplIdleWatchdog`). Observed live 2026-07-12 (run mrhc43f0fb9b): the
271
+ * isolated impl wrote 2 files, emitted its final `llm-done`, then wedged for 30+
272
+ * min with zero events and an idle process — because this `await` had no outer
273
+ * ceiling.
274
+ *
275
+ * This races the isolated task against a hard total-elapsed deadline. On
276
+ * timeout it rejects so the caller's existing try/catch converts the wedge into
277
+ * a visible phaseError (the sprint surfaces + can recover), mirroring what
278
+ * `withImplIdleWatchdog` / `runVerifyWithWatchdog` do for the other stages. The
279
+ * suspended sub-agent promise may leak in the background, but the run recovers.
280
+ * `totalMs <= 0` disables the guard (returns the task unchanged).
281
+ */
282
+ /**
283
+ * Extract why an isolated implementation task failed, from its ToolResult.
284
+ *
285
+ * `output` is checked because StreamRunner reports EVERY sub-agent failure
286
+ * there — "Task failed: …" (stream-runner.ts:1061), "[Cancelled]" (:982), a
287
+ * provider stall (:988), an unknown-agent message (:265) — and never assigns
288
+ * `error`; grep stream-runner.ts for `error:` and there are no hits. Reading
289
+ * `error` alone made `result.error?.trim()` permanently undefined, so every
290
+ * distinct failure collapsed into the contentless fallback and two /ideal runs
291
+ * halted 1s into implementation with the cause already erased.
292
+ *
293
+ * `error` still wins when a caller does populate it — ToolResult declares the
294
+ * field, so a future non-StreamRunner producer may be more specific.
295
+ */
296
+ export function resolveImplFailureReason(result) {
297
+ return result.error?.trim() || result.output?.trim() || "isolated implementation task failed";
298
+ }
299
+ /**
300
+ * Persist an implementation-stage failure to `interaction_logs`.
301
+ *
302
+ * The implementation stage is where /ideal either ships code or does not, so its
303
+ * exception is the single most valuable line in a post-mortem — yet run
304
+ * mrn9yfle9801 halted with only `halt_card_open {trigger:"loop_throw"}` on
305
+ * record and the message itself unrecoverable: stderr belongs to the TUI child
306
+ * (the harness never captures it) and the council path writes no `messages`
307
+ * rows. `elapsedMs` is what separates the two indistinguishable causes — an
308
+ * immediate `!result.success` from a `withIsolatedImplDeadline` watchdog trip.
309
+ *
310
+ * Never throws: a broken audit trail must not take down the sprint it is
311
+ * describing.
312
+ */
313
+ export function logSprintImplError(ctx, info) {
314
+ try {
315
+ logInteraction(ctx.sessionId ?? ctx.runId, "council", {
316
+ eventSubtype: "sprint_impl_error",
317
+ ...(info.implModelId ? { model: info.implModelId } : {}),
318
+ durationMs: info.elapsedMs,
319
+ data: {
320
+ runId: ctx.runId,
321
+ sprintN: info.sprintN,
322
+ isolated: info.isolated,
323
+ message: info.message.slice(0, 2000),
324
+ stack: info.stack,
325
+ },
326
+ });
327
+ }
328
+ catch (err) {
329
+ console.error(`[sprint-runner] failed to persist implementation error (sprint ${info.sprintN}): ${err instanceof Error ? err.message : String(err)}`);
330
+ }
331
+ }
332
+ export async function withIsolatedImplDeadline(task, totalMs, sprintN) {
333
+ if (!(Number.isFinite(totalMs) && totalMs > 0))
334
+ return task;
335
+ let timer;
336
+ const deadline = new Promise((_, reject) => {
337
+ timer = setTimeout(() => {
338
+ reject(new Error(`isolated implementation stage exceeded ${Math.round(totalMs / 1000)}s total watchdog and was ` +
339
+ `treated as stalled (sprint ${sprintN}) — the isolated sub-agent turn never completed ` +
340
+ `(hung on the JS side after its final response; the isolated path has no per-chunk stall guard)`));
341
+ }, totalMs);
342
+ timer.unref?.();
343
+ });
344
+ try {
345
+ return await Promise.race([task, deadline]);
346
+ }
347
+ finally {
348
+ if (timer)
349
+ clearTimeout(timer);
350
+ }
351
+ }
257
352
  export { computeFailureSignature, loadVerifyFailureSignatures, pushFailureToEE, recordVerifyFailureAndMaybePush, saveVerifyFailureSignatures, } from "./verify-failure-tracking.js";
258
353
  /**
259
354
  * Run a single sprint. Yields StreamChunk events for the UI and returns the
@@ -375,9 +470,54 @@ export async function* runSprint(args) {
375
470
  // ── Step 2: Detect verify recipe BEFORE the planner spends any token ──────
376
471
  // CB-3 fires deterministically on sprint 1 if recipe is null or coverage === 0.
377
472
  const verifyAgent = buildVerifyAgent(ctx, cwd);
378
- const verifyRecipe = await verifyAgent.detectVerifyRecipe(verifyAgent.getSandboxSettings());
473
+ // Wall-clock backstop: `detectVerifyRecipe` runs a `verify-detect` LLM
474
+ // sub-agent turn (orchestrator.detectVerifyRecipe → runTaskRequest). Like the
475
+ // impl/verify stages, that turn can finish its stream then wedge on the JS side
476
+ // afterward — and this call site had NO deadline, so a single hung verify-detect
477
+ // turn bricked the entire /ideal run silently, right after "Committed: N sprints
478
+ // planned" and BEFORE the "Sprint N — Planning" yield (observed live 2026-07-13:
479
+ // 8+ min frozen frame, no forward progress). Race it against the shared isolated-
480
+ // task deadline; a timeout falls through to `null` → CB-3 emits the actionable
481
+ // recovery card instead of hanging. (The bridge signature does not thread an
482
+ // abortSignal, so this caller-side race is the guarantee.)
483
+ let verifyRecipe;
484
+ try {
485
+ verifyRecipe = await withDeadlineRace(() => verifyAgent.detectVerifyRecipe(verifyAgent.getSandboxSettings()), getIsolatedTaskDeadlineMs(), `sprint-${sprintN}-detect-verify`);
486
+ }
487
+ catch (err) {
488
+ console.error(`[sprint-runner] detectVerifyRecipe timed out/failed (sprint ${sprintN}, run ${ctx.runId}): ${err instanceof Error ? err.message : String(err)}`);
489
+ verifyRecipe = null;
490
+ }
379
491
  const cb3 = CB3_verifyBlank(sprintN, verifyRecipe);
380
- if (cb3.halt) {
492
+ // Greenfield build-first (Task #8): on a fresh greenfield /ideal run the first
493
+ // sprint has nothing to verify yet — detectVerifyRecipe legitimately returns
494
+ // null / zero-coverage because the code and tests do not exist until THIS sprint
495
+ // builds them. Halting here (CB-3) would trap every greenfield idea before a
496
+ // single line is written. So for sprint 1 of a greenfield run, bypass the halt
497
+ // and let the implement stage scaffold the first increment; the verify stage
498
+ // re-detects the recipe from the code it creates (Step 5 reads
499
+ // verifyResult.verifyRecipe, not this one). The halt is preserved for EXISTING
500
+ // projects, where a missing recipe is a real "I can't tell how to test this"
501
+ // signal that warrants the recovery card. Opt out with
502
+ // MUONROI_IDEAL_GREENFIELD_BUILD_FIRST=0.
503
+ let greenfieldBuildFirst = false;
504
+ if (cb3.halt && sprintN === 1 && process.env.MUONROI_IDEAL_GREENFIELD_BUILD_FIRST !== "0") {
505
+ try {
506
+ const pc = await readProjectContext(ctx.flowDir, ctx.runId);
507
+ greenfieldBuildFirst = pc?.detection?.classification === "greenfield";
508
+ }
509
+ catch {
510
+ greenfieldBuildFirst = false;
511
+ }
512
+ }
513
+ if (greenfieldBuildFirst) {
514
+ yield {
515
+ type: "content",
516
+ content: `\n> Greenfield: no verify recipe exists yet (nothing is built). Proceeding to build the ` +
517
+ `first increment — it will be verified against the code and tests this sprint creates.\n`,
518
+ };
519
+ }
520
+ if (cb3.halt && !greenfieldBuildFirst) {
381
521
  // Yield a structured halt chunk so the TUI can render an actionable recovery
382
522
  // card (Task 5.2). Do NOT throw — callers must discriminate on chunk.type.
383
523
  const haltChunk = {
@@ -562,6 +702,36 @@ export async function* runSprint(args) {
562
702
  // Persist so a resumed/retried sprint reuses this exact plan (and target folder).
563
703
  await persistSprintPlan(planPath, planSynthesis);
564
704
  }
705
+ // Plan-fidelity fix: seed the plan's acceptance_criteria into the criteria store
706
+ // so the done-gate scores against REAL criteria (previously readCriteria returned
707
+ // [] → score always 0.00 → no gate on plan divergence). Idempotent + non-clobbering.
708
+ // Also run a NON-BLOCKING plan-quality check (per-sprint plans are auto-approved
709
+ // with no gate) and fold any issues into a corrective note for the impl prompt.
710
+ let planQualityNote = "";
711
+ try {
712
+ const planCriteria = extractAcceptanceCriteria(planSynthesis ?? "");
713
+ const seeded = await seedCriteriaFromPlan(ctx.flowDir, ctx.runId, planCriteria, sprintN);
714
+ if (seeded > 0) {
715
+ yield {
716
+ type: "content",
717
+ content: `\n> [criteria] Seeded ${seeded} acceptance criteria from the sprint plan (done-gate now scores against them).\n`,
718
+ };
719
+ }
720
+ const issues = planQualityIssues(planSynthesis ?? "", seeded);
721
+ if (issues.length > 0) {
722
+ planQualityNote =
723
+ `\n\n--- PLAN QUALITY WARNINGS (address these while implementing) ---\n` +
724
+ issues.map((i) => `- ${i}`).join("\n") +
725
+ `\nImplement to satisfy the phase goal and every acceptance criterion; do not stop at scaffolding.\n`;
726
+ yield {
727
+ type: "content",
728
+ content: `\n> [plan-check] ${issues.length} plan-quality warning(s): ${issues.join("; ")}\n`,
729
+ };
730
+ }
731
+ }
732
+ catch {
733
+ /* non-critical — a missing criteria seed degrades to the prior empty-criteria behavior */
734
+ }
565
735
  // P4-C: close the planning phase row before opening implementation.
566
736
  yield phaseDone({
567
737
  phaseId: planPhaseId,
@@ -609,7 +779,7 @@ export async function* runSprint(args) {
609
779
  // implement/edit path, not the respond path.
610
780
  // C2: Pre-impl gate — read decisions.lock.md and prepend to implementation prompt.
611
781
  // When lock file is missing (greenfield / no council with runDir), pass-through unchanged.
612
- let implPrompt = planSynthesis.trim() ? IMPL_EXECUTION_DIRECTIVE + planSynthesis : planSynthesis;
782
+ let implPrompt = planSynthesis.trim() ? IMPL_EXECUTION_DIRECTIVE + planSynthesis + planQualityNote : planSynthesis;
613
783
  try {
614
784
  const lockContent = await readDecisionsLock(runDir);
615
785
  if (lockContent) {
@@ -647,6 +817,7 @@ export async function* runSprint(args) {
647
817
  }
648
818
  }
649
819
  let implError = null;
820
+ let implErrorStack;
650
821
  if (ctx.processMessageFn && implPrompt.trim()) {
651
822
  const useIsolated = shouldUseIsolatedImpl(!!ctx.runIsolatedTask);
652
823
  try {
@@ -663,14 +834,29 @@ export async function* runSprint(args) {
663
834
  content: "\n> [isolated impl] Executing the sprint in a fresh sub-agent context " +
664
835
  "(anti-overflow: does not inherit the debate history).\n",
665
836
  };
666
- const result = await ctx.runIsolatedTask({
837
+ // Plan-fidelity fix: allow the implementation turn to run on a stronger
838
+ // model than the cheap session tier (which failed to faithfully follow a
839
+ // rich plan). Opt-in via MUONROI_IDEAL_IMPL_MODEL; defaults to the session
840
+ // model so the cheap-model philosophy stays the default.
841
+ const implModelId = process.env.MUONROI_IDEAL_IMPL_MODEL?.trim() || ctx.sessionModelId;
842
+ if (implModelId !== ctx.sessionModelId) {
843
+ yield {
844
+ type: "content",
845
+ content: `\n> [impl-model] Running implementation on ${implModelId} (override of session model ${ctx.sessionModelId}).\n`,
846
+ };
847
+ }
848
+ // Wall-clock deadline: the isolated path has no per-chunk stall guard,
849
+ // so a post-finish JS-side hang would wedge this await forever (observed
850
+ // live, run mrhc43f0fb9b). Racing the total-elapsed ceiling turns a wedge
851
+ // into a phaseError via the try/catch below. See withIsolatedImplDeadline.
852
+ const result = await withIsolatedImplDeadline(ctx.runIsolatedTask({
667
853
  agent: "general",
668
854
  description: `Sprint ${sprintN} implementation`,
669
855
  prompt: implPrompt,
670
- modelId: ctx.sessionModelId,
671
- });
856
+ modelId: implModelId,
857
+ }), getImplTotalTimeoutMs(), sprintN);
672
858
  if (!result.success) {
673
- implError = result.error?.trim() || "isolated implementation task failed";
859
+ implError = resolveImplFailureReason(result);
674
860
  }
675
861
  else if (result.output?.trim()) {
676
862
  yield { type: "content", content: `\n${result.output.trim()}\n` };
@@ -687,8 +873,11 @@ export async function* runSprint(args) {
687
873
  }
688
874
  catch (e) {
689
875
  implError = e instanceof Error ? e.message : String(e);
876
+ implErrorStack = e instanceof Error ? e.stack?.split("\n").slice(0, 4).join(" | ") : undefined;
690
877
  // No-Silent-Catch: the finally below surfaces a phaseError chunk, but log
691
878
  // here too so the hang/failure is diagnosable from stderr / MUONROI logs.
879
+ // Persisting happens at the single convergence point below — a thrown
880
+ // error and a `!result.success` return must not log differently.
692
881
  console.error(`[sprint-runner] implementation stage failed (sprint ${sprintN}, run ${ctx.runId}): ${implError}`);
693
882
  }
694
883
  finally {
@@ -728,6 +917,21 @@ export async function* runSprint(args) {
728
917
  });
729
918
  }
730
919
  if (implError) {
920
+ // The ONE place both failure shapes converge. The catch above handles a
921
+ // thrown error; a `!result.success` return never reaches it and instead
922
+ // falls through to here — which is why persisting from inside the catch
923
+ // recorded nothing for the two runs that actually failed. This throw
924
+ // escapes to the UI's loop-level catch (use-app-logic.tsx), which renders
925
+ // the message but persists only {reason, trigger, sprintN} — no text. So
926
+ // this is the last point at which the reason still exists.
927
+ logSprintImplError(ctx, {
928
+ sprintN,
929
+ message: implError,
930
+ stack: implErrorStack,
931
+ implModelId: process.env.MUONROI_IDEAL_IMPL_MODEL?.trim() || ctx.sessionModelId,
932
+ elapsedMs: Date.now() - implStartedAt,
933
+ isolated: shouldUseIsolatedImpl(!!ctx.runIsolatedTask),
934
+ });
731
935
  throw new Error(implError);
732
936
  }
733
937
  // ── Step 4b: 4A completeness re-check ─────────────────────────────────────
@@ -800,6 +1004,58 @@ export async function* runSprint(args) {
800
1004
  }
801
1005
  }
802
1006
  }
1007
+ // ── Step 4c: Plan-adherence review gate (strong reviewer → cheap fixer) ────
1008
+ // A high-tier reviewer checks the diff against the approved plan; deviations are
1009
+ // handed to a lower-tier fixer and re-reviewed (bounded). Opt out with
1010
+ // MUONROI_IDEAL_ADHERENCE_REVIEW=0. Never halts — verify + the criteria done-gate
1011
+ // remain the hard gates; this tightens plan fidelity before verification so a
1012
+ // cheap implementer's divergence is caught and corrected, not shipped.
1013
+ // Plan deviations that survive the bounded fixer rounds — carried into the next
1014
+ // sprint's focus (Step 9) so "chưa tuân thủ" work continues rather than being
1015
+ // silently dropped after the review.
1016
+ let residualPlanDeviations = [];
1017
+ if (ctx.runIsolatedTask && planSynthesis.trim() && process.env.MUONROI_IDEAL_ADHERENCE_REVIEW !== "0") {
1018
+ const adhPhaseId = `sprint-${sprintN}-adherence`;
1019
+ const adhStartedAt = Date.now();
1020
+ yield phaseStart({
1021
+ phaseId: adhPhaseId,
1022
+ kind: "sprint_stage",
1023
+ label: `Sprint ${sprintN} — Plan-adherence review`,
1024
+ startedAt: adhStartedAt,
1025
+ });
1026
+ try {
1027
+ const reviewModelId = process.env.MUONROI_IDEAL_REVIEW_MODEL?.trim() || resolveLeaderModel(ctx.sessionModelId);
1028
+ const verdict = yield* runPlanAdherenceReview({
1029
+ sprintN,
1030
+ planSynthesis,
1031
+ cwd,
1032
+ reviewModelId,
1033
+ fixModelId: ctx.sessionModelId,
1034
+ runIsolatedTask: ctx.runIsolatedTask,
1035
+ maxRounds: Number.parseInt(process.env.MUONROI_IDEAL_ADHERENCE_ROUNDS ?? "2", 10) || 2,
1036
+ });
1037
+ idealTrace("sprint.adherence.after", {
1038
+ runId: ctx.runId,
1039
+ sprintN,
1040
+ rounds: verdict.rounds,
1041
+ adherent: verdict.adherent,
1042
+ deviations: verdict.deviations.length,
1043
+ });
1044
+ if (!verdict.adherent)
1045
+ residualPlanDeviations = verdict.deviations;
1046
+ }
1047
+ catch (err) {
1048
+ console.error(`[sprint-runner] plan-adherence review failed (sprint ${sprintN}): ${err.message}`);
1049
+ }
1050
+ finally {
1051
+ yield phaseDone({
1052
+ phaseId: adhPhaseId,
1053
+ kind: "sprint_stage",
1054
+ label: `Sprint ${sprintN} — Plan-adherence review`,
1055
+ startedAt: adhStartedAt,
1056
+ });
1057
+ }
1058
+ }
803
1059
  // ── Step 5: Verify stage ──────────────────────────────────────────────────
804
1060
  yield { type: "content", content: `\n## Sprint ${sprintN} — Verification\n` };
805
1061
  const verifyPhaseId = `sprint-${sprintN}-verification`;
@@ -928,6 +1184,42 @@ export async function* runSprint(args) {
928
1184
  subtype: "sprint_stage",
929
1185
  data: { sprintIndex: sprintN, stage: "judgment", runId: ctx.runId },
930
1186
  });
1187
+ // Plan-fidelity fix: judge the seeded acceptance criteria against what was
1188
+ // actually built (verify output + diff) BEFORE the done-gate reads them.
1189
+ // Without this the criteria stay "unmet" forever → score 0.00 and the gate can
1190
+ // never distinguish an on-plan sprint from a divergent one. Only upgrades on a
1191
+ // PASSing verify with concrete evidence (see judgeCriteriaAgainstVerify).
1192
+ try {
1193
+ const judgeModelId = roleAssignments.get("Reviewer")?.modelId ?? roleAssignments.get("PO")?.modelId ?? ctx.sessionModelId;
1194
+ let diffSummary = "";
1195
+ try {
1196
+ const { spawnSync } = await import("node:child_process");
1197
+ const stat = spawnSync("git", ["diff", "--stat", "HEAD"], { cwd, encoding: "utf8", timeout: 15000 });
1198
+ diffSummary = (stat.stdout ?? "").slice(0, 4000) || "(no diff detected)";
1199
+ }
1200
+ catch {
1201
+ diffSummary = "(diff unavailable)";
1202
+ }
1203
+ const verifyOutputForJudge = (verifyResult.error?.trim() ? verifyResult.error : (verifyResult.output ?? "")).trim();
1204
+ const { judged, total } = await judgeCriteriaAgainstVerify({
1205
+ flowDir: ctx.flowDir,
1206
+ runId: ctx.runId,
1207
+ llm: productLlm,
1208
+ modelId: judgeModelId,
1209
+ verifyVerdict,
1210
+ verifyOutput: verifyOutputForJudge,
1211
+ diffSummary,
1212
+ });
1213
+ if (total > 0) {
1214
+ yield {
1215
+ type: "content",
1216
+ content: `\n> [criteria] Judged ${judged}/${total} acceptance criteria as met/partial against verify+diff.\n`,
1217
+ };
1218
+ }
1219
+ }
1220
+ catch {
1221
+ /* non-critical — judging failure leaves criteria unmet (conservative) */
1222
+ }
931
1223
  const currentCriteria = await readCriteria(ctx.flowDir, ctx.runId);
932
1224
  // When a phaseScope is provided (subsystem E), evaluate the done-gate only
933
1225
  // against criteria belonging to this phase. Full criteria are kept for
@@ -1011,6 +1303,7 @@ export async function* runSprint(args) {
1011
1303
  criteriaMet: currentCriteria.filter((c) => c.status === "met").length,
1012
1304
  criteriaPartial: currentCriteria.filter((c) => c.status === "partial").length,
1013
1305
  criteriaUnmet: currentCriteria.filter((c) => c.status === "unmet").length,
1306
+ totalCriteria: currentCriteria.length,
1014
1307
  costUsd: 0, // Per-sprint cost is observed via the per-product ledger; field kept for compat.
1015
1308
  actualCost: 0,
1016
1309
  score: verdict.score,
@@ -1103,6 +1396,14 @@ export async function* runSprint(args) {
1103
1396
  // ── Step 9: If not done, surface continue-feedback to the user ───────────
1104
1397
  if (!verdict.pass) {
1105
1398
  const fb = buildContinueFeedback(verdict, verifyResult, currentCriteria);
1399
+ // Fold any residual plan deviations (surviving the adherence fixer) into the
1400
+ // carry-over focus so the next sprint continues the non-adherent/risky parts.
1401
+ const deviationNote = residualPlanDeviations.length > 0
1402
+ ? `\n\nPlan deviations still open (address these next):\n${residualPlanDeviations
1403
+ .map((d) => `- ${d}`)
1404
+ .join("\n")}`
1405
+ : "";
1406
+ iter.nextFocus = `${fb.focus}${deviationNote}`;
1106
1407
  yield {
1107
1408
  type: "content",
1108
1409
  content: `\n> Sprint ${sprintN} did not satisfy Definition-of-Done (${verdict.failedCondition ?? "unknown"}). Next focus: ${fb.focus}\n`,