clud-bug 0.7.0-rc.11 → 0.7.0-rc.13

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 (55) hide show
  1. package/dist/cli/hooks.d.ts +40 -0
  2. package/dist/cli/hooks.d.ts.map +1 -0
  3. package/dist/cli/hooks.js +105 -0
  4. package/dist/cli/hooks.js.map +1 -0
  5. package/dist/cli/main.d.ts.map +1 -1
  6. package/dist/cli/main.js +61 -0
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/cli/review-prompt.d.ts +28 -0
  9. package/dist/cli/review-prompt.d.ts.map +1 -0
  10. package/dist/cli/review-prompt.js +169 -0
  11. package/dist/cli/review-prompt.js.map +1 -0
  12. package/dist/cli/update.d.ts.map +1 -1
  13. package/dist/cli/update.js +22 -0
  14. package/dist/cli/update.js.map +1 -1
  15. package/dist/core/budget-plan.d.ts +125 -0
  16. package/dist/core/budget-plan.d.ts.map +1 -0
  17. package/dist/core/budget-plan.js +211 -0
  18. package/dist/core/budget-plan.js.map +1 -0
  19. package/dist/core/index.d.ts +5 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +14 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/multi-pass-aggregate.d.ts +131 -0
  24. package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
  25. package/dist/core/multi-pass-aggregate.js +427 -0
  26. package/dist/core/multi-pass-aggregate.js.map +1 -0
  27. package/dist/core/plan-review.d.ts +57 -0
  28. package/dist/core/plan-review.d.ts.map +1 -0
  29. package/dist/core/plan-review.js +77 -0
  30. package/dist/core/plan-review.js.map +1 -0
  31. package/dist/core/review-plan.d.ts +219 -0
  32. package/dist/core/review-plan.d.ts.map +1 -0
  33. package/dist/core/review-plan.js +274 -0
  34. package/dist/core/review-plan.js.map +1 -0
  35. package/dist/core/review-writeback.d.ts +20 -0
  36. package/dist/core/review-writeback.d.ts.map +1 -1
  37. package/dist/core/review-writeback.js +16 -0
  38. package/dist/core/review-writeback.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/hooks.ts +122 -0
  43. package/src/cli/main.ts +56 -0
  44. package/src/cli/review-prompt.ts +206 -0
  45. package/src/cli/update.ts +22 -0
  46. package/src/core/budget-plan.ts +324 -0
  47. package/src/core/index.ts +64 -0
  48. package/src/core/multi-pass-aggregate.ts +545 -0
  49. package/src/core/plan-review.ts +136 -0
  50. package/src/core/review-plan.ts +485 -0
  51. package/src/core/review-writeback.ts +38 -0
  52. package/src/core/version.ts +1 -1
  53. package/templates/workflow-py.yml.tmpl +1 -1
  54. package/templates/workflow-ts.yml.tmpl +1 -1
  55. package/templates/workflow.yml.tmpl +1 -1
