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,109 @@
1
+ // src/product-loop/discovery-triage.ts
2
+ //
3
+ // Model-decided interview triage.
4
+ //
5
+ // The discover/gather interview used to size itself off `computePromptSpecificity`
6
+ // — a hardcoded word-count + keyword-list heuristic. That misclassifies precise-
7
+ // but-trivial ideas: "build a Python script hello.py that prints … and a pytest
8
+ // test" is ~18 words with no keyword hit → "moderate" → the user is dragged
9
+ // through 6 generic cards (productType/platform/audience/architecture/stack/db)
10
+ // and the productType recommender falls back to "other". That is the "hời hợt /
11
+ // hardcode" UX users complained about, and it violates the project's
12
+ // agent-first-no-regex principle (interview depth must be model-decided).
13
+ //
14
+ // This module asks the leader model to triage the idea into a complexity tier and,
15
+ // for genuinely complex ideas, name which of the fixed required questions actually
16
+ // shape the build. The interview then:
17
+ // - trivial → auto-fill every required answer → ONE summary confirm card
18
+ // - complex → keep only the model-relevant questions as cards, auto-fill the rest
19
+ // - standard → unchanged per-question flow
20
+ //
21
+ // Fail-safe: any parse/LLM failure returns a `fallback` triage derived from the
22
+ // old specificity heuristic, so the interview degrades to today's behaviour rather
23
+ // than breaking. Pure decision + one bounded LLM call → unit-testable.
24
+ import { computePromptSpecificity } from "./discovery-recommender.js";
25
+ import { REQUIRED_QUESTION_IDS } from "./discovery-schema.js";
26
+ const TRIAGE_SYSTEM = "You triage a software build request to decide how much of an interview it warrants. " +
27
+ "Output ONE JSON object, no prose, no code fences: " +
28
+ '{"complexity":"trivial"|"standard"|"complex","relevant":[<question-id>...],"rationale":"<short>"}.\n\n' +
29
+ "Tiers:\n" +
30
+ '- "trivial": a self-contained script, snippet, one-off tool, or exercise with ONE obvious stack and NO ' +
31
+ "persistence / scale / architecture / integration decisions. Examples: a hello-world script + a test, " +
32
+ "a regex to validate an email, a bash one-liner, a single pure function + unit test. The user should just " +
33
+ "confirm sensible defaults — asking about audience scale, backend architecture, or database strategy is noise.\n" +
34
+ '- "standard": a real but conventional app with a few genuine but well-trodden choices. Examples: a todo web ' +
35
+ "app, a CRUD REST API, a small CLI with a couple of subcommands.\n" +
36
+ '- "complex": multiple non-obvious architecture / scale / integration / security decisions. Examples: a ' +
37
+ "multi-tenant SaaS, an OAuth / SSO service, an event-driven pipeline, anything naming enterprise scale.\n\n" +
38
+ 'For "complex" ONLY, set `relevant` to the question ids (from the provided list) whose answers genuinely ' +
39
+ "shape this build and MUST be asked; auto-filled defaults are fine for the rest. For trivial/standard, " +
40
+ "set `relevant` to []. Bias toward FEWER questions: every card the user accepts by reflex becomes locked-in spec.";
41
+ function buildTriagePrompt(idea, requiredQuestions) {
42
+ const qList = requiredQuestions.map((q) => `- ${q.id}: ${q.prompt}`).join("\n");
43
+ return [
44
+ `Build request: ${JSON.stringify(idea)}`,
45
+ "",
46
+ "Required question ids you may cite in `relevant`:",
47
+ qList,
48
+ ].join("\n");
49
+ }
50
+ function stripFences(s) {
51
+ return s
52
+ .trim()
53
+ .replace(/^```(?:json)?\s*/, "")
54
+ .replace(/\s*```$/, "");
55
+ }
56
+ const TIERS = new Set(["trivial", "standard", "complex"]);
57
+ /** Graceful-degrade triage derived from the legacy specificity heuristic. */
58
+ export function fallbackTriage(idea) {
59
+ const specificity = computePromptSpecificity(idea);
60
+ // minimal prompt → treat as trivial (collapse to one confirm card, matching the
61
+ // old minimal-autofill path); everything else stays "standard" so the current
62
+ // per-question behaviour is preserved when the model is unavailable.
63
+ const complexity = specificity === "minimal" ? "trivial" : "standard";
64
+ return { complexity, relevant: [], rationale: `fallback from specificity=${specificity}`, source: "fallback" };
65
+ }
66
+ function parseTriage(raw) {
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(stripFences(raw));
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ const obj = parsed;
75
+ if (typeof obj?.complexity !== "string" || !TIERS.has(obj.complexity))
76
+ return null;
77
+ const requiredSet = new Set(REQUIRED_QUESTION_IDS);
78
+ const relevant = Array.isArray(obj.relevant)
79
+ ? obj.relevant.filter((r) => typeof r === "string" && requiredSet.has(r))
80
+ : [];
81
+ const rationale = typeof obj.rationale === "string" ? obj.rationale : "";
82
+ return { complexity: obj.complexity, relevant, rationale };
83
+ }
84
+ /**
85
+ * Triage the interview depth for `idea` via one leader call. Never throws —
86
+ * returns a fallback triage on any LLM/parse failure. Skips the call entirely
87
+ * (and returns the fallback) when the idea is empty.
88
+ */
89
+ export async function triageInterview(idea, leader, requiredQuestions) {
90
+ if (!idea || idea.trim().length === 0)
91
+ return fallbackTriage(idea);
92
+ try {
93
+ const res = await leader.generate({
94
+ system: TRIAGE_SYSTEM,
95
+ prompt: buildTriagePrompt(idea, requiredQuestions),
96
+ maxTokens: 1024,
97
+ });
98
+ const parsed = parseTriage(res?.content ?? "");
99
+ if (!parsed)
100
+ return fallbackTriage(idea);
101
+ // `relevant` is only meaningful for complex; normalize the others to [].
102
+ const relevant = parsed.complexity === "complex" ? parsed.relevant : [];
103
+ return { complexity: parsed.complexity, relevant, rationale: parsed.rationale, source: "model" };
104
+ }
105
+ catch {
106
+ return fallbackTriage(idea);
107
+ }
108
+ }
109
+ //# sourceMappingURL=discovery-triage.js.map
@@ -2,12 +2,18 @@
2
2
  // Dispatcher that wires the discover phase into the adaptive interview flow.
