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