@almightygpt/core 0.3.0 → 0.5.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 (58) hide show
  1. package/dist/adapters/claude.d.ts +1 -1
  2. package/dist/adapters/claude.d.ts.map +1 -1
  3. package/dist/adapters/claude.js +46 -6
  4. package/dist/adapters/claude.js.map +1 -1
  5. package/dist/adapters/gemini.d.ts +1 -1
  6. package/dist/adapters/gemini.d.ts.map +1 -1
  7. package/dist/adapters/gemini.js.map +1 -1
  8. package/dist/adapters/mock.d.ts +1 -0
  9. package/dist/adapters/mock.d.ts.map +1 -1
  10. package/dist/adapters/mock.js +1 -0
  11. package/dist/adapters/mock.js.map +1 -1
  12. package/dist/adapters/openai.d.ts +1 -1
  13. package/dist/adapters/openai.d.ts.map +1 -1
  14. package/dist/adapters/openai.js +20 -5
  15. package/dist/adapters/openai.js.map +1 -1
  16. package/dist/adapters/types.d.ts +20 -2
  17. package/dist/adapters/types.d.ts.map +1 -1
  18. package/dist/adapters/types.js.map +1 -1
  19. package/dist/git/status.d.ts.map +1 -1
  20. package/dist/git/status.js +18 -5
  21. package/dist/git/status.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/review/diff-filter.d.ts +37 -0
  27. package/dist/review/diff-filter.d.ts.map +1 -0
  28. package/dist/review/diff-filter.js +144 -0
  29. package/dist/review/diff-filter.js.map +1 -0
  30. package/dist/review/events.d.ts +2 -0
  31. package/dist/review/events.d.ts.map +1 -1
  32. package/dist/review/run-diff-review.d.ts +2 -0
  33. package/dist/review/run-diff-review.d.ts.map +1 -1
  34. package/dist/review/run-diff-review.js +34 -8
  35. package/dist/review/run-diff-review.js.map +1 -1
  36. package/dist/review/run-worker-reviewer.d.ts.map +1 -1
  37. package/dist/review/run-worker-reviewer.js +43 -10
  38. package/dist/review/run-worker-reviewer.js.map +1 -1
  39. package/dist/review/write.d.ts +15 -0
  40. package/dist/review/write.d.ts.map +1 -1
  41. package/dist/review/write.js +29 -0
  42. package/dist/review/write.js.map +1 -1
  43. package/dist/runs/types.d.ts +6 -0
  44. package/dist/runs/types.d.ts.map +1 -1
  45. package/package.json +2 -1
  46. package/src/adapters/claude.ts +59 -8
  47. package/src/adapters/gemini.ts +1 -1
  48. package/src/adapters/mock.ts +1 -0
  49. package/src/adapters/openai.ts +35 -7
  50. package/src/adapters/types.ts +20 -2
  51. package/src/git/status.ts +21 -5
  52. package/src/index.ts +3 -1
  53. package/src/review/diff-filter.ts +190 -0
  54. package/src/review/events.ts +2 -0
  55. package/src/review/run-diff-review.ts +46 -8
  56. package/src/review/run-worker-reviewer.ts +53 -10
  57. package/src/review/write.ts +42 -0
  58. package/src/runs/types.ts +6 -0
@@ -37,12 +37,16 @@ import {
37
37
  } from "../runs/folder.js";
38
38
  import type { RunMetadata } from "../runs/types.js";
39
39
  import { collectGitDiff } from "./diff.js";
40
+ import { filterDiffByIgnoreLists } from "./diff-filter.js";
40
41
  import { assembleMemory } from "./memory.js";
41
42
  import {
42
43
  buildReviewerSystemFraming,
43
44
  buildReviewerUserMessage,
44
45
  } from "./prompts.js";
45
- import { writeHumanReviewFile } from "./write.js";
46
+ import {
47
+ preflightReviewFileCollision,
48
+ writeHumanReviewFile,
49
+ } from "./write.js";
46
50
  import { BudgetTracker, BudgetExceededError } from "./budget.js";
47
51
 
