@pimmesz/afterburner 1.0.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.
@@ -0,0 +1,781 @@
1
+ import { z, ZodError } from 'zod';
2
+
3
+ declare const configDir: string;
4
+ declare const dataDir: string;
5
+ declare function defaultRunStorePath(): string;
6
+ /**
7
+ * Claude Code's config directory. CLAUDE_CONFIG_DIR relocates the whole tree
8
+ * (settings.json, skills/, projects/), so everything that reads or writes
9
+ * under ~/.claude must honor it or it targets a directory Claude Code ignores.
10
+ */
11
+ declare function claudeConfigDir(): string;
12
+
13
+ declare const TASK_CATEGORIES: readonly ["security", "tests", "types-lint", "dead-code", "perf", "infra", "docs"];
14
+ type TaskCategory = (typeof TASK_CATEGORIES)[number];
15
+ interface TaskCategoryInfo {
16
+ description: string;
17
+ /** How a human reviewer confirms the task is actually done. */
18
+ verifiabilityCheck: string;
19
+ }
20
+ declare const TASK_TAXONOMY: Record<TaskCategory, TaskCategoryInfo>;
21
+
22
+ declare const DEFAULT_MODEL_BY_CATEGORY: Record<TaskCategory, string>;
23
+ declare const repoConfigSchema: z.ZodObject<{
24
+ url: z.ZodString;
25
+ defaultBranch: z.ZodDefault<z.ZodString>;
26
+ branchPrefix: z.ZodDefault<z.ZodString>;
27
+ enabledTaskCategories: z.ZodArray<z.ZodEnum<{
28
+ security: "security";
29
+ tests: "tests";
30
+ "types-lint": "types-lint";
31
+ "dead-code": "dead-code";
32
+ perf: "perf";
33
+ infra: "infra";
34
+ docs: "docs";
35
+ }>>;
36
+ }, z.core.$strip>;
37
+ declare const budgetConfigSchema: z.ZodPrefault<z.ZodObject<{
38
+ provider: z.ZodDefault<z.ZodEnum<{
39
+ manual: "manual";
40
+ "claude-code-transcripts": "claude-code-transcripts";
41
+ "claude-usage": "claude-usage";
42
+ }>>;
43
+ weeklyAllowanceSonnetTokens: z.ZodDefault<z.ZodNumber>;
44
+ usageCacheMaxAgeHours: z.ZodDefault<z.ZodNumber>;
45
+ minWeeklyHeadroomPct: z.ZodDefault<z.ZodNumber>;
46
+ safetyMarginTokens: z.ZodDefault<z.ZodNumber>;
47
+ requireSessionAvailable: z.ZodDefault<z.ZodBoolean>;
48
+ manual: z.ZodDefault<z.ZodObject<{
49
+ sessionAvailable: z.ZodDefault<z.ZodBoolean>;
50
+ weeklyRemainingPct: z.ZodDefault<z.ZodNumber>;
51
+ weeklyRemainingTokensEst: z.ZodDefault<z.ZodNumber>;
52
+ }, z.core.$strip>>;
53
+ }, z.core.$strip>>;
54
+ declare const agentConfigSchema: z.ZodPrefault<z.ZodObject<{
55
+ backend: z.ZodDefault<z.ZodEnum<{
56
+ "dry-run": "dry-run";
57
+ "claude-code": "claude-code";
58
+ "api-key": "api-key";
59
+ }>>;
60
+ modelByCategory: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
61
+ security: "security";
62
+ tests: "tests";
63
+ "types-lint": "types-lint";
64
+ "dead-code": "dead-code";
65
+ perf: "perf";
66
+ infra: "infra";
67
+ docs: "docs";
68
+ }> & z.core.$partial, z.ZodString>>, z.ZodTransform<{
69
+ security: string;
70
+ tests: string;
71
+ "types-lint": string;
72
+ "dead-code": string;
73
+ perf: string;
74
+ infra: string;
75
+ docs: string;
76
+ }, Partial<Record<"security" | "tests" | "types-lint" | "dead-code" | "perf" | "infra" | "docs", string>>>>;
77
+ maxTaskTokens: z.ZodDefault<z.ZodNumber>;
78
+ allowFable: z.ZodDefault<z.ZodBoolean>;
79
+ }, z.core.$strip>>;
80
+ declare const configSchema: z.ZodObject<{
81
+ repos: z.ZodDefault<z.ZodArray<z.ZodObject<{
82
+ url: z.ZodString;
83
+ defaultBranch: z.ZodDefault<z.ZodString>;
84
+ branchPrefix: z.ZodDefault<z.ZodString>;
85
+ enabledTaskCategories: z.ZodArray<z.ZodEnum<{
86
+ security: "security";
87
+ tests: "tests";
88
+ "types-lint": "types-lint";
89
+ "dead-code": "dead-code";
90
+ perf: "perf";
91
+ infra: "infra";
92
+ docs: "docs";
93
+ }>>;
94
+ }, z.core.$strip>>>;
95
+ budget: z.ZodPrefault<z.ZodObject<{
96
+ provider: z.ZodDefault<z.ZodEnum<{
97
+ manual: "manual";
98
+ "claude-code-transcripts": "claude-code-transcripts";
99
+ "claude-usage": "claude-usage";
100
+ }>>;
101
+ weeklyAllowanceSonnetTokens: z.ZodDefault<z.ZodNumber>;
102
+ usageCacheMaxAgeHours: z.ZodDefault<z.ZodNumber>;
103
+ minWeeklyHeadroomPct: z.ZodDefault<z.ZodNumber>;
104
+ safetyMarginTokens: z.ZodDefault<z.ZodNumber>;
105
+ requireSessionAvailable: z.ZodDefault<z.ZodBoolean>;
106
+ manual: z.ZodDefault<z.ZodObject<{
107
+ sessionAvailable: z.ZodDefault<z.ZodBoolean>;
108
+ weeklyRemainingPct: z.ZodDefault<z.ZodNumber>;
109
+ weeklyRemainingTokensEst: z.ZodDefault<z.ZodNumber>;
110
+ }, z.core.$strip>>;
111
+ }, z.core.$strip>>;
112
+ schedule: z.ZodPrefault<z.ZodObject<{
113
+ cron: z.ZodDefault<z.ZodString>;
114
+ timezone: z.ZodDefault<z.ZodString>;
115
+ }, z.core.$strip>>;
116
+ agent: z.ZodPrefault<z.ZodObject<{
117
+ backend: z.ZodDefault<z.ZodEnum<{
118
+ "dry-run": "dry-run";
119
+ "claude-code": "claude-code";
120
+ "api-key": "api-key";
121
+ }>>;
122
+ modelByCategory: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
123
+ security: "security";
124
+ tests: "tests";
125
+ "types-lint": "types-lint";
126
+ "dead-code": "dead-code";
127
+ perf: "perf";
128
+ infra: "infra";
129
+ docs: "docs";
130
+ }> & z.core.$partial, z.ZodString>>, z.ZodTransform<{
131
+ security: string;
132
+ tests: string;
133
+ "types-lint": string;
134
+ "dead-code": string;
135
+ perf: string;
136
+ infra: string;
137
+ docs: string;
138
+ }, Partial<Record<"security" | "tests" | "types-lint" | "dead-code" | "perf" | "infra" | "docs", string>>>>;
139
+ maxTaskTokens: z.ZodDefault<z.ZodNumber>;
140
+ allowFable: z.ZodDefault<z.ZodBoolean>;
141
+ }, z.core.$strip>>;
142
+ taskCategories: z.ZodPipe<z.ZodDefault<z.ZodRecord<z.ZodEnum<{
143
+ security: "security";
144
+ tests: "tests";
145
+ "types-lint": "types-lint";
146
+ "dead-code": "dead-code";
147
+ perf: "perf";
148
+ infra: "infra";
149
+ docs: "docs";
150
+ }> & z.core.$partial, z.ZodObject<{
151
+ enabled: z.ZodDefault<z.ZodBoolean>;
152
+ weight: z.ZodDefault<z.ZodNumber>;
153
+ }, z.core.$strip>>>, z.ZodTransform<{
154
+ security: {
155
+ enabled: boolean;
156
+ weight: number;
157
+ };
158
+ tests: {
159
+ enabled: boolean;
160
+ weight: number;
161
+ };
162
+ "types-lint": {
163
+ enabled: boolean;
164
+ weight: number;
165
+ };
166
+ "dead-code": {
167
+ enabled: boolean;
168
+ weight: number;
169
+ };
170
+ perf: {
171
+ enabled: boolean;
172
+ weight: number;
173
+ };
174
+ infra: {
175
+ enabled: boolean;
176
+ weight: number;
177
+ };
178
+ docs: {
179
+ enabled: boolean;
180
+ weight: number;
181
+ };
182
+ }, Partial<Record<"security" | "tests" | "types-lint" | "dead-code" | "perf" | "infra" | "docs", {
183
+ enabled: boolean;
184
+ weight: number;
185
+ }>>>>;
186
+ }, z.core.$strip>;
187
+ type AfterburnerConfig = z.output<typeof configSchema>;
188
+ type AfterburnerUserConfig = z.input<typeof configSchema>;
189
+ type RepoConfig = z.output<typeof repoConfigSchema>;
190
+ type BudgetConfig = z.output<typeof budgetConfigSchema>;
191
+ type AgentConfig = z.output<typeof agentConfigSchema>;
192
+ /** Identity helper so user configs get full type checking and completion. */
193
+ declare function defineConfig(config: AfterburnerUserConfig): AfterburnerUserConfig;
194
+
195
+ interface LoadedConfig {
196
+ config: AfterburnerConfig;
197
+ filepath: string;
198
+ }
199
+ /**
200
+ * Loads config via cosmiconfig: explicit path > search from cwd > the global
201
+ * config dir (env-paths). cosmiconfig v9 loads .ts configs natively, but only
202
+ * when the `typescript` package is importable at runtime, true in a project
203
+ * that has typescript installed, false after a bare global CLI install. We
204
+ * keep the defaults and rewrite that failure into an actionable error instead
205
+ * of adding a runtime TypeScript dependency.
206
+ */
207
+ declare function loadConfig(explicitPath?: string): Promise<LoadedConfig>;
208
+ /** Exported for unit tests: rewrites known loader failures into actionable errors. */
209
+ declare function mapConfigLoadError(error: unknown): Error;
210
+ declare function formatConfigError(error: ZodError, filepath: string): string;
211
+
212
+ /**
213
+ * The single source of truth for budget math. ALL budget quantities in
214
+ * Afterburner are expressed in Sonnet-equivalent tokens, normalized through
215
+ * this table. Nothing outside this module may hardcode a model rate.
216
+ *
217
+ * Weights are relative cost factors with Sonnet = 1.0, derived from the
218
+ * published per-MTok API prices (verified 2026-06-11 at
219
+ * https://platform.claude.com/docs/en/about-claude/pricing):
220
+ * Haiku $1/$5, Sonnet $3/$15, Opus $5/$25, Fable $10/$50, input and output
221
+ * ratios are identical, so a single factor per model family suffices. The 1M
222
+ * context window is included at standard pricing, so there is no separate
223
+ * long-context weight. Anthropic publishes no numeric subscription-limit
224
+ * weighting between models (the docs are qualitative only), so these
225
+ * price-derived factors are the best documented proxy.
226
+ */
227
+ interface ModelCostTable {
228
+ /** Relative cost weight for a model id (longest-prefix match). Throws on unknown models. */
229
+ weightFor(modelId: string): number;
230
+ /** Convert raw tokens spent on `modelId` into Sonnet-equivalent tokens. */
231
+ toSonnetTokens(tokens: number, modelId: string): number;
232
+ }
233
+ type ModelWeightEntry = readonly [prefix: string, weight: number];
234
+ declare function createModelCostTable(weights?: readonly ModelWeightEntry[]): ModelCostTable;
235
+ declare const defaultCostTable: ModelCostTable;
236
+
237
+ /**
238
+ * Premium-model gate (still named the "Fable gate" / `allowFable` for the
239
+ * config key). Matches by model-family prefix (not exact id) so new releases in
240
+ * a gated family stay gated without a code change. It covers every top-tier,
241
+ * real-money family the cost table prices at ~2x Opus, currently Fable and
242
+ * Mythos, by reading PREMIUM_MODEL_PREFIXES from the cost table, so the gate
243
+ * and the cost weights can never drift apart. An autonomous run must never
244
+ * reach one of these models without the explicit `agent.allowFable` opt-in,
245
+ * since outside promotional windows they bill usage credits at API rates and
246
+ * would let an unattended scheduler silently spend real money.
247
+ */
248
+ declare function isFableModel(modelId: string): boolean;
249
+ declare function assertModelAllowed(modelId: string, allowFable: boolean): void;
250
+
251
+ /**
252
+ * Budget snapshot of the user's Claude subscription headroom.
253
+ *
254
+ * There is no official API that exposes remaining subscription quota, so every
255
+ * provider is estimate-driven. The fields model the two caps a subscription
256
+ * has: the rolling 5-hour session cap and the weekly cap (what `/usage` shows
257
+ * inside Claude Code). Token amounts are Sonnet-equivalent (see ModelCostTable).
258
+ *
259
+ * Billing nuance (verified 2026-06-11): from June 15, 2026 headless
260
+ * `claude -p` / Agent SDK usage on subscription plans draws from a separate
261
+ * monthly "Agent SDK credit" rather than the interactive weekly limits.
262
+ * Afterburner still models the interactive limits the user actually watches;
263
+ * see README "The economics" for what that means per backend.
264
+ */
265
+ interface Budget {
266
+ /** Is a 5-hour session window currently available? */
267
+ sessionAvailable: boolean;
268
+ /** Remaining share of the weekly cap, 0–100. */
269
+ weeklyRemainingPct: number;
270
+ /** Estimated remaining weekly budget in Sonnet-equivalent tokens. */
271
+ weeklyRemainingTokensEst: number;
272
+ }
273
+ interface BudgetProvider {
274
+ getBudget(): Promise<Budget>;
275
+ }
276
+
277
+ /**
278
+ * Budget values supplied by the user (config `budget.manual` or CLI flags).
279
+ * The reference provider for testing and the default until a reliable
280
+ * automated source for subscription usage exists.
281
+ */
282
+ declare class ManualBudgetProvider implements BudgetProvider {
283
+ private readonly budget;
284
+ constructor(budget: Budget);
285
+ getBudget(): Promise<Budget>;
286
+ }
287
+
288
+ /**
289
+ * Automatic budget provider: derives weekly SPENT tokens from the local
290
+ * Claude Code session transcripts (~/.claude/projects/<project>/<session>.jsonl)
291
+ * and subtracts them from a configured weekly allowance.
292
+ *
293
+ * Why transcripts: there is no documented headless command that exposes the
294
+ * `/usage` panel data, and ~/.claude/stats-cache.json can be months stale.
295
+ * Transcripts are written live by every Claude Code session, so they are the
296
+ * one local source that is always current. The same approach is used by
297
+ * community tools like ccusage.
298
+ *
299
+ * TODO(unverified): the transcript JSONL format is undocumented. Verified
300
+ * empirically against Claude Code 2.1.173 (2026-06-11): assistant entries
301
+ * carry `timestamp` (ISO 8601), `message.model`, `message.id`, and
302
+ * `message.usage` with `input_tokens` / `output_tokens` /
303
+ * `cache_creation_input_tokens` / `cache_read_input_tokens`. Parsing is
304
+ * defensive, unknown shapes are skipped, never fatal.
305
+ *
306
+ * Counting convention: input_tokens + output_tokens per assistant message
307
+ * (cache reads/writes excluded). This matches Claude Code's own
308
+ * stats aggregation and avoids cache traffic dwarfing the numbers. The weekly
309
+ * cap itself is not exposed anywhere locally, so `weeklyAllowanceSonnetTokens`
310
+ * stays a one-time calibration against what /usage shows for your plan.
311
+ */
312
+ interface TranscriptUsageSummary {
313
+ /** Sonnet-equivalent tokens spent inside the window. */
314
+ spentSonnetTokens: number;
315
+ /** Raw tokens per model id, for diagnostics. */
316
+ tokensByModel: Record<string, number>;
317
+ /** Number of assistant messages counted (after dedup). */
318
+ messagesCounted: number;
319
+ }
320
+ declare function defaultClaudeProjectsDir(): string;
321
+ interface ClaudeCodeTranscriptsOptions {
322
+ /** Estimated total weekly capacity in Sonnet-equivalent tokens (calibrate against /usage). */
323
+ weeklyAllowanceSonnetTokens: number;
324
+ costTable: ModelCostTable;
325
+ /**
326
+ * The 5-hour session cap cannot be derived from local data (the limit value
327
+ * is not exposed), so session availability stays a manual input.
328
+ */
329
+ sessionAvailable: boolean;
330
+ /** Rolling window; the weekly reset anchor is unknown, so 7 rolling days is the conservative read. */
331
+ windowDays?: number;
332
+ projectsDir?: string;
333
+ }
334
+ declare class ClaudeCodeTranscriptsBudgetProvider implements BudgetProvider {
335
+ private readonly opts;
336
+ constructor(opts: ClaudeCodeTranscriptsOptions);
337
+ getBudget(): Promise<Budget>;
338
+ }
339
+ /** Exported separately so the scan/dedup/conversion logic is unit-testable. */
340
+ declare function summarizeTranscriptUsage(projectsDir: string, since: Date, costTable: ModelCostTable): Promise<TranscriptUsageSummary>;
341
+
342
+ /**
343
+ * Local mirror of the `rate_limits` object Claude Code pipes to a statusLine
344
+ * command on stdin (verified against the Claude Code statusline docs,
345
+ * 2026-06-11): each window is optional and present only for Pro/Max
346
+ * subscribers after the first API response in a session.
347
+ * rate_limits.five_hour.used_percentage (0–100)
348
+ * rate_limits.five_hour.resets_at (unix epoch SECONDS)
349
+ * rate_limits.seven_day.{used_percentage,resets_at}
350
+ */
351
+ interface RateLimitWindow {
352
+ used_percentage?: number;
353
+ resets_at?: number;
354
+ }
355
+ interface RateLimits {
356
+ five_hour?: RateLimitWindow;
357
+ seven_day?: RateLimitWindow;
358
+ }
359
+ interface UsageCache {
360
+ /** ISO 8601 timestamp of when the statusLine hook captured this. */
361
+ capturedAt: string;
362
+ rateLimits: RateLimits;
363
+ /** model.display_name at capture time, for diagnostics. */
364
+ model?: string;
365
+ }
366
+ declare function defaultUsageCachePath(): string;
367
+ declare function readUsageCache(path?: string): Promise<UsageCache | null>;
368
+ declare function writeUsageCache(path: string, cache: UsageCache): Promise<void>;
369
+ interface UsageCacheBudgetOptions {
370
+ /** Total weekly capacity in Sonnet-equivalent tokens, maps the authoritative % to a token figure. */
371
+ weeklyAllowanceSonnetTokens: number;
372
+ /** Cache older than this is treated as unusable (fall back). */
373
+ maxAgeMs: number;
374
+ }
375
+ interface UsageCacheBudgetResult {
376
+ budget?: Budget;
377
+ /** Present when the cache can't yield a usable budget and the caller should fall back. */
378
+ fallbackReason?: string;
379
+ }
380
+ /**
381
+ * Maps a usage cache to a Budget, using the `resets_at` timestamps to reason
382
+ * about staleness so an old cache is self-correcting:
383
+ *
384
+ * - Weekly cap gates spend, so be conservative: if the cache is older than
385
+ * maxAge, or its weekly window has already reset since capture (so the
386
+ * cached % no longer reflects the current window), fall back to the
387
+ * transcript estimate rather than trust a possibly-optimistic number.
388
+ * - The 5-hour session cap is a boolean: if its window has reset
389
+ * (now ≥ resets_at) the session is available again regardless of the old %;
390
+ * otherwise it's available unless that window is maxed.
391
+ *
392
+ * Pure and deterministic (nowMs injected) so the freshness logic is testable.
393
+ */
394
+ declare function budgetFromUsageCache(cache: UsageCache, opts: UsageCacheBudgetOptions, nowMs: number): UsageCacheBudgetResult;
395
+
396
+ interface ClaudeUsageBudgetOptions {
397
+ weeklyAllowanceSonnetTokens: number;
398
+ maxAgeMs: number;
399
+ /** Used when the usage cache is missing, stale, or incomplete. */
400
+ fallback: BudgetProvider;
401
+ cachePath?: string;
402
+ /** Called with a human-readable reason whenever it falls back. */
403
+ onFallback?: (reason: string) => void;
404
+ }
405
+ /**
406
+ * Reads the authoritative subscription usage figures (5h/7d % + reset times)
407
+ * that Claude Code captures into the usage cache via the statusLine hook
408
+ * (`afterburner statusline install`). Falls back to the supplied provider
409
+ * (normally the transcript estimate) whenever the cache can't be used, so an
410
+ * autonomous run never blocks on a missing or stale cache.
411
+ */
412
+ declare class ClaudeUsageBudgetProvider implements BudgetProvider {
413
+ private readonly opts;
414
+ private readonly cachePath;
415
+ constructor(opts: ClaudeUsageBudgetOptions);
416
+ getBudget(): Promise<Budget>;
417
+ private fallBack;
418
+ }
419
+
420
+ interface BudgetProviderOptions {
421
+ /** Optional sink for diagnostic notes (e.g. "usage cache stale; falling back"). */
422
+ onNote?: (message: string) => void;
423
+ }
424
+ /**
425
+ * Picks the budget source from config:
426
+ * - 'manual': the values in budget.manual (and/or run-once flags).
427
+ * - 'claude-code-transcripts': spend computed from local Claude Code transcripts.
428
+ * - 'claude-usage': the authoritative subscription limits Claude Code captures
429
+ * via the statusLine hook, with the transcript estimate as fallback.
430
+ * Session availability is a manual input for the non-statusLine providers (the
431
+ * 5-hour cap can't be derived from local data).
432
+ */
433
+ declare function createBudgetProvider(config: AfterburnerConfig, opts?: BudgetProviderOptions): {
434
+ provider: BudgetProvider;
435
+ source: string;
436
+ };
437
+
438
+ interface TaskIdentity {
439
+ repoUrl: string;
440
+ category: TaskCategory;
441
+ /** Target path or finding identifier, whatever makes the task unique. */
442
+ target: string;
443
+ }
444
+ /**
445
+ * Deterministic task identity: a stable hash of (repo, category, target).
446
+ * Branch names and PR titles derive from it, the run store records it, and
447
+ * idempotency is enforced by refusing to re-run a fingerprint that already
448
+ * has an open or merged PR. The repo url is normalized first so the identity
449
+ * survives a config rewrite from a relative path to the absolute one (and two
450
+ * different repos reached via the same relative url can't collide).
451
+ */
452
+ declare function taskFingerprint(id: TaskIdentity): string;
453
+ declare function deriveBranchName(task: {
454
+ category: TaskCategory;
455
+ fingerprint: string;
456
+ }, branchPrefix: string): string;
457
+ declare function derivePrTitle(task: {
458
+ title: string;
459
+ fingerprint: string;
460
+ }): string;
461
+
462
+ interface CandidateTask {
463
+ repoUrl: string;
464
+ category: TaskCategory;
465
+ title: string;
466
+ /** Target path or finding identifier, the unique subject of the task. */
467
+ target: string;
468
+ /** Estimated cost in Sonnet-equivalent tokens (via ModelCostTable). */
469
+ estCostSonnetTokens: number;
470
+ fingerprint: string;
471
+ }
472
+ /**
473
+ * The replacement seam for a future Claude-powered ranker: exactly what the
474
+ * orchestrator consumes. (DeterministicTaskSelector also exposes a public
475
+ * rank() for inspection and tests, but implementations only owe select().)
476
+ */
477
+ interface TaskSelector {
478
+ /** Highest-value candidate that fits within the budget minus the safety margin. */
479
+ select(repo: RepoConfig, budget: Budget): Promise<CandidateTask | null>;
480
+ }
481
+ interface DeterministicSelectorOptions {
482
+ costTable: ModelCostTable;
483
+ modelByCategory: Record<TaskCategory, string>;
484
+ taskCategories: Record<TaskCategory, {
485
+ enabled: boolean;
486
+ weight: number;
487
+ }>;
488
+ safetyMarginTokens: number;
489
+ maxTaskTokens: number;
490
+ }
491
+ /**
492
+ * Rough per-task spend in the routed model's own tokens; converted to
493
+ * Sonnet-equivalent through the ModelCostTable at estimation time. These are
494
+ * deliberately coarse, the selector's judgment is the part designed to be
495
+ * replaced by a Claude-powered ranker later.
496
+ */
497
+ declare const BASE_TASK_TOKENS: Record<TaskCategory, number>;
498
+ /**
499
+ * The one way run-once, watch, and tests derive selector options from config,
500
+ * so they can never drift (mirrors createBudgetProvider / createRunner).
501
+ */
502
+ declare function createSelector(config: {
503
+ agent: {
504
+ modelByCategory: Record<TaskCategory, string>;
505
+ maxTaskTokens: number;
506
+ };
507
+ budget: {
508
+ safetyMarginTokens: number;
509
+ };
510
+ taskCategories: Record<TaskCategory, {
511
+ enabled: boolean;
512
+ weight: number;
513
+ }>;
514
+ }): DeterministicTaskSelector;
515
+ /**
516
+ * Deterministic, no-LLM selector: cheap static heuristics over a local
517
+ * checkout. Designed so a Claude-powered ranker can drop in behind the same
518
+ * TaskSelector interface later.
519
+ */
520
+ declare class DeterministicTaskSelector implements TaskSelector {
521
+ private readonly opts;
522
+ constructor(opts: DeterministicSelectorOptions);
523
+ rank(repo: RepoConfig, _budget: Budget): Promise<CandidateTask[]>;
524
+ select(repo: RepoConfig, budget: Budget): Promise<CandidateTask | null>;
525
+ private isCategoryEnabled;
526
+ /** Estimate flows through the ModelCostTable using the category's ROUTED model. */
527
+ private estimate;
528
+ private makeTask;
529
+ private docsCandidates;
530
+ private testGapCandidates;
531
+ private deadCodeCandidates;
532
+ }
533
+
534
+ type RunnerBackend = 'dry-run' | 'claude-code' | 'api-key';
535
+ type RunOutcome = 'dry-run' | 'pr-opened' | 'abandoned' | 'failed';
536
+ interface RunResult {
537
+ outcome: RunOutcome;
538
+ /** Always claude/-prefixed; never the default branch. */
539
+ branch: string;
540
+ prTitle: string;
541
+ prUrl?: string;
542
+ summary: string;
543
+ }
544
+ /**
545
+ * Executes ONE bounded task against a repo. Live backends commit only to a
546
+ * claude/-prefixed branch and open a PR, never push to the default branch,
547
+ * never merge. A killed run must leave a resumable claude/ branch, never a
548
+ * broken state.
549
+ */
550
+ interface AgentRunner {
551
+ readonly backend: RunnerBackend;
552
+ run(task: CandidateTask, repo: RepoConfig): Promise<RunResult>;
553
+ }
554
+
555
+ /** The default backend: reports exactly what a live run WOULD do. No side effects. */
556
+ declare class DryRunRunner implements AgentRunner {
557
+ readonly backend: "dry-run";
558
+ run(task: CandidateTask, repo: RepoConfig): Promise<RunResult>;
559
+ }
560
+
561
+ /**
562
+ * Recommended live backend (stub): wraps Claude Code headless using the
563
+ * user's locally authenticated subscription, preserving the leftover-quota
564
+ * economics.
565
+ *
566
+ * Verified invocation surface (https://code.claude.com/docs/en/headless.md and
567
+ * /en/cli-reference.md, 2026-06-11):
568
+ * claude -p --output-format json --permission-mode acceptEdits \
569
+ * --allowedTools "…" --max-turns N --model <routed-model>
570
+ * - JSON output carries `result`, `session_id`, and `total_cost_usd`.
571
+ * - Subscription OAuth credentials (from `claude /login`, or a
572
+ * `claude setup-token` → CLAUDE_CODE_OAUTH_TOKEN in CI) are the default
573
+ * auth for `-p`.
574
+ * - NEVER pass --bare: it skips OAuth/keychain reads, breaking subscription
575
+ * auth. Docs say --bare will become the `-p` default in a future release,
576
+ * revisit this invocation when that lands.
577
+ *
578
+ * TODO(unverified): exact prompt template, tool allowlist, and the PR-creation
579
+ * step (gh/glab vs API) are not implemented yet, live execution is out of
580
+ * scope for the scaffold.
581
+ */
582
+ declare class ClaudeCodeRunner implements AgentRunner {
583
+ readonly backend: "claude-code";
584
+ run(_task: CandidateTask, _repo: RepoConfig): Promise<RunResult>;
585
+ }
586
+ interface ClaudeCodeInvocation {
587
+ command: 'claude';
588
+ /** Fixed flags only, untrusted text never lands in argv. */
589
+ args: string[];
590
+ /** Sanitized child environment. */
591
+ env: Record<string, string | undefined>;
592
+ /** The task prompt, delivered via stdin. */
593
+ stdinPrompt: string;
594
+ }
595
+ /**
596
+ * Pure builder for the headless invocation, kept separate from the stub so the
597
+ * injection-safety properties are unit-testable today.
598
+ *
599
+ * Injection class defended against: repo contents, issue titles, PR bodies and
600
+ * comments are UNTRUSTED input. Interpolating them into a shell command string
601
+ * (or a CI `run:` block) lets `$(…)`, backticks or quote-breaking text execute
602
+ * arbitrary commands. Defense: spawn with an argv array (no shell), keep argv
603
+ * limited to fixed flags, and deliver all untrusted text via stdin and
604
+ * environment variables that the child reads through process.env.
605
+ */
606
+ declare function buildClaudeCodeInvocation(task: CandidateTask, repo: RepoConfig, opts: {
607
+ model: string;
608
+ maxTurns: number;
609
+ allowedTools: readonly string[];
610
+ }): ClaudeCodeInvocation;
611
+ /**
612
+ * Strip API-key auth from the child environment. Verified behavior
613
+ * (code.claude.com/docs/en/authentication.md, 2026-06-11): in `-p` mode a
614
+ * present ANTHROPIC_API_KEY is ALWAYS used and outranks the subscription
615
+ * login, a leftover exported key would silently bill the API instead of
616
+ * spending subscription quota. CLAUDE_CODE_OAUTH_TOKEN is kept: it IS
617
+ * subscription auth (the documented CI path).
618
+ */
619
+ declare function sanitizeSpawnEnv(env: Record<string, string | undefined>): Record<string, string | undefined>;
620
+
621
+ /**
622
+ * Universal fallback backend (interface stub only): drives the task through
623
+ * the Anthropic API with an API key. Unlike the claude-code backend this is
624
+ * BILLED PER TOKEN at API rates, every run costs real money instead of
625
+ * spending subscription quota you already paid for.
626
+ *
627
+ * TODO(unverified): implementation is out of scope for the scaffold.
628
+ */
629
+ declare class ApiKeyRunner implements AgentRunner {
630
+ readonly backend: "api-key";
631
+ run(_task: CandidateTask, _repo: RepoConfig): Promise<RunResult>;
632
+ }
633
+
634
+ /**
635
+ * Dry-run is the DEFAULT. Live execution requires BOTH opt-ins:
636
+ * 1. the --live flag on the invocation, AND
637
+ * 2. a live backend in the config (agent.backend !== 'dry-run').
638
+ * Either one missing → dry-run. This is a non-negotiable safety rule; do not
639
+ * collapse it into a single switch.
640
+ */
641
+ declare function createRunner(config: AfterburnerConfig, liveFlag: boolean): AgentRunner;
642
+
643
+ interface RunRecord {
644
+ timestamp: string;
645
+ repoUrl: string;
646
+ fingerprint: string;
647
+ category: TaskCategory;
648
+ title: string;
649
+ estCostSonnetTokens: number;
650
+ branch: string;
651
+ prUrl?: string;
652
+ outcome: RunOutcome;
653
+ }
654
+ /**
655
+ * Append-only run records, the canonical "what happened" surface (exposed via
656
+ * `afterburner log`) and the source for idempotency checks.
657
+ */
658
+ interface RunStore {
659
+ append(record: RunRecord): Promise<void>;
660
+ list(): Promise<RunRecord[]>;
661
+ /** Duplicate-PR guard: has this fingerprint already produced an open or merged PR? */
662
+ hasOpenOrMergedPr(fingerprint: string): Promise<boolean>;
663
+ }
664
+ declare class JsonlRunStore implements RunStore {
665
+ private readonly filePath;
666
+ constructor(filePath?: string);
667
+ append(record: RunRecord): Promise<void>;
668
+ list(): Promise<RunRecord[]>;
669
+ hasOpenOrMergedPr(fingerprint: string): Promise<boolean>;
670
+ }
671
+
672
+ /**
673
+ * Notification seam. Core ships ONLY ConsoleNotifier: the pull request is the
674
+ * real notification and the run store is the audit trail. This interface is
675
+ * the documented extension point for a future opt-in generic webhook, vendor
676
+ * SDKs are never bundled into core.
677
+ */
678
+ interface Notifier {
679
+ notify(record: RunRecord): Promise<void>;
680
+ }
681
+
682
+ declare class ConsoleNotifier implements Notifier {
683
+ notify(record: RunRecord): Promise<void>;
684
+ }
685
+
686
+ interface GateConfig {
687
+ minWeeklyHeadroomPct: number;
688
+ safetyMarginTokens: number;
689
+ requireSessionAvailable: boolean;
690
+ }
691
+ interface GateDecision {
692
+ go: boolean;
693
+ reason: string;
694
+ }
695
+ /**
696
+ * Both-caps gate, a non-negotiable rule encoded as a pure function: only fire
697
+ * when a session window is available AND the estimated cost plus the safety
698
+ * margin fits inside the remaining weekly budget (with the configured
699
+ * headroom). Reasons are returned, not just booleans, so every skipped run is
700
+ * explainable in the log.
701
+ */
702
+ declare function shouldIgnite(budget: Budget, estCostSonnetTokens: number, config: GateConfig): GateDecision;
703
+
704
+ interface WatchHandle {
705
+ stop(): void;
706
+ }
707
+ /**
708
+ * Foreground scheduler daemon (`afterburner watch`). Useful for development,
709
+ * temporary runs, and cron expressions native schedulers cannot express. For
710
+ * normal unattended use, prefer `afterburner schedule install`.
711
+ */
712
+ declare function startWatch(opts: {
713
+ cron: string;
714
+ timezone: string;
715
+ onTick: () => Promise<void>;
716
+ onError?: (error: unknown) => void;
717
+ }): WatchHandle;
718
+
719
+ type SupportedPlatform = 'darwin' | 'linux' | 'win32';
720
+ interface ScheduleArtifacts {
721
+ kind: 'launchd' | 'systemd-user' | 'schtasks';
722
+ /** Files to write (empty for schtasks, which is command-driven). */
723
+ files: Array<{
724
+ path: string;
725
+ content: string;
726
+ }>;
727
+ /** Commands the user runs to activate the schedule. */
728
+ activationHint: string;
729
+ /** Commands/steps to undo the installation. */
730
+ removalHint: string;
731
+ }
732
+ interface SimpleCronSchedule {
733
+ minute: number;
734
+ /** Explicit list of hours the job fires at. */
735
+ hours: number[];
736
+ }
737
+ /**
738
+ * Native schedulers don't speak cron uniformly (launchd wants calendar
739
+ * intervals, schtasks wants /SC switches), so we support the simple cron
740
+ * shapes Afterburner actually defaults to and fail loudly on anything fancier;
741
+ * `afterburner watch` handles full cron expressions.
742
+ *
743
+ * Supported: hourly ("M * * * *"), daily ("M H * * *"), every N hours
744
+ * (step syntax on the hour field), and specific hours ("M H1,H2 * * *").
745
+ */
746
+ declare function parseSimpleCron(cron: string): SimpleCronSchedule;
747
+ interface NativeScheduleOptions {
748
+ cron: string;
749
+ timezone: string;
750
+ /** Absolute path to the node binary. */
751
+ nodePath: string;
752
+ /** Absolute path to the afterburner CLI entry script. */
753
+ cliPath: string;
754
+ configPath?: string;
755
+ }
756
+ declare function generateScheduleArtifacts(platform: SupportedPlatform, opts: NativeScheduleOptions): ScheduleArtifacts;
757
+
758
+ interface RunOnceDeps {
759
+ config: AfterburnerConfig;
760
+ budgetProvider: BudgetProvider;
761
+ selector: TaskSelector;
762
+ runner: AgentRunner;
763
+ store: RunStore;
764
+ notifier: Notifier;
765
+ }
766
+ interface RepoRunOutcome {
767
+ repoUrl: string;
768
+ status: 'completed' | 'skipped';
769
+ reason: string;
770
+ task?: CandidateTask;
771
+ result?: RunResult;
772
+ record?: RunRecord;
773
+ }
774
+ /**
775
+ * One ignition cycle: budget → selection → gates → run → record. Encodes the
776
+ * non-negotiables, one bounded task per run, both-caps gating, fingerprint
777
+ * idempotency, and the Fable gate re-checked at run time.
778
+ */
779
+ declare function runOnce(deps: RunOnceDeps): Promise<RepoRunOutcome[]>;
780
+
781
+ export { type AfterburnerConfig, type AfterburnerUserConfig, type AgentConfig, type AgentRunner, ApiKeyRunner, BASE_TASK_TOKENS, type Budget, type BudgetConfig, type BudgetProvider, type BudgetProviderOptions, type CandidateTask, type ClaudeCodeInvocation, ClaudeCodeRunner, ClaudeCodeTranscriptsBudgetProvider, type ClaudeCodeTranscriptsOptions, type ClaudeUsageBudgetOptions, ClaudeUsageBudgetProvider, ConsoleNotifier, DEFAULT_MODEL_BY_CATEGORY, type DeterministicSelectorOptions, DeterministicTaskSelector, DryRunRunner, type GateConfig, type GateDecision, JsonlRunStore, type LoadedConfig, ManualBudgetProvider, type ModelCostTable, type ModelWeightEntry, type NativeScheduleOptions, type Notifier, type RateLimitWindow, type RateLimits, type RepoConfig, type RepoRunOutcome, type RunOnceDeps, type RunOutcome, type RunRecord, type RunResult, type RunStore, type RunnerBackend, type ScheduleArtifacts, type SupportedPlatform, TASK_CATEGORIES, TASK_TAXONOMY, type TaskCategory, type TaskCategoryInfo, type TaskIdentity, type TaskSelector, type TranscriptUsageSummary, type UsageCache, type UsageCacheBudgetOptions, type UsageCacheBudgetResult, type WatchHandle, assertModelAllowed, budgetFromUsageCache, buildClaudeCodeInvocation, claudeConfigDir, configDir, configSchema, createBudgetProvider, createModelCostTable, createRunner, createSelector, dataDir, defaultClaudeProjectsDir, defaultCostTable, defaultRunStorePath, defaultUsageCachePath, defineConfig, deriveBranchName, derivePrTitle, formatConfigError, generateScheduleArtifacts, isFableModel, loadConfig, mapConfigLoadError, parseSimpleCron, readUsageCache, repoConfigSchema, runOnce, sanitizeSpawnEnv, shouldIgnite, startWatch, summarizeTranscriptUsage, taskFingerprint, writeUsageCache };