tend-cli 0.1.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.
@@ -0,0 +1,1274 @@
1
+ import { Command } from "commander";
2
+ import { z } from "zod";
3
+ import { SimpleGit } from "simple-git";
4
+
5
+ //#region src/findings/finding.d.ts
6
+ declare const TOOLS: readonly ["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"];
7
+ declare const FindingSchema: z.ZodObject<{
8
+ id: z.ZodString;
9
+ retryId: z.ZodOptional<z.ZodString>;
10
+ tool: z.ZodEnum<["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"]>;
11
+ rule: z.ZodString;
12
+ category: z.ZodEnum<["bug", "smell", "dead-code", "duplication", "security", "secret", "vuln-dep"]>;
13
+ severity: z.ZodEnum<["error", "warning", "info"]>;
14
+ file: z.ZodString;
15
+ range: z.ZodObject<{
16
+ startLine: z.ZodNumber;
17
+ startCol: z.ZodNumber;
18
+ endLine: z.ZodNumber;
19
+ endCol: z.ZodNumber;
20
+ }, "strip", z.ZodTypeAny, {
21
+ startLine: number;
22
+ startCol: number;
23
+ endLine: number;
24
+ endCol: number;
25
+ }, {
26
+ startLine: number;
27
+ startCol: number;
28
+ endLine: number;
29
+ endCol: number;
30
+ }>;
31
+ message: z.ZodString;
32
+ helpUri: z.ZodOptional<z.ZodString>;
33
+ flowPath: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
+ file: z.ZodString;
35
+ line: z.ZodNumber;
36
+ }, "strip", z.ZodTypeAny, {
37
+ file: string;
38
+ line: number;
39
+ }, {
40
+ file: string;
41
+ line: number;
42
+ }>, "many">>;
43
+ remediation: z.ZodOptional<z.ZodString>;
44
+ track: z.ZodEnum<["ai-fix", "deterministic", "report-only"]>;
45
+ status: z.ZodEnum<["pending", "fixing", "fixed", "reverted", "unfixable", "skipped"]>;
46
+ attempts: z.ZodNumber;
47
+ revertReason: z.ZodOptional<z.ZodEnum<["broke-test", "suppression", "regression", "typecheck", "session-error"]>>;
48
+ firstSeenLoop: z.ZodNumber;
49
+ lastSeenLoop: z.ZodNumber;
50
+ inScope: z.ZodOptional<z.ZodBoolean>;
51
+ }, "strip", z.ZodTypeAny, {
52
+ id: string;
53
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
54
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
55
+ message: string;
56
+ rule: string;
57
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
58
+ severity: "error" | "warning" | "info";
59
+ file: string;
60
+ range: {
61
+ startLine: number;
62
+ startCol: number;
63
+ endLine: number;
64
+ endCol: number;
65
+ };
66
+ track: "ai-fix" | "deterministic" | "report-only";
67
+ attempts: number;
68
+ firstSeenLoop: number;
69
+ lastSeenLoop: number;
70
+ retryId?: string | undefined;
71
+ helpUri?: string | undefined;
72
+ flowPath?: {
73
+ file: string;
74
+ line: number;
75
+ }[] | undefined;
76
+ remediation?: string | undefined;
77
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
78
+ inScope?: boolean | undefined;
79
+ }, {
80
+ id: string;
81
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
82
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
83
+ message: string;
84
+ rule: string;
85
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
86
+ severity: "error" | "warning" | "info";
87
+ file: string;
88
+ range: {
89
+ startLine: number;
90
+ startCol: number;
91
+ endLine: number;
92
+ endCol: number;
93
+ };
94
+ track: "ai-fix" | "deterministic" | "report-only";
95
+ attempts: number;
96
+ firstSeenLoop: number;
97
+ lastSeenLoop: number;
98
+ retryId?: string | undefined;
99
+ helpUri?: string | undefined;
100
+ flowPath?: {
101
+ file: string;
102
+ line: number;
103
+ }[] | undefined;
104
+ remediation?: string | undefined;
105
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
106
+ inScope?: boolean | undefined;
107
+ }>;
108
+ type Finding = z.infer<typeof FindingSchema>;
109
+ type Tool = (typeof TOOLS)[number];
110
+ type Track = Finding["track"];
111
+ /** The components that give a finding its stable identity. */
112
+ type FingerprintInput = {
113
+ tool: string;
114
+ rule: string;
115
+ file: string;
116
+ line: number;
117
+ message: string;
118
+ };
119
+ /**
120
+ * Stable identity for a finding: hash(tool | rule | file | line | message).
121
+ * Same components → same fingerprint, across loops and runs.
122
+ */
123
+ declare function fingerprint(input: FingerprintInput): string; //#endregion
124
+ //#region src/findings/normalize.d.ts
125
+ /** A scanner-produced record, before tend assigns identity, track, and loop state. */
126
+ type RawFinding = {
127
+ tool: Tool;
128
+ rule: string;
129
+ category: Finding["category"];
130
+ severity: Finding["severity"];
131
+ file: string;
132
+ range: Finding["range"];
133
+ message: string;
134
+ helpUri?: string;
135
+ flowPath?: Finding["flowPath"];
136
+ remediation?: string;
137
+ };
138
+ /** Which track a tool's findings flow into. */
139
+ declare function trackForTool(tool: Tool): Track;
140
+ /** Turn a raw scanner record into a tracked `Finding` for the given loop. */
141
+ declare function normalize(raw: RawFinding, loop: number): Finding;
142
+
143
+ //#endregion
144
+ //#region src/findings/store.d.ts
145
+ type RevertReason$1 = NonNullable<Finding["revertReason"]>;
146
+ /** Holds Finding records keyed by fingerprint and tracks their state across loops. */
147
+ declare class FindingStore {
148
+ private readonly findings;
149
+ add(finding: Finding): void;
150
+ get(id: string): Finding | undefined;
151
+ all(): Finding[];
152
+ /**
153
+ * Diff a fresh audit against what the store knows, by fingerprint:
154
+ * - known but absent now → marked `fixed`
155
+ * - present both loops → stays as-is, carries attempts/history, bumps lastSeenLoop
156
+ * - new fingerprint → added `pending`, firstSeenLoop = loop
157
+ */
158
+ reconcile(fresh: Finding[], loop: number): void;
159
+ /** Findings matching every provided filter (track / status / file). */
160
+ query(filter: {
161
+ track?: Finding["track"];
162
+ status?: Finding["status"];
163
+ file?: string;
164
+ }): Finding[];
165
+ /** Record a failed fix attempt against a finding's fingerprint. */
166
+ recordFailedAttempt(id: string, reason: RevertReason$1): void;
167
+ /** A finding's per-issue budget is exhausted once it has used `budget` attempts. */
168
+ isBudgetExhausted(id: string, budget: number): boolean;
169
+ /** Serialize to a plain array — `report.json`'s findings section. */
170
+ toJSON(): Finding[];
171
+ /** Rebuild a store from serialized findings, validating each against the schema. */
172
+ static fromJSON(data: unknown): FindingStore;
173
+ }
174
+
175
+ //#endregion
176
+ //#region src/findings/router.d.ts
177
+ type RouteResult = {
178
+ aiFix: Finding[];
179
+ deterministic: Finding[];
180
+ reportOnly: Finding[];
181
+ skipped: Finding[];
182
+ };
183
+ /** Split findings into their assigned fix tracks; unknown tools are skipped with a warning. */
184
+ declare function route(findings: Finding[], opts?: {
185
+ warn?: (message: string) => void;
186
+ }): RouteResult;
187
+
188
+ //#endregion
189
+ //#region src/scanners/scanner.d.ts
190
+ type ScanContext = {
191
+ cwd: string;
192
+ /** Files in scope (e.g. changed vs HEAD); a scanner may ignore this if it scans wide. */
193
+ files: string[];
194
+ loop: number;
195
+ };
196
+ type SpawnResult = {
197
+ stdout: string;
198
+ stderr: string;
199
+ exitCode: number;
200
+ };
201
+ /** Runs a binary, never throwing on non-zero exit (parse decides), but may throw on timeout/ENOENT. */
202
+ type Spawn = (binary: string, args: string[], opts: {
203
+ cwd: string;
204
+ timeout?: number;
205
+ }) => Promise<SpawnResult>;
206
+ /** Resolves whether a binary is on PATH. */
207
+ type Which = (binary: string) => Promise<boolean>;
208
+ interface Scanner {
209
+ readonly tool: Tool;
210
+ readonly binary: string;
211
+ buildArgs(ctx: ScanContext): string[];
212
+ /** Parse raw process output into findings. Throws on malformed output. */
213
+ parse(raw: SpawnResult, ctx: ScanContext): RawFinding[];
214
+ }
215
+ type ScanResult = {
216
+ tool: Tool;
217
+ findings: Finding[];
218
+ skipped: boolean;
219
+ error?: string;
220
+ };
221
+ /** Outcome of a scanner this loop, for the report's scanner-status line. */
222
+ type ScannerStatusKind = "ran" | "skipped" | "failed";
223
+ type ScannerStatus$1 = {
224
+ tool: Tool;
225
+ status: ScannerStatusKind;
226
+ reason?: string;
227
+ };
228
+ /** Collapse a ScanResult to its reportable status: skipped → ran → failed (error present). */
229
+
230
+ declare function isAvailable(scanner: Scanner, which: Which): Promise<boolean>;
231
+ /**
232
+ * Shared run sequence for every scanner:
233
+ * availability → args → spawn → parse → normalize.
234
+ * Missing binary → skipped (not fatal). Timeout/spawn error or malformed output → error result.
235
+ */
236
+ declare function runScanner(scanner: Scanner, ctx: ScanContext, deps: {
237
+ which: Which;
238
+ spawn: Spawn;
239
+ timeout?: number;
240
+ }): Promise<ScanResult>;
241
+
242
+ //#endregion
243
+ //#region src/scanners/scope.d.ts
244
+ /**
245
+ * Files changed vs `HEAD` (tracked modifications/additions/renames plus untracked),
246
+ * scoped and re-based to `git`'s working directory — see `changedVsHead` in git/repo.ts
247
+ * for why this matters when tend runs from a subdirectory of the repo.
248
+ */
249
+ declare function changedFiles(git: SimpleGit): Promise<string[]>;
250
+ /** Keep only findings whose file is in the changed set. */
251
+ declare function filterToChanged(findings: Finding[], changed: string[]): Finding[];
252
+ /** Apply the fix scope: `--all` fixes everything, otherwise only changed files. */
253
+ declare function scopeFindings(findings: Finding[], opts: {
254
+ all: boolean;
255
+ changed: string[];
256
+ }): Finding[];
257
+
258
+ //#endregion
259
+ //#region src/gate/check.d.ts
260
+ type RevertReason = NonNullable<Finding["revertReason"]>;
261
+ type CheckResult = {
262
+ ok: true;
263
+ } | {
264
+ ok: false;
265
+ reason: RevertReason;
266
+ detail: string;
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/gate/gate.d.ts
271
+ type Check = {
272
+ name: string;
273
+ run: () => Promise<CheckResult> | CheckResult;
274
+ };
275
+ type GateOutcome = {
276
+ kept: true;
277
+ } | {
278
+ kept: false;
279
+ reason: RevertReason;
280
+ detail: string;
281
+ failedCheck: string;
282
+ };
283
+ /**
284
+ * Run the verification checks in order, stopping at the first rejection and
285
+ * surfacing its revert reason. A fix is kept only if every check passes.
286
+ */
287
+ declare function runGate(checks: Check[]): Promise<GateOutcome>;
288
+
289
+ //#endregion
290
+ //#region src/fixing/change-set.d.ts
291
+ type FileEdit = {
292
+ path: string;
293
+ contents: string;
294
+ };
295
+ /**
296
+ * The atomic unit of a fix: edits to one file plus (optionally) its sibling test,
297
+ * applied and reverted together. Captures each file's prior state on apply so a
298
+ * revert — even after a partial apply — restores the working tree exactly.
299
+ */
300
+ declare class ChangeSet {
301
+ private readonly edits;
302
+ /** path → original contents, or null if the file did not exist before. */
303
+ private readonly originals;
304
+ constructor(edits: FileEdit[]);
305
+ apply(): void;
306
+ revert(): void;
307
+ }
308
+
309
+ //#endregion
310
+ //#region src/fixing/dispatch.d.ts
311
+ type WorkUnit = {
312
+ /** The code file this unit owns (the group key). */
313
+ file: string;
314
+ /** Every file this worker reserves — the code file plus any sibling test. */
315
+ files: string[];
316
+ findings: Finding[];
317
+ };
318
+ /** Whether a repo-relative path is a test file (`*.test.*` / `*.spec.*`). */
319
+
320
+ /**
321
+ * Group findings into work units so each worker owns a disjoint set of files
322
+ * (a code file plus its sibling test). No two sessions ever touch the same file.
323
+ */
324
+ declare function planWork(findings: Finding[]): WorkUnit[];
325
+ /** Run each work unit through `runUnit`, capped at `concurrency` concurrent sessions. */
326
+ declare function dispatch<T>(units: WorkUnit[], runUnit: (unit: WorkUnit) => Promise<T>, opts: {
327
+ concurrency: number;
328
+ }): Promise<T[]>;
329
+
330
+ //#endregion
331
+ //#region src/session/types.d.ts
332
+ type SessionRequest = {
333
+ /** The file this session owns (plus its sibling test). */
334
+ file: string;
335
+ /** Findings to fix in this file. */
336
+ findings: Finding[];
337
+ /** The fully-rendered prompt for the AI. */
338
+ prompt: string;
339
+ };
340
+ /**
341
+ * Estimated AI cost/usage for a unit of work. `total_cost_usd` from Claude's
342
+ * stream-json `result` message is a **client-side estimate**, never authoritative
343
+ * billing — always surface it as "estimated AI cost".
344
+ */
345
+ type AiUsage = {
346
+ /** Claude's `total_cost_usd` estimate (USD). A client-side estimate, not a bill. */
347
+ estimatedCostUsd: number;
348
+ inputTokens: number;
349
+ outputTokens: number;
350
+ cacheCreationInputTokens: number;
351
+ cacheReadInputTokens: number;
352
+ /** Number of Claude sessions (result messages) observed. */
353
+ sessions: number;
354
+ };
355
+ /** Cost/token portion of usage parsed from a single stream (sessions tracked separately). */
356
+
357
+ /** A usage record with everything zeroed. */
358
+ declare const zeroUsage: () => AiUsage;
359
+ /** Sum two usage records field-by-field (used to roll usage up through the run). */
360
+ declare function addUsage(a: AiUsage, b: AiUsage): AiUsage;
361
+ type SessionResult = {
362
+ ok: true;
363
+ edits: FileEdit[];
364
+ usage?: AiUsage;
365
+ } | {
366
+ ok: false;
367
+ error: string;
368
+ rateLimited: boolean;
369
+ usage?: AiUsage;
370
+ };
371
+ /** One of the two interfaces in tend: drives an AI fix session. */
372
+ interface SessionRunner {
373
+ run(request: SessionRequest): Promise<SessionResult>;
374
+ }
375
+
376
+ //#endregion
377
+ //#region src/session/claude.d.ts
378
+ type ClaudeSpawn = (request: SessionRequest) => Promise<{
379
+ stdout: string;
380
+ exitCode: number;
381
+ }>;
382
+ /** Drives a real `claude -p` session and parses its stream-json into edits. */
383
+ declare class ClaudeSession implements SessionRunner {
384
+ private readonly deps;
385
+ constructor(deps: {
386
+ spawn: ClaudeSpawn;
387
+ });
388
+ run(request: SessionRequest): Promise<SessionResult>;
389
+ }
390
+
391
+ //#endregion
392
+ //#region src/git/snapshot.d.ts
393
+ /**
394
+ * A silent restore point for the working tree, stored as a git commit object pinned by a private
395
+ * ref (`refs/tend/snapshot`) — nothing committed to any branch, the editor sees no change. Backs
396
+ * `tend undo` (exact restore) and `tend diff` (only the tool's edits). Reuses git's content store,
397
+ * so the on-disk record is a 40-char id rather than a copy of every file.
398
+ */
399
+ declare class Snapshot {
400
+ private readonly cwd;
401
+ private readonly root;
402
+ private readonly sha;
403
+ private constructor();
404
+ static capture(git: SimpleGit, cwd: string): Promise<Snapshot>;
405
+ /** Serialize to a tiny object for `.tend/snapshot.json` (powers `undo` across invocations). */
406
+ toJSON(): {
407
+ cwd: string;
408
+ root: string;
409
+ sha: string;
410
+ };
411
+ static fromJSON(data: {
412
+ cwd: string;
413
+ root: string;
414
+ sha: string;
415
+ }): Snapshot;
416
+ /** Files whose contents differ from the snapshot, or that are new/deleted since it (sorted). */
417
+ changedSince(_git: SimpleGit): Promise<string[]>;
418
+ /** Restore a single file to its captured contents (worktree only — the user's index is untouched). */
419
+ restoreFile(rel: string): Promise<void>;
420
+ /** Restore the working tree exactly to the captured state (incl. deleting files created since). */
421
+ restore(_git: SimpleGit): Promise<void>;
422
+ }
423
+
424
+ //#endregion
425
+ //#region src/git/repo.d.ts
426
+ /** Refuse to run outside a git repo — the snapshot/restore safety net needs it. */
427
+ declare function assertGitRepo(git: SimpleGit): Promise<void>;
428
+ /**
429
+ * Files changed vs `HEAD`: tracked modifications/additions/renames plus untracked files,
430
+ * scoped and re-based to `git`'s working directory (so a run from `apps/foo` only sees
431
+ * `apps/foo`'s changes, pathed as the scanners path them).
432
+ */
433
+ declare function changedVsHead(git: SimpleGit): Promise<string[]>;
434
+ /**
435
+ * Concrete files under the given path(s) — tracked plus untracked (so newly-added files
436
+ * are scoped too, mirroring `changedVsHead`). `git ls-files` reports paths relative to
437
+ * `git`'s working directory and interprets the pathspecs the same way, so the result is
438
+ * already in the coordinate system the scanners and `filterToChanged` expect. Expanding to
439
+ * concrete files (not bare directories) matters: `filterToChanged` matches exact paths.
440
+ */
441
+
442
+ /** Revert a single file to its snapshot state. */
443
+ declare function revertFile(snapshot: Snapshot, file: string): Promise<void>;
444
+
445
+ //#endregion
446
+ //#region src/detect/package-manager.d.ts
447
+ type PackageManager = "pnpm" | "yarn" | "bun" | "npm";
448
+ /** Detect the package manager from the lockfile present; defaults to npm. */
449
+ declare function detectPackageManager(cwd: string): PackageManager;
450
+
451
+ //#endregion
452
+ //#region src/config/config.d.ts
453
+ declare const ConfigSchema: z.ZodObject<{
454
+ maxSessions: z.ZodDefault<z.ZodNumber>;
455
+ maxLoops: z.ZodDefault<z.ZodNumber>;
456
+ perIssueBudget: z.ZodDefault<z.ZodNumber>;
457
+ test: z.ZodOptional<z.ZodString>;
458
+ teethCheck: z.ZodDefault<z.ZodBoolean>;
459
+ includeTests: z.ZodDefault<z.ZodBoolean>;
460
+ /** Model passed to `claude -p` for fixes — an alias (sonnet/opus/haiku) or a full model id. */
461
+ model: z.ZodDefault<z.ZodString>;
462
+ /** Reasoning effort for fixes; unset → claude's own default. */
463
+ effort: z.ZodOptional<z.ZodEnum<["low", "medium", "high", "xhigh", "max"]>>;
464
+ tools: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
465
+ enabled: z.ZodDefault<z.ZodBoolean>;
466
+ configPath: z.ZodOptional<z.ZodString>;
467
+ }, "strip", z.ZodTypeAny, {
468
+ enabled: boolean;
469
+ configPath?: string | undefined;
470
+ }, {
471
+ enabled?: boolean | undefined;
472
+ configPath?: string | undefined;
473
+ }>>>;
474
+ }, "strip", z.ZodTypeAny, {
475
+ maxSessions: number;
476
+ maxLoops: number;
477
+ perIssueBudget: number;
478
+ teethCheck: boolean;
479
+ includeTests: boolean;
480
+ model: string;
481
+ tools: Record<string, {
482
+ enabled: boolean;
483
+ configPath?: string | undefined;
484
+ }>;
485
+ test?: string | undefined;
486
+ effort?: "low" | "medium" | "high" | "xhigh" | "max" | undefined;
487
+ }, {
488
+ maxSessions?: number | undefined;
489
+ maxLoops?: number | undefined;
490
+ perIssueBudget?: number | undefined;
491
+ test?: string | undefined;
492
+ teethCheck?: boolean | undefined;
493
+ includeTests?: boolean | undefined;
494
+ model?: string | undefined;
495
+ effort?: "low" | "medium" | "high" | "xhigh" | "max" | undefined;
496
+ tools?: Record<string, {
497
+ enabled?: boolean | undefined;
498
+ configPath?: string | undefined;
499
+ }> | undefined;
500
+ }>;
501
+ type TendConfig = z.infer<typeof ConfigSchema>;
502
+ /** CLI flags that can override config; only defined keys take effect. */
503
+ type CliOverrides = Partial<Pick<TendConfig, "maxSessions" | "maxLoops" | "perIssueBudget" | "test" | "teethCheck" | "includeTests" | "model" | "effort">>;
504
+ /**
505
+ * Load config via cosmiconfig (searching from `cwd`), validate with zod, and apply
506
+ * zero-config defaults when no file is found. Invalid config throws a clear message.
507
+ */
508
+ declare function loadConfig(cwd: string): Promise<TendConfig>;
509
+ /** Overlay CLI flags onto a loaded config (flags win). */
510
+ declare function applyCliOverrides(config: TendConfig, overrides: CliOverrides): TendConfig;
511
+
512
+ //#endregion
513
+ //#region src/report/retry-id.d.ts
514
+ type RetryIdGenerator = () => string;
515
+
516
+ //#endregion
517
+ //#region src/report/schema.d.ts
518
+ /** Ensure every finding in a persisted report has a human-facing id unique to that report. */
519
+ /** Per-scanner outcome for a run: did it run clean, get skipped, or fail (with a reason). */
520
+ declare const ScannerStatusSchema: z.ZodObject<{
521
+ tool: z.ZodEnum<["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"]>;
522
+ status: z.ZodEnum<["ran", "skipped", "failed"]>;
523
+ reason: z.ZodOptional<z.ZodString>;
524
+ }, "strip", z.ZodTypeAny, {
525
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
526
+ status: "skipped" | "ran" | "failed";
527
+ reason?: string | undefined;
528
+ }, {
529
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
530
+ status: "skipped" | "ran" | "failed";
531
+ reason?: string | undefined;
532
+ }>;
533
+ declare const BehaviorChangeSchema: z.ZodObject<{
534
+ findingId: z.ZodString;
535
+ file: z.ZodString;
536
+ note: z.ZodString;
537
+ }, "strip", z.ZodTypeAny, {
538
+ file: string;
539
+ findingId: string;
540
+ note: string;
541
+ }, {
542
+ file: string;
543
+ findingId: string;
544
+ note: string;
545
+ }>;
546
+ /**
547
+ * Estimated AI cost/usage for a run. `estimatedCostUsd` is Claude's client-side
548
+ * `total_cost_usd` estimate — never authoritative billing.
549
+ */
550
+ declare const AiUsageSchema: z.ZodObject<{
551
+ estimatedCostUsd: z.ZodNumber;
552
+ inputTokens: z.ZodNumber;
553
+ outputTokens: z.ZodNumber;
554
+ cacheCreationInputTokens: z.ZodNumber;
555
+ cacheReadInputTokens: z.ZodNumber;
556
+ sessions: z.ZodNumber;
557
+ }, "strip", z.ZodTypeAny, {
558
+ estimatedCostUsd: number;
559
+ inputTokens: number;
560
+ outputTokens: number;
561
+ cacheCreationInputTokens: number;
562
+ cacheReadInputTokens: number;
563
+ sessions: number;
564
+ }, {
565
+ estimatedCostUsd: number;
566
+ inputTokens: number;
567
+ outputTokens: number;
568
+ cacheCreationInputTokens: number;
569
+ cacheReadInputTokens: number;
570
+ sessions: number;
571
+ }>;
572
+ declare const ReportSchema: z.ZodObject<{
573
+ findings: z.ZodArray<z.ZodObject<{
574
+ id: z.ZodString;
575
+ retryId: z.ZodOptional<z.ZodString>;
576
+ tool: z.ZodEnum<["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"]>;
577
+ rule: z.ZodString;
578
+ category: z.ZodEnum<["bug", "smell", "dead-code", "duplication", "security", "secret", "vuln-dep"]>;
579
+ severity: z.ZodEnum<["error", "warning", "info"]>;
580
+ file: z.ZodString;
581
+ range: z.ZodObject<{
582
+ startLine: z.ZodNumber;
583
+ startCol: z.ZodNumber;
584
+ endLine: z.ZodNumber;
585
+ endCol: z.ZodNumber;
586
+ }, "strip", z.ZodTypeAny, {
587
+ startLine: number;
588
+ startCol: number;
589
+ endLine: number;
590
+ endCol: number;
591
+ }, {
592
+ startLine: number;
593
+ startCol: number;
594
+ endLine: number;
595
+ endCol: number;
596
+ }>;
597
+ message: z.ZodString;
598
+ helpUri: z.ZodOptional<z.ZodString>;
599
+ flowPath: z.ZodOptional<z.ZodArray<z.ZodObject<{
600
+ file: z.ZodString;
601
+ line: z.ZodNumber;
602
+ }, "strip", z.ZodTypeAny, {
603
+ file: string;
604
+ line: number;
605
+ }, {
606
+ file: string;
607
+ line: number;
608
+ }>, "many">>;
609
+ remediation: z.ZodOptional<z.ZodString>;
610
+ track: z.ZodEnum<["ai-fix", "deterministic", "report-only"]>;
611
+ status: z.ZodEnum<["pending", "fixing", "fixed", "reverted", "unfixable", "skipped"]>;
612
+ attempts: z.ZodNumber;
613
+ revertReason: z.ZodOptional<z.ZodEnum<["broke-test", "suppression", "regression", "typecheck", "session-error"]>>;
614
+ firstSeenLoop: z.ZodNumber;
615
+ lastSeenLoop: z.ZodNumber;
616
+ inScope: z.ZodOptional<z.ZodBoolean>;
617
+ }, "strip", z.ZodTypeAny, {
618
+ id: string;
619
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
620
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
621
+ message: string;
622
+ rule: string;
623
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
624
+ severity: "error" | "warning" | "info";
625
+ file: string;
626
+ range: {
627
+ startLine: number;
628
+ startCol: number;
629
+ endLine: number;
630
+ endCol: number;
631
+ };
632
+ track: "ai-fix" | "deterministic" | "report-only";
633
+ attempts: number;
634
+ firstSeenLoop: number;
635
+ lastSeenLoop: number;
636
+ retryId?: string | undefined;
637
+ helpUri?: string | undefined;
638
+ flowPath?: {
639
+ file: string;
640
+ line: number;
641
+ }[] | undefined;
642
+ remediation?: string | undefined;
643
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
644
+ inScope?: boolean | undefined;
645
+ }, {
646
+ id: string;
647
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
648
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
649
+ message: string;
650
+ rule: string;
651
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
652
+ severity: "error" | "warning" | "info";
653
+ file: string;
654
+ range: {
655
+ startLine: number;
656
+ startCol: number;
657
+ endLine: number;
658
+ endCol: number;
659
+ };
660
+ track: "ai-fix" | "deterministic" | "report-only";
661
+ attempts: number;
662
+ firstSeenLoop: number;
663
+ lastSeenLoop: number;
664
+ retryId?: string | undefined;
665
+ helpUri?: string | undefined;
666
+ flowPath?: {
667
+ file: string;
668
+ line: number;
669
+ }[] | undefined;
670
+ remediation?: string | undefined;
671
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
672
+ inScope?: boolean | undefined;
673
+ }>, "many">;
674
+ secrets: z.ZodArray<z.ZodObject<{
675
+ id: z.ZodString;
676
+ retryId: z.ZodOptional<z.ZodString>;
677
+ tool: z.ZodEnum<["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"]>;
678
+ rule: z.ZodString;
679
+ category: z.ZodEnum<["bug", "smell", "dead-code", "duplication", "security", "secret", "vuln-dep"]>;
680
+ severity: z.ZodEnum<["error", "warning", "info"]>;
681
+ file: z.ZodString;
682
+ range: z.ZodObject<{
683
+ startLine: z.ZodNumber;
684
+ startCol: z.ZodNumber;
685
+ endLine: z.ZodNumber;
686
+ endCol: z.ZodNumber;
687
+ }, "strip", z.ZodTypeAny, {
688
+ startLine: number;
689
+ startCol: number;
690
+ endLine: number;
691
+ endCol: number;
692
+ }, {
693
+ startLine: number;
694
+ startCol: number;
695
+ endLine: number;
696
+ endCol: number;
697
+ }>;
698
+ message: z.ZodString;
699
+ helpUri: z.ZodOptional<z.ZodString>;
700
+ flowPath: z.ZodOptional<z.ZodArray<z.ZodObject<{
701
+ file: z.ZodString;
702
+ line: z.ZodNumber;
703
+ }, "strip", z.ZodTypeAny, {
704
+ file: string;
705
+ line: number;
706
+ }, {
707
+ file: string;
708
+ line: number;
709
+ }>, "many">>;
710
+ remediation: z.ZodOptional<z.ZodString>;
711
+ track: z.ZodEnum<["ai-fix", "deterministic", "report-only"]>;
712
+ status: z.ZodEnum<["pending", "fixing", "fixed", "reverted", "unfixable", "skipped"]>;
713
+ attempts: z.ZodNumber;
714
+ revertReason: z.ZodOptional<z.ZodEnum<["broke-test", "suppression", "regression", "typecheck", "session-error"]>>;
715
+ firstSeenLoop: z.ZodNumber;
716
+ lastSeenLoop: z.ZodNumber;
717
+ inScope: z.ZodOptional<z.ZodBoolean>;
718
+ }, "strip", z.ZodTypeAny, {
719
+ id: string;
720
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
721
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
722
+ message: string;
723
+ rule: string;
724
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
725
+ severity: "error" | "warning" | "info";
726
+ file: string;
727
+ range: {
728
+ startLine: number;
729
+ startCol: number;
730
+ endLine: number;
731
+ endCol: number;
732
+ };
733
+ track: "ai-fix" | "deterministic" | "report-only";
734
+ attempts: number;
735
+ firstSeenLoop: number;
736
+ lastSeenLoop: number;
737
+ retryId?: string | undefined;
738
+ helpUri?: string | undefined;
739
+ flowPath?: {
740
+ file: string;
741
+ line: number;
742
+ }[] | undefined;
743
+ remediation?: string | undefined;
744
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
745
+ inScope?: boolean | undefined;
746
+ }, {
747
+ id: string;
748
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
749
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
750
+ message: string;
751
+ rule: string;
752
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
753
+ severity: "error" | "warning" | "info";
754
+ file: string;
755
+ range: {
756
+ startLine: number;
757
+ startCol: number;
758
+ endLine: number;
759
+ endCol: number;
760
+ };
761
+ track: "ai-fix" | "deterministic" | "report-only";
762
+ attempts: number;
763
+ firstSeenLoop: number;
764
+ lastSeenLoop: number;
765
+ retryId?: string | undefined;
766
+ helpUri?: string | undefined;
767
+ flowPath?: {
768
+ file: string;
769
+ line: number;
770
+ }[] | undefined;
771
+ remediation?: string | undefined;
772
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
773
+ inScope?: boolean | undefined;
774
+ }>, "many">;
775
+ depBumps: z.ZodArray<z.ZodObject<{
776
+ findingId: z.ZodString;
777
+ remediation: z.ZodString;
778
+ }, "strip", z.ZodTypeAny, {
779
+ remediation: string;
780
+ findingId: string;
781
+ }, {
782
+ remediation: string;
783
+ findingId: string;
784
+ }>, "many">;
785
+ flaggedBehaviorChanges: z.ZodArray<z.ZodObject<{
786
+ findingId: z.ZodString;
787
+ file: z.ZodString;
788
+ note: z.ZodString;
789
+ }, "strip", z.ZodTypeAny, {
790
+ file: string;
791
+ findingId: string;
792
+ note: string;
793
+ }, {
794
+ file: string;
795
+ findingId: string;
796
+ note: string;
797
+ }>, "many">;
798
+ scannerStatuses: z.ZodDefault<z.ZodArray<z.ZodObject<{
799
+ tool: z.ZodEnum<["sonarjs", "knip", "jscpd", "semgrep", "osv", "gitleaks"]>;
800
+ status: z.ZodEnum<["ran", "skipped", "failed"]>;
801
+ reason: z.ZodOptional<z.ZodString>;
802
+ }, "strip", z.ZodTypeAny, {
803
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
804
+ status: "skipped" | "ran" | "failed";
805
+ reason?: string | undefined;
806
+ }, {
807
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
808
+ status: "skipped" | "ran" | "failed";
809
+ reason?: string | undefined;
810
+ }>, "many">>;
811
+ aiUsage: z.ZodDefault<z.ZodObject<{
812
+ estimatedCostUsd: z.ZodNumber;
813
+ inputTokens: z.ZodNumber;
814
+ outputTokens: z.ZodNumber;
815
+ cacheCreationInputTokens: z.ZodNumber;
816
+ cacheReadInputTokens: z.ZodNumber;
817
+ sessions: z.ZodNumber;
818
+ }, "strip", z.ZodTypeAny, {
819
+ estimatedCostUsd: number;
820
+ inputTokens: number;
821
+ outputTokens: number;
822
+ cacheCreationInputTokens: number;
823
+ cacheReadInputTokens: number;
824
+ sessions: number;
825
+ }, {
826
+ estimatedCostUsd: number;
827
+ inputTokens: number;
828
+ outputTokens: number;
829
+ cacheCreationInputTokens: number;
830
+ cacheReadInputTokens: number;
831
+ sessions: number;
832
+ }>>;
833
+ loops: z.ZodNumber;
834
+ durationMs: z.ZodNumber;
835
+ exitStatus: z.ZodNumber;
836
+ }, "strip", z.ZodTypeAny, {
837
+ findings: {
838
+ id: string;
839
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
840
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
841
+ message: string;
842
+ rule: string;
843
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
844
+ severity: "error" | "warning" | "info";
845
+ file: string;
846
+ range: {
847
+ startLine: number;
848
+ startCol: number;
849
+ endLine: number;
850
+ endCol: number;
851
+ };
852
+ track: "ai-fix" | "deterministic" | "report-only";
853
+ attempts: number;
854
+ firstSeenLoop: number;
855
+ lastSeenLoop: number;
856
+ retryId?: string | undefined;
857
+ helpUri?: string | undefined;
858
+ flowPath?: {
859
+ file: string;
860
+ line: number;
861
+ }[] | undefined;
862
+ remediation?: string | undefined;
863
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
864
+ inScope?: boolean | undefined;
865
+ }[];
866
+ secrets: {
867
+ id: string;
868
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
869
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
870
+ message: string;
871
+ rule: string;
872
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
873
+ severity: "error" | "warning" | "info";
874
+ file: string;
875
+ range: {
876
+ startLine: number;
877
+ startCol: number;
878
+ endLine: number;
879
+ endCol: number;
880
+ };
881
+ track: "ai-fix" | "deterministic" | "report-only";
882
+ attempts: number;
883
+ firstSeenLoop: number;
884
+ lastSeenLoop: number;
885
+ retryId?: string | undefined;
886
+ helpUri?: string | undefined;
887
+ flowPath?: {
888
+ file: string;
889
+ line: number;
890
+ }[] | undefined;
891
+ remediation?: string | undefined;
892
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
893
+ inScope?: boolean | undefined;
894
+ }[];
895
+ depBumps: {
896
+ remediation: string;
897
+ findingId: string;
898
+ }[];
899
+ flaggedBehaviorChanges: {
900
+ file: string;
901
+ findingId: string;
902
+ note: string;
903
+ }[];
904
+ scannerStatuses: {
905
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
906
+ status: "skipped" | "ran" | "failed";
907
+ reason?: string | undefined;
908
+ }[];
909
+ aiUsage: {
910
+ estimatedCostUsd: number;
911
+ inputTokens: number;
912
+ outputTokens: number;
913
+ cacheCreationInputTokens: number;
914
+ cacheReadInputTokens: number;
915
+ sessions: number;
916
+ };
917
+ loops: number;
918
+ durationMs: number;
919
+ exitStatus: number;
920
+ }, {
921
+ findings: {
922
+ id: string;
923
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
924
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
925
+ message: string;
926
+ rule: string;
927
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
928
+ severity: "error" | "warning" | "info";
929
+ file: string;
930
+ range: {
931
+ startLine: number;
932
+ startCol: number;
933
+ endLine: number;
934
+ endCol: number;
935
+ };
936
+ track: "ai-fix" | "deterministic" | "report-only";
937
+ attempts: number;
938
+ firstSeenLoop: number;
939
+ lastSeenLoop: number;
940
+ retryId?: string | undefined;
941
+ helpUri?: string | undefined;
942
+ flowPath?: {
943
+ file: string;
944
+ line: number;
945
+ }[] | undefined;
946
+ remediation?: string | undefined;
947
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
948
+ inScope?: boolean | undefined;
949
+ }[];
950
+ secrets: {
951
+ id: string;
952
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
953
+ status: "pending" | "fixing" | "fixed" | "reverted" | "unfixable" | "skipped";
954
+ message: string;
955
+ rule: string;
956
+ category: "bug" | "smell" | "dead-code" | "duplication" | "security" | "secret" | "vuln-dep";
957
+ severity: "error" | "warning" | "info";
958
+ file: string;
959
+ range: {
960
+ startLine: number;
961
+ startCol: number;
962
+ endLine: number;
963
+ endCol: number;
964
+ };
965
+ track: "ai-fix" | "deterministic" | "report-only";
966
+ attempts: number;
967
+ firstSeenLoop: number;
968
+ lastSeenLoop: number;
969
+ retryId?: string | undefined;
970
+ helpUri?: string | undefined;
971
+ flowPath?: {
972
+ file: string;
973
+ line: number;
974
+ }[] | undefined;
975
+ remediation?: string | undefined;
976
+ revertReason?: "broke-test" | "suppression" | "regression" | "typecheck" | "session-error" | undefined;
977
+ inScope?: boolean | undefined;
978
+ }[];
979
+ depBumps: {
980
+ remediation: string;
981
+ findingId: string;
982
+ }[];
983
+ flaggedBehaviorChanges: {
984
+ file: string;
985
+ findingId: string;
986
+ note: string;
987
+ }[];
988
+ loops: number;
989
+ durationMs: number;
990
+ exitStatus: number;
991
+ scannerStatuses?: {
992
+ tool: "sonarjs" | "knip" | "jscpd" | "semgrep" | "osv" | "gitleaks";
993
+ status: "skipped" | "ran" | "failed";
994
+ reason?: string | undefined;
995
+ }[] | undefined;
996
+ aiUsage?: {
997
+ estimatedCostUsd: number;
998
+ inputTokens: number;
999
+ outputTokens: number;
1000
+ cacheCreationInputTokens: number;
1001
+ cacheReadInputTokens: number;
1002
+ sessions: number;
1003
+ } | undefined;
1004
+ }>;
1005
+ type Report = z.infer<typeof ReportSchema>;
1006
+ type BehaviorChange = z.infer<typeof BehaviorChangeSchema>;
1007
+ type ScannerStatus = z.infer<typeof ScannerStatusSchema>;
1008
+ type AiUsage$1 = z.infer<typeof AiUsageSchema>;
1009
+
1010
+ //#endregion
1011
+ //#region src/report/builder.d.ts
1012
+ /** Accumulates per-finding outcomes and run metadata into a validated report.json. */
1013
+ declare class ReportBuilder {
1014
+ private readonly generateRetryId?;
1015
+ private readonly outcomes;
1016
+ private readonly flagged;
1017
+ private scannerStatuses;
1018
+ constructor(generateRetryId?: RetryIdGenerator | undefined);
1019
+ /** Record (or update) a finding's final outcome by fingerprint. */
1020
+ recordOutcome(finding: Finding): void;
1021
+ recordOutcomes(findings: Finding[]): void;
1022
+ /** Flag a semantic test change for human review. */
1023
+ flagBehaviorChange(entry: BehaviorChange): void;
1024
+ /** Record per-scanner run outcomes (ran / skipped / failed) for the scanner-status line. */
1025
+ recordScannerStatuses(statuses: ScannerStatus[]): void;
1026
+ build(meta: {
1027
+ loops: number;
1028
+ durationMs: number;
1029
+ exitStatus: number;
1030
+ aiUsage?: AiUsage$1;
1031
+ }): Report;
1032
+ }
1033
+
1034
+ //#endregion
1035
+ //#region src/output/events.d.ts
1036
+ /** What happened to a file's fix attempt. "left" = in scope but never dispatched. */
1037
+ type FileOutcome = "fixed" | "reverted" | "left";
1038
+ type TendEvent = {
1039
+ type: "snapshot";
1040
+ } | {
1041
+ type: "detected";
1042
+ packageManager: string;
1043
+ typescript: boolean;
1044
+ testRunner?: string;
1045
+ } | {
1046
+ type: "scan-start";
1047
+ loop: number;
1048
+ } | {
1049
+ type: "audit";
1050
+ loop: number;
1051
+ findings: number;
1052
+ files: number;
1053
+ scanned?: number;
1054
+ } | {
1055
+ type: "loop-start";
1056
+ loop: number;
1057
+ files: string[];
1058
+ concurrency: number;
1059
+ } | {
1060
+ type: "file-start";
1061
+ loop: number;
1062
+ file: string;
1063
+ rule?: string;
1064
+ } | {
1065
+ type: "file-result";
1066
+ loop: number;
1067
+ file: string;
1068
+ outcome: FileOutcome;
1069
+ reason?: string;
1070
+ } | {
1071
+ type: "loop-complete";
1072
+ loop: number;
1073
+ fixed: number;
1074
+ } | {
1075
+ type: "done";
1076
+ exitStatus: number;
1077
+ };
1078
+ type Listener = (event: TendEvent) => void;
1079
+ /** Minimal synchronous event bus. With no listener, emit is a no-op (silent mode). */
1080
+ declare class EventBus {
1081
+ private readonly listeners;
1082
+ on(listener: Listener): () => void;
1083
+ emit(event: TendEvent): void;
1084
+ }
1085
+
1086
+ //#endregion
1087
+ //#region src/output/theme.d.ts
1088
+ /**
1089
+ * The visual language. Restraint is the point: one accent color used for ~2 things (the
1090
+ * wordmark + the active spinner), muted semantic colors for outcomes, everything else
1091
+ * achromatic. Hierarchy comes from indentation + dim metadata, not from color.
1092
+ */
1093
+ type Theme = {
1094
+ /** The single accent — wordmark + active spinner only. */
1095
+ accent: Style;
1096
+ /** Soft green — a fix that landed. */
1097
+ fixed: Style;
1098
+ /** Soft amber — a fix that was reverted. */
1099
+ reverted: Style;
1100
+ /** Soft red — an error / something that needs the human. */
1101
+ error: Style;
1102
+ /** Dim grey — metadata, queued, rules, hints. */
1103
+ dim: Style;
1104
+ bold: Style;
1105
+ plain: Style;
1106
+ /** The "t e n d" wordmark: a subtle gradient when truecolor is available, else flat. */
1107
+ wordmark: () => string;
1108
+ glyph: Glyphs;
1109
+ };
1110
+ type Style = (s: string) => string;
1111
+ type Glyphs = {
1112
+ fixed: string;
1113
+ reverted: string;
1114
+ left: string;
1115
+ scanned: string;
1116
+ bullet: string;
1117
+ rule: string;
1118
+ arrow: string;
1119
+ spinner: string[];
1120
+ };
1121
+
1122
+ //#endregion
1123
+ //#region src/output/summary.d.ts
1124
+ type SummaryOptions = {
1125
+ theme?: Theme;
1126
+ verbose?: boolean;
1127
+ plain?: boolean;
1128
+ };
1129
+ /**
1130
+ * The final summary: a real headline (fixed / couldn't-fix / left / secrets + elapsed),
1131
+ * grouped by what the user must do, with revert reasons surfaced per file, and ending in
1132
+ * next-step affordances. Brief by default; `--verbose` adds the full per-finding listing.
1133
+ */
1134
+ declare function renderSummary(report: Report, opts?: SummaryOptions): string;
1135
+ type RemainingKey = "secrets" | "security" | "couldnt-fix" | "review";
1136
+ type RemainingGroup = {
1137
+ key: RemainingKey;
1138
+ title: string;
1139
+ count: number;
1140
+ };
1141
+ /**
1142
+ * Group the issues that still need a human, ordered by urgency:
1143
+ * secrets → security → couldn't-fix → needs-review. Empty groups are omitted.
1144
+ */
1145
+ declare function groupRemaining(report: Report): RemainingGroup[];
1146
+
1147
+ //#endregion
1148
+ //#region src/orchestrator.d.ts
1149
+ type AuditResult = {
1150
+ findings: Finding[];
1151
+ allScannersMissing?: boolean;
1152
+ scanned?: number;
1153
+ scannerStatuses?: ScannerStatus$1[];
1154
+ };
1155
+ type FixOutcome = {
1156
+ kept: boolean;
1157
+ reason?: RevertReason;
1158
+ usage?: AiUsage;
1159
+ };
1160
+ type OrchestrateDeps = {
1161
+ /** Run the scanners for a loop and return normalized findings. */
1162
+ audit: (loop: number) => Promise<AuditResult>;
1163
+ /** Fix one work unit (session + gate); returns whether the fix was kept. */
1164
+ fixUnit: (unit: WorkUnit, loop: number) => Promise<FixOutcome>;
1165
+ config: {
1166
+ maxLoops: number;
1167
+ perIssueBudget: number;
1168
+ maxSessions: number;
1169
+ includeTests?: boolean;
1170
+ };
1171
+ /** Restrict findings to the fix scope (changed files); defaults to all. */
1172
+ inScope?: (findings: Finding[]) => Finding[];
1173
+ bus?: EventBus;
1174
+ };
1175
+ type Termination = "converged" | "max-loops" | "no-progress" | "no-scanners";
1176
+ type OrchestrateResult = {
1177
+ termination: Termination;
1178
+ loops: number;
1179
+ exitStatus: number;
1180
+ findings: Finding[];
1181
+ secrets: Finding[];
1182
+ depBumps: Finding[];
1183
+ scannerStatuses: ScannerStatus$1[];
1184
+ /** Estimated AI cost/usage summed across every fix attempt (including reverted ones). */
1185
+ usage: AiUsage;
1186
+ };
1187
+ /**
1188
+ * The scan → fix → re-audit loop. Terminates on the first of: converged (0 fixable),
1189
+ * no-progress (no dispatchable units or an attempted loop changed no attempt/status
1190
+ * state), per-issue budget exhaustion (mark unfixable, keep going), or max-loops.
1191
+ */
1192
+ declare function orchestrate(deps: OrchestrateDeps): Promise<OrchestrateResult>;
1193
+
1194
+ //#endregion
1195
+ //#region src/cli.d.ts
1196
+ type CliHandlers = {
1197
+ run: (opts: {
1198
+ paths?: string[];
1199
+ all?: boolean;
1200
+ maxLoops?: number;
1201
+ maxSessions?: number;
1202
+ model?: string;
1203
+ effort?: string;
1204
+ includeTests?: boolean;
1205
+ plain?: boolean;
1206
+ color?: boolean;
1207
+ verbose?: boolean;
1208
+ }) => Promise<void> | void;
1209
+ diff: () => Promise<void> | void;
1210
+ undo: () => Promise<void> | void;
1211
+ show: (id: string) => Promise<void> | void;
1212
+ retry: (id: string) => Promise<void> | void;
1213
+ };
1214
+ /** Build the commander program wiring each subcommand to a handler. */
1215
+ declare function buildProgram(handlers: CliHandlers): Command;
1216
+
1217
+ //#endregion
1218
+ //#region src/commands/run.d.ts
1219
+ type RunDeps = OrchestrateDeps & {
1220
+ now?: () => number;
1221
+ };
1222
+ /** `tend run` — wire audit → fix loop → report. */
1223
+ declare function runCommand(deps: RunDeps): Promise<{
1224
+ report: Report;
1225
+ exitStatus: number;
1226
+ }>;
1227
+
1228
+ //#endregion
1229
+ //#region src/commands/diff.d.ts
1230
+ /** `tend diff` — the files the tool edited (snapshot vs now), the dev's own changes filtered out. */
1231
+ declare function diffCommand(deps: {
1232
+ snapshot: Snapshot;
1233
+ git: SimpleGit;
1234
+ }): Promise<string[]>;
1235
+
1236
+ //#endregion
1237
+ //#region src/commands/undo.d.ts
1238
+ /** `tend undo` — restore the pre-run snapshot exactly. */
1239
+ declare function undoCommand(deps: {
1240
+ snapshot: Snapshot;
1241
+ git: SimpleGit;
1242
+ }): Promise<void>;
1243
+
1244
+ //#endregion
1245
+ //#region src/commands/show.d.ts
1246
+ /** `tend show <id>` — full detail on one finding: attempts, revert reason, taint flow path. */
1247
+ declare function showCommand(id: string, findings: Finding[]): string;
1248
+
1249
+ //#endregion
1250
+ //#region src/commands/retry.d.ts
1251
+ type RetryDeps = {
1252
+ report?: Report;
1253
+ findings?: Finding[];
1254
+ baseBudget: number;
1255
+ /** Re-run the fix for a finding with the given (larger) attempt budget. */
1256
+ runFix: (finding: Finding, budget: number) => Promise<FixOutcome>;
1257
+ };
1258
+ type RetryResult = {
1259
+ outcome: "fixed";
1260
+ finding: Finding;
1261
+ budget: number;
1262
+ } | {
1263
+ outcome: "reverted";
1264
+ finding: Finding;
1265
+ budget: number;
1266
+ reason: RevertReason;
1267
+ } | {
1268
+ error: string;
1269
+ };
1270
+ /** `tend retry <id>` — re-attempt a stubborn finding with a larger attempt budget. */
1271
+ declare function retryCommand(id: string, deps: RetryDeps): Promise<RetryResult>;
1272
+
1273
+ //#endregion
1274
+ export { AiUsage, AuditResult, ChangeSet, Check, ClaudeSession, CliHandlers, ConfigSchema, EventBus, FileEdit, Finding, FindingSchema, FindingStore, FixOutcome, GateOutcome, OrchestrateDeps, OrchestrateResult, PackageManager, RawFinding, Report, ReportBuilder, ReportSchema, RetryDeps, RetryResult, RouteResult, RunDeps, ScanContext, ScanResult, Scanner, SessionRequest, SessionResult, SessionRunner, Snapshot, Spawn, TendConfig, TendEvent, Termination, Tool, Track, Which, WorkUnit, addUsage, applyCliOverrides, assertGitRepo, buildProgram, changedFiles, changedVsHead, detectPackageManager, diffCommand, dispatch, filterToChanged, fingerprint, groupRemaining, isAvailable, loadConfig, normalize, orchestrate, planWork, renderSummary, retryCommand, revertFile, route, runCommand, runGate, runScanner, scopeFindings, showCommand, trackForTool, undoCommand, zeroUsage };