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,15 @@
1
+ import type { RepoStructureHint } from "./repo-structure-hints.js";
2
+ export type GroundingBucket = "none" | "small" | "medium" | "large";
3
+ export interface RepoGroundingProbeResult {
4
+ ran: boolean;
5
+ targets: string[];
6
+ matchedFiles: number;
7
+ totalLoc: number;
8
+ matchedDirs: number;
9
+ collision: boolean;
10
+ bucket: GroundingBucket;
11
+ groundingUncertainty: boolean;
12
+ }
13
+ export declare function probeRepoGrounding(prompt: string, hints: RepoStructureHint[], opts?: {
14
+ cwd?: string;
15
+ }): RepoGroundingProbeResult;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * src/pil/repo-grounding-probe.ts
3
+ *
4
+ * Deterministic repo-grounding probe (Design B). Given a prompt + the
5
+ * checked-in REPO_DEEP_MAP index, measure how much repository surface the
6
+ * prompt's targets actually cover — file count, total LOC, directory spread,
7
+ * symbol collisions — so routing sizes on facts, not on the sentence length.
8
+ *
9
+ * PURE + deterministic: NO LLM call, NO network. Filesystem access is bounded
10
+ * to at most one existsSync + line-count read per distinct target that is an
11
+ * exact path absent from the index. Buckets come from a monotonic threshold
12
+ * FORMULA over measured counts — never a fixed depth→route table.
13
+ *
14
+ * See docs/superpowers/plans/2026-07-16-pil-repo-grounding-probe.md and the
15
+ * council synthesis (Design B) for the accepted invariants.
16
+ */
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { extractPathTokens } from "./layer1_5-complexity-size.js";
20
+ /** Distinct top-two path segments, e.g. "src/auth/login.ts" → "src/auth". */
21
+ function topDir(path) {
22
+ const segs = path.split("/");
23
+ return segs.length >= 2 ? `${segs[0]}/${segs[1]}` : (segs[0] ?? path);
24
+ }
25
+ /** A target is a "bare basename" when it has no slash (a symbol/file name, not a path). */
26
+ function isBareName(target) {
27
+ return !target.includes("/");
28
+ }
29
+ /**
30
+ * Measured LOC/file bucket. Monotonic in every input: more files, more LOC, or
31
+ * wider directory spread never lowers the bucket. Thresholds are counts (not a
32
+ * depth map) and are covered by fixture tests — the only knob the council
33
+ * permits for sizing.
34
+ */
35
+ function bucketOf(matchedFiles, totalLoc, matchedDirs) {
36
+ if (matchedFiles === 0)
37
+ return "none";
38
+ if (matchedFiles >= 8 || totalLoc >= 4000 || matchedDirs >= 4)
39
+ return "large";
40
+ if (matchedFiles <= 2 && totalLoc < 500 && matchedDirs <= 1)
41
+ return "small";
42
+ return "medium";
43
+ }
44
+ export function probeRepoGrounding(prompt, hints, opts) {
45
+ const targets = extractPathTokens(prompt);
46
+ if (targets.length === 0) {
47
+ return {
48
+ ran: false,
49
+ targets: [],
50
+ matchedFiles: 0,
51
+ totalLoc: 0,
52
+ matchedDirs: 0,
53
+ collision: false,
54
+ bucket: "none",
55
+ groundingUncertainty: false,
56
+ };
57
+ }
58
+ const index = new Map(hints.map((h) => [h.path.toLowerCase(), h]));
59
+ // basename → the distinct indexed paths carrying it (for collision detection).
60
+ const byBasename = new Map();
61
+ for (const h of hints) {
62
+ const base = h.path.toLowerCase().split("/").pop() ?? h.path.toLowerCase();
63
+ const set = byBasename.get(base) ?? new Set();
64
+ set.add(h.path.toLowerCase());
65
+ byBasename.set(base, set);
66
+ }
67
+ const matchedPaths = new Set();
68
+ let totalLoc = 0;
69
+ let collision = false;
70
+ for (const target of targets) {
71
+ // 1. Exact indexed path.
72
+ const exact = index.get(target);
73
+ if (exact) {
74
+ if (!matchedPaths.has(exact.path.toLowerCase())) {
75
+ matchedPaths.add(exact.path.toLowerCase());
76
+ totalLoc += exact.lineCount;
77
+ }
78
+ continue;
79
+ }
80
+ // 2. Bare basename/symbol resolving across >1 indexed path → collision.
81
+ if (isBareName(target)) {
82
+ const carriers = byBasename.get(target);
83
+ if (carriers && carriers.size > 1) {
84
+ collision = true;
85
+ for (const p of carriers) {
86
+ if (!matchedPaths.has(p)) {
87
+ matchedPaths.add(p);
88
+ totalLoc += index.get(p)?.lineCount ?? 0;
89
+ }
90
+ }
91
+ continue;
92
+ }
93
+ if (carriers && carriers.size === 1) {
94
+ const only = [...carriers][0];
95
+ if (only && !matchedPaths.has(only)) {
96
+ matchedPaths.add(only);
97
+ totalLoc += index.get(only)?.lineCount ?? 0;
98
+ }
99
+ continue;
100
+ }
101
+ }
102
+ // 3. Exact path not in the index but present on disk → confirm + count (bounded).
103
+ const cwd = opts?.cwd;
104
+ if (cwd && target.includes("/")) {
105
+ const abs = join(cwd, target);
106
+ try {
107
+ if (existsSync(abs)) {
108
+ const loc = readFileSync(abs, "utf8").split(/\r?\n/).length;
109
+ if (!matchedPaths.has(target)) {
110
+ matchedPaths.add(target);
111
+ totalLoc += loc;
112
+ }
113
+ }
114
+ }
115
+ catch (err) {
116
+ console.error(`[repo-grounding-probe] on-disk LOC read failed for ${target}: ${err?.message}`);
117
+ }
118
+ }
119
+ // else: unmatched target — contributes to a zero-match uncertainty signal.
120
+ }
121
+ const matchedFiles = matchedPaths.size;
122
+ const matchedDirs = new Set([...matchedPaths].map(topDir)).size;
123
+ const bucket = bucketOf(matchedFiles, totalLoc, matchedDirs);
124
+ const groundingUncertainty = matchedFiles === 0 || collision;
125
+ return {
126
+ ran: true,
127
+ targets,
128
+ matchedFiles,
129
+ totalLoc,
130
+ matchedDirs,
131
+ collision,
132
+ bucket,
133
+ groundingUncertainty,
134
+ };
135
+ }
136
+ //# sourceMappingURL=repo-grounding-probe.js.map
@@ -0,0 +1,7 @@
1
+ export interface RepoStructureHint {
2
+ path: string;
3
+ lineCount: number;
4
+ }
5
+ export declare function parseRepoStructureHints(markdown: string): RepoStructureHint[];
6
+ export declare function getRepoStructureHints(cwd: string): RepoStructureHint[];
7
+ export declare function clearRepoStructureHintsCache(): void;
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const MAP_FILE = "REPO_DEEP_MAP.md";
4
+ const MAP_LINE_RE = /`([^`]+)`[^\n]*?\(~(\d+)\s+lines\b/gi;
5
+ const cache = new Map();
6
+ export function parseRepoStructureHints(markdown) {
7
+ const hints = [];
8
+ const seen = new Set();
9
+ for (const match of markdown.matchAll(MAP_LINE_RE)) {
10
+ const path = match[1]?.trim();
11
+ const lineCount = Number.parseInt(match[2] ?? "", 10);
12
+ if (!path || !Number.isFinite(lineCount))
13
+ continue;
14
+ const key = path.toLowerCase();
15
+ if (seen.has(key))
16
+ continue;
17
+ seen.add(key);
18
+ hints.push({ path, lineCount });
19
+ }
20
+ return hints;
21
+ }
22
+ export function getRepoStructureHints(cwd) {
23
+ const cached = cache.get(cwd);
24
+ if (cached)
25
+ return cached;
26
+ const mapPath = join(cwd, MAP_FILE);
27
+ if (!existsSync(mapPath)) {
28
+ cache.set(cwd, []);
29
+ return [];
30
+ }
31
+ try {
32
+ const raw = readFileSync(mapPath, "utf8");
33
+ const hints = parseRepoStructureHints(raw);
34
+ cache.set(cwd, hints);
35
+ return hints;
36
+ }
37
+ catch {
38
+ cache.set(cwd, []);
39
+ return [];
40
+ }
41
+ }
42
+ export function clearRepoStructureHintsCache() {
43
+ cache.clear();
44
+ }
45
+ //# sourceMappingURL=repo-structure-hints.js.map
@@ -92,6 +92,8 @@ export async function appendIteration(flowDir, runId, entry) {
92
92
  `CriteriaPartial: ${entry.criteriaPartial}`,
93
93
  `CriteriaUnmet: ${entry.criteriaUnmet}`,
94
94
  ];
95
+ if (entry.totalCriteria !== undefined)
96
+ lines.push(`TotalCriteria: ${entry.totalCriteria}`);
95
97
  if (entry.crashed)
96
98
  lines.push("Crashed: true");
97
99
  if (entry.retryOf !== undefined)
@@ -137,6 +139,8 @@ export async function readIterations(flowDir, runId) {
137
139
  iter.crashed = true;
138
140
  if (data.RetryOf !== undefined)
139
141
  iter.retryOf = Number.parseInt(data.RetryOf, 10);
142
+ if (data.TotalCriteria !== undefined)
143
+ iter.totalCriteria = Number.parseInt(data.TotalCriteria, 10);
140
144
  results.push(iter);
141
145
  }
142
146
  return results.sort((a, b) => a.sprintN - b.sprintN);
@@ -0,0 +1,51 @@
1
+ import type { CouncilLLM } from "../council/types.js";
2
+ /**
3
+ * Extract acceptance-criteria strings from a council synthesis blob. The synthesis
4
+ * is `<json>---READABLE---<prose>` (see council/planner.parseOutcome); the JSON
5
+ * block carries `acceptance_criteria: string[]`. Falls back to a markdown
6
+ * "Acceptance Criteria" bullet section when JSON is absent/malformed so a
7
+ * readable-only plan still seeds something.
8
+ */
9
+ export declare function extractAcceptanceCriteria(planSynthesis: string): string[];
10
+ /**
11
+ * Stable, single-line id for a criterion (used as the gray-areas.md section
12
+ * heading). Deterministic so re-seeding the same criterion is idempotent.
13
+ */
14
+ export declare function criterionIdFromText(text: string): string;
15
+ /**
16
+ * Merge the plan's acceptance criteria into gray-areas.md as unmet Criterion rows.
17
+ * Idempotent and non-clobbering: criteria already present (any status) are left
18
+ * untouched so a re-plan/retry never resets progress.
19
+ * Returns the count of NEW criteria seeded.
20
+ */
21
+ export declare function seedCriteriaFromPlan(flowDir: string, runId: string, criteriaTexts: string[], sprintN: number): Promise<number>;
22
+ /**
23
+ * Non-blocking plan-quality check. The per-sprint plan is auto-approved (there is
24
+ * no plan-check gate by design, to avoid stranding the loop), so a weak plan reaches
25
+ * the implementer silently. This surfaces the two failure modes that let Sprint 1
26
+ * diverge undetected: (1) no acceptance_criteria → done-gate can't score/gate,
27
+ * (2) no file_edits → the plan is prose, not an executable target list. Callers
28
+ * emit these as warnings and may inject a corrective note; they never halt.
29
+ */
30
+ export declare function planQualityIssues(planSynthesis: string, seededCriteriaCount: number): string[];
31
+ /**
32
+ * Judge the still-unmet criteria against the sprint's verify output + a diff
33
+ * summary, using a single bounded LLM call, then persist met/partial statuses
34
+ * with evidence. This is what lets the done-gate score reflect what was actually
35
+ * implemented (and blocks ship when the impl diverged from the plan).
36
+ *
37
+ * Fail-open: any parse/LLM error leaves criteria unmet (conservative — a sprint
38
+ * cannot pass on a judging failure). Returns the number of criteria upgraded.
39
+ */
40
+ export declare function judgeCriteriaAgainstVerify(args: {
41
+ flowDir: string;
42
+ runId: string;
43
+ llm: CouncilLLM;
44
+ modelId: string;
45
+ verifyVerdict: string;
46
+ verifyOutput: string;
47
+ diffSummary: string;
48
+ }): Promise<{
49
+ judged: number;
50
+ total: number;
51
+ }>;
@@ -0,0 +1,200 @@
1
+ import { readCriteria, updateCriteria } from "./artifact-io.js";
2
+ /**
3
+ * Slice fidelity fix (2026-07-11): the per-sprint council synthesis (planSynthesis)
4
+ * already carries a rich `acceptance_criteria` array, but nothing extracted it into
5
+ * the criteria store (gray-areas.md). readCriteria therefore returned [], the
6
+ * done-gate's calculateScore returned 0, and every sprint scored 0.00 — the
7
+ * implementation could diverge from the plan (wrong LSP op, stub tools) with no
8
+ * gate to catch it. This module closes that gap: seed the plan's acceptance
9
+ * criteria as real Criterion rows, then judge them against the verify output so the
10
+ * score reflects what was actually built and a failing criterion forces a retry.
11
+ */
12
+ const MAX_CRITERIA_PER_SPRINT = 24;
13
+ const ID_MAX_LEN = 70;
14
+ /**
15
+ * Extract acceptance-criteria strings from a council synthesis blob. The synthesis
16
+ * is `<json>---READABLE---<prose>` (see council/planner.parseOutcome); the JSON
17
+ * block carries `acceptance_criteria: string[]`. Falls back to a markdown
18
+ * "Acceptance Criteria" bullet section when JSON is absent/malformed so a
19
+ * readable-only plan still seeds something.
20
+ */
21
+ export function extractAcceptanceCriteria(planSynthesis) {
22
+ if (!planSynthesis || !planSynthesis.trim())
23
+ return [];
24
+ // 1) Prefer the JSON block before the ---READABLE--- separator.
25
+ const jsonPart = planSynthesis.includes("---READABLE---") ? planSynthesis.split("---READABLE---")[0] : planSynthesis;
26
+ const jsonMatch = jsonPart.match(/\{[\s\S]*\}/);
27
+ if (jsonMatch) {
28
+ try {
29
+ const parsed = JSON.parse(jsonMatch[0]);
30
+ const raw = parsed.acceptance_criteria ?? parsed.acceptanceCriteria;
31
+ if (Array.isArray(raw)) {
32
+ const items = raw
33
+ .map((c) => (typeof c === "string" ? c : typeof c === "object" && c ? JSON.stringify(c) : ""))
34
+ .map((s) => s.trim())
35
+ .filter((s) => s.length > 0);
36
+ if (items.length > 0)
37
+ return dedupe(items).slice(0, MAX_CRITERIA_PER_SPRINT);
38
+ }
39
+ }
40
+ catch {
41
+ /* fall through to markdown */
42
+ }
43
+ }
44
+ // 2) Markdown fallback: bullets under an "Acceptance Criteria" heading.
45
+ const lines = planSynthesis.split("\n");
46
+ const out = [];
47
+ let inSection = false;
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+ if (/^#{1,6}\s|^\*\*/.test(trimmed) && /acceptance\s+criteria/i.test(trimmed)) {
51
+ inSection = true;
52
+ continue;
53
+ }
54
+ if (inSection) {
55
+ if (/^#{1,6}\s/.test(trimmed) || /^\*\*[A-Z]/.test(trimmed))
56
+ break; // next heading
57
+ const bullet = trimmed.match(/^[-*]\s+(.*)$/) ?? trimmed.match(/^\d+\.\s+(.*)$/);
58
+ if (bullet && bullet[1].trim())
59
+ out.push(bullet[1].trim());
60
+ }
61
+ }
62
+ return dedupe(out).slice(0, MAX_CRITERIA_PER_SPRINT);
63
+ }
64
+ function dedupe(items) {
65
+ const seen = new Set();
66
+ const out = [];
67
+ for (const it of items) {
68
+ const key = it.toLowerCase().replace(/\s+/g, " ");
69
+ if (!seen.has(key)) {
70
+ seen.add(key);
71
+ out.push(it);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Stable, single-line id for a criterion (used as the gray-areas.md section
78
+ * heading). Deterministic so re-seeding the same criterion is idempotent.
79
+ */
80
+ export function criterionIdFromText(text) {
81
+ const oneLine = text.replace(/\s+/g, " ").trim();
82
+ if (oneLine.length <= ID_MAX_LEN)
83
+ return oneLine;
84
+ // Truncate but append a short hash so two long criteria that share a prefix
85
+ // don't collapse to the same heading.
86
+ let hash = 0;
87
+ for (let i = 0; i < oneLine.length; i++)
88
+ hash = (hash * 31 + oneLine.charCodeAt(i)) | 0;
89
+ const suffix = (hash >>> 0).toString(36).slice(0, 6);
90
+ return `${oneLine.slice(0, ID_MAX_LEN - 8).trim()}… #${suffix}`;
91
+ }
92
+ /**
93
+ * Merge the plan's acceptance criteria into gray-areas.md as unmet Criterion rows.
94
+ * Idempotent and non-clobbering: criteria already present (any status) are left
95
+ * untouched so a re-plan/retry never resets progress.
96
+ * Returns the count of NEW criteria seeded.
97
+ */
98
+ export async function seedCriteriaFromPlan(flowDir, runId, criteriaTexts, sprintN) {
99
+ if (criteriaTexts.length === 0)
100
+ return 0;
101
+ const existing = await readCriteria(flowDir, runId);
102
+ const existingIds = new Set(existing.map((c) => c.id.trim()));
103
+ const fresh = [];
104
+ for (const text of criteriaTexts) {
105
+ const id = criterionIdFromText(text);
106
+ if (existingIds.has(id.trim()))
107
+ continue;
108
+ existingIds.add(id.trim());
109
+ fresh.push({ id, status: "unmet", sprint: sprintN });
110
+ }
111
+ if (fresh.length === 0)
112
+ return 0;
113
+ await updateCriteria(flowDir, runId, fresh);
114
+ return fresh.length;
115
+ }
116
+ /**
117
+ * Non-blocking plan-quality check. The per-sprint plan is auto-approved (there is
118
+ * no plan-check gate by design, to avoid stranding the loop), so a weak plan reaches
119
+ * the implementer silently. This surfaces the two failure modes that let Sprint 1
120
+ * diverge undetected: (1) no acceptance_criteria → done-gate can't score/gate,
121
+ * (2) no file_edits → the plan is prose, not an executable target list. Callers
122
+ * emit these as warnings and may inject a corrective note; they never halt.
123
+ */
124
+ export function planQualityIssues(planSynthesis, seededCriteriaCount) {
125
+ const issues = [];
126
+ if (seededCriteriaCount === 0) {
127
+ issues.push("plan carries no acceptance_criteria — the done-gate cannot score this sprint against the plan");
128
+ }
129
+ if (!/"?file_edits"?\s*:/.test(planSynthesis) && !/##\s*file edits/i.test(planSynthesis)) {
130
+ issues.push("plan lists no file_edits — implementation has no concrete target files to follow");
131
+ }
132
+ return issues;
133
+ }
134
+ /**
135
+ * Judge the still-unmet criteria against the sprint's verify output + a diff
136
+ * summary, using a single bounded LLM call, then persist met/partial statuses
137
+ * with evidence. This is what lets the done-gate score reflect what was actually
138
+ * implemented (and blocks ship when the impl diverged from the plan).
139
+ *
140
+ * Fail-open: any parse/LLM error leaves criteria unmet (conservative — a sprint
141
+ * cannot pass on a judging failure). Returns the number of criteria upgraded.
142
+ */
143
+ export async function judgeCriteriaAgainstVerify(args) {
144
+ const criteria = await readCriteria(args.flowDir, args.runId);
145
+ const unmet = criteria.filter((c) => c.status !== "met");
146
+ if (unmet.length === 0)
147
+ return { judged: 0, total: criteria.length };
148
+ // Evidence must be verifiable (done-gate condition #2 rejects criteria marked
149
+ // met/partial without a valid evidence string), so only allow upgrades when
150
+ // verify did not hard-fail. On FAIL/ERROR, leave everything unmet.
151
+ if (args.verifyVerdict !== "PASS") {
152
+ return { judged: 0, total: criteria.length };
153
+ }
154
+ const list = unmet.map((c, i) => `${i + 1}. [${c.id}] ${c.id}`).join("\n");
155
+ const prompt = `You are grading whether each acceptance criterion is satisfied by a sprint's ` +
156
+ `actual work. Be strict: mark "met" ONLY when the diff + verify output show it ` +
157
+ `is truly satisfied; "partial" if started but incomplete; "unmet" otherwise.\n\n` +
158
+ `Verify verdict: ${args.verifyVerdict}\n` +
159
+ `Verify output (truncated):\n${args.verifyOutput.slice(0, 4000)}\n\n` +
160
+ `Changed files / diff summary (truncated):\n${args.diffSummary.slice(0, 4000)}\n\n` +
161
+ `Criteria:\n${list}\n\n` +
162
+ `Return ONLY a JSON array: [{"n": <number>, "status": "met"|"partial"|"unmet", ` +
163
+ `"evidence": "<one concrete sentence citing a file/test/output; required for met/partial>"}]`;
164
+ let raw;
165
+ try {
166
+ raw = await args.llm.generate(args.modelId, "You are a strict acceptance-criteria grader.", prompt);
167
+ }
168
+ catch {
169
+ return { judged: 0, total: criteria.length };
170
+ }
171
+ const jsonMatch = raw.match(/\[[\s\S]*\]/);
172
+ if (!jsonMatch)
173
+ return { judged: 0, total: criteria.length };
174
+ let verdicts;
175
+ try {
176
+ verdicts = JSON.parse(jsonMatch[0]);
177
+ }
178
+ catch {
179
+ return { judged: 0, total: criteria.length };
180
+ }
181
+ const updates = [];
182
+ for (const v of verdicts) {
183
+ if (typeof v.n !== "number" || v.n < 1 || v.n > unmet.length)
184
+ continue;
185
+ const target = unmet[v.n - 1];
186
+ const status = v.status === "met" || v.status === "partial" ? v.status : "unmet";
187
+ const evidence = typeof v.evidence === "string" ? v.evidence.trim() : "";
188
+ // Don't upgrade without evidence — the done-gate would reject it anyway.
189
+ if ((status === "met" || status === "partial") && evidence.length < 8)
190
+ continue;
191
+ if (status === "unmet")
192
+ continue;
193
+ updates.push({ id: target.id, status, evidence, sprint: target.sprint });
194
+ }
195
+ if (updates.length > 0) {
196
+ await updateCriteria(args.flowDir, args.runId, updates);
197
+ }
198
+ return { judged: updates.length, total: criteria.length };
199
+ }
200
+ //# sourceMappingURL=criteria-seed.js.map
@@ -1,4 +1,5 @@
1
1
  import { type RecommendInput, type RecommendOutput } from "./discovery-recommender.js";
