@polpo-ai/node 0.11.1

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.
Files changed (238) hide show
  1. package/LICENSE +13 -0
  2. package/dist/adapters/engine.d.ts +51 -0
  3. package/dist/adapters/engine.d.ts.map +1 -0
  4. package/dist/adapters/engine.js +316 -0
  5. package/dist/adapters/engine.js.map +1 -0
  6. package/dist/adapters/index.d.ts +2 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +4 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/loop-engine.d.ts +36 -0
  11. package/dist/adapters/loop-engine.d.ts.map +1 -0
  12. package/dist/adapters/loop-engine.js +534 -0
  13. package/dist/adapters/loop-engine.js.map +1 -0
  14. package/dist/adapters/node-filesystem.d.ts +22 -0
  15. package/dist/adapters/node-filesystem.d.ts.map +1 -0
  16. package/dist/adapters/node-filesystem.js +59 -0
  17. package/dist/adapters/node-filesystem.js.map +1 -0
  18. package/dist/adapters/node-shell.d.ts +5 -0
  19. package/dist/adapters/node-shell.d.ts.map +1 -0
  20. package/dist/adapters/node-shell.js +34 -0
  21. package/dist/adapters/node-shell.js.map +1 -0
  22. package/dist/adapters/node-spawner.d.ts +14 -0
  23. package/dist/adapters/node-spawner.d.ts.map +1 -0
  24. package/dist/adapters/node-spawner.js +103 -0
  25. package/dist/adapters/node-spawner.js.map +1 -0
  26. package/dist/assessment/assessor.d.ts +6 -0
  27. package/dist/assessment/assessor.d.ts.map +1 -0
  28. package/dist/assessment/assessor.js +37 -0
  29. package/dist/assessment/assessor.js.map +1 -0
  30. package/dist/assessment/index.d.ts +6 -0
  31. package/dist/assessment/index.d.ts.map +1 -0
  32. package/dist/assessment/index.js +6 -0
  33. package/dist/assessment/index.js.map +1 -0
  34. package/dist/assessment/llm-review.d.ts +34 -0
  35. package/dist/assessment/llm-review.d.ts.map +1 -0
  36. package/dist/assessment/llm-review.js +669 -0
  37. package/dist/assessment/llm-review.js.map +1 -0
  38. package/dist/assessment/schemas.d.ts +5 -0
  39. package/dist/assessment/schemas.d.ts.map +1 -0
  40. package/dist/assessment/schemas.js +5 -0
  41. package/dist/assessment/schemas.js.map +1 -0
  42. package/dist/assessment/scoring.d.ts +5 -0
  43. package/dist/assessment/scoring.d.ts.map +1 -0
  44. package/dist/assessment/scoring.js +5 -0
  45. package/dist/assessment/scoring.js.map +1 -0
  46. package/dist/assessment/transcript-parser.d.ts +31 -0
  47. package/dist/assessment/transcript-parser.d.ts.map +1 -0
  48. package/dist/assessment/transcript-parser.js +203 -0
  49. package/dist/assessment/transcript-parser.js.map +1 -0
  50. package/dist/auth/index.d.ts +2 -0
  51. package/dist/auth/index.d.ts.map +1 -0
  52. package/dist/auth/index.js +4 -0
  53. package/dist/auth/index.js.map +1 -0
  54. package/dist/core/adapter.d.ts +62 -0
  55. package/dist/core/adapter.d.ts.map +1 -0
  56. package/dist/core/adapter.js +2 -0
  57. package/dist/core/adapter.js.map +1 -0
  58. package/dist/core/assessment-orchestrator.d.ts +18 -0
  59. package/dist/core/assessment-orchestrator.d.ts.map +1 -0
  60. package/dist/core/assessment-orchestrator.js +93 -0
  61. package/dist/core/assessment-orchestrator.js.map +1 -0
  62. package/dist/core/config.d.ts +17 -0
  63. package/dist/core/config.d.ts.map +1 -0
  64. package/dist/core/config.js +272 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/constants.d.ts +10 -0
  67. package/dist/core/constants.d.ts.map +1 -0
  68. package/dist/core/constants.js +11 -0
  69. package/dist/core/constants.js.map +1 -0
  70. package/dist/core/deadlock-resolver.d.ts +35 -0
  71. package/dist/core/deadlock-resolver.d.ts.map +1 -0
  72. package/dist/core/deadlock-resolver.js +317 -0
  73. package/dist/core/deadlock-resolver.js.map +1 -0
  74. package/dist/core/drizzle-sqlite-schema.d.ts +8 -0
  75. package/dist/core/drizzle-sqlite-schema.d.ts.map +1 -0
  76. package/dist/core/drizzle-sqlite-schema.js +250 -0
  77. package/dist/core/drizzle-sqlite-schema.js.map +1 -0
  78. package/dist/core/events.d.ts +26 -0
  79. package/dist/core/events.d.ts.map +1 -0
  80. package/dist/core/events.js +39 -0
  81. package/dist/core/events.js.map +1 -0
  82. package/dist/core/index.d.ts +37 -0
  83. package/dist/core/index.d.ts.map +1 -0
  84. package/dist/core/index.js +31 -0
  85. package/dist/core/index.js.map +1 -0
  86. package/dist/core/ink.d.ts +165 -0
  87. package/dist/core/ink.d.ts.map +1 -0
  88. package/dist/core/ink.js +445 -0
  89. package/dist/core/ink.js.map +1 -0
  90. package/dist/core/orchestrator.d.ts +387 -0
  91. package/dist/core/orchestrator.d.ts.map +1 -0
  92. package/dist/core/orchestrator.js +1015 -0
  93. package/dist/core/orchestrator.js.map +1 -0
  94. package/dist/core/question-detector.d.ts +19 -0
  95. package/dist/core/question-detector.d.ts.map +1 -0
  96. package/dist/core/question-detector.js +29 -0
  97. package/dist/core/question-detector.js.map +1 -0
  98. package/dist/core/runner.d.ts +14 -0
  99. package/dist/core/runner.d.ts.map +1 -0
  100. package/dist/core/runner.js +299 -0
  101. package/dist/core/runner.js.map +1 -0
  102. package/dist/core/session-reader.d.ts +46 -0
  103. package/dist/core/session-reader.d.ts.map +1 -0
  104. package/dist/core/session-reader.js +166 -0
  105. package/dist/core/session-reader.js.map +1 -0
  106. package/dist/index.d.ts +24 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +29 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/llm/index.d.ts +8 -0
  111. package/dist/llm/index.d.ts.map +1 -0
  112. package/dist/llm/index.js +21 -0
  113. package/dist/llm/index.js.map +1 -0
  114. package/dist/llm/pi-client.d.ts +8 -0
  115. package/dist/llm/pi-client.d.ts.map +1 -0
  116. package/dist/llm/pi-client.js +8 -0
  117. package/dist/llm/pi-client.js.map +1 -0
  118. package/dist/llm/prompts.d.ts +14 -0
  119. package/dist/llm/prompts.d.ts.map +1 -0
  120. package/dist/llm/prompts.js +1618 -0
  121. package/dist/llm/prompts.js.map +1 -0
  122. package/dist/llm/retry.d.ts +5 -0
  123. package/dist/llm/retry.d.ts.map +1 -0
  124. package/dist/llm/retry.js +5 -0
  125. package/dist/llm/retry.js.map +1 -0
  126. package/dist/llm/skills.d.ts +265 -0
  127. package/dist/llm/skills.d.ts.map +1 -0
  128. package/dist/llm/skills.js +804 -0
  129. package/dist/llm/skills.js.map +1 -0
  130. package/dist/quality/index.d.ts +3 -0
  131. package/dist/quality/index.d.ts.map +1 -0
  132. package/dist/quality/index.js +3 -0
  133. package/dist/quality/index.js.map +1 -0
  134. package/dist/quality/quality-controller.d.ts +2 -0
  135. package/dist/quality/quality-controller.d.ts.map +1 -0
  136. package/dist/quality/quality-controller.js +2 -0
  137. package/dist/quality/quality-controller.js.map +1 -0
  138. package/dist/quality/sla-monitor.d.ts +2 -0
  139. package/dist/quality/sla-monitor.d.ts.map +1 -0
  140. package/dist/quality/sla-monitor.js +2 -0
  141. package/dist/quality/sla-monitor.js.map +1 -0
  142. package/dist/scheduling/cron.d.ts +2 -0
  143. package/dist/scheduling/cron.d.ts.map +1 -0
  144. package/dist/scheduling/cron.js +2 -0
  145. package/dist/scheduling/cron.js.map +1 -0
  146. package/dist/scheduling/index.d.ts +3 -0
  147. package/dist/scheduling/index.d.ts.map +1 -0
  148. package/dist/scheduling/index.js +3 -0
  149. package/dist/scheduling/index.js.map +1 -0
  150. package/dist/scheduling/scheduler.d.ts +2 -0
  151. package/dist/scheduling/scheduler.d.ts.map +1 -0
  152. package/dist/scheduling/scheduler.js +2 -0
  153. package/dist/scheduling/scheduler.js.map +1 -0
  154. package/dist/server/app.d.ts +19 -0
  155. package/dist/server/app.d.ts.map +1 -0
  156. package/dist/server/app.js +302 -0
  157. package/dist/server/app.js.map +1 -0
  158. package/dist/server/deps.d.ts +157 -0
  159. package/dist/server/deps.d.ts.map +1 -0
  160. package/dist/server/deps.js +9 -0
  161. package/dist/server/deps.js.map +1 -0
  162. package/dist/server/index.d.ts +43 -0
  163. package/dist/server/index.d.ts.map +1 -0
  164. package/dist/server/index.js +128 -0
  165. package/dist/server/index.js.map +1 -0
  166. package/dist/server/middleware/auth.d.ts +3 -0
  167. package/dist/server/middleware/auth.d.ts.map +1 -0
  168. package/dist/server/middleware/auth.js +35 -0
  169. package/dist/server/middleware/auth.js.map +1 -0
  170. package/dist/server/middleware/error.d.ts +15 -0
  171. package/dist/server/middleware/error.d.ts.map +1 -0
  172. package/dist/server/middleware/error.js +44 -0
  173. package/dist/server/middleware/error.js.map +1 -0
  174. package/dist/server/middleware/rate-limit.d.ts +16 -0
  175. package/dist/server/middleware/rate-limit.d.ts.map +1 -0
  176. package/dist/server/middleware/rate-limit.js +51 -0
  177. package/dist/server/middleware/rate-limit.js.map +1 -0
  178. package/dist/server/routes/config.d.ts +25 -0
  179. package/dist/server/routes/config.d.ts.map +1 -0
  180. package/dist/server/routes/config.js +446 -0
  181. package/dist/server/routes/config.js.map +1 -0
  182. package/dist/server/routes/files.d.ts +11 -0
  183. package/dist/server/routes/files.d.ts.map +1 -0
  184. package/dist/server/routes/files.js +514 -0
  185. package/dist/server/routes/files.js.map +1 -0
  186. package/dist/server/routes/filesystem.d.ts +3 -0
  187. package/dist/server/routes/filesystem.d.ts.map +1 -0
  188. package/dist/server/routes/filesystem.js +152 -0
  189. package/dist/server/routes/filesystem.js.map +1 -0
  190. package/dist/server/routes/providers.d.ts +7 -0
  191. package/dist/server/routes/providers.d.ts.map +1 -0
  192. package/dist/server/routes/providers.js +256 -0
  193. package/dist/server/routes/providers.js.map +1 -0
  194. package/dist/server/routes/skills.d.ts +11 -0
  195. package/dist/server/routes/skills.d.ts.map +1 -0
  196. package/dist/server/routes/skills.js +543 -0
  197. package/dist/server/routes/skills.js.map +1 -0
  198. package/dist/server/security.d.ts +34 -0
  199. package/dist/server/security.d.ts.map +1 -0
  200. package/dist/server/security.js +118 -0
  201. package/dist/server/security.js.map +1 -0
  202. package/dist/server/sse-bridge.d.ts +36 -0
  203. package/dist/server/sse-bridge.d.ts.map +1 -0
  204. package/dist/server/sse-bridge.js +110 -0
  205. package/dist/server/sse-bridge.js.map +1 -0
  206. package/dist/server/types.d.ts +69 -0
  207. package/dist/server/types.d.ts.map +1 -0
  208. package/dist/server/types.js +2 -0
  209. package/dist/server/types.js.map +1 -0
  210. package/dist/setup/env-persistence.d.ts +9 -0
  211. package/dist/setup/env-persistence.d.ts.map +1 -0
  212. package/dist/setup/env-persistence.js +42 -0
  213. package/dist/setup/env-persistence.js.map +1 -0
  214. package/dist/setup/index.d.ts +32 -0
  215. package/dist/setup/index.d.ts.map +1 -0
  216. package/dist/setup/index.js +24 -0
  217. package/dist/setup/index.js.map +1 -0
  218. package/dist/setup/models.d.ts +20 -0
  219. package/dist/setup/models.d.ts.map +1 -0
  220. package/dist/setup/models.js +33 -0
  221. package/dist/setup/models.js.map +1 -0
  222. package/dist/setup/providers.d.ts +26 -0
  223. package/dist/setup/providers.d.ts.map +1 -0
  224. package/dist/setup/providers.js +31 -0
  225. package/dist/setup/providers.js.map +1 -0
  226. package/dist/vault/encrypted-store.d.ts +45 -0
  227. package/dist/vault/encrypted-store.d.ts.map +1 -0
  228. package/dist/vault/encrypted-store.js +149 -0
  229. package/dist/vault/encrypted-store.js.map +1 -0
  230. package/dist/vault/index.d.ts +4 -0
  231. package/dist/vault/index.d.ts.map +1 -0
  232. package/dist/vault/index.js +3 -0
  233. package/dist/vault/index.js.map +1 -0
  234. package/dist/vault/resolver.d.ts +8 -0
  235. package/dist/vault/resolver.d.ts.map +1 -0
  236. package/dist/vault/resolver.js +8 -0
  237. package/dist/vault/resolver.js.map +1 -0
  238. package/package.json +76 -0
