muonroi-cli 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  4. package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
  5. package/dist/src/agent-harness/mock-model.d.ts +11 -0
  6. package/dist/src/agent-harness/mock-model.js +21 -0
  7. package/dist/src/cli/cost-forensics.js +12 -12
  8. package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
  9. package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
  10. package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
  11. package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
  12. package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
  13. package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
  14. package/dist/src/council/clarifier.js +9 -1
  15. package/dist/src/council/debate.js +5 -1
  16. package/dist/src/council/decisions-lock.js +3 -3
  17. package/dist/src/council/index.js +12 -5
  18. package/dist/src/council/leader.d.ts +0 -17
  19. package/dist/src/council/leader.js +22 -15
  20. package/dist/src/council/planner.js +1 -1
  21. package/dist/src/council/prompts.js +63 -57
  22. package/dist/src/council/types.d.ts +7 -0
  23. package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
  24. package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
  25. package/dist/src/ee/auth.d.ts +9 -0
  26. package/dist/src/ee/auth.js +19 -0
  27. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  28. package/dist/src/ee/ee-onboarding.js +76 -0
  29. package/dist/src/generated/version.d.ts +1 -1
  30. package/dist/src/generated/version.js +1 -1
  31. package/dist/src/headless/output.js +6 -4
  32. package/dist/src/headless/output.test.js +4 -3
  33. package/dist/src/index.js +20 -1
  34. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  35. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  36. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  37. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  38. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  39. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  40. package/dist/src/mcp/auto-setup.js +56 -2
  41. package/dist/src/mcp/client-pool.d.ts +46 -0
  42. package/dist/src/mcp/client-pool.js +212 -0
  43. package/dist/src/mcp/oauth-callback.js +2 -2
  44. package/dist/src/mcp/parse-headers.test.js +14 -14
  45. package/dist/src/mcp/runtime.d.ts +28 -0
  46. package/dist/src/mcp/runtime.js +117 -51
  47. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  48. package/dist/src/mcp/self-verify-runner.js +38 -0
  49. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  50. package/dist/src/mcp/setup-guide-text.js +84 -0
  51. package/dist/src/mcp/smart-filter.js +49 -0
  52. package/dist/src/mcp/smoke.test.js +43 -43
  53. package/dist/src/mcp/tools-server.d.ts +7 -0
  54. package/dist/src/mcp/tools-server.js +19 -22
  55. package/dist/src/models/catalog.json +349 -349
  56. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  57. package/dist/src/ops/doctor.d.ts +3 -2
  58. package/dist/src/ops/doctor.js +47 -11
  59. package/dist/src/ops/doctor.test.js +4 -3
  60. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  61. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  62. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  63. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  64. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  65. package/dist/src/orchestrator/message-processor.js +57 -27
  66. package/dist/src/orchestrator/orchestrator.js +26 -0
  67. package/dist/src/orchestrator/prompts.d.ts +51 -0
  68. package/dist/src/orchestrator/prompts.js +257 -134
  69. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  70. package/dist/src/orchestrator/stream-runner.js +20 -15
  71. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  72. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  73. package/dist/src/pil/__tests__/config.test.js +1 -17
  74. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  75. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  76. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  77. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  78. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  79. package/dist/src/pil/__tests__/layer6-output.test.js +137 -18
  80. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  81. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  82. package/dist/src/pil/agent-operating-contract.js +2 -0
  83. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  84. package/dist/src/pil/cheap-model-playbook.js +35 -35
  85. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  86. package/dist/src/pil/clarity-gate.d.ts +21 -19
  87. package/dist/src/pil/clarity-gate.js +26 -153
  88. package/dist/src/pil/config.d.ts +9 -1
  89. package/dist/src/pil/config.js +15 -4
  90. package/dist/src/pil/discovery.js +211 -136
  91. package/dist/src/pil/layer1-intent.d.ts +12 -0
  92. package/dist/src/pil/layer1-intent.js +283 -38
  93. package/dist/src/pil/layer1-intent.test.js +210 -4
  94. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  95. package/dist/src/pil/layer16-clarity.js +19 -306
  96. package/dist/src/pil/layer4-gsd.js +18 -6
  97. package/dist/src/pil/layer6-output.d.ts +2 -0
  98. package/dist/src/pil/layer6-output.js +137 -22
  99. package/dist/src/pil/llm-classify.d.ts +26 -0
  100. package/dist/src/pil/llm-classify.js +34 -5
  101. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  102. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  103. package/dist/src/pil/schema.d.ts +8 -0
  104. package/dist/src/pil/schema.js +12 -1
  105. package/dist/src/pil/task-tier-map.js +4 -0
  106. package/dist/src/pil/types.d.ts +11 -1
  107. package/dist/src/product-loop/done-gate.js +3 -3
  108. package/dist/src/product-loop/loop-driver.js +18 -18
  109. package/dist/src/product-loop/progress-snapshot.js +4 -4
  110. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  111. package/dist/src/providers/auth/grok-oauth.js +6 -15
  112. package/dist/src/providers/auth/openai-oauth.js +6 -15
  113. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  114. package/dist/src/reporter/index.js +1 -1
  115. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  116. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  117. package/dist/src/scaffold/continuation-prompt.js +60 -60
  118. package/dist/src/scaffold/init-new.js +453 -453
  119. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  120. package/dist/src/self-qa/agentic-loop.js +24 -19
  121. package/dist/src/self-qa/spec-emitter.js +26 -23
  122. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  123. package/dist/src/storage/interaction-log.js +5 -5
  124. package/dist/src/storage/migrations.js +122 -122
  125. package/dist/src/storage/sessions.js +42 -42
  126. package/dist/src/storage/transcript.js +91 -84
  127. package/dist/src/storage/usage.js +14 -14
  128. package/dist/src/storage/workspaces.js +12 -12
  129. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  130. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  131. package/dist/src/tools/git-safety.d.ts +61 -0
  132. package/dist/src/tools/git-safety.js +141 -0
  133. package/dist/src/tools/git-safety.test.d.ts +1 -0
  134. package/dist/src/tools/git-safety.test.js +111 -0
  135. package/dist/src/tools/native-tools.d.ts +31 -0
  136. package/dist/src/tools/native-tools.js +273 -0
  137. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  138. package/dist/src/tools/registry-git-safety.test.js +92 -0
  139. package/dist/src/tools/registry.js +39 -4
  140. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  141. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  142. package/dist/src/ui/app.js +0 -0
  143. package/dist/src/ui/components/message-view.js +4 -1
  144. package/dist/src/ui/components/structured-response-view.js +7 -3
  145. package/dist/src/ui/components/tool-group.js +7 -1
  146. package/dist/src/ui/markdown-render.d.ts +41 -0
  147. package/dist/src/ui/markdown-render.js +223 -0
  148. package/dist/src/ui/markdown.d.ts +10 -0
  149. package/dist/src/ui/markdown.js +12 -35
  150. package/dist/src/ui/slash/council-inspect.js +4 -4
  151. package/dist/src/ui/slash/export.js +4 -4
  152. package/dist/src/ui/utils/text.d.ts +8 -0
  153. package/dist/src/ui/utils/text.js +16 -0
  154. package/dist/src/ui/utils/text.test.d.ts +1 -0
  155. package/dist/src/ui/utils/text.test.js +23 -0
  156. package/dist/src/usage/ledger.js +48 -15
  157. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  158. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  159. package/dist/src/utils/clipboard-image.js +23 -23
  160. package/dist/src/utils/open-url.d.ts +56 -0
  161. package/dist/src/utils/open-url.js +58 -0
  162. package/dist/src/utils/open-url.test.d.ts +1 -0
  163. package/dist/src/utils/open-url.test.js +86 -0
  164. package/dist/src/utils/settings.d.ts +12 -0
  165. package/dist/src/utils/settings.js +48 -0
  166. package/dist/src/utils/side-question.js +2 -2
  167. package/dist/src/utils/skills.js +3 -3
  168. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  169. package/dist/src/verify/environment.js +2 -1
  170. package/package.json +1 -1
  171. package/dist/src/pil/layer16-clarity.test.js +0 -31
  172. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -1,13 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { shouldAutoPass } from "./clarity-gate.js";
