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,144 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run an isolated sub-agent with a wall-clock backstop. The review/fix agents
|
|
5
|
+
* were bare `await`s — a provider that hangs post-stream would wedge the whole
|
|
6
|
+
* sprint (same class as the impl stall, run mrhc43f0fb9b). On timeout the race
|
|
7
|
+
* rejects; we convert it into a failure ToolResult so the existing `.success`
|
|
8
|
+
* paths handle it (review → leave gate; fix → stop the loop) instead of hanging.
|
|
9
|
+
*/
|
|
10
|
+
async function runIsolatedGuarded(run, req, label) {
|
|
11
|
+
try {
|
|
12
|
+
return await withDeadlineRace(() => run(req), getIsolatedTaskDeadlineMs(), label);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return { success: false, output: "", error: err instanceof Error ? err.message : String(err) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function currentDiff(cwd) {
|
|
19
|
+
try {
|
|
20
|
+
const r = spawnSync("git", ["diff", "HEAD"], {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
timeout: 20000,
|
|
24
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
25
|
+
});
|
|
26
|
+
return (r.stdout ?? "").trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseReview(output) {
|
|
33
|
+
const m = output.match(/\{[\s\S]*\}/);
|
|
34
|
+
if (!m)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(m[0]);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function normalizeDeviations(dev) {
|
|
44
|
+
if (!Array.isArray(dev))
|
|
45
|
+
return [];
|
|
46
|
+
return dev
|
|
47
|
+
.map((d) => {
|
|
48
|
+
if (typeof d === "string")
|
|
49
|
+
return d.trim();
|
|
50
|
+
const where = d.where ? `[${d.where}] ` : "";
|
|
51
|
+
const issue = d.issue ?? "";
|
|
52
|
+
const fix = d.fix ? ` → FIX: ${d.fix}` : "";
|
|
53
|
+
return `${where}${issue}${fix}`.trim();
|
|
54
|
+
})
|
|
55
|
+
.filter((s) => s.length > 0);
|
|
56
|
+
}
|
|
57
|
+
export async function* runPlanAdherenceReview(args) {
|
|
58
|
+
const maxRounds = Math.max(1, Math.min(4, args.maxRounds ?? 2));
|
|
59
|
+
const plan = args.planSynthesis.trim();
|
|
60
|
+
if (!plan)
|
|
61
|
+
return { rounds: 0, adherent: true, deviations: [] };
|
|
62
|
+
const getDiff = args.diffProvider ?? currentDiff;
|
|
63
|
+
let diff = getDiff(args.cwd);
|
|
64
|
+
if (!diff) {
|
|
65
|
+
yield { type: "content", content: `\n> [adherence] No diff to review for sprint ${args.sprintN}; skipping.\n` };
|
|
66
|
+
return { rounds: 0, adherent: true, deviations: [] };
|
|
67
|
+
}
|
|
68
|
+
let lastDeviations = [];
|
|
69
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
70
|
+
const reviewPrompt = `You are a SENIOR code reviewer. Judge whether the implementation faithfully ` +
|
|
71
|
+
`follows the APPROVED PLAN below — both its file_edits (right files, right ` +
|
|
72
|
+
`approach: e.g. pass-through vs re-implementation, correct operation/API) and ` +
|
|
73
|
+
`its acceptance_criteria. Be strict and specific.\n\n` +
|
|
74
|
+
`=== APPROVED PLAN ===\n${plan.slice(0, 9000)}\n\n` +
|
|
75
|
+
`=== ACTUAL GIT DIFF ===\n${diff.slice(0, 12000)}\n\n` +
|
|
76
|
+
`Return ONLY JSON: {"adherent": boolean, "deviations": [{"where":"<file/symbol>",` +
|
|
77
|
+
`"issue":"<what diverges from the plan>","fix":"<concrete instruction to conform>"}]}. ` +
|
|
78
|
+
`adherent=true ONLY if there are no material deviations.`;
|
|
79
|
+
const review = await runIsolatedGuarded(args.runIsolatedTask, {
|
|
80
|
+
agent: "general",
|
|
81
|
+
description: `Sprint ${args.sprintN} plan-adherence review (round ${round})`,
|
|
82
|
+
prompt: reviewPrompt,
|
|
83
|
+
modelId: args.reviewModelId,
|
|
84
|
+
maxToolRounds: 12,
|
|
85
|
+
}, `adherence-review-s${args.sprintN}-r${round}`);
|
|
86
|
+
const parsed = review.success ? parseReview(review.output ?? "") : null;
|
|
87
|
+
if (!parsed) {
|
|
88
|
+
yield {
|
|
89
|
+
type: "content",
|
|
90
|
+
content: `\n> [adherence] Reviewer produced no parseable verdict (round ${round}); leaving verify+criteria as the gate.\n`,
|
|
91
|
+
};
|
|
92
|
+
return { rounds: round, adherent: true, deviations: [] };
|
|
93
|
+
}
|
|
94
|
+
lastDeviations = normalizeDeviations(parsed.deviations);
|
|
95
|
+
const adherent = parsed.adherent === true || lastDeviations.length === 0;
|
|
96
|
+
if (adherent) {
|
|
97
|
+
yield {
|
|
98
|
+
type: "content",
|
|
99
|
+
content: `\n> [adherence] Round ${round}: reviewer (${args.reviewModelId}) confirms the implementation follows the plan.\n`,
|
|
100
|
+
};
|
|
101
|
+
return { rounds: round, adherent: true, deviations: [] };
|
|
102
|
+
}
|
|
103
|
+
yield {
|
|
104
|
+
type: "content",
|
|
105
|
+
content: `\n> [adherence] Round ${round}: ${lastDeviations.length} deviation(s) from plan:\n` +
|
|
106
|
+
lastDeviations.map((d) => ` - ${d}`).join("\n") +
|
|
107
|
+
"\n",
|
|
108
|
+
};
|
|
109
|
+
if (round === maxRounds) {
|
|
110
|
+
yield {
|
|
111
|
+
type: "content",
|
|
112
|
+
content: `\n> [adherence] Max rounds reached; deviations remain for the verify+criteria gate to catch.\n`,
|
|
113
|
+
};
|
|
114
|
+
return { rounds: round, adherent: false, deviations: lastDeviations };
|
|
115
|
+
}
|
|
116
|
+
// Hand the fix to the lower-tier agent.
|
|
117
|
+
const fixPrompt = `The implementation deviates from the APPROVED PLAN. A senior reviewer found ` +
|
|
118
|
+
`these deviations — fix EACH one by editing the code so it conforms to the plan. ` +
|
|
119
|
+
`Apply edits directly; do not narrate or re-plan.\n\n` +
|
|
120
|
+
`Deviations:\n${lastDeviations.map((d, i) => `${i + 1}. ${d}`).join("\n")}\n\n` +
|
|
121
|
+
`=== APPROVED PLAN (for reference) ===\n${plan.slice(0, 6000)}\n`;
|
|
122
|
+
yield {
|
|
123
|
+
type: "content",
|
|
124
|
+
content: `\n> [adherence] Dispatching fix task to ${args.fixModelId} (round ${round})…\n`,
|
|
125
|
+
};
|
|
126
|
+
const fix = await runIsolatedGuarded(args.runIsolatedTask, {
|
|
127
|
+
agent: "general",
|
|
128
|
+
description: `Sprint ${args.sprintN} plan-adherence fix (round ${round})`,
|
|
129
|
+
prompt: fixPrompt,
|
|
130
|
+
modelId: args.fixModelId,
|
|
131
|
+
}, `adherence-fix-s${args.sprintN}-r${round}`);
|
|
132
|
+
if (!fix.success) {
|
|
133
|
+
yield {
|
|
134
|
+
type: "content",
|
|
135
|
+
content: `\n> [adherence] Fix task failed (round ${round}): ${fix.error ?? "unknown"}; stopping the loop.\n`,
|
|
136
|
+
};
|
|
137
|
+
return { rounds: round, adherent: false, deviations: lastDeviations };
|
|
138
|
+
}
|
|
139
|
+
// Re-read the diff for the next review round.
|
|
140
|
+
diff = getDiff(args.cwd);
|
|
141
|
+
}
|
|
142
|
+
return { rounds: maxRounds, adherent: false, deviations: lastDeviations };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=plan-adherence-review.js.map
|
|
@@ -97,6 +97,68 @@ export declare const IMPL_EXECUTION_DIRECTIVE: string;
|
|
|
97
97
|
* orchestrator promise may leak in the background, but the run recovers.
|
|
98
98
|
*/
|
|
99
99
|
export declare function withImplIdleWatchdog(gen: AsyncGenerator<StreamChunk, void, unknown>, idleMs: number, sprintN: number, totalMs?: number): AsyncGenerator<StreamChunk, void, unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Wall-clock deadline for the ISOLATED implementation path.
|
|
102
|
+
*
|
|
103
|
+
* The isolated path (`ctx.runIsolatedTask`) returns a single Promise, not a
|
|
104
|
+
* stream, so `withImplIdleWatchdog` (which guards the streamed non-isolated
|
|
105
|
+
* path) cannot wrap it. Its only protection was the sub-agent's INTERNAL
|
|
106
|
+
* per-chunk stall-watchdog — which does NOT fire once the sub-agent's LLM stream
|
|
107
|
+
* has finished but its orchestrator turn hangs on the JS side afterwards (the
|
|
108
|
+
* exact "wrote N files then went silent" wedge documented on
|
|
109
|
+
* `withImplIdleWatchdog`). Observed live 2026-07-12 (run mrhc43f0fb9b): the
|
|
110
|
+
* isolated impl wrote 2 files, emitted its final `llm-done`, then wedged for 30+
|
|
111
|
+
* min with zero events and an idle process — because this `await` had no outer
|
|
112
|
+
* ceiling.
|
|
113
|
+
*
|
|
114
|
+
* This races the isolated task against a hard total-elapsed deadline. On
|
|
115
|
+
* timeout it rejects so the caller's existing try/catch converts the wedge into
|
|
116
|
+
* a visible phaseError (the sprint surfaces + can recover), mirroring what
|
|
117
|
+
* `withImplIdleWatchdog` / `runVerifyWithWatchdog` do for the other stages. The
|
|
118
|
+
* suspended sub-agent promise may leak in the background, but the run recovers.
|
|
119
|
+
* `totalMs <= 0` disables the guard (returns the task unchanged).
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* Extract why an isolated implementation task failed, from its ToolResult.
|
|
123
|
+
*
|
|
124
|
+
* `output` is checked because StreamRunner reports EVERY sub-agent failure
|
|
125
|
+
* there — "Task failed: …" (stream-runner.ts:1061), "[Cancelled]" (:982), a
|
|
126
|
+
* provider stall (:988), an unknown-agent message (:265) — and never assigns
|
|
127
|
+
* `error`; grep stream-runner.ts for `error:` and there are no hits. Reading
|
|
128
|
+
* `error` alone made `result.error?.trim()` permanently undefined, so every
|
|
129
|
+
* distinct failure collapsed into the contentless fallback and two /ideal runs
|
|
130
|
+
* halted 1s into implementation with the cause already erased.
|
|
131
|
+
*
|
|
132
|
+
* `error` still wins when a caller does populate it — ToolResult declares the
|
|
133
|
+
* field, so a future non-StreamRunner producer may be more specific.
|
|
134
|
+
*/
|
|
135
|
+
export declare function resolveImplFailureReason(result: {
|
|
136
|
+
output?: string;
|
|
137
|
+
error?: string;
|
|
138
|
+
}): string;
|
|
139
|
+
/**
|
|
140
|
+
* Persist an implementation-stage failure to `interaction_logs`.
|
|
141
|
+
*
|
|
142
|
+
* The implementation stage is where /ideal either ships code or does not, so its
|
|
143
|
+
* exception is the single most valuable line in a post-mortem — yet run
|
|
144
|
+
* mrn9yfle9801 halted with only `halt_card_open {trigger:"loop_throw"}` on
|
|
145
|
+
* record and the message itself unrecoverable: stderr belongs to the TUI child
|
|
146
|
+
* (the harness never captures it) and the council path writes no `messages`
|
|
147
|
+
* rows. `elapsedMs` is what separates the two indistinguishable causes — an
|
|
148
|
+
* immediate `!result.success` from a `withIsolatedImplDeadline` watchdog trip.
|
|
149
|
+
*
|
|
150
|
+
* Never throws: a broken audit trail must not take down the sprint it is
|
|
151
|
+
* describing.
|
|
152
|
+
*/
|
|
153
|
+
export declare function logSprintImplError(ctx: DriverContext, info: {
|
|
154
|
+
sprintN: number;
|
|
155
|
+
message: string;
|
|
156
|
+
stack?: string;
|
|
157
|
+
implModelId?: string;
|
|
158
|
+
elapsedMs: number;
|
|
159
|
+
isolated: boolean;
|
|
160
|
+
}): void;
|
|
161
|
+
export declare function withIsolatedImplDeadline<T>(task: Promise<T>, totalMs: number, sprintN: number): Promise<T>;
|
|
100
162
|
export { computeFailureSignature, loadVerifyFailureSignatures, pushFailureToEE, recordVerifyFailureAndMaybePush, saveVerifyFailureSignatures, type VerifyFailureRecord, type VerifyFailureSignatures, } from "./verify-failure-tracking.js";
|
|
101
163
|
export interface RunSprintArgs {
|
|
102
164
|
sprintN: number;
|
|
@@ -26,6 +26,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
26
26
|
import * as path from "node:path";
|
|
27
27
|
import { prependDecisionsLock, readDecisionsLock } from "../council/decisions-lock.js";
|
|
28
28
|
import { runCouncil } from "../council/index.js";
|
|
29
|
+
import { resolveLeaderModel } from "../council/leader.js";
|
|
29
30
|
import { phaseDone, phaseError, phaseStart } from "../council/phase-events.js";
|
|
30
31
|
import { fireAndForgetWorkflowEvent } from "../ee/workflow-event.js";
|
|
31
32
|
import { readArtifact, writeArtifact } from "../flow/artifact-io.js";
|
|
@@ -33,21 +34,24 @@ import { renderResumeDigest, writeSprintOutcome, writeSprintVerify } from "../fl
|
|
|
33
34
|
import { isContextRailEnabled } from "../gsd/flags.js";
|
|
34
35
|
import { SPRINT_EXECUTION_MARKER } from "../pil/layer6-output.js";
|
|
35
36
|
import { detectProviderForModel } from "../providers/runtime.js";
|
|
36
|
-
import { logUIInteraction } from "../storage/index.js";
|
|
37
|
+
import { logInteraction, logUIInteraction } from "../storage/index.js";
|
|
37
38
|
import { commitToProduct, release } from "../usage/ledger.js";
|
|
38
39
|
import { CapBreachError } from "../usage/types.js";
|
|
40
|
+
import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
|
|
39
41
|
import { runVerifyOrchestration } from "../verify/orchestrator.js";
|
|
40
42
|
import { appendIteration, readCriteria } from "./artifact-io.js";
|
|
41
43
|
import { formatUnverifiedForSprintContext, readLedger } from "./assumption-ledger.js";
|
|
42
44
|
import { readBacklog } from "./backlog-store.js";
|
|
43
45
|
import { CB1_costProjection, CB2_oscillation, CB3_verifyBlank } from "./circuit-breakers.js";
|
|
44
46
|
import { reserveForProduct } from "./cost-scoper.js";
|
|
47
|
+
import { extractAcceptanceCriteria, judgeCriteriaAgainstVerify, planQualityIssues, seedCriteriaFromPlan, } from "./criteria-seed.js";
|
|
45
48
|
import { formatProjectContextForPrompt } from "./discovery-context-format.js";
|
|
46
49
|
import { readProjectContext } from "./discovery-persistence.js";
|
|
47
50
|
import { evaluateDoneGate } from "./done-gate.js";
|
|
48
51
|
import { buildContinueFeedback } from "./feedback-routing.js";
|
|
49
52
|
import { idealTrace } from "./ideal-trace.js";
|
|
50
53
|
import { postSprintBoundary } from "./phase-tracker-bridge.js";
|
|
54
|
+
import { runPlanAdherenceReview } from "./plan-adherence-review.js";
|
|
51
55
|
import { computeProgressSnapshot, renderSnapshotMarkdown } from "./progress-snapshot.js";
|
|
52
56
|
import { appendRoleMemory } from "./role-memory.js";
|
|
53
57
|
import { loadVerifyFailureSignatures, recordVerifyFailureAndMaybePush } from "./verify-failure-tracking.js";
|
|
@@ -254,6 +258,97 @@ export async function* withImplIdleWatchdog(gen, idleMs, sprintN, totalMs = getI
|
|
|
254
258
|
clearTimeout(totalTimer);
|
|
255
259
|
}
|
|
256
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Wall-clock deadline for the ISOLATED implementation path.
|
|
263
|
+
*
|
|
264
|
+
* The isolated path (`ctx.runIsolatedTask`) returns a single Promise, not a
|
|
265
|
+
* stream, so `withImplIdleWatchdog` (which guards the streamed non-isolated
|
|
266
|
+
* path) cannot wrap it. Its only protection was the sub-agent's INTERNAL
|
|
267
|
+
* per-chunk stall-watchdog — which does NOT fire once the sub-agent's LLM stream
|
|
268
|
+
* has finished but its orchestrator turn hangs on the JS side afterwards (the
|
|
269
|
+
* exact "wrote N files then went silent" wedge documented on
|
|
270
|
+
* `withImplIdleWatchdog`). Observed live 2026-07-12 (run mrhc43f0fb9b): the
|
|
271
|
+
* isolated impl wrote 2 files, emitted its final `llm-done`, then wedged for 30+
|
|
272
|
+
* min with zero events and an idle process — because this `await` had no outer
|
|
273
|
+
* ceiling.
|
|
274
|
+
*
|
|
275
|
+
* This races the isolated task against a hard total-elapsed deadline. On
|
|
276
|
+
* timeout it rejects so the caller's existing try/catch converts the wedge into
|
|
277
|
+
* a visible phaseError (the sprint surfaces + can recover), mirroring what
|
|
278
|
+
* `withImplIdleWatchdog` / `runVerifyWithWatchdog` do for the other stages. The
|
|
279
|
+
* suspended sub-agent promise may leak in the background, but the run recovers.
|
|
280
|
+
* `totalMs <= 0` disables the guard (returns the task unchanged).
|
|
281
|
+
*/
|
|
282
|
+
/**
|
|
283
|
+
* Extract why an isolated implementation task failed, from its ToolResult.
|
|
284
|
+
*
|
|
285
|
+
* `output` is checked because StreamRunner reports EVERY sub-agent failure
|
|
286
|
+
* there — "Task failed: …" (stream-runner.ts:1061), "[Cancelled]" (:982), a
|
|
287
|
+
* provider stall (:988), an unknown-agent message (:265) — and never assigns
|
|
288
|
+
* `error`; grep stream-runner.ts for `error:` and there are no hits. Reading
|
|
289
|
+
* `error` alone made `result.error?.trim()` permanently undefined, so every
|
|
290
|
+
* distinct failure collapsed into the contentless fallback and two /ideal runs
|
|
291
|
+
* halted 1s into implementation with the cause already erased.
|
|
292
|
+
*
|
|
293
|
+
* `error` still wins when a caller does populate it — ToolResult declares the
|
|
294
|
+
* field, so a future non-StreamRunner producer may be more specific.
|
|
295
|
+
*/
|
|
296
|
+
export function resolveImplFailureReason(result) {
|
|
297
|
+
return result.error?.trim() || result.output?.trim() || "isolated implementation task failed";
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Persist an implementation-stage failure to `interaction_logs`.
|
|
301
|
+
*
|
|
302
|
+
* The implementation stage is where /ideal either ships code or does not, so its
|
|
303
|
+
* exception is the single most valuable line in a post-mortem — yet run
|
|
304
|
+
* mrn9yfle9801 halted with only `halt_card_open {trigger:"loop_throw"}` on
|
|
305
|
+
* record and the message itself unrecoverable: stderr belongs to the TUI child
|
|
306
|
+
* (the harness never captures it) and the council path writes no `messages`
|
|
307
|
+
* rows. `elapsedMs` is what separates the two indistinguishable causes — an
|
|
308
|
+
* immediate `!result.success` from a `withIsolatedImplDeadline` watchdog trip.
|
|
309
|
+
*
|
|
310
|
+
* Never throws: a broken audit trail must not take down the sprint it is
|
|
311
|
+
* describing.
|
|
312
|
+
*/
|
|
313
|
+
export function logSprintImplError(ctx, info) {
|
|
314
|
+
try {
|
|
315
|
+
logInteraction(ctx.sessionId ?? ctx.runId, "council", {
|
|
316
|
+
eventSubtype: "sprint_impl_error",
|
|
317
|
+
...(info.implModelId ? { model: info.implModelId } : {}),
|
|
318
|
+
durationMs: info.elapsedMs,
|
|
319
|
+
data: {
|
|
320
|
+
runId: ctx.runId,
|
|
321
|
+
sprintN: info.sprintN,
|
|
322
|
+
isolated: info.isolated,
|
|
323
|
+
message: info.message.slice(0, 2000),
|
|
324
|
+
stack: info.stack,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.error(`[sprint-runner] failed to persist implementation error (sprint ${info.sprintN}): ${err instanceof Error ? err.message : String(err)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export async function withIsolatedImplDeadline(task, totalMs, sprintN) {
|
|
333
|
+
if (!(Number.isFinite(totalMs) && totalMs > 0))
|
|
334
|
+
return task;
|
|
335
|
+
let timer;
|
|
336
|
+
const deadline = new Promise((_, reject) => {
|
|
337
|
+
timer = setTimeout(() => {
|
|
338
|
+
reject(new Error(`isolated implementation stage exceeded ${Math.round(totalMs / 1000)}s total watchdog and was ` +
|
|
339
|
+
`treated as stalled (sprint ${sprintN}) — the isolated sub-agent turn never completed ` +
|
|
340
|
+
`(hung on the JS side after its final response; the isolated path has no per-chunk stall guard)`));
|
|
341
|
+
}, totalMs);
|
|
342
|
+
timer.unref?.();
|
|
343
|
+
});
|
|
344
|
+
try {
|
|
345
|
+
return await Promise.race([task, deadline]);
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
if (timer)
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
257
352
|
export { computeFailureSignature, loadVerifyFailureSignatures, pushFailureToEE, recordVerifyFailureAndMaybePush, saveVerifyFailureSignatures, } from "./verify-failure-tracking.js";
|
|
258
353
|
/**
|
|
259
354
|
* Run a single sprint. Yields StreamChunk events for the UI and returns the
|
|
@@ -375,9 +470,54 @@ export async function* runSprint(args) {
|
|
|
375
470
|
// ── Step 2: Detect verify recipe BEFORE the planner spends any token ──────
|
|
376
471
|
// CB-3 fires deterministically on sprint 1 if recipe is null or coverage === 0.
|
|
377
472
|
const verifyAgent = buildVerifyAgent(ctx, cwd);
|
|
378
|
-
|
|
473
|
+
// Wall-clock backstop: `detectVerifyRecipe` runs a `verify-detect` LLM
|
|
474
|
+
// sub-agent turn (orchestrator.detectVerifyRecipe → runTaskRequest). Like the
|
|
475
|
+
// impl/verify stages, that turn can finish its stream then wedge on the JS side
|
|
476
|
+
// afterward — and this call site had NO deadline, so a single hung verify-detect
|
|
477
|
+
// turn bricked the entire /ideal run silently, right after "Committed: N sprints
|
|
478
|
+
// planned" and BEFORE the "Sprint N — Planning" yield (observed live 2026-07-13:
|
|
479
|
+
// 8+ min frozen frame, no forward progress). Race it against the shared isolated-
|
|
480
|
+
// task deadline; a timeout falls through to `null` → CB-3 emits the actionable
|
|
481
|
+
// recovery card instead of hanging. (The bridge signature does not thread an
|
|
482
|
+
// abortSignal, so this caller-side race is the guarantee.)
|
|
483
|
+
let verifyRecipe;
|
|
484
|
+
try {
|
|
485
|
+
verifyRecipe = await withDeadlineRace(() => verifyAgent.detectVerifyRecipe(verifyAgent.getSandboxSettings()), getIsolatedTaskDeadlineMs(), `sprint-${sprintN}-detect-verify`);
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
console.error(`[sprint-runner] detectVerifyRecipe timed out/failed (sprint ${sprintN}, run ${ctx.runId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
+
verifyRecipe = null;
|
|
490
|
+
}
|
|
379
491
|
const cb3 = CB3_verifyBlank(sprintN, verifyRecipe);
|
|
380
|
-
|
|
492
|
+
// Greenfield build-first (Task #8): on a fresh greenfield /ideal run the first
|
|
493
|
+
// sprint has nothing to verify yet — detectVerifyRecipe legitimately returns
|
|
494
|
+
// null / zero-coverage because the code and tests do not exist until THIS sprint
|
|
495
|
+
// builds them. Halting here (CB-3) would trap every greenfield idea before a
|
|
496
|
+
// single line is written. So for sprint 1 of a greenfield run, bypass the halt
|
|
497
|
+
// and let the implement stage scaffold the first increment; the verify stage
|
|
498
|
+
// re-detects the recipe from the code it creates (Step 5 reads
|
|
499
|
+
// verifyResult.verifyRecipe, not this one). The halt is preserved for EXISTING
|
|
500
|
+
// projects, where a missing recipe is a real "I can't tell how to test this"
|
|
501
|
+
// signal that warrants the recovery card. Opt out with
|
|
502
|
+
// MUONROI_IDEAL_GREENFIELD_BUILD_FIRST=0.
|
|
503
|
+
let greenfieldBuildFirst = false;
|
|
504
|
+
if (cb3.halt && sprintN === 1 && process.env.MUONROI_IDEAL_GREENFIELD_BUILD_FIRST !== "0") {
|
|
505
|
+
try {
|
|
506
|
+
const pc = await readProjectContext(ctx.flowDir, ctx.runId);
|
|
507
|
+
greenfieldBuildFirst = pc?.detection?.classification === "greenfield";
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
greenfieldBuildFirst = false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (greenfieldBuildFirst) {
|
|
514
|
+
yield {
|
|
515
|
+
type: "content",
|
|
516
|
+
content: `\n> Greenfield: no verify recipe exists yet (nothing is built). Proceeding to build the ` +
|
|
517
|
+
`first increment — it will be verified against the code and tests this sprint creates.\n`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (cb3.halt && !greenfieldBuildFirst) {
|
|
381
521
|
// Yield a structured halt chunk so the TUI can render an actionable recovery
|
|
382
522
|
// card (Task 5.2). Do NOT throw — callers must discriminate on chunk.type.
|
|
383
523
|
const haltChunk = {
|
|
@@ -562,6 +702,36 @@ export async function* runSprint(args) {
|
|
|
562
702
|
// Persist so a resumed/retried sprint reuses this exact plan (and target folder).
|
|
563
703
|
await persistSprintPlan(planPath, planSynthesis);
|
|
564
704
|
}
|
|
705
|
+
// Plan-fidelity fix: seed the plan's acceptance_criteria into the criteria store
|
|
706
|
+
// so the done-gate scores against REAL criteria (previously readCriteria returned
|
|
707
|
+
// [] → score always 0.00 → no gate on plan divergence). Idempotent + non-clobbering.
|
|
708
|
+
// Also run a NON-BLOCKING plan-quality check (per-sprint plans are auto-approved
|
|
709
|
+
// with no gate) and fold any issues into a corrective note for the impl prompt.
|
|
710
|
+
let planQualityNote = "";
|
|
711
|
+
try {
|
|
712
|
+
const planCriteria = extractAcceptanceCriteria(planSynthesis ?? "");
|
|
713
|
+
const seeded = await seedCriteriaFromPlan(ctx.flowDir, ctx.runId, planCriteria, sprintN);
|
|
714
|
+
if (seeded > 0) {
|
|
715
|
+
yield {
|
|
716
|
+
type: "content",
|
|
717
|
+
content: `\n> [criteria] Seeded ${seeded} acceptance criteria from the sprint plan (done-gate now scores against them).\n`,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const issues = planQualityIssues(planSynthesis ?? "", seeded);
|
|
721
|
+
if (issues.length > 0) {
|
|
722
|
+
planQualityNote =
|
|
723
|
+
`\n\n--- PLAN QUALITY WARNINGS (address these while implementing) ---\n` +
|
|
724
|
+
issues.map((i) => `- ${i}`).join("\n") +
|
|
725
|
+
`\nImplement to satisfy the phase goal and every acceptance criterion; do not stop at scaffolding.\n`;
|
|
726
|
+
yield {
|
|
727
|
+
type: "content",
|
|
728
|
+
content: `\n> [plan-check] ${issues.length} plan-quality warning(s): ${issues.join("; ")}\n`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
/* non-critical — a missing criteria seed degrades to the prior empty-criteria behavior */
|
|
734
|
+
}
|
|
565
735
|
// P4-C: close the planning phase row before opening implementation.
|
|
566
736
|
yield phaseDone({
|
|
567
737
|
phaseId: planPhaseId,
|
|
@@ -609,7 +779,7 @@ export async function* runSprint(args) {
|
|
|
609
779
|
// implement/edit path, not the respond path.
|
|
610
780
|
// C2: Pre-impl gate — read decisions.lock.md and prepend to implementation prompt.
|
|
611
781
|
// When lock file is missing (greenfield / no council with runDir), pass-through unchanged.
|
|
612
|
-
let implPrompt = planSynthesis.trim() ? IMPL_EXECUTION_DIRECTIVE + planSynthesis : planSynthesis;
|
|
782
|
+
let implPrompt = planSynthesis.trim() ? IMPL_EXECUTION_DIRECTIVE + planSynthesis + planQualityNote : planSynthesis;
|
|
613
783
|
try {
|
|
614
784
|
const lockContent = await readDecisionsLock(runDir);
|
|
615
785
|
if (lockContent) {
|
|
@@ -647,6 +817,7 @@ export async function* runSprint(args) {
|
|
|
647
817
|
}
|
|
648
818
|
}
|
|
649
819
|
let implError = null;
|
|
820
|
+
let implErrorStack;
|
|
650
821
|
if (ctx.processMessageFn && implPrompt.trim()) {
|
|
651
822
|
const useIsolated = shouldUseIsolatedImpl(!!ctx.runIsolatedTask);
|
|
652
823
|
try {
|
|
@@ -663,14 +834,29 @@ export async function* runSprint(args) {
|
|
|
663
834
|
content: "\n> [isolated impl] Executing the sprint in a fresh sub-agent context " +
|
|
664
835
|
"(anti-overflow: does not inherit the debate history).\n",
|
|
665
836
|
};
|
|
666
|
-
|
|
837
|
+
// Plan-fidelity fix: allow the implementation turn to run on a stronger
|
|
838
|
+
// model than the cheap session tier (which failed to faithfully follow a
|
|
839
|
+
// rich plan). Opt-in via MUONROI_IDEAL_IMPL_MODEL; defaults to the session
|
|
840
|
+
// model so the cheap-model philosophy stays the default.
|
|
841
|
+
const implModelId = process.env.MUONROI_IDEAL_IMPL_MODEL?.trim() || ctx.sessionModelId;
|
|
842
|
+
if (implModelId !== ctx.sessionModelId) {
|
|
843
|
+
yield {
|
|
844
|
+
type: "content",
|
|
845
|
+
content: `\n> [impl-model] Running implementation on ${implModelId} (override of session model ${ctx.sessionModelId}).\n`,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
// Wall-clock deadline: the isolated path has no per-chunk stall guard,
|
|
849
|
+
// so a post-finish JS-side hang would wedge this await forever (observed
|
|
850
|
+
// live, run mrhc43f0fb9b). Racing the total-elapsed ceiling turns a wedge
|
|
851
|
+
// into a phaseError via the try/catch below. See withIsolatedImplDeadline.
|
|
852
|
+
const result = await withIsolatedImplDeadline(ctx.runIsolatedTask({
|
|
667
853
|
agent: "general",
|
|
668
854
|
description: `Sprint ${sprintN} implementation`,
|
|
669
855
|
prompt: implPrompt,
|
|
670
|
-
modelId:
|
|
671
|
-
});
|
|
856
|
+
modelId: implModelId,
|
|
857
|
+
}), getImplTotalTimeoutMs(), sprintN);
|
|
672
858
|
if (!result.success) {
|
|
673
|
-
implError = result
|
|
859
|
+
implError = resolveImplFailureReason(result);
|
|
674
860
|
}
|
|
675
861
|
else if (result.output?.trim()) {
|
|
676
862
|
yield { type: "content", content: `\n${result.output.trim()}\n` };
|
|
@@ -687,8 +873,11 @@ export async function* runSprint(args) {
|
|
|
687
873
|
}
|
|
688
874
|
catch (e) {
|
|
689
875
|
implError = e instanceof Error ? e.message : String(e);
|
|
876
|
+
implErrorStack = e instanceof Error ? e.stack?.split("\n").slice(0, 4).join(" | ") : undefined;
|
|
690
877
|
// No-Silent-Catch: the finally below surfaces a phaseError chunk, but log
|
|
691
878
|
// here too so the hang/failure is diagnosable from stderr / MUONROI logs.
|
|
879
|
+
// Persisting happens at the single convergence point below — a thrown
|
|
880
|
+
// error and a `!result.success` return must not log differently.
|
|
692
881
|
console.error(`[sprint-runner] implementation stage failed (sprint ${sprintN}, run ${ctx.runId}): ${implError}`);
|
|
693
882
|
}
|
|
694
883
|
finally {
|
|
@@ -728,6 +917,21 @@ export async function* runSprint(args) {
|
|
|
728
917
|
});
|
|
729
918
|
}
|
|
730
919
|
if (implError) {
|
|
920
|
+
// The ONE place both failure shapes converge. The catch above handles a
|
|
921
|
+
// thrown error; a `!result.success` return never reaches it and instead
|
|
922
|
+
// falls through to here — which is why persisting from inside the catch
|
|
923
|
+
// recorded nothing for the two runs that actually failed. This throw
|
|
924
|
+
// escapes to the UI's loop-level catch (use-app-logic.tsx), which renders
|
|
925
|
+
// the message but persists only {reason, trigger, sprintN} — no text. So
|
|
926
|
+
// this is the last point at which the reason still exists.
|
|
927
|
+
logSprintImplError(ctx, {
|
|
928
|
+
sprintN,
|
|
929
|
+
message: implError,
|
|
930
|
+
stack: implErrorStack,
|
|
931
|
+
implModelId: process.env.MUONROI_IDEAL_IMPL_MODEL?.trim() || ctx.sessionModelId,
|
|
932
|
+
elapsedMs: Date.now() - implStartedAt,
|
|
933
|
+
isolated: shouldUseIsolatedImpl(!!ctx.runIsolatedTask),
|
|
934
|
+
});
|
|
731
935
|
throw new Error(implError);
|
|
732
936
|
}
|
|
733
937
|
// ── Step 4b: 4A completeness re-check ─────────────────────────────────────
|
|
@@ -800,6 +1004,58 @@ export async function* runSprint(args) {
|
|
|
800
1004
|
}
|
|
801
1005
|
}
|
|
802
1006
|
}
|
|
1007
|
+
// ── Step 4c: Plan-adherence review gate (strong reviewer → cheap fixer) ────
|
|
1008
|
+
// A high-tier reviewer checks the diff against the approved plan; deviations are
|
|
1009
|
+
// handed to a lower-tier fixer and re-reviewed (bounded). Opt out with
|
|
1010
|
+
// MUONROI_IDEAL_ADHERENCE_REVIEW=0. Never halts — verify + the criteria done-gate
|
|
1011
|
+
// remain the hard gates; this tightens plan fidelity before verification so a
|
|
1012
|
+
// cheap implementer's divergence is caught and corrected, not shipped.
|
|
1013
|
+
// Plan deviations that survive the bounded fixer rounds — carried into the next
|
|
1014
|
+
// sprint's focus (Step 9) so "chưa tuân thủ" work continues rather than being
|
|
1015
|
+
// silently dropped after the review.
|
|
1016
|
+
let residualPlanDeviations = [];
|
|
1017
|
+
if (ctx.runIsolatedTask && planSynthesis.trim() && process.env.MUONROI_IDEAL_ADHERENCE_REVIEW !== "0") {
|
|
1018
|
+
const adhPhaseId = `sprint-${sprintN}-adherence`;
|
|
1019
|
+
const adhStartedAt = Date.now();
|
|
1020
|
+
yield phaseStart({
|
|
1021
|
+
phaseId: adhPhaseId,
|
|
1022
|
+
kind: "sprint_stage",
|
|
1023
|
+
label: `Sprint ${sprintN} — Plan-adherence review`,
|
|
1024
|
+
startedAt: adhStartedAt,
|
|
1025
|
+
});
|
|
1026
|
+
try {
|
|
1027
|
+
const reviewModelId = process.env.MUONROI_IDEAL_REVIEW_MODEL?.trim() || resolveLeaderModel(ctx.sessionModelId);
|
|
1028
|
+
const verdict = yield* runPlanAdherenceReview({
|
|
1029
|
+
sprintN,
|
|
1030
|
+
planSynthesis,
|
|
1031
|
+
cwd,
|
|
1032
|
+
reviewModelId,
|
|
1033
|
+
fixModelId: ctx.sessionModelId,
|
|
1034
|
+
runIsolatedTask: ctx.runIsolatedTask,
|
|
1035
|
+
maxRounds: Number.parseInt(process.env.MUONROI_IDEAL_ADHERENCE_ROUNDS ?? "2", 10) || 2,
|
|
1036
|
+
});
|
|
1037
|
+
idealTrace("sprint.adherence.after", {
|
|
1038
|
+
runId: ctx.runId,
|
|
1039
|
+
sprintN,
|
|
1040
|
+
rounds: verdict.rounds,
|
|
1041
|
+
adherent: verdict.adherent,
|
|
1042
|
+
deviations: verdict.deviations.length,
|
|
1043
|
+
});
|
|
1044
|
+
if (!verdict.adherent)
|
|
1045
|
+
residualPlanDeviations = verdict.deviations;
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
console.error(`[sprint-runner] plan-adherence review failed (sprint ${sprintN}): ${err.message}`);
|
|
1049
|
+
}
|
|
1050
|
+
finally {
|
|
1051
|
+
yield phaseDone({
|
|
1052
|
+
phaseId: adhPhaseId,
|
|
1053
|
+
kind: "sprint_stage",
|
|
1054
|
+
label: `Sprint ${sprintN} — Plan-adherence review`,
|
|
1055
|
+
startedAt: adhStartedAt,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
803
1059
|
// ── Step 5: Verify stage ──────────────────────────────────────────────────
|
|
804
1060
|
yield { type: "content", content: `\n## Sprint ${sprintN} — Verification\n` };
|
|
805
1061
|
const verifyPhaseId = `sprint-${sprintN}-verification`;
|
|
@@ -928,6 +1184,42 @@ export async function* runSprint(args) {
|
|
|
928
1184
|
subtype: "sprint_stage",
|
|
929
1185
|
data: { sprintIndex: sprintN, stage: "judgment", runId: ctx.runId },
|
|
930
1186
|
});
|
|
1187
|
+
// Plan-fidelity fix: judge the seeded acceptance criteria against what was
|
|
1188
|
+
// actually built (verify output + diff) BEFORE the done-gate reads them.
|
|
1189
|
+
// Without this the criteria stay "unmet" forever → score 0.00 and the gate can
|
|
1190
|
+
// never distinguish an on-plan sprint from a divergent one. Only upgrades on a
|
|
1191
|
+
// PASSing verify with concrete evidence (see judgeCriteriaAgainstVerify).
|
|
1192
|
+
try {
|
|
1193
|
+
const judgeModelId = roleAssignments.get("Reviewer")?.modelId ?? roleAssignments.get("PO")?.modelId ?? ctx.sessionModelId;
|
|
1194
|
+
let diffSummary = "";
|
|
1195
|
+
try {
|
|
1196
|
+
const { spawnSync } = await import("node:child_process");
|
|
1197
|
+
const stat = spawnSync("git", ["diff", "--stat", "HEAD"], { cwd, encoding: "utf8", timeout: 15000 });
|
|
1198
|
+
diffSummary = (stat.stdout ?? "").slice(0, 4000) || "(no diff detected)";
|
|
1199
|
+
}
|
|
1200
|
+
catch {
|
|
1201
|
+
diffSummary = "(diff unavailable)";
|
|
1202
|
+
}
|
|
1203
|
+
const verifyOutputForJudge = (verifyResult.error?.trim() ? verifyResult.error : (verifyResult.output ?? "")).trim();
|
|
1204
|
+
const { judged, total } = await judgeCriteriaAgainstVerify({
|
|
1205
|
+
flowDir: ctx.flowDir,
|
|
1206
|
+
runId: ctx.runId,
|
|
1207
|
+
llm: productLlm,
|
|
1208
|
+
modelId: judgeModelId,
|
|
1209
|
+
verifyVerdict,
|
|
1210
|
+
verifyOutput: verifyOutputForJudge,
|
|
1211
|
+
diffSummary,
|
|
1212
|
+
});
|
|
1213
|
+
if (total > 0) {
|
|
1214
|
+
yield {
|
|
1215
|
+
type: "content",
|
|
1216
|
+
content: `\n> [criteria] Judged ${judged}/${total} acceptance criteria as met/partial against verify+diff.\n`,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
/* non-critical — judging failure leaves criteria unmet (conservative) */
|
|
1222
|
+
}
|
|
931
1223
|
const currentCriteria = await readCriteria(ctx.flowDir, ctx.runId);
|
|
932
1224
|
// When a phaseScope is provided (subsystem E), evaluate the done-gate only
|
|
933
1225
|
// against criteria belonging to this phase. Full criteria are kept for
|
|
@@ -1011,6 +1303,7 @@ export async function* runSprint(args) {
|
|
|
1011
1303
|
criteriaMet: currentCriteria.filter((c) => c.status === "met").length,
|
|
1012
1304
|
criteriaPartial: currentCriteria.filter((c) => c.status === "partial").length,
|
|
1013
1305
|
criteriaUnmet: currentCriteria.filter((c) => c.status === "unmet").length,
|
|
1306
|
+
totalCriteria: currentCriteria.length,
|
|
1014
1307
|
costUsd: 0, // Per-sprint cost is observed via the per-product ledger; field kept for compat.
|
|
1015
1308
|
actualCost: 0,
|
|
1016
1309
|
score: verdict.score,
|
|
@@ -1103,6 +1396,14 @@ export async function* runSprint(args) {
|
|
|
1103
1396
|
// ── Step 9: If not done, surface continue-feedback to the user ───────────
|
|
1104
1397
|
if (!verdict.pass) {
|
|
1105
1398
|
const fb = buildContinueFeedback(verdict, verifyResult, currentCriteria);
|
|
1399
|
+
// Fold any residual plan deviations (surviving the adherence fixer) into the
|
|
1400
|
+
// carry-over focus so the next sprint continues the non-adherent/risky parts.
|
|
1401
|
+
const deviationNote = residualPlanDeviations.length > 0
|
|
1402
|
+
? `\n\nPlan deviations still open (address these next):\n${residualPlanDeviations
|
|
1403
|
+
.map((d) => `- ${d}`)
|
|
1404
|
+
.join("\n")}`
|
|
1405
|
+
: "";
|
|
1406
|
+
iter.nextFocus = `${fb.focus}${deviationNote}`;
|
|
1106
1407
|
yield {
|
|
1107
1408
|
type: "content",
|
|
1108
1409
|
content: `\n> Sprint ${sprintN} did not satisfy Definition-of-Done (${verdict.failedCondition ?? "unknown"}). Next focus: ${fb.focus}\n`,
|