2
+ import type { InterviewTriage } from "./discovery-triage.js";
2
3
  import type { ExistingProjectSignals, ProjectContext } from "./types.js";
3
4
  export type UserPromptResult = {
4
5
  action: "accept";
@@ -62,5 +63,13 @@ export interface IterateOpts {
62
63
  * sandboxed temp dir to exercise existing-project paths deterministically.
63
64
  */
64
65
  cwd?: string;
66
+ /**
67
+ * Model-decided interview triage (see discovery-triage.ts). When present it is
68
+ * the PRIMARY signal for how deep to interview — replacing the legacy keyword
69
+ * `computePromptSpecificity` heuristic (kept only as the graceful-degrade
70
+ * fallback inside `triageInterview`). Absent → the old specificity behaviour is
71
+ * used verbatim, so existing callers/tests are unaffected.
72
+ */
73
+ triage?: InterviewTriage;
65
74
  }
66
75
  export declare function iterateInterview(opts: IterateOpts): Promise<ProjectContext>;
@@ -27,6 +27,12 @@ export async function iterateInterview(opts) {
27
27
  // but optional ones (baStatus, designStatus, deployment, frontendApproach when
28
28
  // not web) are deferred unless the user explicitly re-runs with more context.
29
29
  const specificity = computePromptSpecificity(opts.idea);
30
+ // Interview-depth tier. PRIMARY signal is the model-decided triage
31
+ // (opts.triage); the legacy keyword specificity is used ONLY when no triage was
32
+ // supplied (older callers / tests), mapped: minimal→trivial, else→standard.
33
+ // "detailed" no longer force-collapses on the heuristic alone — a precise-but-
34
+ // complex idea should be triaged as complex by the model, not auto-filled blind.
35
+ const tier = opts.triage?.complexity ?? (specificity === "minimal" ? "trivial" : "standard");
30
36
  // Existing-codebase work (refactor / migration / feature-add on a repo that
31
37
  // already has source) should NOT re-run the full greenfield product-scoping
32
38
  // questionnaire. productType/audience/targetPlatform/frontend/design/deployment
@@ -38,18 +44,30 @@ export async function iterateInterview(opts) {
38
44
  // prompts). This mirrors the repoBrief-build condition below. Escape hatch:
39
45
  // MUONROI_DISCOVERY_EXISTING_COLLAPSE=0 restores the full per-field interview.
40
46
  const collapseForExisting = detection.classification !== "greenfield" && process.env.MUONROI_DISCOVERY_EXISTING_COLLAPSE !== "0";
41
- const skipOptionalForMinimal = specificity === "minimal" || collapseForExisting;
42
- // G2-b: for a minimal OR well-specified ("detailed") prompt the recommender's
43
- // primary is high-confidence minimal picks smallest-scope defaults, detailed
44
- // respects the stated context. In both cases surfacing a card for EVERY
45
- // required question (productType/targetPlatform/audience/…) is over-asking the
46
- // user already complained about: they accept by reflex. Instead auto-accept
47
- // the recommender primary for required questions and surface ONE summary card
48
- // (the user gate) listing the assumptions so the user can proceed or adjust.
49
- // "moderate" prompts keep the per-question cards (genuinely ambiguous). Escape
50
- // hatch: MUONROI_DISCOVERY_AUTOFILL=0 restores per-question cards everywhere.
51
- const autoFillRequired = (specificity === "minimal" || specificity === "detailed" || collapseForExisting) &&
52
- process.env.MUONROI_DISCOVERY_AUTOFILL !== "0";
47
+ const skipOptionalForMinimal = tier === "trivial" || collapseForExisting;
48
+ // Which REQUIRED questions stay as interactive per-question cards. Everything
49
+ // else auto-fills the recommender primary and is surfaced together on the ONE
50
+ // summary confirm card (__user_gate__). This is the crux of the "sharper, less
51
+ // hardcoded" interview:
52
+ // - existing-repo collapse only the decision-relevant field(s)
53
+ // - trivial NONE (e.g. a hello-world script: don't interrogate audience
54
+ // scale / backend architecture / db strategy just confirm the defaults)
55
+ // - complex ONLY the questions the model flagged as genuinely shaping this
56
+ // build (triage.relevant); auto-fill the rest
57
+ // - standard (or model unavailable) ALL required stay cards (unchanged UX)
58
+ const interactiveRequired = collapseForExisting
59
+ ? new Set(KEEP_CARD_FOR_EXISTING)
60
+ : tier === "trivial"
61
+ ? new Set()
62
+ : tier === "complex" && opts.triage
63
+ ? new Set(opts.triage.relevant)
64
+ : new Set(REQUIRED_QUESTION_IDS);
65
+ // Auto-accept the recommender primary for required questions NOT kept interactive
66
+ // above, and surface ONE summary card listing the assumptions so the user can
67
+ // proceed or adjust. Enabled whenever ANY required question is being auto-filled
68
+ // (i.e. the interactive set is a strict subset of the required set). Escape hatch:
69
+ // MUONROI_DISCOVERY_AUTOFILL=0 restores per-question cards everywhere.
70
+ const autoFillRequired = process.env.MUONROI_DISCOVERY_AUTOFILL !== "0" && REQUIRED_QUESTION_IDS.some((id) => !interactiveRequired.has(id));
53
71
  const assumed = [];
54
72
  // G1 follow-up: keep the recommendation behind each auto-filled assumption so
55
73
  // the user-gate "edit: <field>" path can re-render the SAME per-question card
@@ -113,12 +131,13 @@ export async function iterateInterview(opts) {
113
131
  // policy) so a malformed recommendation falls back to the normal card flow.
114
132
  // The assumed answers are surfaced together on the single user-gate card.
115
133
  let autoAccepted = false;
116
- // Keep backendArchitecture interactive under existing-repo collapse (the one
117
- // field whose answer shapes the technical work), and NEVER silently auto-accept
118
- // a weakly-grounded recommendation (synthFailed = the rationale failed the
119
- // repo-brief citation check twice) — fall through to a per-question card so the
120
- // user can catch a hallucinated value instead of it being assumed.
121
- const keepInteractive = collapseForExisting && KEEP_CARD_FOR_EXISTING.has(question.id);
134
+ // Keep the interview-relevant fields interactive (existing-repo
135
+ // backendArchitecture; complex the model-flagged `triage.relevant`), and
136
+ // NEVER silently auto-accept a weakly-grounded recommendation (synthFailed =
137
+ // the rationale failed the repo-brief citation check twice) — fall through to a
138
+ // per-question card so the user can catch a hallucinated value instead of it
139
+ // being assumed.
140
+ const keepInteractive = interactiveRequired.has(question.id);
122
141
  if (autoFillRequired &&
123
142
  effectivelyRequired &&
124
143
  recommendation.primary?.value != null &&
@@ -79,7 +79,8 @@ const LEADER_SYSTEM = "You are a product context recommender. Output ONE JSON ob
79
79
  "## Scope-sizing discipline\n" +
80
80
  "You will be told the user's original prompt and its specificity bucket (minimal/moderate/detailed).\n" +
81
81
  '- When specificity is "minimal" (e.g. user typed "build a todo app" or "tạo wiki"), pick the SMALLEST-SCOPE primary that still works: ' +
82
- 'productType="other" or "consumer-app" (NOT "saas"), audience scale="1-100" (NOT "100-1k" or above), ' +
82
+ 'productType="consumer-app" for a small app, or "cli-tool"/"script"/"library" for a stand-alone tool/snippet ' +
83
+ '(prefer these over the catch-all "other" — a hello-world script is a "script", not "other"), audience scale="1-100" (NOT "100-1k" or above), ' +
83
84
  "single-user / no auth / web-only / simplest stack. Put richer multi-tenant/team-scale alternatives in `alternatives`, NOT primary. " +
84
85
  "Rationale: short prompts mean the user has NOT asked for enterprise complexity. Inflating scope here cascades into wasted debate and over-built code.\n" +
85
86
  '- When specificity is "moderate", pick pragmatic defaults grounded in any stated context; surface ONE richer alternative.\n' +
@@ -48,7 +48,20 @@ export function isRequiredForPlatform(questionId, platforms) {
48
48
  }
49
49
  return false;
50
50
  }
51
- const PRODUCT_TYPES = new Set(["saas", "internal-tool", "consumer-app", "b2b-platform", "marketplace", "other"]);
51
+ const PRODUCT_TYPES = new Set([
52
+ "saas",
53
+ "internal-tool",
54
+ "consumer-app",
55
+ "b2b-platform",
56
+ "marketplace",
57
+ // Small-scope builds that previously had no home in the enum and so were
58
+ // forced to "other" — the exact "recommend other" bad UX users hit for a
59
+ // hello-world script. A trivial CLI/script/library now maps cleanly.
60
+ "cli-tool",
61
+ "script",
62
+ "library",
63
+ "other",
64
+ ]);
52
65
  const SCALES = new Set(["1-100", "100-1k", "1k-100k", "100k-1M", "1M+"]);
53
66
  /**
54
67
  * Schema hint for the leader prompt — surfaces the enum/shape that
@@ -0,0 +1,23 @@
1
+ import type { LeaderLike } from "./discovery-prompt-parser.js";
2
+ import { type DiscoveryQuestion } from "./discovery-schema.js";
3
+ export type ComplexityTier = "trivial" | "standard" | "complex";
4
+ export interface InterviewTriage {
5
+ complexity: ComplexityTier;
6
+ /**
7
+ * Required question ids (subset of REQUIRED_QUESTION_IDS) whose answers
8
+ * genuinely shape THIS build and must stay interactive cards. Only meaningful
9
+ * for `complexity === "complex"`; empty for trivial (auto-fill all) and ignored
10
+ * for standard (keep all cards).
11
+ */
12
+ relevant: string[];
13
+ rationale: string;
14
+ source: "model" | "fallback";
15
+ }
16
+ /** Graceful-degrade triage derived from the legacy specificity heuristic. */
17
+ export declare function fallbackTriage(idea: string): InterviewTriage;
18
+ /**
19
+ * Triage the interview depth for `idea` via one leader call. Never throws —
20
+ * returns a fallback triage on any LLM/parse failure. Skips the call entirely
21
+ * (and returns the fallback) when the idea is empty.
22
+ */
23
+ export declare function triageInterview(idea: string, leader: LeaderLike, requiredQuestions: DiscoveryQuestion[]): Promise<InterviewTriage>;