2
+ import { detectNoClarifySignal } from "./clarity-gate.js";
3
3
  import { getMaxInterviewQuestions, isDiscoveryEnabled } from "./config.js";
4
4
  import { getCachedProjectContext, setCachedProjectContext } from "./discovery-cache.js";
5
5
  import { isMetaAnalysisPrompt } from "./layer6-output.js";
6
6
  import { scanProjectContext } from "./layer15-context-scan.js";
7
- import { buildInterviewQuestion, detectClarityGaps, getAutofilledOutcome, resolveGapsNonInteractive, } from "./layer16-clarity.js";
7
+ import { buildInterviewQuestion, getAutofilledOutcome, isProvideOwnDetailsSentinel, PROVIDE_OWN_DETAILS_OPTION_EN, resolveGapsNonInteractive, } from "./layer16-clarity.js";
8
8
  import { checkFeasibility } from "./layer17-feasibility.js";
9
9
  import { buildAcceptanceCard, buildAcceptanceQuestion } from "./layer18-acceptance.js";
10
- import { getSessionState, isLikelyFollowUp, markDiscoveryAccepted } from "./session-state.js";
10
+ import { markDiscoveryAccepted } from "./session-state.js";
11
11
  function emptyProjectContext(cwd) {
12
12
  return {
13
13
  language: null,
@@ -44,21 +44,36 @@ export async function runDiscovery(raw, l1, cwd, handler, sessionId = null, clar
44
44
  return baseResult();
45
45
  if (l1.intentKind === "chitchat" || l1.taskType === null)
46
46
  return baseResult();
47
- // Session-continuation guard: when the user is on turn >= 2 of an ongoing
48
- // session AND the new prompt looks like a continuation (short, modal verb
49
- // or context pronoun), skip the interview entirely. The prior turn already
50
- // established context; asking "Which part of the codebase?" on "Can you
51
- // fix it?" forces the user to re-type their intent as a freetext answer,
52
- // which PIL then mis-routes through gap-resolution + acceptance, producing
53
- // duplicate askcards (evidence: session 1f29e238a816 timeline).
54
- const sessionState = getSessionState(sessionId);
55
- if (sessionState && sessionState.turnCount > 1 && isLikelyFollowUp(raw)) {
47
+ // The user explicitly told the agent not to clarify ("don't ask" / "trả lời
48
+ // thẳng"). This is the USER's consent decision, not a classification heuristic,
49
+ // so it stays: skip the entire interview + acceptance ceremony.
50
+ if (detectNoClarifySignal(raw))
56
51
  return baseResult();
57
- }
58
- const l1Signal = { confidence: l1.confidence, taskType: l1.taskType, complexity: l1.complexity };
59
- if (shouldAutoPass(l1Signal, raw))
52
+ // ── Model-driven clarification gate (Phase 2) ──────────────────────────────
53
+ // The configured chat model NOT a regex/keyword heuristic — is the sole
54
+ // decider of whether this turn has a genuine gray area, what to ask, which
55
+ // options to offer, why, and which option is recommended (user directive
56
+ // 2026-06-16: "các askcard bắt buộc xuất phát từ model muốn hỏi"). The CLI only
57
+ // injects the proposer prompt to open the path.
58
+ //
59
+ // There is deliberately NO regex fallback. The old path ran shouldAutoPass() +
60
+ // detectClarityGaps() keyword heuristics and fabricated questions/outcomes from
61
+ // them — the exact "phân loại task qua regex từ khoá ... bad bad bad UX, miss
62
+ // hàng tỷ case" behaviour this phase removes (it also fabricated a build outcome
63
+ // for a yes/no question — live repro f6f7881a5fae). If the model cannot propose
64
+ // questions (no proposer wired, or it throws), we log loudly and proceed WITHOUT
65
+ // an interview; we never invent a question from keywords ("không bao giờ hardcode
66
+ // fallback... có vấn đề = fail = ghi logs"). The agent can still clarify inline.
67
+ if (!clarificationProposer) {
68
+ // Interactive turns always wire a proposer (orchestrator/message-processor).
69
+ // A missing one there is a wiring bug — surface it, never paper over with regex.
70
+ if (handler) {
71
+ console.error("[Agent:discovery] interactive turn has no model clarification proposer — skipping interview (no regex fallback by design)");
72
+ }
60
73
  return baseResult();
61
- // L1.5: Context Discovery (cacheable)
74
+ }
75
+ // L1.5: Context Discovery (cacheable) — gives the model real project facts so
76
+ // it never asks for something it can inspect itself (language/framework/modules).
62
77
  let projectContext;
63
78
  const cached = getCachedProjectContext(cwd);
64
79
  if (cached) {
@@ -76,78 +91,23 @@ export async function runDiscovery(raw, l1, cwd, handler, sessionId = null, clar
76
91
  projectContext = emptyProjectContext(cwd);
77
92
  }
78
93
  }
79
- // L1.6: Clarity Interview
80
- let gaps = detectClarityGaps(raw, l1.taskType, l1.confidence, projectContext);
81
- // Effective model-driven interview: if a clarificationProposer (the actual task model) is provided,
82
- // let the *model* itself generate the questions based on the user request + CLI enrichment so far.
83
- // The model decides what it still needs and is missing from the enrich suggestions.
84
- // Always generate model gaps when proposer wired (even if no handler for non-interactive resolve).
85
- // This ensures model BE recs drive [Discovery] Intent/Outcome/Scope for native meta prompts.
86
- // Handler only decides whether to show interactive askcard.
87
- if (clarificationProposer) {
88
- try {
89
- const additionalContext = [
90
- projectContext.language ? `Language: ${projectContext.language}` : "",
91
- projectContext.framework ? `Framework: ${projectContext.framework}` : "",
92
- projectContext.packageManager ? `Package manager: ${projectContext.packageManager}` : "",
93
- projectContext.relevantModules?.length
94
- ? `Relevant modules: ${projectContext.relevantModules.map((m) => m.path).join(", ")}`
95
- : "",
96
- projectContext.boundedContexts?.length
97
- ? `Bounded contexts: ${projectContext.boundedContexts.map((b) => `${b.name} (${b.path})`).join(", ")}`
98
- : "",
99
- projectContext.eePatterns?.length ? `EE patterns: ${projectContext.eePatterns.slice(0, 3).join(" | ")}` : "",
100
- recentTurnsSummary ? `\nRecent Conversation History:\n${recentTurnsSummary}` : "",
101
- ]
102
- .filter(Boolean)
103
- .join("\n");
104
- const modelQuestions = await clarificationProposer({
105
- raw,
106
- l1: { taskType: l1.taskType, confidence: l1.confidence },
107
- additionalContext: additionalContext || undefined,
108
- });
109
- if (modelQuestions.length > 0) {
110
- gaps = modelQuestions.slice(0, 3).map((line, idx) => {
111
- let q = line;
112
- let recs = ["I will provide my own details / constraints"];
113
- const m = line.match(/\[MODEL RECS:?\s*(.+?)\]/i) || line.match(/RECS:\s*(.+)$/i);
114
- if (m) {
115
- recs = m[1]
116
- .split(/\s*\|\s*/)
117
- .map((r) => r.trim())
118
- .filter(Boolean)
119
- .slice(0, 3);
120
- q = line
121
- .replace(/\[MODEL RECS:?.*?\]/i, "")
122
- .replace(/RECS:.*$/, "")
123
- .trim();
124
- }
125
- return {
126
- dimension: "outcome",
127
- description: `Model-generated clarification #${idx + 1}`,
128
- suggestedQuestion: q || "What else needs clarification?",
129
- options: [...recs, "Other (type free answer)"],
130
- defaultIndex: 0,
131
- };
132
- });
133
- }
134
- }
135
- catch {
136
- // fall through to static
137
- }
94
+ // L1.6: Ask the MODEL what (if anything) it still needs. The model owns the
95
+ // gray-area decision, the questions, the options, the "why", and the
96
+ // recommended default. An empty result means it sees nothing worth asking
97
+ // no interview, no fabricated [Discovery] outcome.
98
+ let gaps;
99
+ try {
100
+ gaps = await proposeModelGaps(clarificationProposer, raw, l1, projectContext, recentTurnsSummary);
138
101
  }
139
- if (!clarificationProposer && (l1.taskType === "analyze" || l1.taskType === "debug") && gaps.length > 0) {
140
- // Fallback open question (non-model path)
141
- gaps = [
142
- {
143
- dimension: "outcome",
144
- description: "Specific outcome and constraints the agent/model needs from the user",
145
- suggestedQuestion: `Để tôi (agent/model) thực hiện chính xác và có được thông tin cần thiết cho task này, bạn hãy cho tôi biết: kết quả mong muốn cụ thể, các ràng buộc quan trọng, hoặc bất kỳ chi tiết nào khác mà tôi cần làm rõ trước khi bắt đầu?`,
146
- options: ["Tôi sẽ trả lời tự do / cung cấp chi tiết cần thiết"],
147
- defaultIndex: 0,
148
- },
149
- ];
102
+ catch (err) {
103
+ // No Silent Catch + fail-loud: log with context, then proceed WITHOUT an
104
+ // interview. We do NOT fall back to regex-generated questions.
105
+ console.error(`[Agent:discovery] model clarification proposer threw — proceeding without interview (no regex fallback): ${err?.message}`, { stack: err?.stack?.split("\n").slice(0, 3) });
106
+ return baseResult();
150
107
  }
108
+ // Model decided there is no gray area worth asking about → proceed directly.
109
+ if (gaps.length === 0)
110
+ return baseResult();
151
111
  let clarifiedIntent;
152
112
  let interviewed = false;
153
113
  if (gaps.length > 0 && handler) {
@@ -168,15 +128,15 @@ export async function runDiscovery(raw, l1, cwd, handler, sessionId = null, clar
168
128
  else {
169
129
  clarifiedIntent = resolveGapsNonInteractive(gaps, projectContext, raw);
170
130
  }
171
- // Auto-fill outcome for analyze/plan/documentation when no outcome gap was asked
172
- // Broaden to override bad generics (Local path, In prompts/..., project root literals) that
173
- // can leak from detectClarityGaps/resolve when raw contains directory mentions or for native meta.
131
+ // Auto-fill outcome for analyze/plan/documentation when no outcome gap was asked.
132
+ // Override only when the resolved outcome is a raw-derived generic ("Complete: …" /
133
+ // "Complete the task …") or a genuine filesystem-path leak (Local path, project root,
134
+ // "src/foo.ts" scope-option shapes). It must NOT clobber a legitimate user answer that
135
+ // merely contains a slash ("support REST/GraphQL", "input/output") — see looksLikePathLeak.
174
136
  const autoOutcome = getAutofilledOutcome(l1.taskType, raw);
175
- if (autoOutcome &&
176
- (!clarifiedIntent.outcome ||
177
- clarifiedIntent.outcome.startsWith("Complete the task") ||
178
- /Local path|In prompts|directory as|project root|\/|absolute|local\/repo/i.test(clarifiedIntent.outcome) ||
179
- clarifiedIntent.outcome.includes("/"))) {
137
+ const currentOutcome = clarifiedIntent.outcome ?? "";
138
+ const isGenericComplete = /^Complete(?::| the task)/i.test(currentOutcome);
139
+ if (autoOutcome && (!currentOutcome || isGenericComplete || looksLikePathLeak(currentOutcome))) {
180
140
  clarifiedIntent = { ...clarifiedIntent, outcome: autoOutcome };
181
141
  }
182
142
  // L1.7: Feasibility Check
@@ -197,33 +157,32 @@ export async function runDiscovery(raw, l1, cwd, handler, sessionId = null, clar
197
157
  accepted = false;
198
158
  }
199
159
  else if (decision === "adjust") {
200
- // Re-run interview once but ONLY for gaps where the user's prior
201
- // freetext answer was empty, identical to the raw prompt itself, or
202
- // a continuation phrase. Re-asking gaps the user already answered
203
- // (with non-trivial text) just produces duplicate askcards (evidence:
204
- // session 1f29e238a816 — user picked "adjust", re-interview fired the
205
- // same "Which part of codebase?" question they had just answered).
206
- const reGaps = detectClarityGaps(raw, l1.taskType, l1.confidence, projectContext);
207
- const priorAnswers = new Map((clarifiedIntent.gaps ?? []).map((g) => [g.dimension, g.answer ?? null]));
208
- const reAnswered = reGaps.map((g) => ({
209
- ...g,
210
- answer: priorAnswers.get(g.dimension) ?? null,
211
- }));
212
- const maxRe = Math.min(reGaps.length, getMaxInterviewQuestions());
213
- for (let i = 0; i < maxRe; i++) {
214
- const gap = reAnswered[i];
215
- const prior = gap.answer?.trim() ?? "";
216
- const isTrivialPriorAnswer = prior === "" || prior.toLowerCase() === raw.trim().toLowerCase() || isLikelyFollowUp(prior);
217
- if (!isTrivialPriorAnswer) {
218
- // User already gave a substantive answer for this gap. Keep it.
219
- continue;
160
+ // The user asked to change something let the MODEL re-propose questions
161
+ // given the same context (it owns the gray-area decision, exactly like the
162
+ // initial pass). On a proposer failure we keep the already-resolved intent
163
+ // rather than fabricate regex questions.
164
+ let reGaps = [];
165
+ try {
166
+ reGaps = await proposeModelGaps(clarificationProposer, raw, l1, projectContext, recentTurnsSummary);
167
+ }
168
+ catch (err) {
169
+ console.error(`[Agent:discovery] re-interview proposer threw — keeping prior intent: ${err?.message}`);
170
+ }
171
+ if (reGaps.length > 0) {
172
+ const reAnswered = reGaps.map((g) => ({
173
+ ...g,
174
+ answer: null,
175
+ }));
176
+ const maxRe = Math.min(reGaps.length, getMaxInterviewQuestions());
177
+ for (let i = 0; i < maxRe; i++) {
178
+ const gap = reAnswered[i];
179
+ const q = buildInterviewQuestion(gap, randomUUID());
180
+ const ans = await handler.askQuestion(q);
181
+ reAnswered[i] = { ...gap, answer: ans.text };
220
182
  }
221
- const q = buildInterviewQuestion(gap, randomUUID());
222
- const ans = await handler.askQuestion(q);
223
- reAnswered[i] = { ...gap, answer: ans.text };
183
+ clarifiedIntent = buildClarifiedIntentFromAnswers(reAnswered, raw, projectContext);
184
+ feasibility = await checkFeasibility(clarifiedIntent, projectContext).catch(() => feasibility);
224
185
  }
225
- clarifiedIntent = buildClarifiedIntentFromAnswers(reAnswered, raw, projectContext);
226
- feasibility = await checkFeasibility(clarifiedIntent, projectContext).catch(() => feasibility);
227
186
  accepted = true;
228
187
  }
229
188
  }
@@ -250,11 +209,39 @@ export async function runDiscovery(raw, l1, cwd, handler, sessionId = null, clar
250
209
  discoveryMs: Date.now() - start,
251
210
  };
252
211
  }
212
+ /**
213
+ * True only for outcomes that are genuine filesystem-path leakage — known
214
+ * leaked-scope phrases ("Local path", "project root", …) or a real path segment
215
+ * ("src/foo.ts", a "src/cli (cli)" scope-option shape) that also carries a
216
+ * path-ish signal (file extension, path keyword, or trailing "(name)").
217
+ *
218
+ * A bare "/" is NOT sufficient: "support REST/GraphQL", "validate input/output",
219
+ * and "details / constraints" use the slash as an "or" separator and must be
220
+ * preserved. The previous implementation matched any "/" and silently clobbered
221
+ * such legitimate answers with the generic taskType default.
222
+ */
223
+ function looksLikePathLeak(outcome) {
224
+ // Anchored known leaked-scope phrases (preserves prior override behaviour).
225
+ if (/(?:\b(?:local path|in prompts|directory as|project root|absolute)\b)|local\/repo/i.test(outcome)) {
226
+ return true;
227
+ }
228
+ // word/word path segment (no spaces around the slash) — e.g. "src/foo.ts".
229
+ if (!/\b[\w.-]+\/[\w./-]+/.test(outcome))
230
+ return false;
231
+ const hasFileExtension = /\/[\w-]+\.[a-z0-9]{1,5}\b/i.test(outcome); // ".../foo.ts"
232
+ const hasPathKeyword = /\b(?:path|dir|directory|folder|repo|root|module|src|lib|dist|tests?)\b/i.test(outcome);
233
+ const hasScopeOptionSuffix = /\([^)]+\)\s*$/.test(outcome); // "src/cli (cli)" scope-option shape
234
+ return hasFileExtension || hasPathKeyword || hasScopeOptionSuffix;
235
+ }
253
236
  function buildClarifiedIntentFromAnswers(answeredGaps, raw, projectContext) {
254
237
  const outcomeGap = answeredGaps.find((g) => g.dimension === "outcome");
255
238
  const scopeGap = answeredGaps.find((g) => g.dimension === "scope");
256
239
  const constraintGap = answeredGaps.find((g) => g.dimension === "constraint");
257
- const outcome = outcomeGap?.answer ?? `Complete: ${raw.slice(0, 80)}`;
240
+ // The "provide my own details" meta-option is a no-answer sentinel; treat it
241
+ // as missing so the raw-derived generic (and downstream inferred outcome) is
242
+ // used instead of the sentinel string surviving verbatim as the outcome.
243
+ const outcomeAnswer = isProvideOwnDetailsSentinel(outcomeGap?.answer) ? null : (outcomeGap?.answer ?? null);
244
+ const outcome = outcomeAnswer ?? `Complete: ${raw.slice(0, 80)}`;
258
245
  const scope = (() => {
259
246
  if (scopeGap?.answer)
260
247
  return [scopeGap.answer.replace(/\s*\(.*\)\s*$/, "").trim()];
@@ -279,6 +266,72 @@ function buildClarifiedIntentFromAnswers(answeredGaps, raw, projectContext) {
279
266
  })),
280
267
  };
281
268
  }
269
+ /**
270
+ * Build the enrichment context the model sees, call the proposer, and map each
271
+ * returned line into a ClarityGap. This is the SINGLE place discovery turns a
272
+ * model response into askcard gaps — the model owns the question, the options
273
+ * (recommends), the recommended default (first option), and the "why" (threaded
274
+ * into the gap description → askcard context). No regex/keyword gap synthesis.
275
+ *
276
+ * Throws on proposer error so the caller can decide how to degrade (initial pass
277
+ * proceeds without interview; the "adjust" pass keeps the prior intent).
278
+ */
279
+ async function proposeModelGaps(proposer, raw, l1, projectContext, recentTurnsSummary) {
280
+ const additionalContext = [
281
+ projectContext.language ? `Language: ${projectContext.language}` : "",
282
+ projectContext.framework ? `Framework: ${projectContext.framework}` : "",
283
+ projectContext.packageManager ? `Package manager: ${projectContext.packageManager}` : "",
284
+ projectContext.relevantModules?.length
285
+ ? `Relevant modules: ${projectContext.relevantModules.map((m) => m.path).join(", ")}`
286
+ : "",
287
+ projectContext.boundedContexts?.length
288
+ ? `Bounded contexts: ${projectContext.boundedContexts.map((b) => `${b.name} (${b.path})`).join(", ")}`
289
+ : "",
290
+ projectContext.eePatterns?.length ? `EE patterns: ${projectContext.eePatterns.slice(0, 3).join(" | ")}` : "",
291
+ recentTurnsSummary ? `\nRecent Conversation History:\n${recentTurnsSummary}` : "",
292
+ ]
293
+ .filter(Boolean)
294
+ .join("\n");
295
+ const modelQuestions = await proposer({
296
+ raw,
297
+ l1: { taskType: l1.taskType, confidence: l1.confidence },
298
+ additionalContext: additionalContext || undefined,
299
+ });
300
+ return modelQuestions.slice(0, 3).map(parseModelQuestionToGap);
301
+ }
302
+ /**
303
+ * Parse one proposer line ("question [MODEL RECS: a | b] [WHY: reason]") into a
304
+ * ClarityGap. The recommends become the askcard options (first = recommended
305
+ * default); the WHY clause becomes the askcard context so the user sees the
306
+ * model's own reason for asking.
307
+ */
308
+ function parseModelQuestionToGap(line, idx) {
309
+ let recs = [PROVIDE_OWN_DETAILS_OPTION_EN];
310
+ const recMatch = line.match(/\[MODEL RECS:?\s*(.+?)\]/i) || line.match(/RECS:\s*(.+)$/i);
311
+ if (recMatch) {
312
+ const parsed = recMatch[1]
313
+ .split(/\s*\|\s*/)
314
+ .map((r) => r.trim())
315
+ .filter(Boolean)
316
+ .slice(0, 3);
317
+ if (parsed.length > 0)
318
+ recs = parsed;
319
+ }
320
+ const whyMatch = line.match(/\[WHY:\s*(.+?)\]/i);
321
+ const why = whyMatch ? whyMatch[1].trim() : "";
322
+ const question = line
323
+ .replace(/\[WHY:.*?\]/i, "")
324
+ .replace(/\[MODEL RECS:?.*?\]/i, "")
325
+ .replace(/RECS:.*$/, "")
326
+ .trim();
327
+ return {
328
+ dimension: "outcome",
329
+ description: why || `Model-generated clarification #${idx + 1}`,
330
+ suggestedQuestion: question || "What else needs clarification?",
331
+ options: [...recs, "Other (type free answer)"],
332
+ defaultIndex: 0,
333
+ };
334
+ }
282
335
  /**
283
336
  * Create a ModelClarificationProposer backed by the actual task model.
284
337
  * The model receives the user raw + CLI enrichment (l1, project modules, etc.)
@@ -297,43 +350,65 @@ export function createModelClarificationProposer(providerFactory, modelId) {
297
350
  const special = isMetaAnalysisPrompt(input.raw)
298
351
  ? `\nIf the request is a self-evaluation, meta-analysis or review of the CLI by the agent running inside it, do NOT ask about repo path, current directory, absolute path, local repo location or "which directory". Scope is always the full project root. Focus questions and recommends on which CLI internals (PIL, discovery, tools, compaction, EE, model BE, loop guard) to evaluate or specific improvements to assess after fixes. Use the enrichment context.`
299
352
  : "";
353
+ // Environment/self header — the main system prompt has buildEnvironmentBlock,
354
+ // but THIS discovery question-generator is a separate LLM call that lacked it,
355
+ // so it assumed Python and asked the user to paste the directory tree despite
356
+ // running inside the repo (live grok session). Escape hatch:
357
+ // MUONROI_DISCOVERY_SKIP_ENV_CONTEXT=1.
358
+ const osLabel = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
359
+ const envHeader = process.env.MUONROI_DISCOVERY_SKIP_ENV_CONTEXT === "1"
360
+ ? ""
361
+ : `Runtime: ${osLabel} (${process.platform}); \`bash\` is POSIX. The project's language/framework is in the context below — do NOT assume Python or a POSIX-only layout. Do NOT ask the user to paste the directory tree, file list, or project structure: you run INSIDE the repository and can inspect it with your own tools. Ask only about genuine intent / scope ambiguities.\n`;
300
362
  const prompt = `You are the AI agent executing inside muonroi-cli.
301
- User request: "${input.raw}"
363
+ ${envHeader}User request: "${input.raw}"
302
364
  Task type from CLI: ${input.l1.taskType}
303
365
  ${contextStr}
304
366
 
305
- Based on the above, output 1-3 specific, concise questions you (the model) still need the user to answer right now so you have all the information required to complete the task accurately, without guessing.
306
- If the User request is a follow-up or continuation of the recent conversation history (if provided above), do NOT ask for new project details; assume the context is already established and return an empty array [] unless there is a critical new ambiguity.
307
- Consider the provided language/framework/modules/EE patterns when suggesting questions and recs — only ask what is missing from this context.${special}
308
- For each question also provide 1-2 short concrete recommendations the user can pick from (model-backed choices).
367
+ You decide whether this turn has a genuine gray area that BLOCKS you from delivering correctly. Output the FEW specific, concise questions you (the model) genuinely still need answered ask only what is truly blocking, not a quota. Most well-scoped requests need 0-1 questions. If everything you need is already inferable from the request + context above, OR the request is a plain question you can simply answer, return an empty array [].
368
+ If the User request is a follow-up or continuation of the recent conversation history (if provided above), do NOT ask for new project details; assume the context is already established and return [] unless there is a critical new ambiguity.
369
+ Consider the provided language/framework/modules/EE patterns when suggesting questions and recs — never ask something the context above already answers.${special}
370
+ For each question: (1) provide 1-2 short concrete recommendations the user can pick from, ALWAYS listing the ONE you would choose FIRST — it becomes the default the user accepts with one keypress; (2) give a short "reason" clause explaining WHY answering it changes what you do. Be decisive; do not hand back an unranked list.
309
371
  Return ONLY valid JSON array, nothing else:
310
- [{"question":"...","recommends":["rec1","rec2"]}, ...]
372
+ [{"question":"...","recommends":["rec1","rec2"],"reason":"why this matters"}, ...]
311
373
  Max 3 items.`;
312
374
  const result = await generateText({
313
375
  model: runtime.model,
314
376
  prompt,
315
- maxOutputTokens: 256,
377
+ maxOutputTokens: 320,
316
378
  });
317
- let items = [];
379
+ let items;
318
380
  try {
319
381
  const txt = result.text
320
382
  .trim()
321
383
  .replace(/```json|```/g, "")
322
384
  .trim();
323
- items = JSON.parse(txt);
385
+ const parsed = JSON.parse(txt);
386
+ items = Array.isArray(parsed) ? parsed : [];
324
387
  }
325
- catch {
326
- // degrade: treat whole text as one question with no recs
327
- items = [{ question: result.text.trim(), recommends: [] }];
388
+ catch (parseErr) {
389
+ // A malformed (non-JSON) model response must NOT be coerced into a junk
390
+ // askcard log and return no questions (proceed without interview).
391
+ console.error(`[Agent:discovery] clarification proposer returned non-JSON — no questions this turn: ${parseErr?.message}`, { sample: result.text.slice(0, 160) });
392
+ return [];
328
393
  }
329
- return items.slice(0, 3).map((it) => {
394
+ return items
395
+ .filter((it) => it && typeof it.question === "string" && it.question.trim())
396
+ .slice(0, 3)
397
+ .map((it) => {
330
398
  const recs = (it.recommends || []).slice(0, 2).join(" | ");
331
- const tag = recs ? ` [MODEL RECS: ${recs}]` : "";
332
- return `${it.question || "Clarify needed details"}${tag}`;
399
+ const recTag = recs ? ` [MODEL RECS: ${recs}]` : "";
400
+ const why = (it.reason || "").trim();
401
+ const whyTag = why ? ` [WHY: ${why}]` : "";
402
+ return `${it.question.trim()}${recTag}${whyTag}`;
333
403
  });
334
404
  }
335
405
  catch (err) {
336
- // Silent degrade: no model questions, fall back to static
406
+ // No Silent Catch + fail-loud: the model call failed. Log with context and
407
+ // return no questions — discovery proceeds WITHOUT an interview rather than
408
+ // fabricating a regex-derived one ("có vấn đề = fail = ghi logs").
409
+ console.error(`[Agent:discovery] clarification proposer failed (${modelId}): ${err?.message}`, {
410
+ stack: err?.stack?.split("\n").slice(0, 3),
411
+ });
337
412
  return [];
338
413
  }
339
414
  };
@@ -51,6 +51,18 @@ export declare function scoreComplexity(input: ComplexityInput): ComplexityOutpu
51
51
  export declare function isTestGenerationTask(raw: string): boolean;
52
52
  /** Detect optimization-verb prompts where refactor is the correct taskType. */
53
53
  export declare function isPerformanceRefactor(raw: string): boolean;
54
+ /**
55
+ * Detect a greenfield CREATE/BUILD request whose correct taskType is `build`.
56
+ * Tight by construction: requires a LEADING creation verb + a software-artifact
57
+ * object, and vetoes build-failure/debug context. When unsure it returns false
58
+ * so the prompt cascades to the classifier + brain (no wrong deterministic pin).
59
+ *
60
+ * `build` is a first-class TaskType (greenfield project/feature creation) — it is
61
+ * the sole producer of that label. It mirrors `generate` for routing (tier/role/
62
+ * tokens/ceiling) but carries greenfield-specific outcome options + output rules.
63
+ * This replaces the F17 band-aid that pinned greenfield prompts to `generate`.
64
+ */
65
+ export declare function isGreenfieldBuildTask(raw: string): boolean;
54
66
  /** Detect short continuation prompts ("tiếp tục", "ok", "continue", …). */
55
67
  export declare function isContinuationPhrase(raw: string): boolean;
56
68
  /**