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.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- 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 =
|
|
42
|
-
//
|
|
43
|
-
// primary is
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
117
|
-
//
|
|
118
|
-
// a weakly-grounded recommendation (synthFailed =
|
|
119
|
-
// repo-brief citation check twice) — fall through to a
|
|
120
|
-
// user can catch a hallucinated value instead of it
|
|
121
|
-
|
|
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="
|
|
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([
|
|
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>;
|