(
tc: ToolCall,
transaction: Transaction,
sessionId: string,
options: AgentOptions,
)
| 2203 | sessionStore.recordTurn(sessionId, [repairMsg], { input: 0, output: 0, costUsd: 0 }); |
| 2204 | yield { type: 'notice', data: { message: `⚠ Auto-verify: ${vr.errorCount} ${vr.checker} error(s) in changed files — repairing (${verifyRepairAttempts}/${maxRepair})` } }; |
| 2205 | logger.info('Auto-verify gate forcing repair', { checker: vr.checker, errors: vr.errorCount, attempt: verifyRepairAttempts }); |
| 2206 | continue; // force the model to fix before it's allowed to finish |
| 2207 | } else if (!verifyGaveUp) { |
| 2208 | // Exhausted repair budget — surface remaining errors once, then let it finish. |
| 2209 | verifyGaveUp = true; |
| 2210 | const giveup: Message = { role: 'user', content: buildVerifyGiveupMessage(vr.errorCount, vr.checker ?? 'type-check') }; |
| 2211 | newMessages.push(giveup); |
| 2212 | sessionStore.recordTurn(sessionId, [giveup], { input: 0, output: 0, costUsd: 0 }); |
| 2213 | logger.warn('Auto-verify gate gave up', { checker: vr.checker, errors: vr.errorCount }); |
| 2214 | continue; |
| 2215 | } |
| 2216 | } else if (vr.ran) { |
| 2217 | // Clean compile — reset so a later edit batch gets a fresh repair budget. |
| 2218 | verifyRepairAttempts = 0; |
| 2219 | } |
| 2220 | } catch (e: any) { |
| 2221 | // The pre-finish safety gate was skipped — a possibly-broken task can |
| 2222 | // now finish as "success". Make that visible, don't bury it in debug. |
| 2223 | logger.warn('Auto-verify gate skipped (error)', { err: e?.message }); |
| 2224 | yield { type: 'notice', data: { message: `⚠ Auto-verify gate skipped due to an error (${e?.message ?? 'unknown'}) — changes were not type-checked` } }; |
| 2225 | } |
| 2226 | } |
| 2227 | |
| 2228 | // ── LLM Critic gate (semantic self-review / test-time compute) ── |
| 2229 | // Mechanical verify only catches syntax/type errors. Before finishing a |
| 2230 | // coding task, run ONE peer-review pass: a Senior-QA prompt reviews the |
| 2231 | // touched files for logic bugs and convention/spec mismatches that a |
| 2232 | // type-checker can't see. A blocking verdict sends the worker back to |
| 2233 | // fix (backtracking). Budget-capped so a model that can't satisfy the |
| 2234 | // critic still finishes. Uses the 'planning' role's model if configured |
| 2235 | // (a stronger/cheaper reviewer), else self-reviews with the same model. |
| 2236 | const criticCfg = (this.config as any).critic ?? {}; |
| 2237 | const criticEnabled = criticCfg.enabled === true // opt-in: it costs an extra round-trip |
| 2238 | && mode.mode === 'normal' |
| 2239 | && process.env.QODEX_CRITIC !== '0'; |
| 2240 | const maxCriticRounds = criticCfg.maxRounds ?? 1; |
| 2241 | if (criticEnabled && touchedSourceFiles.size > 0 && criticRounds < maxCriticRounds && !options.signal?.aborted) { |
| 2242 | try { |
| 2243 | const { readFile: fsReadFile } = await import('fs/promises'); |
| 2244 | const pathMod = await import('path'); |
| 2245 | const files: DiffFile[] = []; |
| 2246 | for (const rel of [...touchedSourceFiles].slice(0, criticCfg.maxFiles ?? 6)) { |
| 2247 | try { |
| 2248 | const content = await fsReadFile(pathMod.resolve(this.cwd, rel), 'utf-8'); |
| 2249 | files.push({ path: rel, content }); |
| 2250 | } catch { /* file may have been deleted; skip */ } |
| 2251 | } |
| 2252 | if (files.length > 0) { |
| 2253 | // Recover the user's task from the message history (this method |
| 2254 | // doesn't carry buildSystemPrompt's locals). Trellis spec is |
| 2255 | // lazily reloaded (its loader caches, so this is cheap). |
| 2256 | const criticUserPrompt = String( |
| 2257 | [...messages].reverse().find(m => m.role === 'user')?.content ?? '', |
| 2258 | ); |
| 2259 | let criticSpecBlock: string | null = null; |
| 2260 | try { |
| 2261 | const tctx = await loadTrellisContext(this.cwd); |
| 2262 | criticSpecBlock = tctx?.specBlock ?? null; |
no test coverage detected