48
52
  export interface DiffReviewOptions {
@@ -63,6 +67,8 @@ export interface DiffReviewResult {
63
67
  provider: string;
64
68
  modelUsed: string;
65
69
  tokensIn: number;
70
+ /** Portion of tokensIn served from prompt cache (0 if no cache hit). */
71
+ cachedTokensIn: number;
66
72
  tokensOut: number;
67
73
  costUsd: number;
68
74
  latencyMs: number;
@@ -109,6 +115,16 @@ export async function runDiffReview(
109
115
  );
110
116
  }
111
117
 
118
+ // Preflight the review-file collision BEFORE any paid adapter call.
119
+ // (Codex review v0.5: previously the check ran post-adapter, burning API
120
+ // money on duplicate-topic runs that were going to fail anyway.)
121
+ await preflightReviewFileCollision(
122
+ opts.repoRoot,
123
+ config.reviewsDir,
124
+ opts.topic,
125
+ opts.force ?? false,
126
+ );
127
+
112
128
  // Create run folder up front so partial failures still leave an artifact.
113
129
  const runFolder = await createRunFolder({
114
130
  repoRoot: opts.repoRoot,
@@ -155,14 +171,29 @@ export async function runDiffReview(
155
171
  );
156
172
  }
157
173
 
174
+ // Codex review v0.5 P1 #2: filter ignored files BEFORE secret redaction
175
+ // and BEFORE the provider call. Honors .almightyignore + .gitignore +
176
+ // config.context.exclude. Redaction still runs on the survivors as
177
+ // defense-in-depth.
178
+ const filtered = await filterDiffByIgnoreLists({
179
+ repoRoot: opts.repoRoot,
180
+ diffText: diffResult.diff,
181
+ files: diffResult.files,
182
+ configExclude: config.context.exclude,
183
+ });
184
+
158
185
  const redaction = config.security.redactSecrets
159
- ? redactSecrets(diffResult.diff)
160
- : { text: diffResult.diff, redactions: [], totalCount: 0 };
186
+ ? redactSecrets(filtered.filteredDiff)
187
+ : { text: filtered.filteredDiff, redactions: [], totalCount: 0 };
161
188
 
162
189
  const manifest = buildContextManifest({
163
190
  inputSource: opts.range ? "diff-range" : "diff",
164
- filesIncluded: diffResult.files.map((p) => ({ path: p, bytes: 0 })),
165
- filesSkipped: [],
191
+ filesIncluded: filtered.filesIncluded.map((p) => ({ path: p, bytes: 0 })),
192
+ filesSkipped: filtered.filesSkipped.map((s) => ({
193
+ path: s.path,
194
+ bytes: 0,
195
+ skippedReason: s.reason,
196
+ })),
166
197
  diffText: redaction.text,
167
198
  redaction: {
168
199
  enabled: config.security.redactSecrets,
@@ -182,15 +213,18 @@ export async function runDiffReview(
182
213
  const userMessage = buildReviewerUserMessage({
183
214
  topic: opts.topic,
184
215
  diff: redaction.text,
185
- files: diffResult.files,
216
+ files: filtered.filesIncluded,
186
217
  });
187
218
 
188
219
  const budget = new BudgetTracker(config.budget);
189
220
  const estimatedTokensIn = Math.ceil(
190
221
  (systemPrompt.length + userMessage.length) / 4,
191
222
  );
223
+ // Codex review v0.5 P2 #5: previously hardcoded "gpt-4o" here, which
224
+ // could block a cheap Gemini run on an OpenAI cost estimate. Use the
225
+ // actual adapter's default model so the preflight matches reality.
192
226
  budget.preflightCheck({
193
- model: "gpt-4o", // estimator only; adapter resolves real model
227
+ model: adapter.defaultModel,
194
228
  estimatedTokensIn,
195
229
  maxOutputTokens: 4096,
196
230
  });
@@ -247,6 +281,9 @@ export async function runDiffReview(
247
281
  provider: adapter.provider,
248
282
  model: adapterOut.modelUsed,
249
283
  tokensIn: adapterOut.tokensIn,
284
+ ...(adapterOut.cachedTokensIn !== undefined && adapterOut.cachedTokensIn > 0
285
+ ? { cachedTokensIn: adapterOut.cachedTokensIn }
286
+ : {}),
250
287
  tokensOut: adapterOut.tokensOut,
251
288
  costUsd: adapterOut.costUsd,
252
289
  latencyMs: adapterOut.latencyMs,
@@ -268,11 +305,12 @@ export async function runDiffReview(
268
305
  provider: adapter.provider,
269
306
  modelUsed: adapterOut.modelUsed,
270
307
  tokensIn: adapterOut.tokensIn,
308
+ cachedTokensIn: adapterOut.cachedTokensIn ?? 0,
271
309
  tokensOut: adapterOut.tokensOut,
272
310
  costUsd: adapterOut.costUsd,
273
311
  latencyMs: adapterOut.latencyMs,
274
312
  diffEmpty: diffResult.empty,
275
- filesReviewed: diffResult.files,
313
+ filesReviewed: filtered.filesIncluded,
276
314
  memorySources: memory.sources,
277
315
  memoryMissing: memory.missing,
278
316
  runId: runFolder.id,
@@ -45,6 +45,7 @@ import {
45
45
  } from "../runs/folder.js";
46
46
  import type { AgentMetrics, RunMetadata } from "../runs/types.js";
47
47
  import { collectGitDiff } from "./diff.js";
48
+ import { filterDiffByIgnoreLists } from "./diff-filter.js";
48
49
  import { assembleMemory } from "./memory.js";
49
50
  import {
50
51
  buildReviewerSystemFraming,
@@ -52,7 +53,10 @@ import {
52
53
  buildWorkerSystemFraming,
53
54
  buildWorkerUserMessage,
54
55
  } from "./prompts.js";
55
- import { writeHumanReviewFile } from "./write.js";
56
+ import {
57
+ preflightReviewFileCollision,
58
+ writeHumanReviewFile,
59
+ } from "./write.js";
56
60
  import { BudgetTracker, BudgetExceededError } from "./budget.js";
57
61
  import type { ReviewEventHandler } from "./events.js";
58
62
 
@@ -153,6 +157,16 @@ export async function runWorkerReviewerReview(
153
157
  ? `Worker and Reviewer are both ${workerCfg.provider}. Cross-vendor pairing catches more issues.`
154
158
  : undefined;
155
159
 
160
+ // Preflight the review-file collision BEFORE Worker or Reviewer fires.
161
+ // (Codex review v0.5: previously the check ran post-adapter, burning two
162
+ // adapter calls of API money on duplicate-topic runs.)
163
+ await preflightReviewFileCollision(
164
+ opts.repoRoot,
165
+ config.reviewsDir,
166
+ opts.topic,
167
+ opts.force ?? false,
168
+ );
169
+
156
170
  const runFolder = await createRunFolder({
157
171
  repoRoot: opts.repoRoot,
158
172
  runsDir: config.runsDir,
@@ -207,9 +221,19 @@ export async function runWorkerReviewerReview(
207
221
  );
208
222
  }
209
223
 
224
+ // Codex review v0.5 P1 #2: filter ignored files BEFORE redaction and
225
+ // BEFORE either adapter call. Honors .almightyignore + .gitignore +
226
+ // config.context.exclude.
227
+ const filtered = await filterDiffByIgnoreLists({
228
+ repoRoot: opts.repoRoot,
229
+ diffText: diffResult.diff,
230
+ files: diffResult.files,
231
+ configExclude: config.context.exclude,
232
+ });
233
+
210
234
  const redaction = config.security.redactSecrets
211
- ? redactSecrets(diffResult.diff)
212
- : { text: diffResult.diff, redactions: [], totalCount: 0 };
235
+ ? redactSecrets(filtered.filteredDiff)
236
+ : { text: filtered.filteredDiff, redactions: [], totalCount: 0 };
213
237
  emit({
214
238
  type: "redaction_complete",
215
239
  totalCount: redaction.totalCount,
@@ -218,8 +242,12 @@ export async function runWorkerReviewerReview(
218
242
 
219
243
  const manifest = buildContextManifest({
220
244
  inputSource: opts.range ? "diff-range" : "diff",
221
- filesIncluded: diffResult.files.map((p) => ({ path: p, bytes: 0 })),
222
- filesSkipped: [],
245
+ filesIncluded: filtered.filesIncluded.map((p) => ({ path: p, bytes: 0 })),
246
+ filesSkipped: filtered.filesSkipped.map((s) => ({
247
+ path: s.path,
248
+ bytes: 0,
249
+ skippedReason: s.reason,
250
+ })),
223
251
  diffText: redaction.text,
224
252
  redaction: {
225
253
  enabled: config.security.redactSecrets,
@@ -245,11 +273,14 @@ export async function runWorkerReviewerReview(
245
273
  const workerUser = buildWorkerUserMessage({
246
274
  topic: opts.topic,
247
275
  diff: redaction.text,
248
- files: diffResult.files,
276
+ files: filtered.filesIncluded,
249
277
  });
250
278
 
279
+ // Codex review v0.5 P2 #5: use the actual adapter's default model
280
+ // for the preflight estimate so a cheap Gemini Worker isn't blocked
281
+ // by an OpenAI cost estimate.
251
282
  budget.preflightCheck({
252
- model: "gpt-4o",
283
+ model: workerAdapter.defaultModel,
253
284
  estimatedTokensIn: Math.ceil(
254
285
  (workerSystem.length + workerUser.length) / 4,
255
286
  ),
@@ -281,6 +312,9 @@ export async function runWorkerReviewerReview(
281
312
  model: workerOut.modelUsed,
282
313
  outputPath: `${runFolder.relPath}/outputs/worker.md`,
283
314
  tokensIn: workerOut.tokensIn,
315
+ ...(workerOut.cachedTokensIn !== undefined && workerOut.cachedTokensIn > 0
316
+ ? { cachedTokensIn: workerOut.cachedTokensIn }
317
+ : {}),
284
318
  tokensOut: workerOut.tokensOut,
285
319
  costUsd: workerOut.costUsd,
286
320
  latencyMs: workerOut.latencyMs,
@@ -296,14 +330,14 @@ export async function runWorkerReviewerReview(
296
330
  const reviewerUser = buildReviewerOfWorkerUserMessage({
297
331
  topic: opts.topic,
298
332
  diff: redaction.text,
299
- files: diffResult.files,
333
+ files: filtered.filesIncluded,
300
334
  workerOutput: workerOut.content,
301
335
  workerAgent: workerName,
302
336
  workerProvider: workerAdapter.provider,
303
337
  });
304
338
 
305
339
  budget.preflightCheck({
306
- model: "gpt-4o",
340
+ model: reviewerAdapter.defaultModel,
307
341
  estimatedTokensIn: Math.ceil(
308
342
  (reviewerSystem.length + reviewerUser.length) / 4,
309
343
  ),
@@ -335,6 +369,9 @@ export async function runWorkerReviewerReview(
335
369
  model: reviewerOut.modelUsed,
336
370
  outputPath: `${runFolder.relPath}/outputs/reviewer.md`,
337
371
  tokensIn: reviewerOut.tokensIn,
372
+ ...(reviewerOut.cachedTokensIn !== undefined && reviewerOut.cachedTokensIn > 0
373
+ ? { cachedTokensIn: reviewerOut.cachedTokensIn }
374
+ : {}),
338
375
  tokensOut: reviewerOut.tokensOut,
339
376
  costUsd: reviewerOut.costUsd,
340
377
  latencyMs: reviewerOut.latencyMs,
@@ -390,6 +427,9 @@ export async function runWorkerReviewerReview(
390
427
  provider: workerAdapter.provider,
391
428
  model: workerOut.modelUsed,
392
429
  tokensIn: workerOut.tokensIn,
430
+ ...(workerOut.cachedTokensIn !== undefined && workerOut.cachedTokensIn > 0
431
+ ? { cachedTokensIn: workerOut.cachedTokensIn }
432
+ : {}),
393
433
  tokensOut: workerOut.tokensOut,
394
434
  costUsd: workerOut.costUsd,
395
435
  latencyMs: workerOut.latencyMs,
@@ -400,6 +440,9 @@ export async function runWorkerReviewerReview(
400
440
  provider: reviewerAdapter.provider,
401
441
  model: reviewerOut.modelUsed,
402
442
  tokensIn: reviewerOut.tokensIn,
443
+ ...(reviewerOut.cachedTokensIn !== undefined && reviewerOut.cachedTokensIn > 0
444
+ ? { cachedTokensIn: reviewerOut.cachedTokensIn }
445
+ : {}),
403
446
  tokensOut: reviewerOut.tokensOut,
404
447
  costUsd: reviewerOut.costUsd,
405
448
  latencyMs: reviewerOut.latencyMs,
@@ -442,7 +485,7 @@ export async function runWorkerReviewerReview(
442
485
  },
443
486
  metrics,
444
487
  totals,
445
- filesReviewed: diffResult.files,
488
+ filesReviewed: filtered.filesIncluded,
446
489
  redactionsTotal: redaction.totalCount,
447
490
  memoryMissing: [
448
491
  ...new Set([...workerMemory.missing, ...reviewerMemory.missing]),
@@ -86,6 +86,48 @@ export async function writeHumanReviewFile(
86
86
  return { path: relPath, bytes: content.length };
87
87
  }
88
88
 
89
+ /**
90
+ * Compute the review file path for a topic without writing anything. Used
91
+ * by the review pipelines to **preflight** collisions *before* any paid
92
+ * adapter call. Same sanitization rules as writeHumanReviewFile.
93
+ */
94
+ export function reviewFilePathFor(
95
+ reviewsDir: string,
96
+ topic: string,
97
+ ): string {
98
+ return join(reviewsDir, `${sanitizeTopic(topic)}.md`);
99
+ }
100
+
101
+ /**
102
+ * Preflight check: throws ReviewFileExistsError if the topic's review file
103
+ * already exists and `force` isn't set. Surfaces the error BEFORE any
104
+ * adapter is invoked so the user doesn't spend API money discovering an
105
+ * easily-avoidable collision.
106
+ *
107
+ * Returns the (relative) path so the caller doesn't have to recompute it.
108
+ */
109
+ export async function preflightReviewFileCollision(
110
+ repoRoot: string,
111
+ reviewsDir: string,
112
+ topic: string,
113
+ force = false,
114
+ ): Promise<string> {
115
+ const relPath = reviewFilePathFor(reviewsDir, topic);
116
+ const absPath = join(repoRoot, relPath);
117
+ if (existsSync(absPath) && !force) {
118
+ // Also run the git-status check to give a richer dirty-file message
119
+ // when applicable; falls through to the existence error below.
120
+ await assertSafeToWrite(repoRoot, relPath, false);
121
+ throw new ReviewFileExistsError(
122
+ `Refusing to overwrite ${relPath}. Pass --force to overwrite (the ` +
123
+ `previous version remains in git history). Alternatively pick a ` +
124
+ `different --topic to keep both reviews.`,
125
+ relPath,
126
+ );
127
+ }
128
+ return relPath;
129
+ }
130
+
89
131
  function sanitizeTopic(topic: string): string {
90
132
  // Allow letters, digits, dot, dash, underscore. Replace everything else
91
133
  // with a single dash. Trim leading/trailing dashes. Lowercase.
package/src/runs/types.ts CHANGED
@@ -24,6 +24,12 @@ export interface AgentMetrics {
24
24
  provider: string;
25
25
  model: string;
26
26
  tokensIn: number;
27
+ /**
28
+ * Portion of tokensIn served from the provider's prompt cache (Anthropic
29
+ * cache_read, OpenAI prompt_tokens_details.cached_tokens). Always less
30
+ * than or equal to tokensIn. Zero when caching didn't trigger.
31
+ */
32
+ cachedTokensIn?: number;
27
33
  tokensOut: number;
28
34
  costUsd: number;
29
35
  latencyMs: number;