@almightygpt/core 0.2.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.
Files changed (126) hide show
  1. package/dist/adapters/claude.d.ts +31 -0
  2. package/dist/adapters/claude.d.ts.map +1 -0
  3. package/dist/adapters/claude.js +90 -0
  4. package/dist/adapters/claude.js.map +1 -0
  5. package/dist/adapters/gemini.d.ts +42 -0
  6. package/dist/adapters/gemini.d.ts.map +1 -0
  7. package/dist/adapters/gemini.js +133 -0
  8. package/dist/adapters/gemini.js.map +1 -0
  9. package/dist/adapters/index.d.ts +16 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +15 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/mock.d.ts +23 -0
  14. package/dist/adapters/mock.d.ts.map +1 -0
  15. package/dist/adapters/mock.js +107 -0
  16. package/dist/adapters/mock.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +38 -0
  18. package/dist/adapters/openai.d.ts.map +1 -0
  19. package/dist/adapters/openai.js +105 -0
  20. package/dist/adapters/openai.js.map +1 -0
  21. package/dist/adapters/types.d.ts +65 -0
  22. package/dist/adapters/types.d.ts.map +1 -0
  23. package/dist/adapters/types.js +26 -0
  24. package/dist/adapters/types.js.map +1 -0
  25. package/dist/config/load.d.ts +15 -0
  26. package/dist/config/load.d.ts.map +1 -0
  27. package/dist/config/load.js +46 -0
  28. package/dist/config/load.js.map +1 -0
  29. package/dist/config/schema.d.ts +260 -0
  30. package/dist/config/schema.d.ts.map +1 -0
  31. package/dist/config/schema.js +58 -0
  32. package/dist/config/schema.js.map +1 -0
  33. package/dist/context/manifest.d.ts +58 -0
  34. package/dist/context/manifest.d.ts.map +1 -0
  35. package/dist/context/manifest.js +49 -0
  36. package/dist/context/manifest.js.map +1 -0
  37. package/dist/context/redact.d.ts +26 -0
  38. package/dist/context/redact.d.ts.map +1 -0
  39. package/dist/context/redact.js +67 -0
  40. package/dist/context/redact.js.map +1 -0
  41. package/dist/git/status.d.ts +48 -0
  42. package/dist/git/status.d.ts.map +1 -0
  43. package/dist/git/status.js +79 -0
  44. package/dist/git/status.js.map +1 -0
  45. package/dist/index.d.ts +33 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +38 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/review/budget.d.ts +46 -0
  50. package/dist/review/budget.d.ts.map +1 -0
  51. package/dist/review/budget.js +83 -0
  52. package/dist/review/budget.js.map +1 -0
  53. package/dist/review/diff.d.ts +21 -0
  54. package/dist/review/diff.d.ts.map +1 -0
  55. package/dist/review/diff.js +55 -0
  56. package/dist/review/diff.js.map +1 -0
  57. package/dist/review/events.d.ts +76 -0
  58. package/dist/review/events.d.ts.map +1 -0
  59. package/dist/review/events.js +13 -0
  60. package/dist/review/events.js.map +1 -0
  61. package/dist/review/memory.d.ts +23 -0
  62. package/dist/review/memory.d.ts.map +1 -0
  63. package/dist/review/memory.js +42 -0
  64. package/dist/review/memory.js.map +1 -0
  65. package/dist/review/prompts.d.ts +34 -0
  66. package/dist/review/prompts.d.ts.map +1 -0
  67. package/dist/review/prompts.js +174 -0
  68. package/dist/review/prompts.js.map +1 -0
  69. package/dist/review/run-diff-review.d.ts +52 -0
  70. package/dist/review/run-diff-review.d.ts.map +1 -0
  71. package/dist/review/run-diff-review.js +258 -0
  72. package/dist/review/run-diff-review.js.map +1 -0
  73. package/dist/review/run-worker-reviewer.d.ts +72 -0
  74. package/dist/review/run-worker-reviewer.d.ts.map +1 -0
  75. package/dist/review/run-worker-reviewer.js +407 -0
  76. package/dist/review/run-worker-reviewer.js.map +1 -0
  77. package/dist/review/write.d.ts +44 -0
  78. package/dist/review/write.d.ts.map +1 -0
  79. package/dist/review/write.js +152 -0
  80. package/dist/review/write.js.map +1 -0
  81. package/dist/runs/decide.d.ts +45 -0
  82. package/dist/runs/decide.d.ts.map +1 -0
  83. package/dist/runs/decide.js +93 -0
  84. package/dist/runs/decide.js.map +1 -0
  85. package/dist/runs/folder.d.ts +42 -0
  86. package/dist/runs/folder.d.ts.map +1 -0
  87. package/dist/runs/folder.js +82 -0
  88. package/dist/runs/folder.js.map +1 -0
  89. package/dist/runs/list.d.ts +58 -0
  90. package/dist/runs/list.d.ts.map +1 -0
  91. package/dist/runs/list.js +117 -0
  92. package/dist/runs/list.js.map +1 -0
  93. package/dist/runs/types.d.ts +96 -0
  94. package/dist/runs/types.d.ts.map +1 -0
  95. package/dist/runs/types.js +13 -0
  96. package/dist/runs/types.js.map +1 -0
  97. package/dist/templates/install.d.ts +49 -0
  98. package/dist/templates/install.d.ts.map +1 -0
  99. package/dist/templates/install.js +154 -0
  100. package/dist/templates/install.js.map +1 -0
  101. package/package.json +34 -0
  102. package/src/adapters/claude.ts +133 -0
  103. package/src/adapters/gemini.ts +183 -0
  104. package/src/adapters/index.ts +21 -0
  105. package/src/adapters/mock.ts +125 -0
  106. package/src/adapters/openai.ts +150 -0
  107. package/src/adapters/types.ts +73 -0
  108. package/src/config/load.ts +61 -0
  109. package/src/config/schema.ts +64 -0
  110. package/src/context/manifest.ts +94 -0
  111. package/src/context/redact.ts +93 -0
  112. package/src/git/status.ts +108 -0
  113. package/src/index.ts +127 -0
  114. package/src/review/budget.ts +116 -0
  115. package/src/review/diff.ts +85 -0
  116. package/src/review/events.ts +86 -0
  117. package/src/review/memory.ts +57 -0
  118. package/src/review/prompts.ts +208 -0
  119. package/src/review/run-diff-review.ts +353 -0
  120. package/src/review/run-worker-reviewer.ts +528 -0
  121. package/src/review/write.ts +208 -0
  122. package/src/runs/decide.ts +153 -0
  123. package/src/runs/folder.ts +137 -0
  124. package/src/runs/list.ts +152 -0
  125. package/src/runs/types.ts +98 -0
  126. package/src/templates/install.ts +198 -0
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Worker / Reviewer two-role review pipeline.
3
+ *
4
+ * This is the headline AlmightyGPT flow: one AI does the work, a different
5
+ * AI critiques it, and the human gets a single artifact with both passes.
6
+ *
7
+ * Pipeline:
8
+ * 1. Load config + create run folder (type: review-worker-reviewer).
9
+ * 2. Resolve Worker adapter (--worker or config.defaults.worker).
10
+ * 3. Resolve Reviewer adapter (--reviewer or config.defaults.reviewer).
11
+ * Same vendor on both sides is allowed but discouraged via a warning.
12
+ * 4. Collect git diff (+ optional range).
13
+ * 5. Redact secrets.
14
+ * 6. Build context manifest, write input.md + context-manifest.json.
15
+ * 7. Pre-flight budget check.
16
+ * 8. Worker stage: assemble Worker memory, run, persist output.
17
+ * 9. Reviewer stage: assemble Reviewer memory, give it the Worker output
18
+ * and the diff, run, persist output.
19
+ * 10. Shallow-review detection on the Reviewer's response.
20
+ * 11. Write the human review file (Worker plan summary lives inside the
21
+ * Reviewer's response per the prompt's required-sections list).
22
+ * 12. Write run.json with both metrics + totals.
23
+ *
24
+ * Events are emitted throughout via the optional onEvent callback so the
25
+ * CLI / future extension can drive progress UIs without polling.
26
+ */
27
+
28
+ import { AdapterError, type Adapter } from "../adapters/types.js";
29
+ import { MockAdapter } from "../adapters/mock.js";
30
+ import { OpenAIAdapter } from "../adapters/openai.js";
31
+ import { ClaudeAdapter } from "../adapters/claude.js";
32
+ import { GeminiAdapter } from "../adapters/gemini.js";
33
+ import { loadConfig } from "../config/load.js";
34
+ import { redactSecrets } from "../context/redact.js";
35
+ import {
36
+ buildContextManifest,
37
+ writeContextManifest,
38
+ } from "../context/manifest.js";
39
+ import {
40
+ collectGitContext,
41
+ createRunFolder,
42
+ writeAgentOutput,
43
+ writeRunInput,
44
+ writeRunMetadata,
45
+ } from "../runs/folder.js";
46
+ import type { AgentMetrics, RunMetadata } from "../runs/types.js";
47
+ import { collectGitDiff } from "./diff.js";
48
+ import { assembleMemory } from "./memory.js";
49
+ import {
50
+ buildReviewerSystemFraming,
51
+ buildReviewerOfWorkerUserMessage,
52
+ buildWorkerSystemFraming,
53
+ buildWorkerUserMessage,
54
+ } from "./prompts.js";
55
+ import { writeHumanReviewFile } from "./write.js";
56
+ import { BudgetTracker, BudgetExceededError } from "./budget.js";
57
+ import type { ReviewEventHandler } from "./events.js";
58
+
59
+ export interface WorkerReviewerOptions {
60
+ repoRoot: string;
61
+ topic: string;
62
+ /** Worker agent name. Falls back to config.defaults.worker. */
63
+ worker?: string;
64
+ /** Reviewer agent name. Falls back to config.defaults.reviewer. */
65
+ reviewer?: string;
66
+ /** Optional git range like "HEAD~1..HEAD". */
67
+ range?: string;
68
+ /** Bypass the git status safety check / refuse-overwrite check. */
69
+ force?: boolean;
70
+ /** Optional event stream subscriber. */
71
+ onEvent?: ReviewEventHandler;
72
+ }
73
+
74
+ export interface WorkerReviewerResult {
75
+ reviewPath: string;
76
+ reviewBytes: number;
77
+ runId: string;
78
+ runFolder: string;
79
+ worker: { name: string; provider: string; model: string };
80
+ reviewer: { name: string; provider: string; model: string };
81
+ metrics: AgentMetrics[];
82
+ totals: {
83
+ tokensIn: number;
84
+ tokensOut: number;
85
+ costUsd: number;
86
+ latencyMs: number;
87
+ };
88
+ filesReviewed: string[];
89
+ redactionsTotal: number;
90
+ shallowWarning?: string;
91
+ sameProviderWarning?: string;
92
+ memoryMissing: string[];
93
+ }
94
+
95
+ export async function runWorkerReviewerReview(
96
+ opts: WorkerReviewerOptions,
97
+ ): Promise<WorkerReviewerResult> {
98
+ const emit = opts.onEvent ?? (() => {});
99
+ const config = await loadConfig(opts.repoRoot);
100
+
101
+ const workerName = opts.worker ?? config.defaults.worker;
102
+ const reviewerName = opts.reviewer ?? config.defaults.reviewer;
103
+ if (!workerName) {
104
+ throw new Error(
105
+ "No worker specified. Pass --worker <name> or set defaults.worker in .almightygpt/config.yaml.",
106
+ );
107
+ }
108
+ if (!reviewerName) {
109
+ throw new Error(
110
+ "No reviewer specified. Pass --reviewer <name> or set defaults.reviewer in .almightygpt/config.yaml.",
111
+ );
112
+ }
113
+
114
+ const workerCfg = config.agents[workerName];
115
+ const reviewerCfg = config.agents[reviewerName];
116
+ if (!workerCfg) {
117
+ throw new Error(
118
+ `Worker "${workerName}" not found in .almightygpt/config.yaml agents map.`,
119
+ );
120
+ }
121
+ if (!reviewerCfg) {
122
+ throw new Error(
123
+ `Reviewer "${reviewerName}" not found in .almightygpt/config.yaml agents map.`,
124
+ );
125
+ }
126
+ if (!workerCfg.enabled) {
127
+ throw new Error(`Worker "${workerName}" is disabled in config.`);
128
+ }
129
+ if (!reviewerCfg.enabled) {
130
+ throw new Error(`Reviewer "${reviewerName}" is disabled in config.`);
131
+ }
132
+
133
+ const workerAdapter = makeAdapter(workerName, workerCfg.provider);
134
+ const reviewerAdapter = makeAdapter(reviewerName, reviewerCfg.provider);
135
+
136
+ if (!(await workerAdapter.isAvailable())) {
137
+ throw new AdapterError(
138
+ `Worker adapter "${workerName}" (${workerCfg.provider}) is not available. ` +
139
+ envHintFor(workerCfg.provider),
140
+ workerName,
141
+ );
142
+ }
143
+ if (!(await reviewerAdapter.isAvailable())) {
144
+ throw new AdapterError(
145
+ `Reviewer adapter "${reviewerName}" (${reviewerCfg.provider}) is not available. ` +
146
+ envHintFor(reviewerCfg.provider),
147
+ reviewerName,
148
+ );
149
+ }
150
+
151
+ const sameProviderWarning =
152
+ workerCfg.provider === reviewerCfg.provider
153
+ ? `Worker and Reviewer are both ${workerCfg.provider}. Cross-vendor pairing catches more issues.`
154
+ : undefined;
155
+
156
+ const runFolder = await createRunFolder({
157
+ repoRoot: opts.repoRoot,
158
+ runsDir: config.runsDir,
159
+ topic: opts.topic,
160
+ type: "review-worker-reviewer",
161
+ });
162
+
163
+ emit({
164
+ type: "run_started",
165
+ runId: runFolder.id,
166
+ runType: "review-worker-reviewer",
167
+ topic: opts.topic,
168
+ reviewsDir: config.reviewsDir,
169
+ runFolder: runFolder.relPath,
170
+ });
171
+
172
+ const git = await collectGitContext(opts.repoRoot);
173
+ const baseMeta: RunMetadata = {
174
+ id: runFolder.id,
175
+ type: "review-worker-reviewer",
176
+ createdAt: new Date().toISOString(),
177
+ workspacePath: opts.repoRoot,
178
+ topic: opts.topic,
179
+ git,
180
+ input: opts.range
181
+ ? { source: "diff-range", range: opts.range }
182
+ : { source: "diff" },
183
+ agents: Object.entries(config.agents).map(([name, a]) => ({
184
+ name,
185
+ role: a.role,
186
+ provider: a.provider,
187
+ enabled: a.enabled,
188
+ })),
189
+ adapterVersions: [
190
+ { adapter: workerAdapter.name, version: "0.0.0" },
191
+ { adapter: reviewerAdapter.name, version: "0.0.0" },
192
+ ],
193
+ status: "running",
194
+ metrics: [],
195
+ totals: { tokensIn: 0, tokensOut: 0, costUsd: 0, latencyMs: 0 },
196
+ budget: config.budget,
197
+ };
198
+ await writeRunMetadata(runFolder.absPath, baseMeta);
199
+
200
+ try {
201
+ const diffArgs: { range?: string } = {};
202
+ if (opts.range) diffArgs.range = opts.range;
203
+ const diffResult = await collectGitDiff(opts.repoRoot, diffArgs);
204
+ if (diffResult.empty && !opts.range) {
205
+ throw new Error(
206
+ "No uncommitted changes. Stage changes, pick a range with --range, or commit something to review.",
207
+ );
208
+ }
209
+
210
+ const redaction = config.security.redactSecrets
211
+ ? redactSecrets(diffResult.diff)
212
+ : { text: diffResult.diff, redactions: [], totalCount: 0 };
213
+ emit({
214
+ type: "redaction_complete",
215
+ totalCount: redaction.totalCount,
216
+ byKind: redaction.redactions,
217
+ });
218
+
219
+ const manifest = buildContextManifest({
220
+ inputSource: opts.range ? "diff-range" : "diff",
221
+ filesIncluded: diffResult.files.map((p) => ({ path: p, bytes: 0 })),
222
+ filesSkipped: [],
223
+ diffText: redaction.text,
224
+ redaction: {
225
+ enabled: config.security.redactSecrets,
226
+ totalCount: redaction.totalCount,
227
+ byKind: redaction.redactions,
228
+ },
229
+ });
230
+ const manifestRelPath = await writeContextManifest(
231
+ runFolder.absPath,
232
+ manifest,
233
+ );
234
+ await writeRunInput(runFolder.absPath, redaction.text);
235
+
236
+ const budget = new BudgetTracker(config.budget);
237
+
238
+ // ─── Worker stage ───────────────────────────────────────────────
239
+ const workerMemory = await assembleMemory(
240
+ opts.repoRoot,
241
+ workerCfg.memoryFile,
242
+ );
243
+ const workerSystem =
244
+ buildWorkerSystemFraming() + "\n\n" + workerMemory.text;
245
+ const workerUser = buildWorkerUserMessage({
246
+ topic: opts.topic,
247
+ diff: redaction.text,
248
+ files: diffResult.files,
249
+ });
250
+
251
+ budget.preflightCheck({
252
+ model: "gpt-4o",
253
+ estimatedTokensIn: Math.ceil(
254
+ (workerSystem.length + workerUser.length) / 4,
255
+ ),
256
+ maxOutputTokens: 4096,
257
+ });
258
+
259
+ emit({
260
+ type: "agent_started",
261
+ role: "worker",
262
+ agent: workerName,
263
+ provider: workerAdapter.provider,
264
+ });
265
+ const workerOut = await workerAdapter.execute({
266
+ role: "worker",
267
+ systemPrompt: workerSystem,
268
+ userMessage: workerUser,
269
+ });
270
+ budget.record({
271
+ tokensIn: workerOut.tokensIn,
272
+ tokensOut: workerOut.tokensOut,
273
+ costUsd: workerOut.costUsd,
274
+ });
275
+ await writeAgentOutput(runFolder.absPath, "worker", workerOut.content);
276
+ emit({
277
+ type: "agent_completed",
278
+ role: "worker",
279
+ agent: workerName,
280
+ provider: workerAdapter.provider,
281
+ model: workerOut.modelUsed,
282
+ outputPath: `${runFolder.relPath}/outputs/worker.md`,
283
+ tokensIn: workerOut.tokensIn,
284
+ tokensOut: workerOut.tokensOut,
285
+ costUsd: workerOut.costUsd,
286
+ latencyMs: workerOut.latencyMs,
287
+ });
288
+
289
+ // ─── Reviewer stage ─────────────────────────────────────────────
290
+ const reviewerMemory = await assembleMemory(
291
+ opts.repoRoot,
292
+ reviewerCfg.memoryFile,
293
+ );
294
+ const reviewerSystem =
295
+ buildReviewerSystemFraming() + "\n\n" + reviewerMemory.text;
296
+ const reviewerUser = buildReviewerOfWorkerUserMessage({
297
+ topic: opts.topic,
298
+ diff: redaction.text,
299
+ files: diffResult.files,
300
+ workerOutput: workerOut.content,
301
+ workerAgent: workerName,
302
+ workerProvider: workerAdapter.provider,
303
+ });
304
+
305
+ budget.preflightCheck({
306
+ model: "gpt-4o",
307
+ estimatedTokensIn: Math.ceil(
308
+ (reviewerSystem.length + reviewerUser.length) / 4,
309
+ ),
310
+ maxOutputTokens: 4096,
311
+ });
312
+
313
+ emit({
314
+ type: "agent_started",
315
+ role: "reviewer",
316
+ agent: reviewerName,
317
+ provider: reviewerAdapter.provider,
318
+ });
319
+ const reviewerOut = await reviewerAdapter.execute({
320
+ role: "reviewer",
321
+ systemPrompt: reviewerSystem,
322
+ userMessage: reviewerUser,
323
+ });
324
+ budget.record({
325
+ tokensIn: reviewerOut.tokensIn,
326
+ tokensOut: reviewerOut.tokensOut,
327
+ costUsd: reviewerOut.costUsd,
328
+ });
329
+ await writeAgentOutput(runFolder.absPath, "reviewer", reviewerOut.content);
330
+ emit({
331
+ type: "agent_completed",
332
+ role: "reviewer",
333
+ agent: reviewerName,
334
+ provider: reviewerAdapter.provider,
335
+ model: reviewerOut.modelUsed,
336
+ outputPath: `${runFolder.relPath}/outputs/reviewer.md`,
337
+ tokensIn: reviewerOut.tokensIn,
338
+ tokensOut: reviewerOut.tokensOut,
339
+ costUsd: reviewerOut.costUsd,
340
+ latencyMs: reviewerOut.latencyMs,
341
+ });
342
+
343
+ const shallowWarning = detectShallowReview(
344
+ reviewerOut.content,
345
+ config.review.requireConcreteWeaknesses,
346
+ );
347
+
348
+ // Write human review file using the Reviewer's response. The Worker
349
+ // plan summary is one of the required sections in the Reviewer's prompt,
350
+ // so it's already embedded in the response.
351
+ const writeOpts: Parameters<typeof writeHumanReviewFile>[0] = {
352
+ repoRoot: opts.repoRoot,
353
+ reviewsDir: config.reviewsDir,
354
+ topic: opts.topic,
355
+ reviewerName,
356
+ reviewerProvider: reviewerAdapter.provider,
357
+ modelUsed: reviewerOut.modelUsed,
358
+ body: reviewerOut.content,
359
+ metrics: {
360
+ tokensIn: workerOut.tokensIn + reviewerOut.tokensIn,
361
+ tokensOut: workerOut.tokensOut + reviewerOut.tokensOut,
362
+ costUsd: workerOut.costUsd + reviewerOut.costUsd,
363
+ latencyMs: workerOut.latencyMs + reviewerOut.latencyMs,
364
+ },
365
+ runFolder: runFolder.relPath,
366
+ };
367
+ if (shallowWarning) writeOpts.shallowWarning = shallowWarning;
368
+ if (opts.force) writeOpts.force = opts.force;
369
+ const written = await writeHumanReviewFile(writeOpts);
370
+
371
+ const reviewWrittenEvent: import("./events.js").ReviewWrittenEvent = {
372
+ type: "review_written",
373
+ reviewPath: written.path,
374
+ bytes: written.bytes,
375
+ };
376
+ if (shallowWarning) reviewWrittenEvent.shallowWarning = shallowWarning;
377
+ emit(reviewWrittenEvent);
378
+
379
+ const totals = {
380
+ tokensIn: workerOut.tokensIn + reviewerOut.tokensIn,
381
+ tokensOut: workerOut.tokensOut + reviewerOut.tokensOut,
382
+ costUsd: workerOut.costUsd + reviewerOut.costUsd,
383
+ latencyMs: workerOut.latencyMs + reviewerOut.latencyMs,
384
+ };
385
+
386
+ const metrics: AgentMetrics[] = [
387
+ {
388
+ agent: workerName,
389
+ role: "worker",
390
+ provider: workerAdapter.provider,
391
+ model: workerOut.modelUsed,
392
+ tokensIn: workerOut.tokensIn,
393
+ tokensOut: workerOut.tokensOut,
394
+ costUsd: workerOut.costUsd,
395
+ latencyMs: workerOut.latencyMs,
396
+ },
397
+ {
398
+ agent: reviewerName,
399
+ role: "reviewer",
400
+ provider: reviewerAdapter.provider,
401
+ model: reviewerOut.modelUsed,
402
+ tokensIn: reviewerOut.tokensIn,
403
+ tokensOut: reviewerOut.tokensOut,
404
+ costUsd: reviewerOut.costUsd,
405
+ latencyMs: reviewerOut.latencyMs,
406
+ },
407
+ ];
408
+
409
+ const finalMeta: RunMetadata = {
410
+ ...baseMeta,
411
+ finishedAt: new Date().toISOString(),
412
+ status: "completed",
413
+ contextManifestPath: manifestRelPath,
414
+ reviewPath: written.path,
415
+ metrics,
416
+ totals,
417
+ };
418
+ await writeRunMetadata(runFolder.absPath, finalMeta);
419
+
420
+ emit({
421
+ type: "run_completed",
422
+ runId: runFolder.id,
423
+ reviewPath: written.path,
424
+ runFolder: runFolder.relPath,
425
+ totals,
426
+ });
427
+
428
+ const result: WorkerReviewerResult = {
429
+ reviewPath: written.path,
430
+ reviewBytes: written.bytes,
431
+ runId: runFolder.id,
432
+ runFolder: runFolder.relPath,
433
+ worker: {
434
+ name: workerName,
435
+ provider: workerAdapter.provider,
436
+ model: workerOut.modelUsed,
437
+ },
438
+ reviewer: {
439
+ name: reviewerName,
440
+ provider: reviewerAdapter.provider,
441
+ model: reviewerOut.modelUsed,
442
+ },
443
+ metrics,
444
+ totals,
445
+ filesReviewed: diffResult.files,
446
+ redactionsTotal: redaction.totalCount,
447
+ memoryMissing: [
448
+ ...new Set([...workerMemory.missing, ...reviewerMemory.missing]),
449
+ ],
450
+ };
451
+ if (shallowWarning) result.shallowWarning = shallowWarning;
452
+ if (sameProviderWarning) result.sameProviderWarning = sameProviderWarning;
453
+ return result;
454
+ } catch (err) {
455
+ const failedMeta: RunMetadata = {
456
+ ...baseMeta,
457
+ finishedAt: new Date().toISOString(),
458
+ status: err instanceof BudgetExceededError ? "aborted_budget" : "failed",
459
+ error: {
460
+ name: err instanceof Error ? err.name : "Error",
461
+ message: err instanceof Error ? err.message : String(err),
462
+ },
463
+ };
464
+ await writeRunMetadata(runFolder.absPath, failedMeta);
465
+ emit({
466
+ type: "run_failed",
467
+ runId: runFolder.id,
468
+ error: {
469
+ name: err instanceof Error ? err.name : "Error",
470
+ message: err instanceof Error ? err.message : String(err),
471
+ },
472
+ });
473
+ throw err;
474
+ }
475
+ }
476
+
477
+ function makeAdapter(name: string, provider: string): Adapter {
478
+ switch (provider) {
479
+ case "openai":
480
+ return new OpenAIAdapter(name);
481
+ case "anthropic":
482
+ return new ClaudeAdapter(name);
483
+ case "google":
484
+ return new GeminiAdapter(name);
485
+ case "mock":
486
+ return new MockAdapter();
487
+ default:
488
+ throw new Error(
489
+ `Provider "${provider}" not supported. Use "openai", "anthropic", "google", or "mock".`,
490
+ );
491
+ }
492
+ }
493
+
494
+ function envHintFor(provider: string): string {
495
+ switch (provider) {
496
+ case "openai":
497
+ return "Export OPENAI_API_KEY in your environment.";
498
+ case "anthropic":
499
+ return "Export ANTHROPIC_API_KEY in your environment.";
500
+ case "google":
501
+ return "Export GOOGLE_API_KEY (or GEMINI_API_KEY) in your environment.";
502
+ default:
503
+ return "";
504
+ }
505
+ }
506
+
507
+ function detectShallowReview(
508
+ content: string,
509
+ minWeaknesses: number,
510
+ ): string | undefined {
511
+ const fileRefs = content.match(/[\w./_-]+\.(ts|tsx|js|jsx|py|go|rb|md|json|yaml|yml)(?::\d+)?/g);
512
+ const fileRefCount = fileRefs ? fileRefs.length : 0;
513
+
514
+ const weaknessSection = content.match(
515
+ /##\s+Concrete\s+Weaknesses\s*\n([\s\S]*?)(\n##|$)/i,
516
+ );
517
+ const weaknessCount = weaknessSection
518
+ ? (weaknessSection[1]!.match(/^\s*\d+\.\s+/gm) ?? []).length
519
+ : 0;
520
+
521
+ if (fileRefCount === 0) {
522
+ return `Reviewer output contains zero file/line references. Likely shallow.`;
523
+ }
524
+ if (weaknessCount < minWeaknesses) {
525
+ return `Reviewer listed ${weaknessCount} concrete weaknesses; config requires ${minWeaknesses}. Output may be shallow.`;
526
+ }
527
+ return undefined;
528
+ }