3
3
  // buildDiscoveryDebateRunner ← Task 16
4
4
  // buildGatherUserPrompt ← Task 17
5
- import { resolveLeaderModelDetailed } from "../council/leader.js";
5
+ import { runClarification } from "../council/clarifier.js";
6
+ import { resolveLeaderModelDetailed, resolveParticipants } from "../council/leader.js";
7
+ import { isCouncilMultiProviderPreferred } from "../utils/settings.js";
6
8
  import { detectExistingProject } from "./discovery-detection.js";
7
9
  import { iterateInterview, } from "./discovery-interview.js";
8
- import { acquireRunLock, initDiscoveryState, readDiscoveryState, readProjectContext, releaseRunLock, resumeArtifactWriteIfNeeded, } from "./discovery-persistence.js";
10
+ import { acquireRunLock, initDiscoveryState, markDone, readDiscoveryState, readProjectContext, releaseRunLock, resumeArtifactWriteIfNeeded, writeProjectContext, } from "./discovery-persistence.js";
9
11
  import { parsePromptForContext } from "./discovery-prompt-parser.js";
10
12
  import { councilRecommend, leaderRecommend, shouldFallbackToLeader } from "./discovery-recommender.js";
13
+ import { DISCOVERY_QUESTIONS } from "./discovery-schema.js";
14
+ import { triageInterview } from "./discovery-triage.js";
15
+ import { buildRepoBrief } from "./repo-brief.js";
16
+ import { SEED_DIMENSIONS } from "./seed-questions.js";
11
17
  /**
12
18
  * Decide whether detection signals are STRONG enough to synthesize a
13
19
  * backendStack answer (skipping the interview question). Returns null when
@@ -251,6 +257,30 @@ llm, sessionModelId, io) {
251
257
  },
252
258
  prefillAnswers: { ...prefillFromDetection, ...prompted },
253
259
  });
260
+ // Unified agent-driven interview (default). The LLM leader generates every
261
+ // clarification question itself from the idea + injected context and emits
262
+ // its own askcards — the same engine `/council` uses (runClarification) —
263
+ // instead of walking the fixed DISCOVERY_QUESTIONS list. The CLI only
264
+ // injects context (repo brief / prompt-extracted hints); it hardcodes no
265
+ // questions. Requires a wired driver (emit + respondToQuestion); pure-unit
266
+ // callers without io fall through to the legacy fixed-question path below.
267
+ // Opt out with MUONROI_IDEAL_AGENT_INTERVIEW=0.
268
+ if (process.env.MUONROI_IDEAL_AGENT_INTERVIEW !== "0" && io?.emit && io?.respondToQuestion) {
269
+ return await runAgentDrivenGather({
270
+ flowDir,
271
+ runId,
272
+ idea,
273
+ cwd,
274
+ detection,
275
+ leaderModelId,
276
+ sessionModelId,
277
+ llm: llm,
278
+ prompted,
279
+ prefillFromDetection,
280
+ emit: io.emit,
281
+ respondToQuestion: io.respondToQuestion,
282
+ });
283
+ }
254
284
  // Recommender: always leader-only. The council debate runner (Task 16) is
255
285
  // a stub that throws; until it is wired we delegate council asks to the
256
286
  // leader so the gather phase doesn't crash on greenfield projects with
@@ -270,6 +300,22 @@ llm, sessionModelId, io) {
270
300
  // (only hit by tests that don't supply io).
271
301
  const tuiAsk = io?.emit && io?.respondToQuestion ? buildLiveTuiAsk(io.emit, io.respondToQuestion) : async () => "";
272
302
  const userPrompt = buildGatherUserPrompt(tuiAsk);
303
+ // Model-decided interview triage — the PRIMARY signal for how deep to
304
+ // interview (replaces the keyword `computePromptSpecificity` heuristic as the
305
+ // lead). A trivial idea (e.g. a hello-world script) collapses to ONE confirm
306
+ // card instead of 6 generic productType/audience/architecture/db cards; a
307
+ // complex idea keeps only the questions the model flags as decision-shaping.
308
+ // Only the required questions are triaged (optional ones are gated separately).
309
+ // Never throws — degrades to the specificity fallback (see discovery-triage.ts).
310
+ const requiredQuestions = DISCOVERY_QUESTIONS.filter((q) => q.required);
311
+ const triage = await triageInterview(idea, leader, requiredQuestions);
312
+ if (process.env.MUONROI_DEBUG_LEADER === "1") {
313
+ process.stderr.write(`[interview-triage] ${JSON.stringify({
314
+ complexity: triage.complexity,
315
+ relevant: triage.relevant,
316
+ source: triage.source,
317
+ })}\n`);
318
+ }
273
319
  return await iterateInterview({
274
320
  flowDir,
275
321
  runId,
@@ -278,6 +324,7 @@ llm, sessionModelId, io) {
278
324
  detection,
279
325
  userPrompt,
280
326
  recommender,
327
+ triage,
281
328
  });
282
329
  }
283
330
  finally {
@@ -285,6 +332,90 @@ llm, sessionModelId, io) {
285
332
  }
286
333
  }
287
334
  // ---------------------------------------------------------------------------
335
+ // Agent-driven interview (unified clarifier engine)
336
+ // ---------------------------------------------------------------------------
337
+ /**
338
+ * Run the gather interview by delegating to the SAME agent-driven clarifier the
339
+ * council uses (`runClarification`): the LLM leader generates its own
340
+ * clarification questions from the idea + injected context, emits its own
341
+ * `council_question` askcards, self-judges readiness, and synthesizes a
342
+ * `ClarifiedSpec`. The CLI contributes only context (repo brief for existing
343
+ * repos, prompt-extracted hints) and the askcard plumbing — it hardcodes no
344
+ * questions and no answer enums.
345
+ *
346
+ * Output is a `ProjectContext` that carries the clarifier's `ClarifiedSpec`
347
+ * verbatim (`clarified`), so `clarifiedSpecFromContext` returns the real
348
+ * LLM-synthesized successCriteria/constraints/scope instead of re-deriving a
349
+ * thin persona+scale spec. `context` is populated best-effort from detection +
350
+ * prompt hints purely as a supplement for the (fully-guarded) downstream
351
+ * `formatProjectContextForPrompt`.
352
+ */
353
+ async function runAgentDrivenGather(args) {
354
+ // Context the CLI injects for the agent to interview AGAINST — never questions.
355
+ let conversationContext = "";
356
+ if (args.detection.classification !== "greenfield") {
357
+ try {
358
+ const brief = await buildRepoBrief(args.cwd, args.detection);
359
+ // The "## Current Project" header switches the clarifier into existing-repo
360
+ // mode (drops generic greenfield questions, grounds rationales in real files).
361
+ conversationContext += `## Current Project\n${brief.markdown}\n\n`;
362
+ }
363
+ catch {
364
+ // Brief failure must NEVER block the interview — fall through context-light.
365
+ }
366
+ }
367
+ if (Object.keys(args.prompted).length > 0) {
368
+ conversationContext += `## Extracted from the request\n${JSON.stringify(args.prompted, null, 2)}\n`;
369
+ }
370
+ // Reachable panel models — passed as the clarifier's fallbackModels so its
371
+ // research-first step can prefer a native-web-research model on any reachable
372
+ // provider (and spec-synth can fall back off a flaky leader proxy). Never
373
+ // blocks gather: resolution failure degrades to leader-only.
374
+ let panelModels = [];
375
+ try {
376
+ const participants = await resolveParticipants(args.sessionModelId, isCouncilMultiProviderPreferred());
377
+ panelModels = participants.map((p) => p.model).filter(Boolean);
378
+ }
379
+ catch {
380
+ // leader-only reachability is fine — research-first still runs on the leader.
381
+ }
382
+ // Drive the clarifier generator; forward each emitted chunk to the driver so
383
+ // the TUI renders each dynamically-generated askcard, then capture the spec.
384
+ // seedQuestions is intentionally empty — the clarifier's own research-first
385
+ // step (runClarification) now grounds the questions; no CLI-owned seed list.
386
+ const gen = runClarification(args.idea, args.leaderModelId, conversationContext, args.respondToQuestion, args.llm, undefined, // signal — no abort wired through gather today
387
+ undefined, // seedQuestions
388
+ undefined, // maxRounds → clarifier default
389
+ undefined, // prefillAnswers
390
+ false, // costAware
391
+ panelModels);
392
+ let result;
393
+ do {
394
+ result = await gen.next();
395
+ if (!result.done && result.value)
396
+ args.emit(result.value);
397
+ } while (!result.done);
398
+ const clarified = result.value;
399
+ const context = { ...args.prefillFromDetection, ...args.prompted };
400
+ const projectContext = {
401
+ version: 1,
402
+ schemaName: "project-context",
403
+ generatedAt: new Date().toISOString(),
404
+ idea: args.idea,
405
+ detection: args.detection,
406
+ context,
407
+ clarified,
408
+ recommendations: {
409
+ byField: {},
410
+ constraints: { fePolicy: "headless-ui-only", feEnforced: true },
411
+ },
412
+ userOverrides: [],
413
+ };
414
+ await writeProjectContext(args.flowDir, args.runId, projectContext);
415
+ await markDone(args.flowDir, args.runId);
416
+ return projectContext;
417
+ }
418
+ // ---------------------------------------------------------------------------
288
419
  // ClarifiedSpec bridge