@@ -0,0 +1,669 @@
1
+ /**
2
+ * G-Eval LLM-as-Judge review using Vercel AI SDK.
3
+ *
4
+ * Architecture: 2-phase review for reliability.
5
+ *
6
+ * Phase 1 — EXPLORATION (tool loop via generateText + maxSteps)
7
+ * The reviewer explores the codebase using read_file, glob, grep.
8
+ * AI SDK handles the tool loop internally via `stopWhen: stepCountIs(20)`.
9
+ * After exploration, we collect all assistant text as the "analysis".
10
+ *
11
+ * Phase 2 — SCORING (structured output via Output.object)
12
+ * A separate generateText call receives the full analysis from Phase 1
13
+ * and produces structured scores via Output.object() — the provider
14
+ * enforces JSON schema compliance, zero manual parsing needed.
15
+ *
16
+ * This separation makes the system robust: exploration failures don't
17
+ * block scoring, and scoring failures are isolated from exploration.
18
+ *
19
+ * Runs 3 independent reviewers in parallel (multi-evaluator consensus).
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { execSync } from "node:child_process";
23
+ import { resolve, relative } from "node:path";
24
+ import { z } from "zod";
25
+ import { generateText, Output, tool, stepCountIs, } from "ai";
26
+ import { DEFAULT_DIMENSIONS, buildRubricSection, computeWeightedScore, computeMedianScores } from "./scoring.js";
27
+ import { validateReviewPayload } from "./schemas.js";
28
+ import { ReviewPayloadSchema } from "@polpo-ai/core/assessment-schemas";
29
+ import { withRetry } from "../llm/retry.js";
30
+ import { resolveModel, mapReasoningToProviderOptions } from "../llm/pi-client.js";
31
+ // ── Tool Execution Helpers ────────────────────────────────────────────
32
+ function readFileImpl(cwd, args) {
33
+ const filePath = resolve(cwd, args.path);
34
+ try {
35
+ const raw = readFileSync(filePath, "utf-8");
36
+ const lines = raw.split("\n");
37
+ const limit = args.limit ?? 500;
38
+ const sliced = lines.slice(0, limit);
39
+ return sliced.map((l, i) => `${i + 1}\t${l}`).join("\n") +
40
+ (lines.length > limit ? `\n... (${lines.length - limit} more lines)` : "");
41
+ }
42
+ catch (err) {
43
+ return `Error reading ${args.path}: ${err instanceof Error ? err.message : String(err)}`;
44
+ }
45
+ }
46
+ function globImpl(cwd, args) {
47
+ try {
48
+ const result = execSync(`find ${JSON.stringify(cwd)} -type f -name ${JSON.stringify(args.pattern)} 2>/dev/null | head -200`, { encoding: "utf-8", timeout: 10_000 }).trim();
49
+ return result ? result.split("\n").map(f => relative(cwd, f)).join("\n") : "No files found";
50
+ }
51
+ catch {
52
+ return "No files found";
53
+ }
54
+ }
55
+ function grepImpl(cwd, args) {
56
+ const includeFlag = args.include ? `--include=${JSON.stringify(args.include)}` : "";
57
+ try {
58
+ const result = execSync(`grep -rn ${includeFlag} -E ${JSON.stringify(args.pattern)} ${JSON.stringify(cwd)} 2>/dev/null | head -100`, { encoding: "utf-8", timeout: 15_000 }).trim();
59
+ return result || "No matches found";
60
+ }
61
+ catch {
62
+ return "No matches found";
63
+ }
64
+ }
65
+ // ── Build exploration tools for AI SDK ────────────────────────────────
66
+ function buildExplorationTools(cwd, filesRead, onProgress) {
67
+ return {
68
+ read_file: tool({
69
+ description: "Read the contents of a file. Returns numbered lines.",
70
+ inputSchema: z.object({
71
+ path: z.string().describe("File path relative to project root"),
72
+ limit: z.number().optional().describe("Max lines to read (default: 500)"),
73
+ }),
74
+ execute: async (args) => {
75
+ filesRead.push(args.path);
76
+ onProgress?.(`Reading ${args.path.split("/").pop()}`);
77
+ return readFileImpl(cwd, args);
78
+ },
79
+ }),
80
+ glob: tool({
81
+ description: "Find files matching a pattern. Returns file paths.",
82
+ inputSchema: z.object({
83
+ pattern: z.string().describe("Glob pattern (e.g. '*.ts', 'src/**/*.js')"),
84
+ }),
85
+ execute: async (args) => {
86
+ onProgress?.(`Searching ${args.pattern}`);
87
+ return globImpl(cwd, args);
88
+ },
89
+ }),
90
+ grep: tool({
91
+ description: "Search for a pattern in files. Returns matching lines with paths and line numbers.",
92
+ inputSchema: z.object({
93
+ pattern: z.string().describe("Regex pattern to search for"),
94
+ include: z.string().optional().describe("File glob filter (e.g. '*.ts')"),
95
+ }),
96
+ execute: async (args) => {
97
+ onProgress?.(`Grep: ${String(args.pattern).slice(0, 30)}`);
98
+ return grepImpl(cwd, args);
99
+ },
100
+ }),
101
+ };
102
+ }
103
+ // ── Trace Serialization ───────────────────────────────────────────────
104
+ /** Convert AI SDK steps to serializable ReviewerMessage[] for persistence */
105
+ function serializeSteps(steps) {
106
+ const messages = [];
107
+ for (const step of steps) {
108
+ // Assistant message with text + tool calls
109
+ const toolCalls = step.toolCalls.map(tc => ({
110
+ id: tc.toolCallId,
111
+ name: tc.toolName,
112
+ arguments: tc.input,
113
+ }));
114
+ messages.push({
115
+ role: "assistant",
116
+ content: step.text || "",
117
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
118
+ timestamp: Date.now(),
119
+ });
120
+ // Tool result messages
121
+ for (const tr of step.toolResults) {
122
+ messages.push({
123
+ role: "toolResult",
124
+ content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result),
125
+ toolCallId: tr.toolCallId,
126
+ toolName: tr.toolName,
127
+ isError: false,
128
+ timestamp: Date.now(),
129
+ });
130
+ }
131
+ }
132
+ return messages;
133
+ }
134
+ // ── Phase 1: Exploration ───────────────────────────────────────────────
135
+ const MAX_EXPLORATION_STEPS = 20;
136
+ /**
137
+ * Phase 1: Let the reviewer freely explore the codebase.
138
+ * Returns the accumulated analysis text from all assistant messages.
139
+ */
140
+ async function runExploration(reviewPrompt, cwd, model, onProgress, reasoning) {
141
+ const m = resolveModel(model);
142
+ const providerOptions = mapReasoningToProviderOptions(m.provider, reasoning, m.maxTokens);
143
+ const filesRead = [];
144
+ const explorationTools = buildExplorationTools(cwd, filesRead, onProgress);
145
+ const result = await generateText({
146
+ model: m.aiModel,
147
+ system: "You are a thorough code reviewer. Use tools to explore the codebase. Focus on finding evidence for each evaluation dimension. Do NOT attempt to output scores as text \u2014 you will be given a dedicated scoring step after exploration.",
148
+ prompt: reviewPrompt,
149
+ tools: explorationTools,
150
+ stopWhen: stepCountIs(MAX_EXPLORATION_STEPS),
151
+ ...(providerOptions ? { providerOptions: providerOptions } : {}),
152
+ });
153
+ // Collect all assistant text across steps as the analysis
154
+ const analysisBlocks = [];
155
+ for (const step of result.steps) {
156
+ if (step.text.trim()) {
157
+ analysisBlocks.push(step.text);
158
+ }
159
+ }
160
+ return {
161
+ analysis: analysisBlocks.join("\n\n") || "The reviewer explored the codebase but produced no written analysis.",
162
+ filesRead,
163
+ messages: serializeSteps(result.steps),
164
+ };
165
+ }
166
+ // ── Phase 2: Scoring ───────────────────────────────────────────────────
167
+ const SCORING_SYSTEM_PROMPT = `You are a code review scorer. You have received a detailed analysis of code from Phase 1.
168
+ Your ONLY job is to convert this analysis into structured scores.
169
+ Output a JSON object matching the required schema. Score each dimension 1-5 based on the rubric.
170
+ Each reasoning MUST include specific file:line references from the analysis.`;
171
+ /**
172
+ * Phase 2: Given the exploration analysis, force the model to produce
173
+ * structured scores via Output.object() — the provider enforces JSON schema.
174
+ *
175
+ * This replaces the old 3-strategy fallback chain (toolChoice -> prompt -> raw JSON)
176
+ * with a single call. AI SDK handles structured output at the provider level.
177
+ */
178
+ async function runScoring(analysis, rubricSection, dimNames, model, onProgress, reasoning) {
179
+ const m = resolveModel(model);
180
+ const providerOptions = mapReasoningToProviderOptions(m.provider, reasoning, m.maxTokens);
181
+ const scoringPrompt = `Based on the following code analysis, produce structured scores for each dimension.
182
+
183
+ ANALYSIS FROM CODE EXPLORATION:
184
+ ${analysis.slice(0, 12000)}
185
+
186
+ EVALUATION DIMENSIONS AND RUBRICS:
187
+ ${rubricSection}
188
+
189
+ DIMENSIONS TO SCORE: ${dimNames}
190
+
191
+ RULES:
192
+ - Score each dimension 1-5 as an integer.
193
+ - Your reasoning MUST reference specific file:line evidence from the analysis above.
194
+ - Include ALL dimension scores and a summary.`;
195
+ const attemptErrors = [];
196
+ onProgress?.("Scoring with structured output...");
197
+ try {
198
+ const result = await generateText({
199
+ model: m.aiModel,
200
+ system: SCORING_SYSTEM_PROMPT,
201
+ prompt: scoringPrompt,
202
+ output: Output.object({ schema: ReviewPayloadSchema }),
203
+ ...(providerOptions ? { providerOptions: providerOptions } : {}),
204
+ });
205
+ const raw = result.output;
206
+ if (!raw) {
207
+ attemptErrors.push("Output.object() returned null/undefined");
208
+ return { payload: null, attemptErrors };
209
+ }
210
+ // Run through validateReviewPayload for normalization (coercion, clamping, etc.)
211
+ const validated = validateReviewPayload(raw);
212
+ if (validated.success) {
213
+ return { payload: validated.data, attemptErrors };
214
+ }
215
+ attemptErrors.push(`Structured output validation failed: ${validated.error}`);
216
+ }
217
+ catch (err) {
218
+ attemptErrors.push(`Structured output: ${err instanceof Error ? err.message : String(err)}`);
219
+ }
220
+ // Fallback: try plain text generation and parse JSON from response
221
+ onProgress?.("Fallback: requesting plain text JSON scores...");
222
+ try {
223
+ const fallbackResult = await generateText({
224
+ model: m.aiModel,
225
+ system: "You are a JSON-only scorer. Output ONLY valid JSON matching the requested schema. No markdown fences, no explanations, no commentary \u2014 just the raw JSON object.",
226
+ prompt: `Score these dimensions based on the analysis below. Return ONLY a JSON object, no other text.
227
+
228
+ DIMENSIONS: ${dimNames}
229
+
230
+ ANALYSIS:
231
+ ${analysis.slice(0, 8000)}
232
+
233
+ Return this exact JSON structure (nothing else):
234
+ {"scores":[{"dimension":"<name>","score":<1-5>,"reasoning":"<brief>"}],"summary":"<overall summary>"}`,
235
+ ...(providerOptions ? { providerOptions: providerOptions } : {}),
236
+ });
237
+ const parsed = tryParseReviewJSON(fallbackResult.text);
238
+ if (parsed)
239
+ return { payload: parsed, attemptErrors };
240
+ attemptErrors.push(`Fallback JSON parse failed. Text preview: ${fallbackResult.text.slice(0, 200)}`);
241
+ }
242
+ catch (err) {
243
+ attemptErrors.push(`Fallback: ${err instanceof Error ? err.message : String(err)}`);
244
+ }
245
+ onProgress?.(`All scoring strategies failed (provider: ${m.provider}):\n${attemptErrors.join("\n")}`);
246
+ return { payload: null, attemptErrors };
247
+ }
248
+ /** Try to extract a ReviewPayload from free-text JSON output, validated with Zod. */
249
+ function tryParseReviewJSON(output) {
250
+ if (!output || !output.trim())
251
+ return null;
252
+ let text = output.trim();
253
+ // Strip markdown code fences (```json ... ``` or ``` ... ```)
254
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
255
+ if (fenceMatch)
256
+ text = fenceMatch[1].trim();
257
+ // Find the outermost JSON object
258
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
259
+ if (!jsonMatch)
260
+ return null;
261
+ let jsonStr = jsonMatch[0];
262
+ // Common LLM JSON quirks: trailing commas, single quotes, comments
263
+ jsonStr = jsonStr.replace(/,\s*([}\]])/g, "$1"); // trailing commas
264
+ jsonStr = jsonStr.replace(/\/\/[^\n]*/g, ""); // single-line comments
265
+ jsonStr = jsonStr.replace(/\/\*[\s\S]*?\*\//g, ""); // block comments
266
+ // Try parsing and validating with Zod
267
+ const tryParse = (s) => {
268
+ try {
269
+ const parsed = JSON.parse(s);
270
+ const result = validateReviewPayload(parsed);
271
+ if (result.success)
272
+ return result.data;
273
+ }
274
+ catch { /* fall through */ }
275
+ return null;
276
+ };
277
+ // Attempt 1: direct parse
278
+ const direct = tryParse(jsonStr);
279
+ if (direct)
280
+ return direct;
281
+ // Attempt 2: replace single quotes with double quotes (common LLM mistake)
282
+ if (!jsonStr.includes('"') && jsonStr.includes("'")) {
283
+ const doubleQuoted = jsonStr.replace(/'/g, '"');
284
+ const sq = tryParse(doubleQuoted);
285
+ if (sq)
286
+ return sq;
287
+ }
288
+ return null;
289
+ }
290
+ // ── Combined Single Review (Phase 1 + Phase 2) ────────────────────────
291
+ async function runSingleReview(explorationPrompt, rubricSection, dimNames, cwd, model, onProgress, reasoning, skipExploration) {
292
+ let analysis;
293
+ let filesRead = [];
294
+ let explorationMessages = [];
295
+ if (skipExploration) {
296
+ // Output-based review: the prompt already contains all the evidence.
297
+ // Run a single LLM call to produce the analysis from the provided context.
298
+ onProgress?.("Analyzing execution evidence (no file exploration needed)...");
299
+ const m = resolveModel(model);
300
+ const providerOptions = mapReasoningToProviderOptions(m.provider, reasoning, m.maxTokens);
301
+ const result = await generateText({
302
+ model: m.aiModel,
303
+ system: "You are a thorough reviewer. Analyze the provided execution evidence and write a detailed assessment for each evaluation dimension. Do NOT attempt to output scores as text \u2014 you will be given a dedicated scoring step after your analysis.",
304
+ prompt: explorationPrompt,
305
+ ...(providerOptions ? { providerOptions: providerOptions } : {}),
306
+ });
307
+ analysis = result.text.trim() || "The reviewer analyzed the execution evidence but produced no written analysis.";
308
+ explorationMessages = [{
309
+ role: "user",
310
+ content: explorationPrompt,
311
+ timestamp: Date.now(),
312
+ }, {
313
+ role: "assistant",
314
+ content: result.text,
315
+ timestamp: Date.now(),
316
+ }];
317
+ }
318
+ else {
319
+ // File-based review: explore the codebase with tools
320
+ onProgress?.("Phase 1: Exploring codebase...");
321
+ const result = await runExploration(explorationPrompt, cwd, model, onProgress, reasoning);
322
+ analysis = result.analysis;
323
+ filesRead = result.filesRead;
324
+ explorationMessages = result.messages;
325
+ onProgress?.(`Exploration complete \u2014 read ${filesRead.length} files, ${analysis.length} chars of analysis.`);
326
+ }
327
+ // Phase 2: Score
328
+ onProgress?.("Phase 2: Producing structured scores...");
329
+ const { payload, attemptErrors } = await runScoring(analysis, rubricSection, dimNames, model, onProgress, reasoning);
330
+ if (payload) {
331
+ onProgress?.(`Scoring complete \u2014 ${payload.scores.length} dimensions scored.`);
332
+ // Attach exploration trace and scoring attempt errors to the payload
333
+ payload.exploration = { analysis, filesRead, messages: explorationMessages };
334
+ if (attemptErrors.length > 0)
335
+ payload.scoringAttemptErrors = attemptErrors;
336
+ return payload;
337
+ }
338
+ else {
339
+ onProgress?.("Scoring failed \u2014 reviewer could not produce structured scores.");
340
+ return null;
341
+ }
342
+ }
343
+ // ── Single Review with Retry ───────────────────────────────────────────
344
+ async function runSingleReviewWithRetry(explorationPrompt, rubricSection, dimNames, cwd, model, onProgress, reasoning, skipExploration) {
345
+ try {
346
+ return await withRetry(async () => {
347
+ const result = await runSingleReview(explorationPrompt, rubricSection, dimNames, cwd, model, onProgress, reasoning, skipExploration);
348
+ if (!result)
349
+ throw new Error("Reviewer produced no structured result after Phase 1 + Phase 2");
350
+ return result;
351
+ }, { maxRetries: 1, initialDelayMs: 2000, checkTransient: false });
352
+ }
353
+ catch (err) {
354
+ onProgress?.(`Reviewer failed: ${err instanceof Error ? err.message : String(err)}`);
355
+ return null;
356
+ }
357
+ }
358
+ // ── Context Classification ─────────────────────────────────────────────
359
+ /**
360
+ * Determine if this review involves file changes that need filesystem exploration,
361
+ * or if the execution evidence (stdout, transcript, outcomes) is sufficient.
362
+ */
363
+ function hasFileChanges(context) {
364
+ if (!context)
365
+ return true; // conservative: explore when unknown
366
+ return (context.filesCreated?.length ?? 0) > 0 || (context.filesEdited?.length ?? 0) > 0;
367
+ }
368
+ // ── Prompt Builder (Phase 1 only — no submit_review instructions) ──────
369
+ function buildContextSection(context) {
370
+ if (!context)
371
+ return "";
372
+ const parts = [
373
+ `TASK CONTEXT:`,
374
+ `Title: ${context.taskTitle}`,
375
+ `Description: ${context.taskDescription}`,
376
+ ];
377
+ if (context.exitCode !== undefined)
378
+ parts.push(`Exit code: ${context.exitCode}`);
379
+ if (context.duration !== undefined)
380
+ parts.push(`Duration: ${Math.round(context.duration / 1000)}s`);
381
+ if (context.toolCalls !== undefined)
382
+ parts.push(`Total tool calls: ${context.toolCalls}`);
383
+ if (context.toolsSummary)
384
+ parts.push(`Tools used: ${context.toolsSummary}`);
385
+ if (context.filesCreated?.length)
386
+ parts.push(`\nFiles created by agent: ${context.filesCreated.join(", ")}`);
387
+ if (context.filesEdited?.length)
388
+ parts.push(`Files edited by agent: ${context.filesEdited.join(", ")}`);
389
+ if (context.outcomes?.length) {
390
+ parts.push(`\nREGISTERED OUTCOMES:`);
391
+ for (const o of context.outcomes) {
392
+ let desc = `- [${o.type}] ${o.label}`;
393
+ if (o.path)
394
+ desc += ` (${o.path})`;
395
+ if (o.url)
396
+ desc += ` \u2192 ${o.url}`;
397
+ if (o.text)
398
+ desc += `: ${o.text.slice(0, 300)}${o.text.length > 300 ? "..." : ""}`;
399
+ parts.push(desc);
400
+ }
401
+ }
402
+ if (context.executionSummary) {
403
+ parts.push(`\n${context.executionSummary}`);
404
+ }
405
+ if (context.agentOutput) {
406
+ parts.push(`\nAGENT FINAL OUTPUT:\n${context.agentOutput.slice(-3000)}`);
407
+ }
408
+ if (context.agentStderr) {
409
+ parts.push(`\nAGENT STDERR:\n${context.agentStderr.slice(-1000)}`);
410
+ }
411
+ return parts.join("\n");
412
+ }
413
+ function buildExplorationPrompt(criteria, rubricSection, dimNames, context) {
414
+ const contextSection = buildContextSection(context);
415
+ return `You are a senior code reviewer performing a G-Eval evaluation.
416
+ Your task is to EXPLORE the codebase and build a detailed analysis for each evaluation dimension.
417
+
418
+ ACCEPTANCE CRITERIA:
419
+ ${criteria}
420
+
421
+ ${contextSection}
422
+
423
+ EVALUATION DIMENSIONS:
424
+ ${rubricSection}
425
+
426
+ INSTRUCTIONS:
427
+ 1. Use read_file, glob, and grep tools to explore the codebase and find relevant files.
428
+ 2. Start by examining the files listed above (created/edited by the agent) \u2014 they are the primary evidence.
429
+ 3. Read the code carefully and understand what it does relative to the acceptance criteria.
430
+ 4. For EACH dimension (${dimNames}), write your analysis noting:
431
+ - Specific file:line references as evidence
432
+ - How the code performs on this dimension
433
+ - What score (1-5) you would give based on the rubric
434
+ 5. Be thorough \u2014 read all relevant files before concluding.
435
+
436
+ OUTPUT:
437
+ Write your analysis as free text. Include specific file:line references for each dimension.
438
+ You will be asked to submit structured scores in a separate step after exploration.`;
439
+ }
440
+ /**
441
+ * Build a prompt for output-based review (no file exploration needed).
442
+ * Used when the agent worked via external tools, APIs, or text output only.
443
+ */
444
+ function buildOutputBasedReviewPrompt(criteria, rubricSection, dimNames, context) {
445
+ const contextSection = buildContextSection(context);
446
+ return `You are a senior reviewer performing a G-Eval evaluation.
447
+ The agent completed a task that did NOT produce file changes on disk. Instead, the agent's work
448
+ is evidenced by its execution timeline, tool usage, registered outcomes, and text output below.
449
+
450
+ ACCEPTANCE CRITERIA:
451
+ ${criteria}
452
+
453
+ ${contextSection}
454
+
455
+ EVALUATION DIMENSIONS:
456
+ ${rubricSection}
457
+
458
+ INSTRUCTIONS:
459
+ 1. Carefully review the execution timeline, agent output, and registered outcomes above.
460
+ 2. Assess whether the agent correctly completed the task based on the acceptance criteria.
461
+ 3. For EACH dimension (${dimNames}), write your analysis noting:
462
+ - Specific evidence from the execution timeline or agent output
463
+ - How the agent's work performs on this dimension
464
+ - What score (1-5) you would give based on the rubric
465
+ 4. Note: the agent may have used external tools (email, APIs, web requests, etc.) \u2014 the tool call
466
+ results in the timeline ARE the evidence of work. Do NOT penalize for lack of file changes.
467
+
468
+ OUTPUT:
469
+ Write your analysis as free text. Reference specific timeline entries or output sections as evidence.
470
+ You will be asked to submit structured scores in a separate step.`;
471
+ }
472
+ // ── CheckResult Builder ────────────────────────────────────────────────
473
+ function buildCheckResult(parsed, dimensions, threshold, individualReviews) {
474
+ const dimScores = parsed.scores.map(s => {
475
+ const dim = dimensions.find(d => d.name === s.dimension);
476
+ return {
477
+ dimension: s.dimension,
478
+ score: Math.max(1, Math.min(5, Math.round(s.score))),
479
+ reasoning: s.reasoning,
480
+ weight: dim?.weight ?? (1 / dimensions.length),
481
+ evidence: s.evidence,
482
+ };
483
+ });
484
+ const globalScore = computeWeightedScore(dimScores);
485
+ const passed = globalScore >= threshold;
486
+ const scoreLines = dimScores.map(s => ` ${s.dimension}: ${s.score}/5 (weight: ${s.weight}) \u2014 ${s.reasoning}`).join("\n");
487
+ const details = `Global score: ${globalScore.toFixed(2)}/5 (threshold: ${threshold})\n\n${scoreLines}\n\nSummary: ${parsed.summary}`;
488
+ const msg = passed
489
+ ? `Score ${globalScore.toFixed(1)}/5 \u2014 ${parsed.summary.slice(0, 100)}`
490
+ : `Score ${globalScore.toFixed(1)}/5 (below ${threshold}) \u2014 ${parsed.summary.slice(0, 100)}`;
491
+ // Build individual reviewer results for transparency
492
+ const reviewers = individualReviews?.map((review, i) => {
493
+ const reviewDimScores = review.scores.map(s => {
494
+ const dim = dimensions.find(d => d.name === s.dimension);
495
+ return {
496
+ dimension: s.dimension,
497
+ score: Math.max(1, Math.min(5, Math.round(s.score))),
498
+ reasoning: s.reasoning,
499
+ weight: dim?.weight ?? (1 / dimensions.length),
500
+ evidence: s.evidence,
501
+ };
502
+ });
503
+ return {
504
+ index: i + 1,
505
+ scores: review.scores,
506
+ summary: review.summary,
507
+ globalScore: Math.round(computeWeightedScore(reviewDimScores) * 100) / 100,
508
+ exploration: review.exploration,
509
+ scoringAttemptErrors: review.scoringAttemptErrors,
510
+ };
511
+ });
512
+ return {
513
+ type: "llm_review",
514
+ passed,
515
+ message: msg,
516
+ details,
517
+ scores: dimScores,
518
+ globalScore: Math.round(globalScore * 100) / 100,
519
+ reviewers,
520
+ };
521
+ }
522
+ // ── Dynamic Dimension Generation ───────────────────────────────────────
523
+ /**
524
+ * Ask the LLM to generate 3-4 evaluation dimensions tailored to the specific task.
525
+ * Falls back to DEFAULT_DIMENSIONS if the LLM call fails or returns invalid data.
526
+ */
527
+ async function generateDimensions(context, model, onProgress) {
528
+ if (!context?.taskTitle)
529
+ return DEFAULT_DIMENSIONS;
530
+ onProgress?.("Generating task-specific evaluation dimensions...");
531
+ try {
532
+ const m = resolveModel(model);
533
+ const result = await generateText({
534
+ model: m.aiModel,
535
+ system: "You are an evaluation expert. Output ONLY a valid JSON array. No markdown fences, no explanations.",
536
+ prompt: `Generate 3-4 evaluation dimensions for assessing the following task. Each dimension must be specific and relevant to THIS task \u2014 do NOT use generic coding metrics unless the task is about writing code.
537
+
538
+ TASK TITLE: ${context.taskTitle}
539
+ TASK DESCRIPTION: ${context.taskDescription}
540
+ ${context.filesCreated?.length ? `FILES CREATED: ${context.filesCreated.join(", ")}` : ""}
541
+
542
+ Return ONLY a JSON array (no markdown fences, no explanation). Each element must have:
543
+ - "name": snake_case identifier (e.g. "visual_quality", "data_accuracy")
544
+ - "description": one sentence explaining what this dimension measures
545
+ - "weight": number 0.15-0.40 (all weights must sum to 1.0)
546
+ - "rubric": object with keys 1-5, each a one-sentence description for that score level
547
+
548
+ Example for an image generation task:
549
+ [{"name":"visual_quality","description":"Is the generated image sharp, well-composed, and visually appealing?","weight":0.30,"rubric":{"1":"Unusable \u2014 heavily distorted or corrupted","2":"Poor quality \u2014 major visual artifacts","3":"Acceptable \u2014 meets basic standards","4":"Good \u2014 clean and well-composed","5":"Excellent \u2014 professional-grade visual quality"}},{"name":"prompt_adherence","description":"Does the image match the requested subject, style, and details?","weight":0.35,"rubric":{"1":"Completely ignores the prompt","2":"Loosely related but misses key elements","3":"Partially matches \u2014 some elements present","4":"Good match \u2014 most details captured","5":"Perfect match \u2014 every detail faithfully rendered"}},{"name":"completeness","description":"Are all requested deliverables produced in the correct formats?","weight":0.35,"rubric":{"1":"No deliverables produced","2":"Partial output \u2014 missing files or formats","3":"Core deliverables present but extras missing","4":"Nearly complete \u2014 minor omissions","5":"All deliverables produced correctly"}}]`,
550
+ });
551
+ let text = result.text.trim();
552
+ // Strip markdown fences if present
553
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
554
+ if (fenceMatch)
555
+ text = fenceMatch[1].trim();
556
+ const parsed = JSON.parse(text);
557
+ if (!Array.isArray(parsed) || parsed.length < 2 || parsed.length > 6) {
558
+ throw new Error(`Expected 2-6 dimensions, got ${Array.isArray(parsed) ? parsed.length : typeof parsed}`);
559
+ }
560
+ // Validate and normalize
561
+ const dimensions = [];
562
+ let totalWeight = 0;
563
+ for (const d of parsed) {
564
+ if (!d.name || !d.description || typeof d.weight !== "number")
565
+ continue;
566
+ const rubric = {};
567
+ if (d.rubric && typeof d.rubric === "object") {
568
+ for (const [k, v] of Object.entries(d.rubric)) {
569
+ const num = Number(k);
570
+ if (num >= 1 && num <= 5 && typeof v === "string")
571
+ rubric[num] = v;
572
+ }
573
+ }
574
+ dimensions.push({
575
+ name: String(d.name),
576
+ description: String(d.description),
577
+ weight: d.weight,
578
+ ...(Object.keys(rubric).length > 0 ? { rubric } : {}),
579
+ });
580
+ totalWeight += d.weight;
581
+ }
582
+ if (dimensions.length < 2) {
583
+ throw new Error(`Only ${dimensions.length} valid dimensions parsed`);
584
+ }
585
+ // Normalize weights to sum to 1.0
586
+ if (Math.abs(totalWeight - 1.0) > 0.01) {
587
+ for (const dim of dimensions) {
588
+ dim.weight = dim.weight / totalWeight;
589
+ }
590
+ }
591
+ onProgress?.(`Generated ${dimensions.length} task-specific dimensions: ${dimensions.map(d => d.name).join(", ")}`);
592
+ return dimensions;
593
+ }
594
+ catch (err) {
595
+ onProgress?.(`Dimension generation failed (${err instanceof Error ? err.message : String(err)}), using defaults.`);
596
+ return DEFAULT_DIMENSIONS;
597
+ }
598
+ }
599
+ // ── Main Entry Point ───────────────────────────────────────────────────
600
+ /**
601
+ * Run a G-Eval LLM-as-Judge review (2-phase architecture).
602
+ *
603
+ * Phase 1: Each reviewer explores the codebase with tools (read_file, glob, grep).
604
+ * Phase 2: Structured scoring via Output.object() extracts validated scores.
605
+ *
606
+ * Runs 3 independent reviewers in parallel (multi-evaluator consensus).
607
+ * Falls back to single-reviewer if <2 succeed.
608
+ */
609
+ export async function runLLMReview(expectation, cwd, onProgress, context, reasoning) {
610
+ const criteria = expectation.criteria || "The work should be correct, well-structured, and meet the task requirements.";
611
+ const threshold = expectation.threshold ?? 3.0;
612
+ const reviewModel = process.env.POLPO_JUDGE_MODEL || process.env.POLPO_MODEL || "anthropic/claude-sonnet-4.5";
613
+ // Generate task-specific dimensions via LLM when not explicitly provided
614
+ const dimensions = expectation.dimensions ?? await generateDimensions(context, reviewModel, onProgress);
615
+ const dimNames = dimensions.map((d) => d.name).join(", ");
616
+ const rubricSection = buildRubricSection(dimensions);
617
+ // Decide review mode: file-based (with exploration) or output-based (no exploration)
618
+ const needsExploration = hasFileChanges(context);
619
+ const skipExploration = !needsExploration;
620
+ const reviewPrompt = needsExploration
621
+ ? buildExplorationPrompt(criteria, rubricSection, dimNames, context)
622
+ : buildOutputBasedReviewPrompt(criteria, rubricSection, dimNames, context);
623
+ // Judge reasoning: explicit param > POLPO_JUDGE_REASONING env var > undefined
624
+ const judgeReasoning = reasoning ?? process.env.POLPO_JUDGE_REASONING;
625
+ const modeLabel = skipExploration ? "output-based" : "file-exploration";
626
+ onProgress?.(`Starting 3 independent review agents (${modeLabel} \u2192 score)...`);
627
+ // Stagger reviewers by 1s to reduce rate-limit collisions on same provider
628
+ const delay = (ms) => new Promise(r => setTimeout(r, ms));
629
+ const settled = await Promise.allSettled([
630
+ runSingleReviewWithRetry(reviewPrompt, rubricSection, dimNames, cwd, reviewModel, onProgress, judgeReasoning, skipExploration),
631
+ delay(1000).then(() => runSingleReviewWithRetry(reviewPrompt, rubricSection, dimNames, cwd, reviewModel, onProgress, judgeReasoning, skipExploration)),
632
+ delay(2000).then(() => runSingleReviewWithRetry(reviewPrompt, rubricSection, dimNames, cwd, reviewModel, onProgress, judgeReasoning, skipExploration)),
633
+ ]);
634
+ const successfulReviews = [];
635
+ const failures = [];
636
+ for (let i = 0; i < settled.length; i++) {
637
+ const result = settled[i];
638
+ if (result.status === "fulfilled" && result.value) {
639
+ successfulReviews.push(result.value);
640
+ }
641
+ else {
642
+ const reason = result.status === "rejected"
643
+ ? (result.reason instanceof Error ? result.reason.message : String(result.reason))
644
+ : "Reviewer returned null \u2014 scoring failed";
645
+ failures.push(reason);
646
+ onProgress?.(`Reviewer ${i + 1}/3 failed: ${reason}`);
647
+ }
648
+ }
649
+ if (successfulReviews.length >= 2) {
650
+ onProgress?.(`Computing consensus from ${successfulReviews.length} reviewers...`);
651
+ const consensus = computeMedianScores(successfulReviews, dimensions);
652
+ return buildCheckResult(consensus, dimensions, threshold, successfulReviews);
653
+ }
654
+ if (successfulReviews.length === 1) {
655
+ onProgress?.("Only 1 reviewer succeeded, using single review...");
656
+ return buildCheckResult(successfulReviews[0], dimensions, threshold, successfulReviews);
657
+ }
658
+ const failureDetail = failures.length > 0
659
+ ? `\n\nFailure reasons:\n${failures.map((f, i) => ` Reviewer ${i + 1}: ${f}`).join("\n")}`
660
+ : "";
661
+ const modelInfo = reviewModel ? ` (judge model: ${reviewModel})` : " (no explicit judge model \u2014 using default)";
662
+ return {
663
+ type: "llm_review",
664
+ passed: false,
665
+ message: `Review failed \u2014 all evaluators failed to produce structured scores${modelInfo}`,
666
+ details: `All 3 reviewers failed after structured output + fallback strategies.${modelInfo}\n\nThis usually means: (1) the judge model doesn't support structured output well, (2) API auth/rate-limit errors, or (3) the model can't produce valid JSON.\n\nTry: set POLPO_JUDGE_MODEL to a capable model (e.g. anthropic:claude-sonnet-4.5, openai:gpt-4o), check API keys, or reduce concurrent tasks.${failureDetail}`,
667
+ };
668
+ }
669
+ //# sourceMappingURL=llm-review.js.map