@aihq/harness 0.2.0-rc.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,706 @@
1
+ import { Command } from 'commander';
2
+ import { z } from 'zod';
3
+
4
+ type Posture = "vibe" | "team" | "enterprise";
5
+ type PostureSource = "flag" | "marker" | "env" | "default" | "org-floor";
6
+
7
+ /**
8
+ * The single external-process seam for the whole harness. PowerShell, nvidia-smi,
9
+ * curl, gitleaks, docker — every subprocess goes through a {@link Runner}. Tests
10
+ * inject a fake so no unit test ever spawns a real process or touches the network.
11
+ */
12
+ interface RunResult {
13
+ /** Process exit code; null when terminated by signal. */
14
+ code: number | null;
15
+ stdout: string;
16
+ stderr: string;
17
+ /** True when the executable could not be found / spawned (ENOENT, timeout). */
18
+ spawnError?: boolean;
19
+ }
20
+ interface RunOptions {
21
+ input?: string;
22
+ cwd?: string;
23
+ env?: NodeJS.ProcessEnv;
24
+ timeoutMs?: number;
25
+ }
26
+ type Runner = (argv: string[], opts?: RunOptions) => Promise<RunResult>;
27
+ /**
28
+ * Default runner backed by `child_process.execFile`. Never rejects on non-zero
29
+ * exit — it resolves a {@link RunResult} so callers branch on `code`/`spawnError`
30
+ * instead of try/catch. `argv[0]` is the executable; remaining items are args
31
+ * (no shell, so no quoting/injection surface).
32
+ */
33
+ declare const defaultRunner: Runner;
34
+ /**
35
+ * Build a fake runner for tests. The handler maps an argv to a partial result;
36
+ * returning `undefined` yields a clean exit-0 with empty output.
37
+ */
38
+ declare function fakeRunner(handler: (argv: string[], opts?: RunOptions) => Partial<RunResult> | undefined): Runner;
39
+ /** A runner that fails as if no executable exists — for "tool absent" test paths. */
40
+ declare const missingToolRunner: Runner;
41
+
42
+ type Platform = "windows" | "darwin" | "linux";
43
+ type GpuVendor = "nvidia" | "apple" | "amd" | "none";
44
+ type AccelBackend = "cuda" | "mps" | "rocm" | "cpu";
45
+ type EnvShell = "posix" | "powershell";
46
+ interface GpuInfo {
47
+ vendor: GpuVendor;
48
+ backend: AccelBackend;
49
+ /** Total VRAM in GB; 0 when unknown or no discrete GPU. */
50
+ vramGb: number;
51
+ name?: string;
52
+ }
53
+ interface VdiInfo {
54
+ isVdi: boolean;
55
+ /** The signal that matched, or why none did. */
56
+ reason: string;
57
+ kind?: "citrix" | "workspaces" | "res" | "rdp" | "generic";
58
+ }
59
+ interface CertEntry {
60
+ subject: string;
61
+ /** PEM-encoded certificate (BEGIN/END CERTIFICATE). */
62
+ pem: string;
63
+ }
64
+ /**
65
+ * OS-specific behaviour behind one interface. Only the adapter matching the host
66
+ * is `verified` (smoke-tested on real metal); the others are implemented and
67
+ * unit-tested against captured fixture output but flagged unverified. Every
68
+ * method that shells out does so through the injected {@link Runner}.
69
+ */
70
+ interface HostAdapter {
71
+ readonly platform: Platform;
72
+ readonly verified: boolean;
73
+ /** Corporate root CAs whose subject contains `pattern`, from the OS trust store. */
74
+ trustStoreCerts(pattern: string): Promise<CertEntry[]>;
75
+ /** The argv that would restrict `path` to the current user (icacls/chmod). Not executed here. */
76
+ lockDownFileArgv(path: string): string[];
77
+ /** The argv that creates a directory symlink/junction at `linkPath` → `targetPath`. */
78
+ symlinkDirArgv(linkPath: string, targetPath: string): string[];
79
+ cpuPhysicalCores(): Promise<number>;
80
+ totalRamGb(): Promise<number>;
81
+ gpu(): Promise<GpuInfo>;
82
+ detectVdi(): VdiInfo;
83
+ /** Local, non-synced scratch root for caches/SQLite on this host. */
84
+ scratchDir(user: string): string;
85
+ /** Shell profile file(s) where env exports belong. */
86
+ shellProfilePaths(): string[];
87
+ envShell(): EnvShell;
88
+ /**
89
+ * argv that persists a user-level env var SESSION-INDEPENDENTLY — i.e. where
90
+ * GUI-launched apps (Kiro, Claude Desktop, an IDE) inherit it, not just new
91
+ * shells. On Windows that is the per-user registry environment
92
+ * (`[Environment]::SetEnvironmentVariable(k,v,'User')`); on POSIX the durable
93
+ * seam is already the shell-profile `envblock`, so this returns `[]` (the
94
+ * caller emits no exec). A local mutation only — never contacts a remote.
95
+ */
96
+ persistentEnvArgv(key: string, value: string): string[];
97
+ /**
98
+ * Absolute path to npm's `npm-cli.js` relative to the running Node binary, used
99
+ * to compose the doc'd npm self-heal (`node <npm-cli.js> install -g npm`).
100
+ * `undefined` when it cannot be located (npm not installed alongside Node).
101
+ */
102
+ npmCliPath(): string | undefined;
103
+ /**
104
+ * argv for a read-only TLS reachability probe of `url`. Exit 0 = handshake OK;
105
+ * a non-zero exit = TLS/proxy failure; a spawn error (tool absent) lets the
106
+ * caller `skip`. Never mutates; the URL is a trusted module constant.
107
+ */
108
+ tlsProbeArgv(url: string): string[];
109
+ }
110
+ /** Construction shape shared by the concrete adapters. */
111
+ type AdapterFactory = (run: Runner, env: NodeJS.ProcessEnv) => HostAdapter;
112
+ /** Wrap raw base64 DER into a PEM certificate block with 64-char lines. */
113
+ declare function derBase64ToPem(base64: string): string;
114
+ /** Validate a CA subject-match pattern (used in shell commands). Conservative allowlist. */
115
+ declare function safeCaPattern(pattern: string): string;
116
+ /**
117
+ * Cross-platform, env-based VDI signals shared by every host adapter, checked
118
+ * before the per-OS heuristics:
119
+ * - `AIH_VDI_KIND=<citrix|workspaces|res|rdp|generic>` lets fleet imaging pin the
120
+ * platform deterministically — the only reliable way to flag Amazon WorkSpaces
121
+ * or AVD, which expose no dependable env marker (this is what finally wires the
122
+ * `workspaces` kind into a reachable code path);
123
+ * - `AIH_FORCE_VDI=1` forces a generic VDI (back-compat; now honored on Windows
124
+ * too, which previously ignored it);
125
+ * - VMware / Omnissa Horizon exports `ViewClient_*` into the session, a genuine
126
+ * env-detectable marker.
127
+ * Returns undefined when nothing matches, so the caller's OS-specific heuristics run.
128
+ */
129
+ declare function vdiFromEnv(env: NodeJS.ProcessEnv): VdiInfo | undefined;
130
+
131
+ /**
132
+ * The AI coding CLIs the harness can target. Capabilities that install agent
133
+ * tooling (ECC, Superpowers) and write IDE adapters use the user's selection
134
+ * (`--cli claude,codex` or `--all-tools`) so the harness only touches the tools
135
+ * the user actually runs. Names match each tool's own CLI / config conventions.
136
+ */
137
+ declare const SUPPORTED_CLIS: readonly ["claude", "codex", "cursor", "antigravity", "gemini", "copilot", "windsurf", "opencode", "zed", "kimi", "kiro"];
138
+ type Cli = (typeof SUPPORTED_CLIS)[number];
139
+
140
+ interface EnvVar {
141
+ key: string;
142
+ value: string;
143
+ }
144
+ /** Format a single env assignment for the target shell. */
145
+ declare function formatExport(v: EnvVar, shell: EnvShell): string;
146
+ /**
147
+ * Insert or replace the aih-managed block for `scope` in a shell profile.
148
+ *
149
+ * Idempotent by construction: the region between the begin/end markers is
150
+ * replaced wholesale, so re-running with the same vars yields byte-identical
151
+ * output and lines outside the markers are never touched. Preserves the file's
152
+ * existing EOL style (CRLF vs LF).
153
+ */
154
+ declare function upsertManagedBlock(existing: string, scope: string, vars: EnvVar[], shell: EnvShell): string;
155
+ /**
156
+ * Insert or replace the aih-managed `scope` block carrying arbitrary `body` text
157
+ * (the format-agnostic core of {@link upsertManagedBlock}). The `#`-comment markers
158
+ * are valid in any `#`-commented format — shell profiles AND TOML — so `aih mcp`
159
+ * reuses this to fold its `[mcp_servers.*]` tables into Codex's `~/.codex/config.toml`
160
+ * without clobbering the rest of the file. Idempotent: the region between the markers
161
+ * is replaced wholesale, content outside is untouched, and the file's EOL style
162
+ * (CRLF vs LF) is preserved.
163
+ */
164
+ declare function upsertTextBlock(existing: string, scope: string, body: string): string;
165
+ /** Remove the managed block for `scope` if present (used by uninstall paths). */
166
+ declare function removeManagedBlock(existing: string, scope: string): string;
167
+
168
+ /**
169
+ * A minimal question/answer seam so interactive prompts stay testable: production
170
+ * code wires {@link makeReadlinePrompter}; tests inject a fake that returns canned
171
+ * answers. The harness stays non-interactive by default — a prompter is only wired
172
+ * when the user explicitly opts in (e.g. `--detect`) AND the session is a TTY.
173
+ */
174
+ interface Prompter {
175
+ /** Print `question`, read one line, and return the trimmed answer ("" on bare Enter/EOF). */
176
+ ask(question: string): Promise<string>;
177
+ }
178
+
179
+ /** A single verification outcome produced by a probe action or `doctor`. */
180
+ type Verdict = "pass" | "fail" | "skip";
181
+ /**
182
+ * Closed taxonomy of routable verification outcomes. Each member maps 1:1 to a
183
+ * real `fail`/`skip` emitter (see docs/research/check-code-taxonomy-plan.md) so a
184
+ * consumer — support templates, run-ledger findings — can `switch` over it
185
+ * exhaustively rather than string-match `detail` (which rots on a reword). Keep it
186
+ * sealed: a new failure mode means a new member here PLUS the `code` set at the
187
+ * emitter; never derive a code by matching `detail`.
188
+ */
189
+ type CheckCode = "env.node-runtime" | "env.git-missing" | "env.dev-tool-missing" | "env.tool-install-blocked" | "cert.ca-missing" | "tls.verify-failed" | "npm.runtime-broken" | "path.missing" | "mcp.blocked" | "mcp.uv-missing" | "mcp.config-missing" | "mcp.unvendored-offline" | "mcp.policy-denied" | "mcp.hardcoded-secret" | "mcp.allowlist-drift" | "cli.not-detected" | "cli.config-only" | "cli.bootloader-missing" | "cli.bootloader-drift" | "cli.wont-load" | "canon.router-missing" | "canon.context-dir-missing" | "canon.lint-failed" | "canon.adoptable" | "canon.cli-native-unmigrated" | "secrets.plaintext-detected" | "guardrails.gitleaks-missing" | "usage.no-data" | "scale.code-review-graph-missing" | "contract.path-unportable" | "contract.stale" | "org-policy.drift" | "report.context-over-budget" | "report.low-adoption" | "report.contract-untrue" | "trust.fetch-blocked" | "trust.detector-unavailable" | "trust.hidden-unicode" | "trust.prompt-injection" | "trust.source-changed" | "trust.auto-exec-hook" | "trust.dependency-confusion" | "trust.typosquat" | "trust.malicious-code" | "trust.source-drift" | "trust.unpinned-dependency" | "trust.untrusted-publisher" | "trust.unsigned-source";
190
+ interface Check {
191
+ name: string;
192
+ verdict: Verdict;
193
+ detail?: string;
194
+ /**
195
+ * Stable machine code for routing (support templates, run-ledger findings). Set
196
+ * ONLY on `fail`/`skip` emitters a consumer keys off — never on a `pass`, and
197
+ * never derived from `detail`. Absent ⇒ not yet ticket-routed. Optional by
198
+ * design, so a Check that omits it serializes byte-for-byte as before.
199
+ */
200
+ code?: CheckCode;
201
+ /** Optional repo-relative artifact location for file-backed findings. */
202
+ location?: {
203
+ uri: string;
204
+ startLine?: number;
205
+ };
206
+ /** Optional stable fingerprint for code-scanning de-dupe. */
207
+ fingerprint?: string;
208
+ }
209
+ /**
210
+ * Accumulates {@link Check}s and renders a fail-closed report. `skip` never fails
211
+ * the run (used when a tool/daemon is absent); only `fail` flips the exit code.
212
+ */
213
+ declare class VerificationReport {
214
+ readonly checks: Check[];
215
+ add(check: Check): this;
216
+ pass(name: string, detail?: string): this;
217
+ fail(name: string, detail?: string): this;
218
+ skip(name: string, detail?: string): this;
219
+ get ok(): boolean;
220
+ counts(): Record<Verdict, number>;
221
+ /** 0 when no check failed, 1 otherwise. */
222
+ exitCode(): number;
223
+ toJSON(): {
224
+ ok: boolean;
225
+ counts: Record<Verdict, number>;
226
+ checks: Check[];
227
+ };
228
+ summary(): string;
229
+ }
230
+
231
+ /**
232
+ * The harness never performs a remote mutation. Every unit of work is one of:
233
+ * - `write`: create/merge a local file (transactional, with backup);
234
+ * - `doc`: emit guidance / commands for a human (printed, or written to a
235
+ * doc file) — this is where cloud setup steps live, deliberately
236
+ * not run;
237
+ * - `probe`: a read-only verification that yields a {@link Check} under
238
+ * --verify;
239
+ * - `exec`: a LOCAL helper command run after writes under --apply (e.g.
240
+ * icacls/chmod to lock down a PEM, `mklink`/`ln` for a VDI
241
+ * junction, or a read-only quarantined tarball fetch) — it must
242
+ * never mutate a remote system;
243
+ * - `envblock`: upsert an aih-managed env block (one `scope`) into a shell
244
+ * profile; multiple scopes targeting the same file compose
245
+ * instead of clobbering each other;
246
+ * - `digest`: a read-only computed result printed verbatim (an analytics
247
+ * report / roll-up) plus optional structured `data` echoed into
248
+ * `--json` — mutates nothing, never contacts a remote system.
249
+ * Because no action kind can mutate a remote system, an autonomous run cannot
250
+ * "fake provisioning" — the capability simply does not exist.
251
+ */
252
+ type ActionKind = "write" | "probe" | "doc" | "exec" | "envblock" | "digest";
253
+ interface WriteAction {
254
+ kind: "write";
255
+ path: string;
256
+ describe: string;
257
+ /** Raw file contents (for text files). */
258
+ contents?: string;
259
+ /** Structured value (for JSON files); enables `merge`. */
260
+ json?: unknown;
261
+ /** Deep-merge `json` onto an existing file instead of overwriting. */
262
+ merge?: boolean;
263
+ /** POSIX file mode, e.g. 0o755 for hooks. */
264
+ mode?: number;
265
+ /** Write only if the file is absent; never overwrite (user-owned seed files). */
266
+ once?: boolean;
267
+ /**
268
+ * Allow this write to land OUTSIDE the target root (home/system files: PEM
269
+ * bundles, shell profiles, VDI redirects). Repo-scoped writes leave this unset
270
+ * and the executor fails closed if their resolved path escapes the root.
271
+ */
272
+ external?: boolean;
273
+ }
274
+ interface DocAction {
275
+ kind: "doc";
276
+ describe: string;
277
+ text: string;
278
+ /** When set, the guidance is also written to this doc file. */
279
+ path?: string;
280
+ }
281
+ interface ProbeAction {
282
+ kind: "probe";
283
+ describe: string;
284
+ run: (ctx: PlanContext) => Promise<Check> | Check;
285
+ /** Dynamic scans may expand to several 1:1 checks after a prior exec action. */
286
+ runMany?: (ctx: PlanContext) => Promise<Check[]> | Check[];
287
+ }
288
+ /**
289
+ * A LOCAL helper command run after writes under `--apply` (e.g. icacls/chmod to
290
+ * lock down a PEM, `mklink /J` for a VDI junction, `update-ca-certificates`, or
291
+ * a read-only quarantined tarball fetch). It must never mutate a remote system —
292
+ * that is what keeps the "no faked provisioning" guarantee intact.
293
+ */
294
+ interface ExecAction {
295
+ kind: "exec";
296
+ describe: string;
297
+ argv: string[];
298
+ /** Optional working directory for local, quarantined helper commands. */
299
+ cwd?: string;
300
+ /** Optional scrubbed environment for local helper commands. */
301
+ env?: NodeJS.ProcessEnv;
302
+ /** Optional timeout override for long-but-bounded local helpers. */
303
+ timeoutMs?: number;
304
+ /** Optional verification check to emit when the command exits non-zero. */
305
+ failureCheck?: Check | ((result: RunResult) => Check);
306
+ /** Skip follow-on probes when this command fails. */
307
+ blockProbesOnFailure?: boolean;
308
+ /** Continue the plan even if the command exits non-zero. */
309
+ allowFailure?: boolean;
310
+ }
311
+ /**
312
+ * Upsert an aih-managed env block (one `scope`) into a shell profile. Unlike a
313
+ * plain `write`, multiple `envblock` actions targeting the SAME file COMPOSE:
314
+ * the executor folds every scope's block into the file in order (starting from
315
+ * the on-disk content), so e.g. `bootstrap` can layer certs + hardware + vdi +
316
+ * telemetry blocks into one profile without any of them clobbering the others.
317
+ */
318
+ interface EnvBlockAction {
319
+ kind: "envblock";
320
+ path: string;
321
+ scope: string;
322
+ shell: EnvShell;
323
+ vars: EnvVar[];
324
+ describe: string;
325
+ }
326
+ /**
327
+ * A read-only ANALYSIS result surfaced to the operator. Unlike {@link DocAction}
328
+ * (whose body only ever lands in a file), a digest's `text` is printed verbatim
329
+ * beneath its headline by the summary, and its optional `data` rides into
330
+ * `--json` — the shape analytics reports and inventory roll-ups need. It mutates
331
+ * nothing and never contacts a remote system.
332
+ */
333
+ interface DigestAction {
334
+ kind: "digest";
335
+ describe: string;
336
+ /** Report body, printed verbatim beneath the headline in text mode. */
337
+ text?: string;
338
+ /** Machine-readable payload echoed into `--json` output. */
339
+ data?: unknown;
340
+ /** Optional late-bound digest for analyses that depend on earlier exec/probe actions. */
341
+ run?: (ctx: PlanContext) => Promise<string | {
342
+ text: string;
343
+ data?: unknown;
344
+ }> | string | {
345
+ text: string;
346
+ data?: unknown;
347
+ };
348
+ }
349
+ type Action = WriteAction | DocAction | ProbeAction | ExecAction | EnvBlockAction | DigestAction;
350
+ interface Plan {
351
+ capability: string;
352
+ actions: Action[];
353
+ }
354
+ /** Everything a capability needs to compute (and a runner to execute) its plan. */
355
+ interface PlanContext {
356
+ /** Target repository / workstation root. */
357
+ root: string;
358
+ /** Canonical context directory name (default ".ai-context"). */
359
+ contextDir: string;
360
+ /** Harness-wide governance posture dial, resolved by the shared ladder. */
361
+ posture?: Posture;
362
+ /** Where the active posture came from (flag/marker/env/default/org floor). */
363
+ postureSource?: PostureSource;
364
+ /** When false (default), the plan is computed but nothing is written. */
365
+ apply: boolean;
366
+ /** When true, probe actions run and contribute to the verification report. */
367
+ verify: boolean;
368
+ json: boolean;
369
+ run: Runner;
370
+ host: HostAdapter;
371
+ env: NodeJS.ProcessEnv;
372
+ /**
373
+ * Interactive prompt seam. Present only when the user opted into an interactive
374
+ * flow (e.g. `--detect`) in a TTY; undefined keeps the harness non-interactive.
375
+ */
376
+ prompter?: Prompter;
377
+ /**
378
+ * The resolved CLI target set, injected by an orchestrator (`aih init`) that
379
+ * resolves `--detect`/`--cli` ONCE and threads the result into every phase. A
380
+ * tool-specific phase emits a tool's files only when that tool is targeted (see
381
+ * {@link isTargeted}). Undefined when a leaf command runs standalone — it then
382
+ * keeps its single-tool identity (`aih profile` is the Cursor profiler, `aih
383
+ * secrets` the Claude secrets guard) and always writes.
384
+ */
385
+ targets?: Cli[];
386
+ /** Capability-specific options parsed from the CLI. */
387
+ options: Record<string, unknown>;
388
+ }
389
+ type PlanFn = (ctx: PlanContext) => Plan | Promise<Plan>;
390
+ interface CommandOption {
391
+ flags: string;
392
+ description: string;
393
+ default?: string | boolean;
394
+ }
395
+ interface CommandSpec {
396
+ name: string;
397
+ summary: string;
398
+ options?: CommandOption[];
399
+ plan: PlanFn;
400
+ /** Read-only commands (doctor/status) skip the apply path entirely. */
401
+ readOnly?: boolean;
402
+ /**
403
+ * Force `verify` on every run so the capability's probes always populate the
404
+ * verification report — i.e. it DIAGNOSES by default (like `doctor`) yet still
405
+ * mutates under `--apply` (unlike `readOnly`). `heal` uses this so a bare
406
+ * `aih heal` surfaces the health report and a non-zero exit when broken.
407
+ */
408
+ alwaysVerify?: boolean;
409
+ /**
410
+ * Exempt from the dirty-worktree `--apply` preflight. For pure-analytics commands
411
+ * (`aih report`) whose only writes are gitignored OUTPUT artifacts (the `.aih/`
412
+ * report file + its ignore rule) — those never clobber uncommitted work, so
413
+ * blocking the report on a dirty tree is wrong.
414
+ */
415
+ skipWorktreeGate?: boolean;
416
+ }
417
+ declare function writeText(path: string, contents: string, describe: string, opts?: {
418
+ mode?: number;
419
+ once?: boolean;
420
+ external?: boolean;
421
+ }): WriteAction;
422
+ declare function writeJson(path: string, value: unknown, describe: string, opts?: {
423
+ merge?: boolean;
424
+ external?: boolean;
425
+ }): WriteAction;
426
+ declare function doc(describe: string, text: string, path?: string): DocAction;
427
+ declare function digest(describe: string, text: string, data?: unknown): DigestAction;
428
+ declare function dynamicDigest(describe: string, run: NonNullable<DigestAction["run"]>): DigestAction;
429
+ declare function probe(describe: string, run: ProbeAction["run"]): ProbeAction;
430
+ declare function probeMany(describe: string, runMany: NonNullable<ProbeAction["runMany"]>): ProbeAction;
431
+ declare function exec(describe: string, argv: string[], opts?: {
432
+ allowFailure?: boolean;
433
+ cwd?: string;
434
+ env?: NodeJS.ProcessEnv;
435
+ timeoutMs?: number;
436
+ failureCheck?: ExecAction["failureCheck"];
437
+ blockProbesOnFailure?: boolean;
438
+ }): ExecAction;
439
+ declare function envBlock(path: string, scope: string, shell: EnvShell, vars: EnvVar[], describe: string): EnvBlockAction;
440
+ declare function plan(capability: string, ...actions: Action[]): Plan;
441
+
442
+ /** Capability commands (repo/workstation mutators), dry-run by default. */
443
+ declare const CAPABILITIES: CommandSpec[];
444
+ /** Read-only commands (always safe). */
445
+ declare const READONLY: CommandSpec[];
446
+ declare const ALL_COMMANDS: CommandSpec[];
447
+ declare function registerCommands(program: Command): void;
448
+
449
+ /** Resolved runtime settings (env defaults overlaid with CLI flags). */
450
+ interface Settings {
451
+ apply: boolean;
452
+ verify: boolean;
453
+ json: boolean;
454
+ contextDir: string;
455
+ root: string;
456
+ caPattern: string;
457
+ }
458
+ /**
459
+ * A canonical context-dir name: a simple, repo-relative path with no traversal.
460
+ * Exported so {@link readAihConfig} validates the committed `.aih-config.json`
461
+ * marker against the SAME constraints settings enforce — a marker can never carry
462
+ * a dir that a flag/env value would have rejected.
463
+ */
464
+ declare const ContextDir: z.ZodString;
465
+ /**
466
+ * Resolve settings fail-closed: env provides defaults (`AIH_*`), `overrides`
467
+ * (CLI flags) win, and any malformed value throws {@link SettingsError} before a
468
+ * command runs. Dry-run (`apply=false`) is the safe default.
469
+ */
470
+ declare function loadSettings(env: NodeJS.ProcessEnv, overrides?: Partial<Settings>): Settings;
471
+
472
+ /**
473
+ * Typed error hierarchy for the harness. Every error carries a stable machine
474
+ * `code` so `--json` output and `doctor` reports stay parseable across versions.
475
+ */
476
+ declare class AihError extends Error {
477
+ readonly code: string;
478
+ constructor(message: string, code?: string);
479
+ }
480
+ /** Invalid/contradictory configuration (env or CLI). Fail-closed. */
481
+ declare class SettingsError extends AihError {
482
+ constructor(message: string);
483
+ }
484
+ /** A host/platform probe could not be satisfied on this OS. */
485
+ declare class PlatformError extends AihError {
486
+ constructor(message: string);
487
+ }
488
+ /** A staged filesystem transaction failed (and was rolled back). */
489
+ declare class FsTxnError extends AihError {
490
+ constructor(message: string);
491
+ }
492
+ /** A verification probe failed in a way that should halt the run. */
493
+ declare class VerificationError extends AihError {
494
+ constructor(message: string);
495
+ }
496
+ /** Capability not yet implemented (foundation stub). */
497
+ declare class NotImplementedError extends AihError {
498
+ constructor(message: string);
499
+ }
500
+ /** Existing config could not be parsed for a merge — fail closed, never partial-merge. */
501
+ declare class MergeError extends AihError {
502
+ constructor(message: string);
503
+ }
504
+ /** An action path escaped its intended root (path-containment violation). Fail-closed. */
505
+ declare class PathContainmentError extends AihError {
506
+ constructor(message: string);
507
+ }
508
+ /** An `--apply` was attempted on a dirty git worktree without `--force`. Fail-closed. */
509
+ declare class DirtyWorktreeError extends AihError {
510
+ constructor(message: string);
511
+ }
512
+
513
+ interface WriteSummary {
514
+ path: string;
515
+ describe: string;
516
+ merged: boolean;
517
+ /**
518
+ * Effect relative to current disk state. `unchanged` writes are skipped (no
519
+ * backup); `kept` is a write-once file that already exists (left untouched).
520
+ */
521
+ effect: "create" | "overwrite" | "merge" | "unchanged" | "kept";
522
+ }
523
+ interface PlanResult {
524
+ capability: string;
525
+ applied: boolean;
526
+ writes: WriteSummary[];
527
+ docs: {
528
+ describe: string;
529
+ path?: string;
530
+ }[];
531
+ probes: {
532
+ describe: string;
533
+ }[];
534
+ execs: {
535
+ describe: string;
536
+ argv: string[];
537
+ ran: boolean;
538
+ code?: number | null;
539
+ ok?: boolean;
540
+ }[];
541
+ /** Read-only computed reports surfaced verbatim (text) + machine-readable (`data`). */
542
+ digests: {
543
+ describe: string;
544
+ text: string;
545
+ data?: unknown;
546
+ }[];
547
+ backups: string[];
548
+ report?: VerificationReport;
549
+ }
550
+ /**
551
+ * Write a single, explicitly-requested analysis artifact (e.g. a `--sarif` report)
552
+ * to a repo-contained path, transactionally. Returns the backups created (0 or 1).
553
+ *
554
+ * DESIGN — why this is NOT gated on `--apply`: the harness invariant "no writes
555
+ * without --apply" protects the user's MANAGED project surface (bootloaders,
556
+ * configs, the context dir) from being mutated without consent. A `--sarif` file
557
+ * is not part of that surface — it is a report OUTPUT the operator requested by
558
+ * naming its path on the command line, exactly like `report --out` or a test
559
+ * runner writing `junit.xml`. Naming the path IS the consent. Crucially, the
560
+ * primary use case — `aih bootstrap-ai --verify --sarif results.sarif` feeding
561
+ * GitHub code-scanning — runs the drift gate WITHOUT `--apply` (CI must not
562
+ * regenerate the repo it is gating); apply-gating the artifact would make the flag
563
+ * a no-op in exactly the scenario it exists for, or force `--apply` to also rewrite
564
+ * every bootloader. So the artifact is decoupled from the plan's apply gate — but
565
+ * NOT from its safety machinery: the path is still contained to `root`
566
+ * ({@link assertContained}) and an overwrite is still backed up to `*.aih.bak` via
567
+ * {@link FsTransaction}. Re-writing identical bytes is a no-op (no rewrite, no
568
+ * backup churn), matching {@link executePlan}'s idempotency contract.
569
+ */
570
+ declare function writeArtifact(ctx: PlanContext, relPath: string, contents: string): string[];
571
+ /** Compute final file contents for a write action, applying JSON merge if requested. */
572
+ declare function resolveContents(action: WriteAction, absPath: string): string;
573
+ /**
574
+ * Execute a plan. In dry-run (`ctx.apply === false`) nothing is written — the
575
+ * result still reports exactly what would change. With `ctx.apply` writes are
576
+ * committed transactionally; with `ctx.verify` probe actions run and populate a
577
+ * {@link VerificationReport}.
578
+ */
579
+ declare function executePlan(plan: Plan, ctx: PlanContext, opts?: {
580
+ skipWorktreeGate?: boolean;
581
+ }): Promise<PlanResult>;
582
+ /** Human-readable summary of a plan result (used when --json is off). */
583
+ declare function summarizeResult(result: PlanResult): string;
584
+
585
+ /**
586
+ * Run a synchronous fs operation, retrying ONLY the transient Windows lock codes
587
+ * in {@link TRANSIENT_LOCK_CODES} with a short bounded backoff (~0.5s worst case).
588
+ * Any other error — `EEXIST` from an exclusive create, a genuine `EACCES` on a
589
+ * locked-down path that never clears — is re-thrown on its first occurrence, so
590
+ * this absorbs the sub-millisecond scanner window without ever masking a real
591
+ * failure. The retry preserves the caller's atomicity/rollback guarantees: it
592
+ * re-issues the same single syscall, nothing more.
593
+ *
594
+ * Exported for direct unit testing — the FS-level retry is exercised through the
595
+ * real filesystem elsewhere, but a transient lock cannot be reproduced on demand,
596
+ * so the retry/give-up/passthrough contract is pinned here.
597
+ */
598
+ declare function retryTransient<T>(op: () => T): T;
599
+ interface StagedWrite {
600
+ path: string;
601
+ contents: string;
602
+ mode?: number;
603
+ }
604
+ interface FsTxnResult {
605
+ written: string[];
606
+ backups: string[];
607
+ }
608
+ /**
609
+ * Stages writes in memory and commits them atomically. Each existing target is
610
+ * first copied to `<path>.aih.bak`; new content is written to a temp file and
611
+ * `rename`d into place (atomic on the same volume). If any write throws, every
612
+ * write applied so far is rolled back (created files removed, overwritten files
613
+ * restored from their backup).
614
+ */
615
+ declare class FsTransaction {
616
+ private staged;
617
+ stage(path: string, contents: string, mode?: number): void;
618
+ preview(): ReadonlyArray<StagedWrite>;
619
+ commit(): FsTxnResult;
620
+ }
621
+ /** Read a file's text, or `undefined` if it does not exist. */
622
+ declare function readIfExists(path: string): string | undefined;
623
+
624
+ /**
625
+ * Parse JSON or JSONC text (tolerant of comments + trailing commas). Returns
626
+ * `undefined` for empty input. Throws {@link MergeError} on a genuine syntax
627
+ * error: `jsonc-parser` returns a PARTIAL value for malformed input (incomplete
628
+ * braces, trailing garbage), and merging onto a partial parse would silently
629
+ * drop the user's real config — so we fail closed and ask for a manual fix
630
+ * instead of overwriting from a half-read file.
631
+ */
632
+ declare function parseJsoncText(text: string): unknown;
633
+ declare function isPlainObject(v: unknown): v is Record<string, unknown>;
634
+ /**
635
+ * Deep-merge `incoming` (harness-generated) onto `base` (existing user config),
636
+ * preserving every key that exists only in `base`. Objects merge recursively;
637
+ * primitive arrays become a deduped union (base order first) so things like
638
+ * `permissions.deny` accumulate instead of clobbering; for any other type
639
+ * mismatch, `incoming` wins.
640
+ */
641
+ declare function deepMerge(base: unknown, incoming: unknown): unknown;
642
+
643
+ /**
644
+ * Deterministic string-building helpers shared by capability templates. All
645
+ * generated files flow through these so golden-file tests stay stable: no dates,
646
+ * no random ordering, single trailing newline.
647
+ */
648
+ /**
649
+ * Strip trailing newlines in linear time. The idiomatic `/\n+$/` is a
650
+ * polynomial-ReDoS footgun (CodeQL `js/polynomial-redos`): on a long run of
651
+ * newlines followed by a non-newline a backtracking engine retries the run from
652
+ * every start position — O(n²). A reverse scan is provably O(n) and byte-for-byte
653
+ * identical (only `\n` (U+000A) is stripped, exactly as the old regex did, so
654
+ * `\r` in a `\r\n` sequence is preserved either way).
655
+ */
656
+ declare function stripTrailingNewlines(text: string): string;
657
+ /** Join parts (strings or string arrays) with newlines; exactly one trailing newline. */
658
+ declare function lines(...parts: Array<string | string[]>): string;
659
+ /** Indent every non-empty line of `text` by `n` spaces. */
660
+ declare function indent(text: string, n?: number): string;
661
+ /** Render a YAML frontmatter block from ordered key/value pairs (insertion order). */
662
+ declare function frontmatter(fields: Record<string, string | boolean | number | string[]>): string;
663
+ /** Stable 2-space JSON with a trailing newline (insertion order preserved). */
664
+ declare function jsonFile(value: unknown): string;
665
+ /** Ensure exactly one trailing newline. */
666
+ declare function ensureTrailingNewline(text: string): string;
667
+ /** Marker that opens an aih-managed region (comment syntax works in sh + PowerShell). */
668
+ declare function beginMarker(scope: string): string;
669
+ /** Marker that closes an aih-managed region. */
670
+ declare function endMarker(scope: string): string;
671
+ /** Wrap `body` in begin/end markers for in-place regeneration. */
672
+ declare function managedBlock(scope: string, body: string): string;
673
+
674
+ /** Resolve the effective platform, honoring the `AIH_PLATFORM` test override. */
675
+ declare function resolvePlatform(env?: NodeJS.ProcessEnv): Platform;
676
+ interface HostAdapterOptions {
677
+ platform?: Platform;
678
+ run?: Runner;
679
+ env?: NodeJS.ProcessEnv;
680
+ }
681
+ /** Construct the host adapter for this (or an overridden) platform. */
682
+ declare function makeHostAdapter(opts?: HostAdapterOptions): HostAdapter;
683
+
684
+ /** First integer anywhere in `stdout` (handles trailing newlines/whitespace). */
685
+ declare function parseFirstInt(stdout: string): number | undefined;
686
+ /** Parse `nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits`. */
687
+ declare function parseNvidiaSmi(stdout: string): GpuInfo;
688
+ /** Parse tab-separated `base64<TAB>subject` lines (Windows cert export). */
689
+ declare function parseCertLines(stdout: string): CertEntry[];
690
+ /**
691
+ * Extract PEM certificate blocks from `security`/openssl `-p` style output.
692
+ *
693
+ * A linear `indexOf` walk rather than `/BEGIN[\s\S]*?END/g`: that lazy match
694
+ * between two literal anchors is a polynomial-ReDoS footgun (CodeQL
695
+ * `js/polynomial-redos`) — on output with many `BEGIN` markers and no closing
696
+ * `END`, the engine rescans to end from every `BEGIN`, O(n²). The walk finds
697
+ * each `BEGIN` then its nearest following `END` (the same blocks the lazy regex
698
+ * matched, in order) with non-overlapping O(n) scans.
699
+ */
700
+ declare function parsePemBlocks(stdout: string, subject?: string): CertEntry[];
701
+
702
+ declare const VERSION = "0.1.0";
703
+ /** Build the configured commander program. Imported by both the CLI entry and tests. */
704
+ declare function buildProgram(): Command;
705
+
706
+ export { ALL_COMMANDS, type AccelBackend, type Action, type ActionKind, type AdapterFactory, AihError, CAPABILITIES, type CertEntry, type Check, type CheckCode, type CommandOption, type CommandSpec, ContextDir, type DigestAction, DirtyWorktreeError, type DocAction, type EnvBlockAction, type EnvShell, type EnvVar, type ExecAction, FsTransaction, FsTxnError, type FsTxnResult, type GpuInfo, type GpuVendor, type HostAdapter, type HostAdapterOptions, MergeError, NotImplementedError, PathContainmentError, type Plan, type PlanContext, type PlanFn, type PlanResult, type Platform, PlatformError, type ProbeAction, READONLY, type RunOptions, type RunResult, type Runner, type Settings, SettingsError, VERSION, type VdiInfo, type Verdict, VerificationError, VerificationReport, type WriteAction, type WriteSummary, beginMarker, buildProgram, deepMerge, defaultRunner, derBase64ToPem, digest, doc, dynamicDigest, endMarker, ensureTrailingNewline, envBlock, exec, executePlan, fakeRunner, formatExport, frontmatter, indent, isPlainObject, jsonFile, lines, loadSettings, makeHostAdapter, managedBlock, missingToolRunner, parseCertLines, parseFirstInt, parseJsoncText, parseNvidiaSmi, parsePemBlocks, plan, probe, probeMany, readIfExists, registerCommands, removeManagedBlock, resolveContents, resolvePlatform, retryTransient, safeCaPattern, stripTrailingNewlines, summarizeResult, upsertManagedBlock, upsertTextBlock, vdiFromEnv, writeArtifact, writeJson, writeText };