289
420
  // ---------------------------------------------------------------------------
290
421
  /**
@@ -294,6 +425,23 @@ llm, sessionModelId, io) {
294
425
  * has passed (or all required questions are answered).
295
426
  */
296
427
  export function clarifiedSpecFromContext(pc) {
428
+ // Agent-driven path: the clarifier already synthesized a full ClarifiedSpec
429
+ // (real LLM-derived successCriteria/constraints/scope + rawQA). Return it
430
+ // verbatim, deriving the loop-driver resolution gate from the agent's OWN
431
+ // readiness judgment rather than the hardcoded 6-dimension answered-check:
432
+ // when the clarifier declared the spec ready (or didn't flag it not-ready),
433
+ // mark every SEED_DIMENSION resolved so the gate passes; otherwise leave them
434
+ // unspecified so the existing insufficient_resolution safety net still fires.
435
+ if (pc.clarified) {
436
+ const ready = pc.clarified.ready !== false;
437
+ const status = ready ? "answered" : "unspecified";
438
+ const resolved = {
439
+ ...(pc.clarified.resolved ?? {}),
440
+ };
441
+ for (const d of SEED_DIMENSIONS)
442
+ resolved[d.id] = status;
443
+ return { ...pc.clarified, resolved };
444
+ }
297
445
  const ctx = pc.context;
298
446
  const problemStatement = pc.idea;
299
447
  const constraints = [];
@@ -1204,6 +1204,12 @@ async function* runPhasesPath(args) {
1204
1204
  currentPhaseId = sc.phaseId ?? null;
1205
1205
  sprintHistory = [];
1206
1206
  }
1207
+ // Carry the previous sprint's failure focus (unmet criteria / verify failures
1208
+ // / residual plan deviations) into this sprint so each iteration continues the
1209
+ // specific risky parts instead of restarting blind — parity with the legacy
1210
+ // drainSprints path. Without this the phase-orchestrated loop dropped the
1211
+ // feedback-routing focus between sprints.
1212
+ const prevSprint = sprintHistory[sprintHistory.length - 1];
1207
1213
  const inner = runSprint({
1208
1214
  sprintN: sc.sprintN,
1209
1215
  ctx,
@@ -1211,6 +1217,7 @@ async function* runPhasesPath(args) {
1211
1217
  roleAssignments,
1212
1218
  history: [...sprintHistory],
1213
1219
  phaseScope: sc.phaseScope,
1220
+ carryOver: prevSprint?.nextFocus ? { focus: prevSprint.nextFocus } : undefined,
1214
1221
  });
1215
1222
  let result;
1216
1223
  while (true) {
@@ -32,21 +32,33 @@ import { deriveTasksFromSpec, writeTasks } from "./typed-artifacts.js";
32
32
  // the debate llm and double-counted nothing else; recording at the source removes
33
33
  // both the leak and the risk of a double count here.
34
34
  /**
35
- * Best-effort interaction_logs writer for the loop-driver. Swallows failures
36
- * so a broken DB never blocks the FSM. `ctx.sessionId` falls back to runId
37
- * for legacy callers that don't pass a chat session id.
35
+ * Best-effort interaction_logs writer for the loop-driver. Logs and continues on
36
+ * failure so a broken DB never blocks the FSM. `ctx.sessionId` falls back to
37
+ * runId for legacy callers that don't pass a chat session id. `cols` carries
38
+ * fields that belong in real columns rather than metadata_json.
38
39
  */
39
- function logLoopEvent(ctx, subtype, data) {
40
+ function logLoopEvent(ctx, subtype, data, cols) {
40
41
  try {
41
42
  const sid = ctx.sessionId ?? ctx.runId;
42
43
  // Stamp runId on every row so multi-run sessions can be demuxed during
43
44
  // forensics — previously only route_decision carried this field. Caller
44
45
  // overrides win if the payload already contained a runId.
45
46
  const enriched = "runId" in data ? data : { ...data, runId: ctx.runId };
46
- logInteraction(sid, "council", { eventSubtype: subtype, data: enriched });
47
+ // `cols` promotes fields out of metadata_json into real columns so they are
48
+ // aggregatable in SQL. Without it every council_message row landed with
49
+ // duration_ms NULL, leaving `debate_complete` (the whole debate) as the only
50
+ // timed row — an aggregate that cannot name which speaker was slow.
51
+ logInteraction(sid, "council", {
52
+ eventSubtype: subtype,
53
+ data: enriched,
54
+ ...(cols?.model ? { model: cols.model } : {}),
55
+ ...(typeof cols?.durationMs === "number" ? { durationMs: cols.durationMs } : {}),
56
+ });
47
57
  }
48
- catch {
49
- /* non-criticalaudit trail only */
58
+ catch (err) {
59
+ // Audit trail only never block the FSM. Logged so a broken DB surfaces
60
+ // instead of silently emptying the forensics table. (No-Silent-Catch.)
61
+ console.error(`[loop-driver] logLoopEvent failed for subtype "${subtype}": ${err instanceof Error ? err.message : String(err)}`);
50
62
  }
51
63
  }
52
64
  /**
@@ -668,7 +680,8 @@ export async function* runLoopDriver(ctx) {
668
680
  // text limits even for the most verbose speaker turn.
669
681
  textExcerpt: cm.text.slice(0, 4000),
670
682
  textLength: cm.text.length,
671
- });
683
+ durationMs: cm.durationMs ?? null,
684
+ }, { model: cm.speaker.model, durationMs: cm.durationMs });
672
685
  }
673
686
  yield chunk;
674
687
  }
@@ -7,6 +7,22 @@ export declare const PHASE_PLANNER_SYSTEM: string;
7
7
  * Normalise `id` + `dependsOn` on every phase to the canonical string form.
8
8
  * Mutates and returns the same plan object. Safe on already-normalised plans.
9
9
  */
10
+ /**
11
+ * Coerce a plan's per-phase `maxSprints` to a usable sprint COUNT: an integer
12
+ * in [1, 20]. This is a pure VALIDITY FLOOR — it does NOT allocate or divide any
13
+ * budget. The PO/leader decides each phase's `maxSprints` from that phase's
14
+ * difficulty (see PHASE_PLANNER_SYSTEM); this only defends the executor against a
15
+ * malformed value the model might still emit.
16
+ *
17
+ * The sprint loop is `for (sprintN = 1; sprintN <= phase.maxSprints; sprintN++)`,
18
+ * so any value < 1 makes `1 <= 0.n` false → the loop body NEVER runs → `runSprint`
19
+ * is never called → implementation is silently skipped (the historical
20
+ * "plan is great but implement never starts / hangs" bug — a small `--max-sprints`
21
+ * once made the old "divide the budget across phases" prompt emit fractions like
22
+ * `0.3 / 0.2 / 0.5`). Flooring to an integer ≥ 1 guarantees every planned phase
23
+ * executes at least one sprint regardless of what the model returns.
24
+ */
25
+ export declare function clampMaxSprints(raw: unknown): number;
10
26
  export declare function normalizePhasePlan(plan: PhasePlanArtifact): PhasePlanArtifact;
11
27
  export declare function parsePhasePlanJson(raw: string): PhasePlanArtifact;
12
28
  export declare function validatePhasePlan(plan: PhasePlanArtifact, spec: ClarifiedSpec): void;
@@ -3,9 +3,17 @@ import { promises as fs } from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { readArtifact, writeArtifact } from "../flow/artifact-io.js";
5
5
  import { withRateLimitBackoff } from "./discovery-recommender.js";
6
- export const PHASE_PLANNER_SYSTEM = "You decompose a product idea into 3–5 sequential phases. Output strict JSON only. " +
7
- "Each phase covers a subset of successCriteria verbatim from the input spec. " +
8
- "Union of all phases.successCriteria MUST equal the input successCriteria array (no drift, no omission).";
6
+ export const PHASE_PLANNER_SYSTEM = "You are the PO/tech-lead planning an agile delivery. Decompose a product idea into 3–5 " +
7
+ "sequential phases. Output strict JSON only. Each phase covers a subset of successCriteria " +
8
+ "verbatim from the input spec. Union of all phases.successCriteria MUST equal the input " +
9
+ "successCriteria array (no drift, no omission).\n\n" +
10
+ "Size each phase to its REAL difficulty. For every phase, YOU decide its `maxSprints` — the " +
11
+ "maximum sprint iterations that phase may need — from that phase's own scope, risk, and " +
12
+ "uncertainty, exactly as a tech lead would in agile planning: a simple/mechanical phase needs " +
13
+ "1; a phase with hard, uncertain, or risky work needs more (2–5). Do NOT split a fixed budget " +
14
+ "evenly, do NOT pad to a quota, and never emit a fraction. `maxSprints` is a CEILING — the " +
15
+ "done-gate ends a phase early once its criteria are met — so give genuinely hard phases enough " +
16
+ "room to iterate rather than rationing them.";
9
17
  /**
10
18
  * Coerce a phase id / dependsOn element to the canonical `phase-N` string form.
11
19
  *
@@ -32,11 +40,33 @@ function normalizePhaseRef(ref) {
32
40
  * Normalise `id` + `dependsOn` on every phase to the canonical string form.
33
41
  * Mutates and returns the same plan object. Safe on already-normalised plans.
34
42
  */
43
+ /**
44
+ * Coerce a plan's per-phase `maxSprints` to a usable sprint COUNT: an integer
45
+ * in [1, 20]. This is a pure VALIDITY FLOOR — it does NOT allocate or divide any
46
+ * budget. The PO/leader decides each phase's `maxSprints` from that phase's
47
+ * difficulty (see PHASE_PLANNER_SYSTEM); this only defends the executor against a
48
+ * malformed value the model might still emit.
49
+ *
50
+ * The sprint loop is `for (sprintN = 1; sprintN <= phase.maxSprints; sprintN++)`,
51
+ * so any value < 1 makes `1 <= 0.n` false → the loop body NEVER runs → `runSprint`
52
+ * is never called → implementation is silently skipped (the historical
53
+ * "plan is great but implement never starts / hangs" bug — a small `--max-sprints`
54
+ * once made the old "divide the budget across phases" prompt emit fractions like
55
+ * `0.3 / 0.2 / 0.5`). Flooring to an integer ≥ 1 guarantees every planned phase
56
+ * executes at least one sprint regardless of what the model returns.
57
+ */
58
+ export function clampMaxSprints(raw) {
59
+ const n = typeof raw === "number" ? raw : Number(raw);
60
+ if (!Number.isFinite(n) || n < 1)
61
+ return 1;
62
+ return Math.min(20, Math.round(n));
63
+ }
35
64
  export function normalizePhasePlan(plan) {
36
65
  if (Array.isArray(plan?.phases)) {
37
66
  for (const phase of plan.phases) {
38
67
  phase.id = normalizePhaseRef(phase.id);
39
68
  phase.dependsOn = Array.isArray(phase.dependsOn) ? phase.dependsOn.map(normalizePhaseRef) : [];
69
+ phase.maxSprints = clampMaxSprints(phase.maxSprints);
40
70
  }
41
71
  }
42
72
  return plan;
@@ -133,7 +163,15 @@ function buildPhasePlannerPrompt(args) {
133
163
  `Scope: ${args.clarifiedSpec.scope}`,
134
164
  `SuccessCriteria (return these verbatim, distributed across 3-5 phases):`,
135
165
  ...args.clarifiedSpec.successCriteria.map((c, i) => ` ${i + 1}. ${c}`),
136
- `MaxSprints budget: ${args.manifest.maxSprints} (divide across phases).`,
166
+ // maxSprints is PO-decided PER PHASE from that phase's difficulty (see the
167
+ // system prompt) — NOT a global budget to divide. The user's --max-sprints is
168
+ // only a SOFT guide for overall ambition, never an arithmetic cap the planner
169
+ // must partition. Each phase's "maxSprints" MUST be a whole integer >= 1.
170
+ `Sprint sizing: decide each phase's "maxSprints" from its own difficulty/scope/risk — a whole ` +
171
+ `integer >= 1, more for harder phases. The user suggested roughly ${args.manifest.maxSprints} ` +
172
+ `sprint(s) total as a soft guide to overall ambition; you may use fewer if the work is simple ` +
173
+ `or allocate more per phase if the work genuinely needs it. Do not divide it evenly and do not ` +
174
+ `treat it as a hard budget.`,
137
175
  `Output JSON shape: { version:1, generatedAt:<ISO>, phases:[{id,name,goal,successCriteria,scope,exitCondition:{type:"criteria-threshold",min:${args.manifest.doneThreshold}},dependsOn,maxSprints}] }`,
138
176
  ].join("\n");
139
177
  }
@@ -6,6 +6,9 @@ export interface SprintState {
6
6
  scoreAfter: number;
7
7
  criteriaMet: number;
8
8
  totalCriteria: number;
9
+ /** Verify verdict for this sprint (PASS/FAIL/ERROR) — grounds the review so it
10
+ * describes what actually happened instead of confabulating an agile narrative. */
11
+ verifyVerdict?: string;
9
12
  }
10
13
  export declare function generateSprintReview(args: {
11
14
  sprintState: SprintState;
@@ -15,9 +15,14 @@ export async function generateSprintReview(args) {
15
15
  if (args.remainingUsd < reviewFloor(args.capUsd)) {
16
16
  return { summary: deterministicReview(args.sprintState), usedFallback: true };
17
17
  }
18
- const prompt = `Sprint ${args.sprintState.sprintN} of phase ${args.phase.id}: score ${args.sprintState.scoreBefore.toFixed(2)} → ${args.sprintState.scoreAfter.toFixed(2)}, ` +
19
- `met ${args.sprintState.criteriaMet}/${args.sprintState.totalCriteria} criteria. ` +
20
- `Write a ≤500-char demo summary for the customer.`;
18
+ const s = args.sprintState;
19
+ const verifyLine = s.verifyVerdict ? ` Verify: ${s.verifyVerdict}.` : "";
20
+ const goal = args.phase.goal ? ` Phase goal: ${args.phase.goal}.` : "";
21
+ const prompt = `Sprint ${s.sprintN} of phase ${args.phase.id}: score ${s.scoreBefore.toFixed(2)} → ${s.scoreAfter.toFixed(2)}, ` +
22
+ `met ${s.criteriaMet}/${s.totalCriteria} acceptance criteria.${verifyLine}${goal} ` +
23
+ `Write a ≤500-char demo summary describing ONLY what these numbers show — the concrete progress on criteria and verify status. ` +
24
+ `Do NOT invent activities (e.g. "team alignment", "environment setup") that the metrics do not evidence. ` +
25
+ `If 0/${s.totalCriteria} criteria are met, say plainly that no criteria passed yet and why (verify status), not that setup was done.`;
21
26
  try {
22
27
  const res = await withRateLimitBackoff(() => args.leader.generate({ system: "You write concise sprint demo summaries.", prompt, maxTokens: 250 }), { delays: args.backoffDelays });
23
28
  return { summary: res.content.trim().slice(0, 500), usedFallback: false };
@@ -313,7 +313,11 @@ export async function* runPhases(args) {
313
313
  lastSprintState = sprintResult;
314
314
  totalSprints += 1;
315
315
  const review = await generateSprintReview({
316
- sprintState: { sprintN, ...sprintResult },
316
+ sprintState: {
317
+ sprintN,
318
+ ...sprintResult,
319
+ verifyVerdict: sprintResult.lastVerifyResult,
320
+ },
317
321
  phase,
318
322
  leader: args.leader,
319
323
  capUsd: args.capUsd,
@@ -323,15 +327,29 @@ export async function* runPhases(args) {
323
327
  if (!args.suppressPush) {
324
328
  yield { type: "push_notification", content: review.summary };
325
329
  }
326
- await markAwaitingCustomerReview(args.flowDir, args.runId, phase.id, sprintN);
327
- const verdict = await args.awaitCustomerVerdict({
328
- flowDir: args.flowDir,
329
- runId: args.runId,
330
- phaseId: phase.id,
331
- sprintN,
332
- reviewSummary: review.summary,
333
- });
334
- await clearAwaitingCustomerReview(args.flowDir, args.runId, phase.id, sprintN);
330
+ // Autonomy fix: by default /ideal advances between sprints WITHOUT a blocking
331
+ // human verdict (previously the loop parked in discordAwaitVerdict's poll and
332
+ // never reached sprint 2 unless a human replied). Set
333
+ // MUONROI_IDEAL_REQUIRE_VERDICT=1 to restore the human customer-review gate.
334
+ const requireVerdict = process.env.MUONROI_IDEAL_REQUIRE_VERDICT === "1";
335
+ let verdict;
336
+ if (requireVerdict) {
337
+ await markAwaitingCustomerReview(args.flowDir, args.runId, phase.id, sprintN);
338
+ verdict = await args.awaitCustomerVerdict({
339
+ flowDir: args.flowDir,
340
+ runId: args.runId,
341
+ phaseId: phase.id,
342
+ sprintN,
343
+ reviewSummary: review.summary,
344
+ });
345
+ await clearAwaitingCustomerReview(args.flowDir, args.runId, phase.id, sprintN);
346
+ }
347
+ else {
348
+ verdict = {
349
+ verdict: "accept",
350
+ feedback: "[auto-advance: autonomous mode (MUONROI_IDEAL_REQUIRE_VERDICT unset)]",
351
+ };
352
+ }
335
353
  await appendCustomerDecision(args.flowDir, args.runId, {
336
354
  phaseId: phase.id,
337
355
  sprintN,
@@ -360,7 +378,8 @@ export async function* runPhases(args) {
360
378
  // retro skipped
361
379
  }
362
380
  await clearRetroPending(args.flowDir, args.runId, phase.id, sprintN);
363
- const phaseRatio = sprintResult.criteriaMet / Math.max(1, sprintResult.totalCriteria);
381
+ const phaseTotal = sprintResult.totalCriteria ?? phase.successCriteria.length;
382
+ const phaseRatio = sprintResult.criteriaMet / Math.max(1, phaseTotal);
364
383
  if (phaseRatio >= phase.exitCondition.min)
365
384
  break;
366
385
  }
@@ -0,0 +1,26 @@
1
+ import type { StreamChunk, TaskRequest, ToolResult } from "../types/index.js";
2
+ /**
3
+ * Plan-adherence review gate (requested 2026-07-12): after implementation, spawn a
4
+ * HIGH-TIER reviewer agent to check the actual diff against the approved plan
5
+ * (file_edits + acceptance_criteria). When it finds deviations, hand a concrete fix
6
+ * task to a LOWER-TIER agent and re-review. Bounded rounds; never halts the sprint
7
+ * (verify + the criteria done-gate remain the hard gates). This catches the Sprint-1
8
+ * failure mode where a cheap implementer received a rich plan but diverged (wrong
9
+ * LSP op, stub tools) with nothing to notice.
10
+ */
11
+ export interface AdherenceVerdict {
12
+ rounds: number;
13
+ adherent: boolean;
14
+ deviations: string[];
15
+ }
16
+ export declare function runPlanAdherenceReview(args: {
17
+ sprintN: number;
18
+ planSynthesis: string;
19
+ cwd: string;
20
+ reviewModelId: string;
21
+ fixModelId: string;
22
+ runIsolatedTask: (req: TaskRequest) => Promise<ToolResult>;
23
+ maxRounds?: number;
24
+ /** Injectable for tests; defaults to `git diff HEAD` in cwd. */
25
+ diffProvider?: (cwd: string) => string;
26
+ }): AsyncGenerator<StreamChunk, AdherenceVerdict, unknown>;