@underactive/pi-topping-moa-fusion 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +437 -0
- package/agents/mf-plan.md +43 -0
- package/agents/moa-debater.md +37 -0
- package/agents/moa-explore.md +56 -0
- package/agents/moa-opinion.md +29 -0
- package/agents/moa-proposer.md +49 -0
- package/agents/moa-synthesizer.md +124 -0
- package/agents/moa-verifier.md +67 -0
- package/index.ts +3 -0
- package/package.json +61 -0
- package/src/activityMeter.ts +193 -0
- package/src/agents/authoritative.ts +91 -0
- package/src/agents/defaults.ts +123 -0
- package/src/agents/discovery.ts +119 -0
- package/src/config/modelCatalogue.ts +54 -0
- package/src/config/planName.ts +74 -0
- package/src/config/rosters.ts +118 -0
- package/src/config/settings.ts +161 -0
- package/src/debate/debateContract.ts +89 -0
- package/src/debate/debateFanout.ts +285 -0
- package/src/debate/debateFile.ts +38 -0
- package/src/debate/debateResults.ts +115 -0
- package/src/debate/debateRounds.ts +61 -0
- package/src/debate/runDebate.ts +143 -0
- package/src/index.ts +283 -0
- package/src/moa/conflictContract.ts +49 -0
- package/src/moa/conflicts.ts +153 -0
- package/src/moa/contextContract.ts +52 -0
- package/src/moa/fanout.ts +152 -0
- package/src/moa/fanoutWiring.ts +88 -0
- package/src/moa/implementationRetry.ts +292 -0
- package/src/moa/modelRuntime.ts +87 -0
- package/src/moa/orchestration.ts +105 -0
- package/src/moa/planInfo.ts +57 -0
- package/src/moa/planlessRetry.ts +72 -0
- package/src/moa/reviewLoop.ts +170 -0
- package/src/moa/runContext.ts +118 -0
- package/src/moa/synthesis.ts +420 -0
- package/src/moa/verdicts.ts +81 -0
- package/src/moa/verification.ts +791 -0
- package/src/moa/verificationCriteria.ts +127 -0
- package/src/moa/verifyGate.ts +137 -0
- package/src/opinion/opinionContract.ts +21 -0
- package/src/opinion/opinionFanout.ts +135 -0
- package/src/opinion/opinionFile.ts +38 -0
- package/src/opinion/opinionResults.ts +73 -0
- package/src/opinion/runOpinion.ts +156 -0
- package/src/planning/askUserQuestion.ts +83 -0
- package/src/planning/instructions.ts +146 -0
- package/src/planning/modeState.ts +61 -0
- package/src/planning/planFile.ts +273 -0
- package/src/planning/planMode.ts +673 -0
- package/src/planning/tools/enterPlanMode.ts +165 -0
- package/src/planning/tools/exitPlanMode.ts +159 -0
- package/src/planning/tools/mfPlanSubagent.ts +311 -0
- package/src/planning/tools/shared.ts +19 -0
- package/src/planning/tools/writePlan.ts +33 -0
- package/src/runtime/activityTracking.ts +141 -0
- package/src/runtime/cancelRun.ts +134 -0
- package/src/runtime/mutationTripwire.ts +251 -0
- package/src/runtime/processPool.ts +55 -0
- package/src/runtime/results.ts +103 -0
- package/src/runtime/runner.ts +538 -0
- package/src/runtime/wire.ts +177 -0
- package/src/shared/functionKeys.ts +30 -0
- package/src/shared/modelRefs.ts +91 -0
- package/src/ui/agentStatus.ts +84 -0
- package/src/ui/agentTranscript.ts +112 -0
- package/src/ui/cancelOverlay.ts +191 -0
- package/src/ui/chrome.ts +151 -0
- package/src/ui/conflictOverlay.ts +363 -0
- package/src/ui/debateModelPicker.ts +273 -0
- package/src/ui/menu.ts +679 -0
- package/src/ui/moaModelPicker.ts +900 -0
- package/src/ui/moaProgressWidget.ts +910 -0
- package/src/ui/moaSetupOverlay.ts +368 -0
- package/src/ui/modelLabel.ts +61 -0
- package/src/ui/observeOverlay.ts +206 -0
- package/src/ui/opinionModelPicker.ts +246 -0
- package/src/ui/planReviewOverlay.ts +315 -0
- package/src/ui/promptEditor.ts +87 -0
- package/src/ui/rosterEditor.ts +310 -0
- package/src/ui/shimmer.ts +77 -0
- package/src/ui/toolActivity.ts +35 -0
- package/src/ui/twoPaneModelThinking.ts +272 -0
- package/src/ui/verificationFindingsOverlay.ts +137 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-implementation verification + bounded repair phase.
|
|
3
|
+
*
|
|
4
|
+
* Once an approved MoA plan has been implemented in the current session and the
|
|
5
|
+
* turn settles, this phase judges the working tree against the frozen approved
|
|
6
|
+
* plan. It runs the project's own check/lint/test scripts in the parent
|
|
7
|
+
* process, captures a git diff, then launches a read-only `moa-verifier`
|
|
8
|
+
* subprocess (same `--tools read,grep,find,ls` + read-only env handshake as
|
|
9
|
+
* every other planning agent) that emits a parseable verdict. The verifier
|
|
10
|
+
* cannot run tests itself, so the gate results and diff are handed to it as
|
|
11
|
+
* evidence — the implementer's own report is treated as an untrusted claim.
|
|
12
|
+
*
|
|
13
|
+
* A `complete` verdict with a clean script gate ends the phase. Anything less
|
|
14
|
+
* offers the user a bounded repair round (up to MAX_VERIFICATION_REPAIRS): the
|
|
15
|
+
* verifier's gaps and failing checks are shown inline in the repair/accept
|
|
16
|
+
* dialog and sent back to the same in-session implementer via the existing
|
|
17
|
+
* kickoff machinery. The result is then verified again. "Accept implementation
|
|
18
|
+
* as-is" is always available.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { CONFIG_DIR_NAME, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
23
|
+
|
|
24
|
+
import { loadMoaConfig } from "../config/settings.ts";
|
|
25
|
+
import { installShippedAgents, shippedAgentsDir, withAuthoritativeMoaAgents } from "../agents/authoritative.ts";
|
|
26
|
+
import { discoverAgents } from "../agents/discovery.ts";
|
|
27
|
+
import { readRepoPlanFile, saveRepoPlanFile } from "../planning/planFile.ts";
|
|
28
|
+
import { CancelRun, type CancelSession } from "../runtime/cancelRun.ts";
|
|
29
|
+
import { formatMutationWarning, MutationTripwire } from "../runtime/mutationTripwire.ts";
|
|
30
|
+
import { getFinalOutput, getResultOutput, isFailedResult, type SingleResult } from "../runtime/results.ts";
|
|
31
|
+
import { runSingleAgent } from "../runtime/runner.ts";
|
|
32
|
+
import { modelRefLabel, TRIGGER_TURN } from "../shared/modelRefs.ts";
|
|
33
|
+
import { activityLoopCount } from "../ui/agentStatus.ts";
|
|
34
|
+
import { showVerificationFindings } from "../ui/verificationFindingsOverlay.ts";
|
|
35
|
+
import {
|
|
36
|
+
buildImplementationKickoffMessage,
|
|
37
|
+
resolveHandoffPlan,
|
|
38
|
+
type ImplementationHandoff,
|
|
39
|
+
} from "./implementationRetry.ts";
|
|
40
|
+
import { modelExtensionOptions, resolveContextWindow, resolveModelCost } from "./modelRuntime.ts";
|
|
41
|
+
import { parseVerificationCriteria, sectionBullets, type VerificationCriterion } from "./verificationCriteria.ts";
|
|
42
|
+
import type { MoaRunHost } from "./runContext.ts";
|
|
43
|
+
import { captureImplementationDiff, discoverVerifyScripts, runVerifyScript, type VerifyResult } from "./verifyGate.ts";
|
|
44
|
+
|
|
45
|
+
/** How many verifier-driven repair rounds a single implementation may consume. */
|
|
46
|
+
export const MAX_VERIFICATION_REPAIRS = 2;
|
|
47
|
+
|
|
48
|
+
export type VerificationVerdictValue = "complete" | "partial" | "incomplete" | "cannot-verify";
|
|
49
|
+
|
|
50
|
+
export interface CriterionVerdict {
|
|
51
|
+
id: string;
|
|
52
|
+
status: "pass" | "fail" | "cannot-verify";
|
|
53
|
+
evidence: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface VerificationVerdict {
|
|
57
|
+
/** Undefined when the verifier output could not be parsed — treated as non-passing. */
|
|
58
|
+
verdict: VerificationVerdictValue | undefined;
|
|
59
|
+
summary: string;
|
|
60
|
+
gaps: string[];
|
|
61
|
+
steps: string[];
|
|
62
|
+
criteria: CriterionVerdict[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Failure details retained from a verifier subprocess for diagnostics and recovery UI. */
|
|
66
|
+
export interface VerifierFailureDetail {
|
|
67
|
+
output?: string;
|
|
68
|
+
stopReason?: string;
|
|
69
|
+
errorMessage?: string;
|
|
70
|
+
exitCode?: number;
|
|
71
|
+
stderr?: string;
|
|
72
|
+
/** Number of parsed messages; zero distinguishes a verifier that never started. */
|
|
73
|
+
messageCount?: number;
|
|
74
|
+
/** Accepted for direct callers/tests that already have the raw message list. */
|
|
75
|
+
messages?: readonly unknown[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface VerifierRunOutcome extends VerifierFailureDetail {
|
|
79
|
+
output: string;
|
|
80
|
+
cancelled: boolean;
|
|
81
|
+
failed: boolean;
|
|
82
|
+
messageCount: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const MAX_FAILURE_SUMMARY_CHARS = 1_000;
|
|
86
|
+
const MAX_PROMPT_SUMMARY_CHARS = 1_200;
|
|
87
|
+
const MAX_PROMPT_GAP_BULLETS = 15;
|
|
88
|
+
const MAX_PROMPT_BULLET_CHARS = 200;
|
|
89
|
+
const MAX_PROMPT_CHECK_LINES = 6;
|
|
90
|
+
const MAX_PROMPT_CHECK_CHARS = 800;
|
|
91
|
+
/** ExtensionSelectorComponent uses Text(..., paddingX=1), reserving one column on each side of its title. */
|
|
92
|
+
const SELECTOR_TITLE_WIDTH_RESERVE = 2;
|
|
93
|
+
|
|
94
|
+
function oneLineTail(value: string | undefined, maxChars = MAX_FAILURE_SUMMARY_CHARS): string {
|
|
95
|
+
if (!value) return "";
|
|
96
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
97
|
+
if (normalized.length <= maxChars) return normalized;
|
|
98
|
+
return `…${normalized.slice(-(maxChars - 1))}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Turn a failed verifier result into a short classified diagnostic. The full
|
|
103
|
+
* output remains available to the caller for notifications and persistence.
|
|
104
|
+
*/
|
|
105
|
+
export function formatVerifierFailure(result: VerifierFailureDetail): string {
|
|
106
|
+
const messageCount = result.messageCount ?? result.messages?.length;
|
|
107
|
+
const detail = oneLineTail(result.errorMessage)
|
|
108
|
+
|| oneLineTail(result.stderr)
|
|
109
|
+
|| oneLineTail(result.output)
|
|
110
|
+
|| "unknown error";
|
|
111
|
+
if (result.exitCode !== undefined && result.exitCode !== 0 && messageCount === 0) {
|
|
112
|
+
return `verifier never started (exit ${result.exitCode}): ${detail}`;
|
|
113
|
+
}
|
|
114
|
+
if (result.stopReason === "error") return `verifier provider error: ${detail}`;
|
|
115
|
+
return detail;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function verifierOutcomeFromResult(result: SingleResult): VerifierRunOutcome {
|
|
119
|
+
const cancelled = result.cancelled === true;
|
|
120
|
+
const failed = !cancelled && isFailedResult(result);
|
|
121
|
+
return {
|
|
122
|
+
output: cancelled ? "" : failed ? getResultOutput(result) : getFinalOutput(result.messages),
|
|
123
|
+
cancelled,
|
|
124
|
+
failed,
|
|
125
|
+
stopReason: result.stopReason,
|
|
126
|
+
errorMessage: result.errorMessage,
|
|
127
|
+
exitCode: result.exitCode,
|
|
128
|
+
stderr: result.stderr,
|
|
129
|
+
messageCount: result.messages.length,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Read-only verification contract, carried inside the task text so it reaches
|
|
135
|
+
* the model even when a provider bridge replaces the moa-verifier system prompt
|
|
136
|
+
* with its own harness (same reason as SYNTHESIZER_TASK_PREAMBLE).
|
|
137
|
+
*/
|
|
138
|
+
export const VERIFIER_TASK_PREAMBLE =
|
|
139
|
+
"You are the read-only VERIFIER in a Mixture-of-Agents planning run. An implementing agent has already applied an approved plan to the working tree; your only deliverable is a verdict on whether the plan actually landed, emitted as markdown text in your reply. This is a read-only audit: do NOT edit files, and do NOT run builds, tests, or any command — the check/lint/test results are provided to you below as evidence. Having only read-only tools is expected and is never a blocker. Verify each plan step against the LIVE repository, reading the files each step names; the implementer's self-report is an untrusted claim, never proof.";
|
|
140
|
+
|
|
141
|
+
const VERIFIER_OUTPUT_CONTRACT = `## Required output format
|
|
142
|
+
|
|
143
|
+
Judge each plan step against the live repo, then emit exactly these sections at the end of your reply (the markup is parsed — match it precisely):
|
|
144
|
+
|
|
145
|
+
## Step Verdicts
|
|
146
|
+
- **Step 1:** done | partial | missing | cannot-verify — <evidence: file:line, or what is missing>
|
|
147
|
+
(one bullet per approved plan step)
|
|
148
|
+
|
|
149
|
+
## Deviations
|
|
150
|
+
- <anything implemented that the plan did not call for, or done differently> (or "None.")
|
|
151
|
+
|
|
152
|
+
## Verification Result
|
|
153
|
+
**Verdict:** complete | partial | incomplete | cannot-verify
|
|
154
|
+
**Summary:** <one paragraph>
|
|
155
|
+
|
|
156
|
+
### Gaps
|
|
157
|
+
- <unmet or partial plan step> — <evidence>
|
|
158
|
+
|
|
159
|
+
Rules: a change at one of several call sites the plan implies is \`partial\`, not \`done\`. Prefer \`cannot-verify\` over a guess. Use \`complete\` only when every step landed and every provided check passed, and only then omit the \`### Gaps\` section.`;
|
|
160
|
+
|
|
161
|
+
export function buildVerifierTask(input: {
|
|
162
|
+
plan: string;
|
|
163
|
+
implementerReport: string | undefined;
|
|
164
|
+
diff: string | null;
|
|
165
|
+
scriptResults: VerifyResult[];
|
|
166
|
+
cwd: string;
|
|
167
|
+
criteria?: VerificationCriterion[];
|
|
168
|
+
}): string {
|
|
169
|
+
const { plan, implementerReport, diff, scriptResults, criteria } = input;
|
|
170
|
+
const reportSection = implementerReport && implementerReport.trim()
|
|
171
|
+
? implementerReport.trim()
|
|
172
|
+
: "(The implementer produced no final summary message.)";
|
|
173
|
+
const diffSection = diff && diff.trim()
|
|
174
|
+
? diff.trim()
|
|
175
|
+
: "(No git diff available — not a git repository, or no changes detected.)";
|
|
176
|
+
const outputContract = criteria?.length
|
|
177
|
+
? `## Required output format\n\nEmit exactly these sections at the end of your reply:\n\n## Criteria Verdicts\n${criteria.map((criterion) => `- **${criterion.id}:** pass | fail | cannot-verify — <evidence: file:line or what is missing>`).join("\n")}\n(one bullet per supplied criterion; no IDs skipped)\n\n## Deviations\n- <anything implemented differently> (or "None.")\n\n## Verification Result\n**Verdict:** complete | partial | incomplete | cannot-verify\n**Summary:** <one paragraph>\n\n### Gaps\n- <unmet criterion> — <evidence>\n\nUse complete only when every criterion is pass and every provided check passed. Prefer cannot-verify over a guessed pass.`
|
|
178
|
+
: VERIFIER_OUTPUT_CONTRACT;
|
|
179
|
+
const gateSection = scriptResults.length > 0
|
|
180
|
+
? scriptResults
|
|
181
|
+
.map((r) => `- ${r.command} → ${r.status.toUpperCase()} (exit ${r.exitCode})\n\`\`\`\n${r.relevantOutput}\n\`\`\``)
|
|
182
|
+
.join("\n\n")
|
|
183
|
+
: "(No check/lint/test scripts were discovered in package.json.)";
|
|
184
|
+
|
|
185
|
+
return [
|
|
186
|
+
VERIFIER_TASK_PREAMBLE,
|
|
187
|
+
"---",
|
|
188
|
+
`## Approved plan (source of truth)\nVerify the working tree against every step of this plan:\n\n${plan}`,
|
|
189
|
+
criteria?.length ? `## Verification criteria (authoritative pass/fail checklist)\n${criteria.map((criterion) => `- **${criterion.id}:** ${criterion.text}`).join("\n")}\n\nWhen criteria are supplied, emit ## Criteria Verdicts with one \`- **C<n>:** pass | fail | cannot-verify — evidence\` bullet for every criterion and no skipped ids, then ## Deviations and the required ## Verification Result. Use complete only when every criterion is pass; use cannot-verify rather than guessing.` : "",
|
|
190
|
+
"---",
|
|
191
|
+
`## Implementer's self-report (UNTRUSTED CLAIM — not evidence)\n${reportSection}`,
|
|
192
|
+
"---",
|
|
193
|
+
`## Recent changes (git diff HEAD — may include pre-existing edits; attribute only plan-relevant changes)\n${diffSection}`,
|
|
194
|
+
"---",
|
|
195
|
+
`## Project verification script results (evidence)\n${gateSection}`,
|
|
196
|
+
"---",
|
|
197
|
+
outputContract,
|
|
198
|
+
].join("\n\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse the moa-verifier output contract. Defaults to `verdict: undefined`
|
|
203
|
+
* (a non-passing outcome) when the `**Verdict:**` line is absent or malformed,
|
|
204
|
+
* so garbled output can never be mistaken for a pass.
|
|
205
|
+
*/
|
|
206
|
+
export function parseVerificationVerdict(output: string): VerificationVerdict {
|
|
207
|
+
const verdictMatch = output.match(/^\s*\*\*Verdict:\*\*\s*(complete|partial|incomplete|cannot-verify)\b/im);
|
|
208
|
+
const verdict = verdictMatch ? (verdictMatch[1].toLowerCase() as VerificationVerdictValue) : undefined;
|
|
209
|
+
const summaryMatch = output.match(/^\s*\*\*Summary:\*\*\s*(.+)$/im);
|
|
210
|
+
const summary = summaryMatch ? summaryMatch[1].trim() : "";
|
|
211
|
+
const gaps = sectionBullets(output, /^\s*#{2,3}\s+Gaps\b/i);
|
|
212
|
+
const steps = sectionBullets(output, /^\s*##\s+Step Verdicts\b/i);
|
|
213
|
+
const criteria: CriterionVerdict[] = [];
|
|
214
|
+
for (const bullet of sectionBullets(output, /^\s*##\s+Criteria Verdicts\b/i)) {
|
|
215
|
+
const match = bullet.match(/^\*\*(C\d+):?\*\*:?[\s]*(pass|fail|cannot-verify)\b\s*(?:—|-)?\s*(.*)$/i);
|
|
216
|
+
if (match) criteria.push({ id: match[1].toUpperCase(), status: match[2].toLowerCase() as CriterionVerdict["status"], evidence: match[3].trim() });
|
|
217
|
+
}
|
|
218
|
+
return { verdict, summary, gaps, steps, criteria };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function deriveCriteriaVerdict(parsed: VerificationVerdict, expected: VerificationCriterion[]): { verdict: VerificationVerdictValue; gaps: string[] } {
|
|
222
|
+
const byId = new Map(parsed.criteria.map((criterion) => [criterion.id, criterion]));
|
|
223
|
+
const gaps: string[] = [];
|
|
224
|
+
let passed = 0;
|
|
225
|
+
let failed = 0;
|
|
226
|
+
let unverifiable = 0;
|
|
227
|
+
for (const criterion of expected) {
|
|
228
|
+
const result = byId.get(criterion.id);
|
|
229
|
+
if (!result) { unverifiable++; gaps.push(`${criterion.id} was not judged: ${criterion.text}`); continue; }
|
|
230
|
+
if (result.status === "pass") { passed++; continue; }
|
|
231
|
+
if (result.status === "fail") failed++;
|
|
232
|
+
else unverifiable++;
|
|
233
|
+
gaps.push(`${criterion.id} ${result.status}: ${criterion.text} — ${result.evidence}`);
|
|
234
|
+
}
|
|
235
|
+
if (failed > 0) return { verdict: passed > 0 ? "partial" : "incomplete", gaps };
|
|
236
|
+
if (unverifiable > 0) return { verdict: "cannot-verify", gaps };
|
|
237
|
+
return { verdict: "complete", gaps };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Fold the verifier verdict and the script gate into one pass/fail decision.
|
|
242
|
+
* Only a `complete` verdict with a clean gate passes — an unparseable verdict
|
|
243
|
+
* (`undefined`) or any failing script keeps the implementation repair-eligible.
|
|
244
|
+
*/
|
|
245
|
+
export function verificationPassed(
|
|
246
|
+
verdict: VerificationVerdictValue | undefined,
|
|
247
|
+
scriptResults: VerifyResult[],
|
|
248
|
+
): boolean {
|
|
249
|
+
return verdict === "complete" && scriptResults.every((r) => r.status !== "fail");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Split a verifier summary into short paragraphs without breaking common references. */
|
|
253
|
+
export function splitIntoParagraphs(text: string, sentencesPerParagraph = 2): string[] {
|
|
254
|
+
const trimmed = text.trim();
|
|
255
|
+
if (!trimmed) return [];
|
|
256
|
+
const capped = trimmed.length > MAX_PROMPT_SUMMARY_CHARS
|
|
257
|
+
? `${trimmed.slice(0, MAX_PROMPT_SUMMARY_CHARS - 1)}…`
|
|
258
|
+
: trimmed;
|
|
259
|
+
const sentences = capped.split(/(?<!e\.g\.)(?<!i\.e\.)(?<=[.!?])\s+(?=[A-Z0-9`"'(\[])/);
|
|
260
|
+
const paragraphs: string[] = [];
|
|
261
|
+
const paragraphSize = Math.max(1, Math.floor(sentencesPerParagraph));
|
|
262
|
+
for (let i = 0; i < sentences.length; i += paragraphSize) {
|
|
263
|
+
paragraphs.push(sentences.slice(i, i + paragraphSize).join(" "));
|
|
264
|
+
}
|
|
265
|
+
return paragraphs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Build a short, deterministic summary for the repair decision dialog. */
|
|
269
|
+
export function buildVerificationSummary(parsed: VerificationVerdict, scriptResults: VerifyResult[]): string[] {
|
|
270
|
+
const lines = [`Verification result: ${parsed.verdict ?? "unknown"}.`];
|
|
271
|
+
if (parsed.criteria.length > 0) {
|
|
272
|
+
const passed = parsed.criteria.filter((criterion) => criterion.status === "pass").length;
|
|
273
|
+
const failed = parsed.criteria.length - passed;
|
|
274
|
+
lines.push(`${passed} of ${parsed.criteria.length} criteria passed. ${failed} failed.`);
|
|
275
|
+
} else if (parsed.gaps.length > 0) {
|
|
276
|
+
lines.push(`The verifier found ${parsed.gaps.length} gap${parsed.gaps.length === 1 ? "" : "s"}.`);
|
|
277
|
+
}
|
|
278
|
+
if (scriptResults.length > 0) {
|
|
279
|
+
const failed = scriptResults.filter((result) => result.status === "fail").length;
|
|
280
|
+
lines.push(failed === 0
|
|
281
|
+
? "All project checks passed."
|
|
282
|
+
: `${failed} project check${failed === 1 ? "" : "s"} failed.`);
|
|
283
|
+
}
|
|
284
|
+
return lines.slice(0, 3);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function elideMiddle(value: string, width: number): string {
|
|
288
|
+
if (visibleWidth(value) <= width) return value;
|
|
289
|
+
const marker = "…";
|
|
290
|
+
const available = Math.max(2, width - visibleWidth(marker));
|
|
291
|
+
const prefixWidth = Math.floor(available * 0.4);
|
|
292
|
+
const suffixWidth = available - prefixWidth;
|
|
293
|
+
const prefix = truncateToWidth(value, prefixWidth, "", false);
|
|
294
|
+
let suffix = "";
|
|
295
|
+
for (const character of Array.from(value).reverse()) {
|
|
296
|
+
if (visibleWidth(character + suffix) > suffixWidth) break;
|
|
297
|
+
suffix = character + suffix;
|
|
298
|
+
}
|
|
299
|
+
return `${prefix}${marker}${suffix}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Build the compact prompt shown above the fixed repair choices. */
|
|
303
|
+
export function buildVerificationDecisionPrompt(
|
|
304
|
+
parsed: VerificationVerdict,
|
|
305
|
+
scriptResults: VerifyResult[],
|
|
306
|
+
reportPath?: string,
|
|
307
|
+
width = 80,
|
|
308
|
+
): string {
|
|
309
|
+
const budget = Math.max(20, width);
|
|
310
|
+
const lines = [
|
|
311
|
+
...buildVerificationSummary(parsed, scriptResults),
|
|
312
|
+
"",
|
|
313
|
+
reportPath
|
|
314
|
+
? elideMiddle(`Full findings: ${reportPath}`, budget)
|
|
315
|
+
: "Full findings will be sent to the implementer if you continue.",
|
|
316
|
+
];
|
|
317
|
+
return lines
|
|
318
|
+
.map((line) => line ? truncateToWidth(line, budget, "…", false) : line)
|
|
319
|
+
.join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Format the bounded, plain-text findings sent to repairs and terminal notifications. */
|
|
323
|
+
export function formatVerificationReport(parsed: VerificationVerdict, failedScripts: VerifyResult[]): string {
|
|
324
|
+
const sections: string[] = [];
|
|
325
|
+
const summaryParagraphs = splitIntoParagraphs(parsed.summary);
|
|
326
|
+
if (summaryParagraphs.length > 0) sections.push(`Verifier summary:\n${summaryParagraphs.join("\n\n")}`);
|
|
327
|
+
if (parsed.gaps.length > 0) {
|
|
328
|
+
const heading = parsed.criteria.length > 0 ? "Criteria not met:" : "Gaps:";
|
|
329
|
+
const bullets = parsed.gaps
|
|
330
|
+
.slice(0, MAX_PROMPT_GAP_BULLETS)
|
|
331
|
+
.map((gap) => `- ${oneLineTail(gap, MAX_PROMPT_BULLET_CHARS)}`);
|
|
332
|
+
if (parsed.gaps.length > MAX_PROMPT_GAP_BULLETS) {
|
|
333
|
+
bullets.push(`… and ${parsed.gaps.length - MAX_PROMPT_GAP_BULLETS} more (see the saved verification report)`);
|
|
334
|
+
}
|
|
335
|
+
sections.push(`${heading}\n${bullets.join("\n")}`);
|
|
336
|
+
}
|
|
337
|
+
if (failedScripts.length > 0) {
|
|
338
|
+
const checks = failedScripts.map((result) => {
|
|
339
|
+
const outputLines = result.relevantOutput.split("\n");
|
|
340
|
+
const output = outputLines.slice(-MAX_PROMPT_CHECK_LINES).join("\n");
|
|
341
|
+
const marker = "… (output truncated)";
|
|
342
|
+
const clipped = outputLines.length > MAX_PROMPT_CHECK_LINES || output.length > MAX_PROMPT_CHECK_CHARS
|
|
343
|
+
? `${marker}\n${output.slice(-(MAX_PROMPT_CHECK_CHARS - marker.length - 1))}`
|
|
344
|
+
: output;
|
|
345
|
+
return `- ${result.command} failed (exit ${result.exitCode}):\n${clipped.split("\n").map((line) => ` ${line}`).join("\n")}`;
|
|
346
|
+
});
|
|
347
|
+
sections.push(`Failing project checks:\n${checks.join("\n")}`);
|
|
348
|
+
}
|
|
349
|
+
return sections.join("\n\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildRepairNote(parsed: VerificationVerdict, failedScripts: VerifyResult[]): string {
|
|
353
|
+
const parts: string[] = [
|
|
354
|
+
"The verifier reviewed your implementation against the approved plan and found it did not fully land.",
|
|
355
|
+
`Verdict: ${parsed.verdict ?? "unclear"}.`,
|
|
356
|
+
];
|
|
357
|
+
if (parsed.summary) parts.push(`Verifier summary: ${parsed.summary}`);
|
|
358
|
+
if (parsed.criteria.some((criterion) => criterion.status !== "pass")) {
|
|
359
|
+
parts.push(`Failing verification criteria:\n${parsed.criteria.filter((criterion) => criterion.status !== "pass").map((criterion) => `- ${criterion.id} ${criterion.status}: ${criterion.evidence}`).join("\n")}`);
|
|
360
|
+
}
|
|
361
|
+
if (parsed.gaps.length > 0) {
|
|
362
|
+
parts.push(`Unmet or partial plan steps:\n${parsed.gaps.map((g) => `- ${g}`).join("\n")}`);
|
|
363
|
+
}
|
|
364
|
+
if (failedScripts.length > 0) {
|
|
365
|
+
const fails = failedScripts
|
|
366
|
+
.map((r) => `- \`${r.command}\` failed (exit ${r.exitCode}):\n${r.relevantOutput}`)
|
|
367
|
+
.join("\n");
|
|
368
|
+
parts.push(`Failing project checks:\n${fails}`);
|
|
369
|
+
}
|
|
370
|
+
parts.push(
|
|
371
|
+
"Close every gap above. The frozen verification criteria will be re-checked. Re-read the approved plan and the files each unmet step names, make the changes, then run the project's check/lint/test scripts yourself to confirm they pass before finishing.",
|
|
372
|
+
);
|
|
373
|
+
return parts.join("\n\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The interactive decision offered when a settled implementation is
|
|
378
|
+
* repair-eligible and a UI is present. Extracted from
|
|
379
|
+
* `runImplementationVerification` so the view/repair/accept branching can be
|
|
380
|
+
* driven directly in tests.
|
|
381
|
+
*
|
|
382
|
+
* "View full findings" is non-terminal: it opens the read-only findings popup
|
|
383
|
+
* and then reopens the same selector, without consuming a repair round, sending
|
|
384
|
+
* a message, mutating handoff state, or finishing verification. Only "Send
|
|
385
|
+
* verifier findings to the implementer" (repair) and "Accept implementation
|
|
386
|
+
* as-is" (also the selector-cancellation fallthrough) end the decision.
|
|
387
|
+
*
|
|
388
|
+
* The findings popup needs a TUI overlay surface, so "View full findings" is
|
|
389
|
+
* offered only when `isTui` is set. Outside the TUI (e.g. an RPC UI, which still
|
|
390
|
+
* reports `hasUI`), the original two choices are shown, avoiding a third option
|
|
391
|
+
* whose popup would immediately return with nothing to display.
|
|
392
|
+
*/
|
|
393
|
+
export async function runVerificationDecision(deps: {
|
|
394
|
+
ctx: ExtensionContext;
|
|
395
|
+
host: MoaRunHost;
|
|
396
|
+
handoff: ImplementationHandoff;
|
|
397
|
+
parsed: VerificationVerdict;
|
|
398
|
+
scriptResults: VerifyResult[];
|
|
399
|
+
failedScripts: VerifyResult[];
|
|
400
|
+
plan: string;
|
|
401
|
+
/** Raw verifier report, shown verbatim in the read-only findings popup. */
|
|
402
|
+
report: string;
|
|
403
|
+
reportPath: string | undefined;
|
|
404
|
+
repairsUsed: number;
|
|
405
|
+
/** Whether an overlay surface exists; when false, the View option is omitted. */
|
|
406
|
+
isTui: boolean;
|
|
407
|
+
widget: ReturnType<MoaRunHost["getActiveProgressWidget"]>;
|
|
408
|
+
finish: (state: "done" | "error" | "cancelled") => void;
|
|
409
|
+
/** Overridable in tests; defaults to the real read-only overlay. */
|
|
410
|
+
showFindings?: (ctx: ExtensionContext, findingsMarkdown: string) => Promise<void>;
|
|
411
|
+
}): Promise<void> {
|
|
412
|
+
const { ctx, host, handoff, parsed, scriptResults, failedScripts, plan, report, reportPath, repairsUsed, isTui, widget, finish } = deps;
|
|
413
|
+
const showFindings = deps.showFindings ?? showVerificationFindings;
|
|
414
|
+
const promptWidth = Math.max(20, (process.stdout.columns ?? 80) - SELECTOR_TITLE_WIDTH_RESERVE);
|
|
415
|
+
const options = isTui
|
|
416
|
+
? ["View full findings", "Send verifier findings to the implementer", "Accept implementation as-is"]
|
|
417
|
+
: ["Send verifier findings to the implementer", "Accept implementation as-is"];
|
|
418
|
+
while (true) {
|
|
419
|
+
const choice = await ctx.ui.select(
|
|
420
|
+
buildVerificationDecisionPrompt(parsed, scriptResults, reportPath, promptWidth),
|
|
421
|
+
options,
|
|
422
|
+
);
|
|
423
|
+
if (choice === "View full findings") {
|
|
424
|
+
await showFindings(ctx, report);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (choice === "Send verifier findings to the implementer") {
|
|
428
|
+
const updated: ImplementationHandoff = {
|
|
429
|
+
...handoff,
|
|
430
|
+
verificationRepairs: repairsUsed + 1,
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
};
|
|
433
|
+
host.setImplementationHandoff(updated);
|
|
434
|
+
// Reactivate the Implement row for the repair round and leave the table
|
|
435
|
+
// mounted; the next settled turn re-enters this verification flow.
|
|
436
|
+
widget?.settleRoleRow("Verify", "done");
|
|
437
|
+
if (handoff.model) widget?.switchToImplementing(handoff.model, "addressing verifier findings…", handoff.thinking);
|
|
438
|
+
host.markImplementationPending(ctx);
|
|
439
|
+
const repairNote = buildRepairNote(parsed, failedScripts);
|
|
440
|
+
await host.pi.sendUserMessage(
|
|
441
|
+
buildImplementationKickoffMessage(plan, handoff.planFilePath, repairNote),
|
|
442
|
+
TRIGGER_TURN,
|
|
443
|
+
);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// "Accept implementation as-is" — and the selector-cancellation fallthrough.
|
|
447
|
+
ctx.ui.notify(`Verification verdict: ${parsed.verdict}. Implementation accepted as-is.`, "warning");
|
|
448
|
+
finish("done");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Verify a settled implementation against its approved plan and, on gaps, offer
|
|
455
|
+
* a bounded repair round back to the in-session implementer. Best-effort: any
|
|
456
|
+
* unrecoverable step notifies and returns rather than throwing into the caller
|
|
457
|
+
* (which is a fire-and-forget `onAgentSettled` handler).
|
|
458
|
+
*/
|
|
459
|
+
export async function runImplementationVerification(
|
|
460
|
+
ctx: ExtensionContext,
|
|
461
|
+
host: MoaRunHost,
|
|
462
|
+
handoff: ImplementationHandoff,
|
|
463
|
+
implementerReport: string | undefined,
|
|
464
|
+
// Overridable in tests; defaults to the real verifier subprocess launcher.
|
|
465
|
+
// The seam lets a test drive the failure → synthesizer-fallback → success path
|
|
466
|
+
// and record the thinking level handed to each subprocess.
|
|
467
|
+
options: { runSingleAgent?: typeof runSingleAgent } = {},
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
const runVerifier = options.runSingleAgent ?? runSingleAgent;
|
|
470
|
+
const verifier = handoff.verifier;
|
|
471
|
+
if (!verifier) return; // Callers guard this; defensive.
|
|
472
|
+
|
|
473
|
+
const plan = resolveHandoffPlan(handoff, ctx.cwd);
|
|
474
|
+
if (!plan) {
|
|
475
|
+
host.noteRunError(ctx, "Verification skipped — the approved plan content could not be retrieved from disk.");
|
|
476
|
+
ctx.ui.notify("Verification skipped — the approved plan content could not be retrieved from disk.", "error");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const criteriaMarkdown = handoff.verificationCriteria
|
|
481
|
+
?? (handoff.repoPlanSlug ? readRepoPlanFile(ctx.cwd, handoff.repoPlanSlug, "criteria") : undefined);
|
|
482
|
+
const criteria = criteriaMarkdown ? parseVerificationCriteria(criteriaMarkdown) : [];
|
|
483
|
+
|
|
484
|
+
installShippedAgents();
|
|
485
|
+
const discovery = discoverAgents(ctx.cwd, "user");
|
|
486
|
+
const agents = withAuthoritativeMoaAgents(discovery.agents, shippedAgentsDir());
|
|
487
|
+
|
|
488
|
+
// The verification phase reuses the table orchestration adopted on approval
|
|
489
|
+
// (or reconstructed on resume), reactivating the Verify row and pointing the
|
|
490
|
+
// band at Verify.
|
|
491
|
+
const widget = host.getActiveProgressWidget();
|
|
492
|
+
widget?.setActivePhase("Verify");
|
|
493
|
+
// `activeVerifierThinking` is not declared yet; the initial project-checks
|
|
494
|
+
// activation carries the handoff's verifier level, which it is seeded from.
|
|
495
|
+
widget?.switchToVerifying(verifier, "running project checks…", handoff.verifierThinking);
|
|
496
|
+
|
|
497
|
+
// Diff and script gate run in the parent, BEFORE the verifier's mutation
|
|
498
|
+
// tripwire is armed: `npm test` may legitimately write snapshots/coverage,
|
|
499
|
+
// and baking those into the tripwire baseline is what keeps them from
|
|
500
|
+
// tripping a false "verifier modified the working tree" alarm. The diff is
|
|
501
|
+
// captured before the gate so test artifacts do not pollute it.
|
|
502
|
+
const diff = await captureImplementationDiff(ctx.cwd);
|
|
503
|
+
const scripts = await discoverVerifyScripts(ctx.cwd);
|
|
504
|
+
const scriptResults: VerifyResult[] = [];
|
|
505
|
+
if (scripts.length > 0) {
|
|
506
|
+
ctx.ui.notify(`Verifying implementation — running project checks (${scripts.join(", ")})…`);
|
|
507
|
+
const scriptSession: CancelSession = { title: "MoA verification — project checks", run: undefined, overlayOpen: false };
|
|
508
|
+
host.setActiveCancelSession(scriptSession);
|
|
509
|
+
const scriptRun = new CancelRun();
|
|
510
|
+
scriptSession.run = scriptRun;
|
|
511
|
+
try {
|
|
512
|
+
for (const script of scripts) {
|
|
513
|
+
const slot = scriptRun.add(script);
|
|
514
|
+
const result = await runVerifyScript(ctx.cwd, script, slot.signal);
|
|
515
|
+
if (scriptRun.cancelAllRequested) break;
|
|
516
|
+
scriptResults.push(result);
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
host.setActiveCancelSession(undefined);
|
|
520
|
+
}
|
|
521
|
+
if (scriptRun.cancelAllRequested) {
|
|
522
|
+
widget?.settleRoleRow("Verify", "cancelled");
|
|
523
|
+
host.stopActiveProgressWidget();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const config = loadMoaConfig();
|
|
529
|
+
let activeVerifier = verifier;
|
|
530
|
+
let activeVerifierThinking = handoff.verifierThinking;
|
|
531
|
+
let activeHandoff = handoff;
|
|
532
|
+
|
|
533
|
+
const runVerifierPreflight = async (): Promise<VerifierRunOutcome> => {
|
|
534
|
+
const verifier = activeVerifier;
|
|
535
|
+
widget?.switchToVerifying(verifier, "checking verifier…", activeVerifierThinking);
|
|
536
|
+
const result = await runVerifier(
|
|
537
|
+
ctx.cwd, agents, "moa-verifier", "Reply with the single word ok", undefined, undefined, undefined,
|
|
538
|
+
modelRefLabel(verifier), activeVerifierThinking, {
|
|
539
|
+
...modelExtensionOptions(ctx, verifier),
|
|
540
|
+
resolveOnAbort: true,
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
return verifierOutcomeFromResult(result);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const runVerifierOnce = async (verificationTask: string): Promise<VerifierRunOutcome> => {
|
|
547
|
+
const verifier = activeVerifier;
|
|
548
|
+
const tripwire = new MutationTripwire();
|
|
549
|
+
await tripwire.arm(ctx.cwd);
|
|
550
|
+
|
|
551
|
+
widget?.switchToVerifying(verifier, "verifying implementation…", activeVerifierThinking);
|
|
552
|
+
widget?.updateRoleTranscript("Verify", []);
|
|
553
|
+
|
|
554
|
+
const session: CancelSession = { title: "MoA verification", run: undefined, overlayOpen: false };
|
|
555
|
+
host.setActiveCancelSession(session);
|
|
556
|
+
const run = new CancelRun();
|
|
557
|
+
const label = modelRefLabel(verifier);
|
|
558
|
+
const slot = run.add(label);
|
|
559
|
+
session.run = run;
|
|
560
|
+
session.getExtras = () => {
|
|
561
|
+
const s = widget?.getRoleStatus("Verify");
|
|
562
|
+
return {
|
|
563
|
+
contextTokens: s?.contextTokens,
|
|
564
|
+
contextWindow: s?.ref ? resolveContextWindow(ctx, s.ref) : undefined,
|
|
565
|
+
activity: s?.activity,
|
|
566
|
+
loopCount: activityLoopCount(s?.activity, s?.activityHistory),
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const result = await runVerifier(
|
|
572
|
+
ctx.cwd, agents, "moa-verifier", verificationTask, undefined, slot.signal, undefined,
|
|
573
|
+
label, activeVerifierThinking, {
|
|
574
|
+
...modelExtensionOptions(ctx, verifier),
|
|
575
|
+
resolveOnAbort: true,
|
|
576
|
+
onProgress: (r) => {
|
|
577
|
+
widget?.updateRoleUsage(
|
|
578
|
+
"Verify",
|
|
579
|
+
r.usage.contextTokens,
|
|
580
|
+
r.usage.turns,
|
|
581
|
+
r.usage.toolCalls,
|
|
582
|
+
resolveModelCost(ctx, verifier, r.usage),
|
|
583
|
+
);
|
|
584
|
+
if (r.activity) widget?.updateRoleActivity("Verify", r.activity);
|
|
585
|
+
if (r.outputActivity) widget?.updateRoleOutput("Verify", r.outputActivity.tokens, r.outputActivity.revision);
|
|
586
|
+
widget?.updateRoleTranscript("Verify", r.messages, r.partialAssistant);
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
);
|
|
590
|
+
widget?.updateRoleTranscript("Verify", result.messages);
|
|
591
|
+
const changed = await tripwire.check(ctx.cwd);
|
|
592
|
+
if (changed.length > 0) ctx.ui.notify(formatMutationWarning("MoA verification", changed), "error");
|
|
593
|
+
if (result.cancelled || run.cancelAllRequested) {
|
|
594
|
+
return { output: "", cancelled: true, failed: false, messageCount: result.messages.length };
|
|
595
|
+
}
|
|
596
|
+
return verifierOutcomeFromResult(result);
|
|
597
|
+
} finally {
|
|
598
|
+
session.run = undefined;
|
|
599
|
+
session.getExtras = undefined;
|
|
600
|
+
host.setActiveCancelSession(undefined);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// Run the verifier; on a cancelled/failed/unparseable outcome offer a retry
|
|
605
|
+
// that does NOT consume a repair round (the implementation is unchanged).
|
|
606
|
+
const finish = (state: "done" | "error" | "cancelled"): void => {
|
|
607
|
+
widget?.settleRoleRow("Verify", state);
|
|
608
|
+
host.stopActiveProgressWidget();
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const applySynthesizerFallback = (): void => {
|
|
612
|
+
const synthesizer = config.synthesizer;
|
|
613
|
+
if (!synthesizer) return;
|
|
614
|
+
activeVerifier = synthesizer;
|
|
615
|
+
activeVerifierThinking = config.thinkingOverrides[modelRefLabel(synthesizer)] ?? activeVerifierThinking;
|
|
616
|
+
activeHandoff = {
|
|
617
|
+
...activeHandoff,
|
|
618
|
+
verifier: activeVerifier,
|
|
619
|
+
verifierThinking: activeVerifierThinking,
|
|
620
|
+
timestamp: Date.now(),
|
|
621
|
+
};
|
|
622
|
+
host.setImplementationHandoff(activeHandoff);
|
|
623
|
+
const phaseModels = widget?.phaseModels();
|
|
624
|
+
if (phaseModels) widget?.setPhaseModels({ ...phaseModels, Verify: activeVerifier });
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const recoverFromVerifierFailure = async (outcome: VerifierRunOutcome): Promise<"retry" | "fallback" | "stop"> => {
|
|
628
|
+
if (!ctx.hasUI) {
|
|
629
|
+
const reason = outcome.cancelled ? "cancelled" : `failed: ${outcome.output || "unknown error"}`;
|
|
630
|
+
if (!outcome.cancelled) {
|
|
631
|
+
host.noteRunError(ctx, `Verification failed: ${outcome.output || "unknown error"}`);
|
|
632
|
+
}
|
|
633
|
+
ctx.ui.notify(`Verification ${reason}. Implementation left as-is.`, "warning");
|
|
634
|
+
finish(outcome.cancelled ? "cancelled" : "error");
|
|
635
|
+
return "stop";
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (outcome.cancelled) {
|
|
639
|
+
const choice = await ctx.ui.select(
|
|
640
|
+
"Verification was cancelled — what next?",
|
|
641
|
+
["Retry verification", "Skip verification"],
|
|
642
|
+
);
|
|
643
|
+
if (choice === "Retry verification") return "retry";
|
|
644
|
+
ctx.ui.notify("Verification skipped. Implementation left as-is.", "warning");
|
|
645
|
+
finish("cancelled");
|
|
646
|
+
return "stop";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const verifierLabel = modelRefLabel(activeVerifier);
|
|
650
|
+
const fullReason = [
|
|
651
|
+
outcome.output.trim(),
|
|
652
|
+
outcome.stderr && outcome.stderr !== outcome.output ? `stderr:\n${outcome.stderr.trim()}` : "",
|
|
653
|
+
].filter(Boolean).join("\n\n") || "unknown error";
|
|
654
|
+
ctx.ui.notify(`Verification failed (${verifierLabel}):\n\n${fullReason}`, "error");
|
|
655
|
+
const summary = formatVerifierFailure(outcome);
|
|
656
|
+
const titleReason = summary.length > 200 ? `${summary.slice(0, 199)}…` : summary;
|
|
657
|
+
const fallbackOption = config.synthesizer
|
|
658
|
+
? `Retry with ${modelRefLabel(config.synthesizer)}`
|
|
659
|
+
: undefined;
|
|
660
|
+
const options = fallbackOption
|
|
661
|
+
? [fallbackOption, "Retry verification", "Skip verification"]
|
|
662
|
+
: ["Retry verification", "Skip verification"];
|
|
663
|
+
const choice = await ctx.ui.select(
|
|
664
|
+
`Verification could not complete (${verifierLabel}): ${titleReason}`,
|
|
665
|
+
options,
|
|
666
|
+
);
|
|
667
|
+
if (fallbackOption && choice === fallbackOption) return "fallback";
|
|
668
|
+
if (choice === "Retry verification") return "retry";
|
|
669
|
+
ctx.ui.notify("Verification skipped. Implementation left as-is.", "warning");
|
|
670
|
+
finish("cancelled");
|
|
671
|
+
return "stop";
|
|
672
|
+
};
|
|
673
|
+
let verifierPreflightPassed = false;
|
|
674
|
+
let verifierTask = "";
|
|
675
|
+
let verificationReportPath: string | undefined;
|
|
676
|
+
let parsed: VerificationVerdict | undefined;
|
|
677
|
+
// Retain the accepted verifier report verbatim so the read-only findings
|
|
678
|
+
// popup can show the original audit, not the compact decision summary.
|
|
679
|
+
let verificationReport = "";
|
|
680
|
+
while (true) {
|
|
681
|
+
let outcome: VerifierRunOutcome;
|
|
682
|
+
if (!verifierPreflightPassed) {
|
|
683
|
+
const preflight = await runVerifierPreflight();
|
|
684
|
+
if (preflight.cancelled || preflight.failed) {
|
|
685
|
+
outcome = preflight;
|
|
686
|
+
} else {
|
|
687
|
+
verifierPreflightPassed = true;
|
|
688
|
+
verifierTask = buildVerifierTask({ plan, implementerReport, diff, scriptResults, cwd: ctx.cwd, criteria });
|
|
689
|
+
outcome = await runVerifierOnce(verifierTask);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
outcome = await runVerifierOnce(verifierTask);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!outcome.cancelled && host.getPlanRepoSlug()) {
|
|
696
|
+
const verificationOutput = outcome.failed
|
|
697
|
+
? [
|
|
698
|
+
`## Verifier failed — ${formatVerifierFailure(outcome)}`,
|
|
699
|
+
`Stop reason: ${outcome.stopReason ?? "unknown"}`,
|
|
700
|
+
`Exit code: ${outcome.exitCode ?? "unknown"}`,
|
|
701
|
+
"",
|
|
702
|
+
outcome.output || "(No verifier output.)",
|
|
703
|
+
outcome.stderr && outcome.stderr !== outcome.output
|
|
704
|
+
? `\n### Stderr\n${outcome.stderr}`
|
|
705
|
+
: "",
|
|
706
|
+
].join("\n")
|
|
707
|
+
: outcome.output;
|
|
708
|
+
try {
|
|
709
|
+
saveRepoPlanFile(verificationOutput, ctx.cwd, host.getPlanRepoSlug()!, "verification");
|
|
710
|
+
verificationReportPath = `${CONFIG_DIR_NAME}/mf-plan/${host.getPlanRepoSlug()!}__verification.md`;
|
|
711
|
+
} catch {
|
|
712
|
+
// Persisting the report is best-effort; verification proceeds regardless.
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (outcome.cancelled || outcome.failed) {
|
|
717
|
+
const action = await recoverFromVerifierFailure(outcome);
|
|
718
|
+
if (action === "retry") continue;
|
|
719
|
+
if (action === "fallback") {
|
|
720
|
+
applySynthesizerFallback();
|
|
721
|
+
verifierPreflightPassed = false;
|
|
722
|
+
verifierTask = "";
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const candidate = parseVerificationVerdict(outcome.output);
|
|
729
|
+
if (!candidate.verdict || (criteria.length > 0 && candidate.criteria.length === 0)) {
|
|
730
|
+
if (!ctx.hasUI) {
|
|
731
|
+
host.noteRunError(ctx, "Verifier produced no parseable verdict.");
|
|
732
|
+
ctx.ui.notify("Verifier produced no parseable verdict. Implementation left as-is.", "warning");
|
|
733
|
+
finish("error");
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const choice = await ctx.ui.select(
|
|
737
|
+
"The verifier produced no parseable verdict — what next?",
|
|
738
|
+
["Retry verification", "Skip verification"],
|
|
739
|
+
);
|
|
740
|
+
if (choice === "Retry verification") continue;
|
|
741
|
+
ctx.ui.notify("Verification inconclusive — implementation left as-is.", "warning");
|
|
742
|
+
finish("error");
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (criteria.length > 0) {
|
|
747
|
+
const derived = deriveCriteriaVerdict(candidate, criteria);
|
|
748
|
+
candidate.verdict = derived.verdict;
|
|
749
|
+
candidate.gaps = derived.gaps;
|
|
750
|
+
}
|
|
751
|
+
parsed = candidate;
|
|
752
|
+
verificationReport = outcome.output;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const failedScripts = scriptResults.filter((r) => r.status === "fail");
|
|
757
|
+
if (verificationPassed(parsed.verdict, scriptResults)) {
|
|
758
|
+
const checksNote = scriptResults.length > 0 ? " and all project checks passed" : "";
|
|
759
|
+
ctx.ui.notify(criteria.length > 0
|
|
760
|
+
? `Verification passed — all ${criteria.length} criteria met${checksNote}.`
|
|
761
|
+
: `Verification passed — the implementation matches the approved plan${checksNote}.`);
|
|
762
|
+
finish("done");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const repairsUsed = activeHandoff.verificationRepairs ?? 0;
|
|
767
|
+
if (ctx.hasUI && repairsUsed < MAX_VERIFICATION_REPAIRS) {
|
|
768
|
+
await runVerificationDecision({
|
|
769
|
+
ctx,
|
|
770
|
+
host,
|
|
771
|
+
handoff: activeHandoff,
|
|
772
|
+
parsed,
|
|
773
|
+
scriptResults,
|
|
774
|
+
failedScripts,
|
|
775
|
+
plan,
|
|
776
|
+
report: verificationReport,
|
|
777
|
+
reportPath: verificationReportPath,
|
|
778
|
+
repairsUsed,
|
|
779
|
+
isTui: ctx.mode === "tui",
|
|
780
|
+
widget,
|
|
781
|
+
finish,
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const summary = buildVerificationSummary(parsed, scriptResults).join("\n");
|
|
787
|
+
const report = formatVerificationReport(parsed, failedScripts);
|
|
788
|
+
const exhaustedNote = repairsUsed >= MAX_VERIFICATION_REPAIRS ? "\n\nRepair rounds are exhausted." : "";
|
|
789
|
+
ctx.ui.notify(`${summary}\n\n${report}${exhaustedNote}`, "warning");
|
|
790
|
+
finish("done");
|
|
791
|
+
}
|