@@ -0,0 +1,324 @@
1
+ import type { ResolvedReviewPasses } from './review-plan.js';
2
+
3
+ /**
4
+ * Layer 1 cost gate — pre-flight estimator for multi-pass review.
5
+ *
6
+ * D.2.5 introduces a cost multiplier — a 3-pass review of a 5-skill catalog
7
+ * is 15 AI calls instead of 5. Without a gate, a misconfigured `reviewPasses`
8
+ * block can quietly burn through the AI Gateway budget on a single PR. This
9
+ * module decides BEFORE any AI call whether the review is cheap enough to
10
+ * run, and if not, returns a structured "deny" verdict the orchestrator
11
+ * surfaces as a friendly comment.
12
+ *
13
+ * Two layers, only the first lands in D.2.5:
14
+ *
15
+ * - Layer 1 (this module): per-PR estimate. Sums `skill_loads × pass_count`
16
+ * to produce an `estimatedCalls` count and an `estimatedCostUsd`
17
+ * ceiling. If `estimatedCostUsd > cap`, we deny.
18
+ *
19
+ * - Layer 2 (D.4 — NOT in this module): per-install rolling spend. Reads
20
+ * `INSTALLS:{id}.spend_usd_30d` from Redis. D.4 wires the actual gating;
21
+ * here we just stub the interface.
22
+ *
23
+ * The gate is BEST-EFFORT, intentionally pessimistic. We over-estimate cost
24
+ * (worst-case tokens × highest-tier model) because the alternative is the
25
+ * customer eating a surprise bill. A 20% over-estimate that occasionally
26
+ * causes a "we paused; raise the cap" comment is the right failure mode.
27
+ *
28
+ * Until D.4 wires real per-install budgets, this module always returns
29
+ * `{ verdict: 'allow' }`. The implementation is intentionally complete so
30
+ * D.4 can flip a single switch without changing the orchestrator surface.
31
+ */
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Constants — worst-case cost ceilings
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Rough USD-per-call ceiling across the model tiers we route to.
39
+ *
40
+ * Numbers are deliberately over-stated — we'd rather deny a borderline
41
+ * review than blow past a billing cap. Reconciled monthly from the AI
42
+ * Gateway dashboard; the D.4 billing module replaces this with live data.
43
+ *
44
+ * Picking the max(model) across the roles array keeps the math local — we
45
+ * don't need per-pass attribution for the estimate, only for billing
46
+ * reconciliation later.
47
+ */
48
+ const MODEL_CEILING_USD: Record<string, number> = {
49
+ 'anthropic/claude-haiku-4.5': 0.02,
50
+ 'anthropic/claude-sonnet-4.6': 0.08,
51
+ 'anthropic/claude-opus-4.6': 0.4,
52
+ 'anthropic/claude-opus-4.7': 0.5,
53
+ 'openai/gpt-5': 0.4,
54
+ 'openai/gpt-5.2': 0.04,
55
+ 'google/gemini-3-flash': 0.02,
56
+ 'google/gemini-3.1-pro-preview': 0.3,
57
+ };
58
+
59
+ /** Fallback ceiling for unknown model slugs. Conservative. */
60
+ const DEFAULT_PER_CALL_USD = 0.5;
61
+
62
+ /**
63
+ * Default per-PR cap. Real number lives in `env.REVIEW_SPEND_CAP_USD` once
64
+ * D.4 wires that env var — for now this is the hard-coded ceiling.
65
+ *
66
+ * $5 = a 3-pass review across 10 expensive skills (~30 calls × $0.15 avg).
67
+ * Anything above that is almost certainly a misconfigured `reviewPasses`.
68
+ */
69
+ export const DEFAULT_PER_PR_CAP_USD = 5.0;
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Public types
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export interface BudgetEstimateInput {
76
+ /** Resolved per-skill config — one entry per skill in the catalog. */
77
+ resolved: ResolvedReviewPasses[];
78
+ /** Model slugs the orchestrator plans to route to, in pass order. */
79
+ roleModels: string[];
80
+ /**
81
+ * Optional per-PR USD cap override. Defaults to `DEFAULT_PER_PR_CAP_USD`.
82
+ * D.4 wires per-install caps via `INSTALLS:{id}.spend_cap_usd`.
83
+ */
84
+ perPrCapUsd?: number;
85
+ /**
86
+ * Installation flag — `billing === 'exempt'` orgs bypass the cap entirely.
87
+ * D.4 reads this from the install record. For D.2.5, the orchestrator
88
+ * passes the env-allowlist result here.
89
+ */
90
+ billingExempt?: boolean;
91
+ }
92
+
93
+ export interface BudgetEstimate {
94
+ /** Total AI calls = sum(skill_count × pass_count). */
95
+ estimatedCalls: number;
96
+ /** Worst-case cost in USD across all planned calls. */
97
+ estimatedCostUsd: number;
98
+ /** Per-call ceiling used (max across the role models). */
99
+ perCallCeilingUsd: number;
100
+ /** The cap the estimate was checked against. */
101
+ capUsd: number;
102
+ }
103
+
104
+ export type BudgetVerdict =
105
+ | {
106
+ verdict: 'allow';
107
+ estimate: BudgetEstimate;
108
+ }
109
+ | {
110
+ verdict: 'deny';
111
+ estimate: BudgetEstimate;
112
+ /** Friendly reason for the orchestrator's "we paused this review" comment. */
113
+ reason: string;
114
+ };
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Estimator
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Returns the per-call USD ceiling — max across the role models.
122
+ */
123
+ export function perCallCeiling(models: string[]): number {
124
+ if (models.length === 0) return DEFAULT_PER_CALL_USD;
125
+ let max = 0;
126
+ for (const m of models) {
127
+ const c = MODEL_CEILING_USD[m] ?? DEFAULT_PER_CALL_USD;
128
+ if (c > max) max = c;
129
+ }
130
+ return max;
131
+ }
132
+
133
+ /**
134
+ * Layer-1 cost gate. Decides whether the planned passes fit under the cap.
135
+ *
136
+ * Behavior matrix:
137
+ * billingExempt = true → allow (skip cap entirely)
138
+ * estimatedCostUsd > cap → deny + friendly reason
139
+ * estimatedCostUsd <= cap → allow
140
+ * resolved.length === 0 → allow (single-pass D.2.0 path)
141
+ *
142
+ * Until D.4 wires real per-install spending, callers see allow on every
143
+ * normal path — the deny branch only fires on truly absurd estimates.
144
+ */
145
+ export function estimateBudget(input: BudgetEstimateInput): BudgetVerdict {
146
+ const cap = input.perPrCapUsd ?? DEFAULT_PER_PR_CAP_USD;
147
+ const ceiling = perCallCeiling(input.roleModels);
148
+ // The orchestrator (runMultiPass) sends all skills in a SINGLE prompt
149
+ // each pass and loops only the max skill's count times. Real call count
150
+ // is max(counts), not sum(counts). Summing over-estimates linearly with
151
+ // catalog size, making multi-pass unreachable for medium+ skill sets.
152
+ const calls = input.resolved.length === 0
153
+ ? 0
154
+ : input.resolved.reduce((max, r) => Math.max(max, r.count), 0);
155
+ const cost = calls * ceiling;
156
+
157
+ const estimate: BudgetEstimate = {
158
+ estimatedCalls: calls,
159
+ estimatedCostUsd: cost,
160
+ perCallCeilingUsd: ceiling,
161
+ capUsd: cap,
162
+ };
163
+
164
+ if (input.billingExempt) {
165
+ return { verdict: 'allow', estimate };
166
+ }
167
+ if (cost > cap) {
168
+ return {
169
+ verdict: 'deny',
170
+ estimate,
171
+ reason: friendlyDenyReason({ cost, cap, calls, ceiling }),
172
+ };
173
+ }
174
+ return { verdict: 'allow', estimate };
175
+ }
176
+
177
+ function friendlyDenyReason(args: {
178
+ cost: number;
179
+ cap: number;
180
+ calls: number;
181
+ ceiling: number;
182
+ }): string {
183
+ return [
184
+ `clud-bug paused this review — estimated cost \`$${args.cost.toFixed(2)}\``,
185
+ `exceeds the per-PR cap of \`$${args.cap.toFixed(2)}\`.`,
186
+ `(${args.calls} planned AI call${args.calls === 1 ? '' : 's'} ×`,
187
+ `worst-case \`$${args.ceiling.toFixed(2)}\` per call.)`,
188
+ 'Lower `reviewPasses.count` in `.claude/skills/.clud-bug.json` and re-trigger.',
189
+ ].join(' ');
190
+ }
191
+
192
+ /**
193
+ * Exposed for tests + future D.4 wiring. Lets callers register a cost
194
+ * ceiling for a newly-routed model without editing this module.
195
+ */
196
+ export function __setModelCeilingForTests(
197
+ model: string,
198
+ usd: number | null,
199
+ ): void {
200
+ if (usd === null) {
201
+ delete MODEL_CEILING_USD[model];
202
+ return;
203
+ }
204
+ MODEL_CEILING_USD[model] = usd;
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // D.2.6 — auto-resolve verifier cost surface
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Per-call ceiling for the D.2.6 fix-verifier. Each verifier call is small —
213
+ * ~500 token I/O at Sonnet rates — so a more aggressive ceiling makes sense
214
+ * vs the main-review per-call ceiling. We over-state by ~2x to keep the
215
+ * "$0.10 per PR with 5 findings × 3 fix-pushes" plan budget honest.
216
+ *
217
+ * Sonnet 4.6 at $3/$15 per million tokens × 500 token I/O ≈ $0.005;
218
+ * we use $0.01 as the gate ceiling so a borderline-overbudget verifier doesn't
219
+ * silently silently flip the install over the cap.
220
+ */
221
+ const VERIFIER_PER_CALL_CEILING_USD = 0.01;
222
+
223
+ /**
224
+ * Default per-PR budget for D.2.6 verifier calls. The spec calls $0.10 the
225
+ * "5 findings × 3 fix-pushes" ceiling; we use 2x that as the cap so a
226
+ * sufficiently-large PR (20 threads × 3 fix-pushes = 60 calls) still fits.
227
+ *
228
+ * D.4 will plumb per-install caps; for D.2.6 we use a single repo-wide
229
+ * default. Installs in BILLING_EXEMPT_ORGS bypass this entirely.
230
+ */
231
+ export const DEFAULT_VERIFIER_PER_PR_CAP_USD = 0.6;
232
+
233
+ export interface VerifierBudgetInput {
234
+ /** Number of open threads we plan to verify on this fix-push. */
235
+ threadCount: number;
236
+ /**
237
+ * Pass count from D.2.5 multi-pass — we call the verifier once per pass
238
+ * per thread (see resolve-verifier integration doc). Defaults to 1
239
+ * for single-pass installs.
240
+ */
241
+ passesPerThread?: number;
242
+ /**
243
+ * Optional per-PR cap override. Defaults to DEFAULT_VERIFIER_PER_PR_CAP_USD.
244
+ */
245
+ perPrCapUsd?: number;
246
+ /** BILLING_EXEMPT installs bypass the cap. */
247
+ billingExempt?: boolean;
248
+ }
249
+
250
+ export interface VerifierBudgetEstimate {
251
+ /** Total verifier AI calls = threadCount × passesPerThread. */
252
+ estimatedCalls: number;
253
+ /** Worst-case cost. */
254
+ estimatedCostUsd: number;
255
+ /** Per-call ceiling used. */
256
+ perCallCeilingUsd: number;
257
+ /** Cap checked against. */
258
+ capUsd: number;
259
+ }
260
+
261
+ export type VerifierBudgetVerdict =
262
+ | { verdict: 'allow'; estimate: VerifierBudgetEstimate }
263
+ | {
264
+ verdict: 'deny';
265
+ estimate: VerifierBudgetEstimate;
266
+ /** Friendly reason for the orchestrator's "skipped: verifier-budget" log. */
267
+ reason: string;
268
+ };
269
+
270
+ /**
271
+ * Pre-flight gate for the D.2.6 fix-verifier. Called once per fix-push,
272
+ * BEFORE any verifier call runs. If denied, the orchestrator emits
273
+ * `skipped: budget` for the entire auto-resolve cycle and routes the
274
+ * threads through the heuristic fallback (see auto-resolve).
275
+ *
276
+ * Behavior:
277
+ * billingExempt = true → allow (bypass cap)
278
+ * estimated > cap → deny
279
+ * threadCount === 0 → allow with 0 calls (no-op fix-push, no threads to verify)
280
+ * otherwise → allow
281
+ */
282
+ export function estimateVerifierBudget(
283
+ input: VerifierBudgetInput,
284
+ ): VerifierBudgetVerdict {
285
+ const cap = input.perPrCapUsd ?? DEFAULT_VERIFIER_PER_PR_CAP_USD;
286
+ const passes = input.passesPerThread ?? 1;
287
+ const calls = input.threadCount * Math.max(1, passes);
288
+ const cost = calls * VERIFIER_PER_CALL_CEILING_USD;
289
+
290
+ const estimate: VerifierBudgetEstimate = {
291
+ estimatedCalls: calls,
292
+ estimatedCostUsd: cost,
293
+ perCallCeilingUsd: VERIFIER_PER_CALL_CEILING_USD,
294
+ capUsd: cap,
295
+ };
296
+
297
+ if (input.billingExempt) {
298
+ return { verdict: 'allow', estimate };
299
+ }
300
+ if (cost > cap) {
301
+ return {
302
+ verdict: 'deny',
303
+ estimate,
304
+ reason: friendlyVerifierDenyReason({ cost, cap, calls }),
305
+ };
306
+ }
307
+ return { verdict: 'allow', estimate };
308
+ }
309
+
310
+ function friendlyVerifierDenyReason(args: {
311
+ cost: number;
312
+ cap: number;
313
+ calls: number;
314
+ }): string {
315
+ return [
316
+ `clud-bug paused D.2.6 fix-verification — estimated cost`,
317
+ `\`$${args.cost.toFixed(2)}\` exceeds the per-PR cap of`,
318
+ `\`$${args.cap.toFixed(2)}\`. (${args.calls} planned verifier call${args.calls === 1 ? '' : 's'} ×`,
319
+ `worst-case \`$${VERIFIER_PER_CALL_CEILING_USD.toFixed(2)}\` per call.)`,
320
+ 'Lower `autoResolve.max_threads_per_fix_push` in',
321
+ '`.claude/skills/.clud-bug.json` or set `autoResolve.mode = "heuristic"`',
322
+ 'to opt out of verified-mode for this install.',
323
+ ].join(' ');
324
+ }
package/src/core/index.ts CHANGED
@@ -119,6 +119,7 @@ export {
119
119
  type RenderedFindingRef,
120
120
  type CacheStats,
121
121
  type RenderMultiPassMarkdownInput,
122
+ type Consensus,
122
123
  type MultiPassReview,
123
124
  type UnifiedFinding,
124
125
  type PassAttribution,
@@ -216,6 +217,69 @@ export {
216
217
  type ApplyCanonicalRulesetParams,
217
218
  type ApplyResult,
218
219
  } from './configure-github.js';
220
+ // Wave 6b — review-planning "brain" ported from clud-bug-app/lib. Three pure
221
+ // modules: the multi-pass config resolver (`review-plan`), the Layer-1 cost
222
+ // gate (`budget-plan`), and the multi-pass aggregator (`multi-pass-aggregate`).
223
+ //
224
+ // `ReviewPassMode` is intentionally NOT re-exported here — `review-writeback`
225
+ // above already owns that name in the barrel (both declarations are the same
226
+ // `'cross-check' | 'consensus' | 'independent'` union). Likewise the
227
+ // aggregator's result types (`MultiPassReview`, `UnifiedFinding`,
228
+ // `PassAttribution`, `PassSource`) come from `review-writeback` and are NOT
229
+ // re-declared by the aggregator module — it imports them.
230
+ export {
231
+ readReviewPassesConfig,
232
+ extractSkillReviewPassesOverride,
233
+ resolveReviewPasses,
234
+ roleForPass,
235
+ anyMultiPass,
236
+ totalPassCount,
237
+ REVIEW_PASS_MODES,
238
+ MAX_PASSES,
239
+ MIN_PASSES,
240
+ BUILTIN_DEFAULT,
241
+ BUILTIN_ROLES,
242
+ type ApplyTo,
243
+ type ReviewPassesEntry,
244
+ type ReviewRoleTier,
245
+ type ReviewRole,
246
+ type ResolvedReviewPasses,
247
+ type ReviewPassesConfig,
248
+ type SkillReviewPassesFrontmatter,
249
+ type ReviewPlanSkill,
250
+ type ResolveReviewPassesInput,
251
+ type ResolveReviewPassesResult,
252
+ } from './review-plan.js';
253
+ export {
254
+ perCallCeiling,
255
+ estimateBudget,
256
+ estimateVerifierBudget,
257
+ __setModelCeilingForTests,
258
+ DEFAULT_PER_PR_CAP_USD,
259
+ DEFAULT_VERIFIER_PER_PR_CAP_USD,
260
+ type BudgetEstimateInput,
261
+ type BudgetEstimate,
262
+ type BudgetVerdict,
263
+ type VerifierBudgetInput,
264
+ type VerifierBudgetEstimate,
265
+ type VerifierBudgetVerdict,
266
+ } from './budget-plan.js';
267
+ export {
268
+ planReview,
269
+ LARGE_DIFF_THRESHOLD_BYTES,
270
+ type PlanReviewInput,
271
+ type ReviewPlan,
272
+ type ReviewTrigger,
273
+ type TierDownReason,
274
+ } from './plan-review.js';
275
+ export {
276
+ aggregatePasses,
277
+ deriveConsensus,
278
+ resolveVerdict,
279
+ type CrossCheckVerdict,
280
+ type CrossCheckPassResult,
281
+ type AggregateInput,
282
+ } from './multi-pass-aggregate.js';
219
283
  export {
220
284
  API_BASE,
221
285
  MAX_SKILLS,