/** * Hard memory ceiling injected into every heavy tool CLEO spawns (T12096). * * ## Why a cap at the spawn point, and not just a semaphore * * T12091 bounded how many `tool:test` INVOCATIONS run at once. It counts an * entire process tree as one slot — and in a workspace, one invocation is not * one test run. Measured 2026-08-09 in `/mnt/projects/PepsVida`: * * cleo verify T1046 --gate testsPassed --evidence tool:test * └─ npm test * └─ pnpm -r --if-present run test ← fans out * └─ concurrent vitest in {apps,lib}/* (15 packages have tests) * * CLEO's semaphore saw ONE slot in use. The project has 15 packages with a * `test` script and **none of its three vitest configs cap `maxWorkers` or * heap** — the memory-safe SSoT and its lint gate live in cleocode and protect * only cleocode. So a single evidence atom could expand to 15 concurrent * unbounded fork pools on a 62 GiB machine. * * A consuming project cannot be relied on to configure this. CLEO is the one * spawning the process, so CLEO sets the ceiling — and the child inherits it * whether or not the project has ever heard of `vitest.memory-safe.ts`. * * ## The three levers (each verified against the installed tool, not assumed) * * | Variable | Effect | Verified by | * |---|---|---| * | `NODE_OPTIONS=--max-old-space-size=N` | Heap ceiling for EVERY node process in the tree, inherited | node docs; appended, never clobbered | * | `VITEST_MAX_WORKERS=N` | `if (process.env.VITEST_MAX_WORKERS) resolved.maxWorkers = parseInt(...)` — overrides the resolved config, so it binds projects that set their own, and RAISES one that is serial ({@link vitestWorkerCeiling}) | read out of vitest 4.1.4 `dist/chunks/coverage.*.js` | * | `npm_config_workspace_concurrency=N` / `pnpm_config_workspace_concurrency=N` | Bounds `pnpm -r` fan-out across workspace packages (pnpm 10 reads the first, pnpm 11+ the second) | `npm_config_workspace_concurrency=1 pnpm config get workspace-concurrency` → `1` on pnpm 10.30; `pnpm_config_…` → `1` on pnpm 12.6 (T13122) | * * Together these bound the product the semaphore could not see: * `packages in flight × workers per run × heap per worker`. * * The worker levers apply ONLY to `test` / `build`, the tools that fork. * `typecheck` and `lint` are single processes, but not cheap ones: one * TypeScript program on a large monorepo holds 2–5 GB (a live `tsc --noEmit` * held 4.7 GB on 2026-10-03), so since T13123 they get the heap ceiling and the * workspace-concurrency bound too ({@link isMemoryBoundTool}), and no worker * variables, which nothing they run reads. * * ## An inherited value can tighten the plan, never loosen it (T13122) * * The overlay is merged over the CALLER's environment: whatever the shell * profile, the agent harness or a parent process exported. Until T13122 an * inherited value always won, on the theory that "a deliberate setting beats our * default". A shell profile is not a per-project decision: on 2026-10-03 a * `~/.zprofile` export of `NODE_OPTIONS=--max-old-space-size=8192` reached a * `cleo verify --evidence tool:test`, and the run got 6 workers × 8 GiB = all of * a 48 GiB Mac, because the worker count was sized for a 4 GiB heap it was not * getting. * * So the run has a heap budget — the default worker count times the default heap * ({@link heavyRunBudgetMb}) — and the overlay is planned against it * ({@link planHeavyToolEnv}): * * - an inherited `NODE_OPTIONS` heap that fits the budget is kept, and the * worker count shrinks so `workers × heap` still fits; one above the budget * is clamped to it; * - an inherited worker count or workspace concurrency at or below the plan * is kept; one above it is clamped; * - only an explicit CLEO override asks for more: `CLEO_HEAVY_HEAP_MB`, * `CLEO_HEAVY_WORKERS`, `CLEO_HEAVY_WORKSPACE_CONCURRENCY`. * * A project that wants more for its own suite still sets it in its own script * (`"test": "NODE_OPTIONS=… vitest run"`): that runs below the overlay and is * untouched by it. The plan, and every value it clamped, is reported by * `cleo verify` and `cleo run`. * * ## A project's own worker limit is a ceiling too (T14033) * * vitest applies `VITEST_MAX_WORKERS` AFTER it resolves the config, over the * `maxWorkers = 1` that `fileParallelism: false` (or `--no-file-parallelism`) * set, so the overlay alone turned a serial suite parallel. A test run plans at * most {@link vitestWorkerCeiling} workers: the overlay only ever lowers a * project's parallelism. * * @task T12096 * @task T13122 * @task T13123 * @task T14033 */ import type { HeavyToolResourcePlan, ProjectSettingOverride } from '@cleocode/contracts'; import type { CanonicalTool } from './tool-resolver.js'; /** * Heap ceiling per node process, in MiB. * * Matches `FORK_HEAP_MB` in `vitest.memory-safe.ts`. A worker that needs more * than 4 GiB of JS heap for a unit test has a leak, and the whole point is to * make that fail loudly in one worker rather than take the machine down. */ export declare const HEAVY_TOOL_HEAP_MB = 4096; /** RAM assumed consumable per concurrent worker, in GiB, when sizing the pool. */ export declare const GIB_PER_WORKER = 6; /** Never grant more than this many workers, however large the machine. */ export declare const MAX_HEAVY_WORKERS = 6; /** Never grant fewer than this many — one worker must always be able to run. */ export declare const MIN_HEAVY_WORKERS = 1; /** * RAM the admission ledger never hands out: the OS, resident apps, VMs and * agent CLIs — `max(4 GiB, 25%)`. * * @param totalBytes - physical RAM in bytes. * @task T13133 */ export declare function admissionReserveBytes(totalBytes: number): number; /** * The machine-wide budget the admission ledger shares between heavy runs, in * bytes (at least 1 GiB): total RAM minus {@link admissionReserveBytes}. * * @param totalBytes - physical RAM. @defaultValue os.totalmem() * * @example * ```ts * admissionCapacityBytes(48 * 1024 ** 3); // 36 GiB * ``` * * @task T13133 */ export declare function admissionCapacityBytes(totalBytes?: number): number; /** * Share of the admission budget one heavy run plans its workers for (T13132). * A whole-suite run sized to the entire budget left nothing for anyone else: * on a 48 GiB Mac it planned 6 workers × 6 GiB = all 36 GiB, and 49 single-file * runs queued behind it for 17 minutes. Planning for half keeps the other half * open, at the cost of a slower whole-suite run. */ export declare const PER_RUN_BUDGET_SHARE = 0.5; /** Overrides the per-run share: a number in `(0, 1]` (e.g. `1` on a dedicated box). */ export declare const PER_RUN_SHARE_ENV = "CLEO_PER_RUN_SHARE"; /** * Share of the admission budget one heavy run plans for: * `CLEO_PER_RUN_SHARE` when it is a number in `(0, 1]`; the whole budget on a * hosted CI runner (GitHub Actions, GitLab CI, Buildkite, CircleCI, Azure * Pipelines), which is single-tenant, so CI keeps its parallelism (2 workers on * a 16 GiB GitHub runner, as before T13132); else {@link PER_RUN_BUDGET_SHARE}. * Fixed per environment, so the worker count in the tool cache key is stable. * * @param env - the environment. * * @example * ```ts * perRunBudgetShare({}); // 0.5 * perRunBudgetShare({ GITHUB_ACTIONS: 'true' }); // 1 * perRunBudgetShare({ CI: '1' }); // 0.5 (a bare CI is not trusted) * perRunBudgetShare({ CLEO_PER_RUN_SHARE: '1' }); // 1 * ``` */ export declare function perRunBudgetShare(env: NodeJS.ProcessEnv): number; /** * Workspace packages allowed to run their test/build script concurrently. * * Deliberately 1. The fan-out is the multiplier the semaphore was blind to, and * a serialised workspace sweep is slower but finishes; a parallel one on a * memory-bound box does not finish at all. */ export declare const WORKSPACE_CONCURRENCY = 1; /** * The default heap never drops below this, in MiB, however small the machine — * one worker must be able to run a real suite. * * @task T13122 */ export declare const MIN_HEAVY_HEAP_MB = 1024; /** * Fraction of total RAM the DEFAULT heap may reach. {@link HEAVY_TOOL_HEAP_MB} * on an 8 GiB laptop is half the machine; on a 4 GiB box it would be all of it, * which is no ceiling at all. Machines of 8 GiB and up are unaffected. * * @task T13122 */ export declare const HEAVY_HEAP_RAM_FRACTION = 0.5; /** * Explicit heap ceiling for heavy tools, in MiB. The only way to ask for a heap * above the run's budget: unlike an inherited `NODE_OPTIONS`, it is never * clamped. * * @task T13122 */ export declare const HEAVY_HEAP_ENV = "CLEO_HEAVY_HEAP_MB"; /** * Explicit worker count for heavy tools. Replaces the planned count for every * runner variable, even when that puts the run over its budget. * * @task T13122 */ export declare const HEAVY_WORKERS_ENV = "CLEO_HEAVY_WORKERS"; /** * Explicit workspace concurrency for heavy tools (`pnpm -r` packages in flight). * * @task T13122 */ export declare const HEAVY_WORKSPACE_CONCURRENCY_ENV = "CLEO_HEAVY_WORKSPACE_CONCURRENCY"; /** * The two spellings of pnpm's `workspace-concurrency` in the environment. pnpm * 10 reads only `npm_config_workspace_concurrency`; pnpm 11+ reads only * `pnpm_config_workspace_concurrency` (measured: pnpm 10.30.0 and 12.6.0 each * report `undefined` for the other spelling). Setting one bounded `pnpm -r` * fan-out on one pnpm major and silently nothing on the other, so the overlay * sets both — for every launcher, because an `npm test` script that runs * `pnpm -r` still reads them. Both are read case-insensitively, and an * uppercase spelling outranks the lowercase one, so an inherited * `NPM_CONFIG_WORKSPACE_CONCURRENCY` is planned and overlaid as well * ({@link workspaceConcurrencyNames}). * * @task T13122 */ export declare const WORKSPACE_CONCURRENCY_VARS: readonly ["npm_config_workspace_concurrency", "pnpm_config_workspace_concurrency"]; /** * Every environment name that sets pnpm's workspace concurrency in `env`: the * two canonical spellings, plus any case or dash variant of them present (pnpm * reads these case-insensitively, uppercase first, and `workspace-concurrency` * with a dash as readily). * * @param env - The caller's environment. * @returns The names to plan and overlay, canonical first. * @task T13122 */ export declare function workspaceConcurrencyNames(env: NodeJS.ProcessEnv): string[]; /** * V8 young-generation (semi-space) ceiling a plan allows, in MiB: Node 24's own * default on a 64-bit host. `heap_size_limit` is old space plus three * semi-spaces (measured on Node 24: `--max-old-space-size=4096` with * `--max-semi-space-size=4096` reports 16384 MiB), so an inherited large * semi-space would multiply the planned heap without touching the old-space * flag; above this it is clamped. * * @task T13122 */ export declare const MAX_SEMI_SPACE_MB = 64; /** Environment overlay to merge into a heavy tool's spawn env. */ export type HeavyToolEnv = Readonly>; /** * The overlay for one heavy or memory-bound tool spawn, with the plan behind it * (T13122). * * `resources` is `null` for a tool that is not memory-bound, whose overlay is * empty. */ export interface HeavyToolSpawnPlan { /** Variables to merge over the caller's environment. */ readonly overlay: HeavyToolEnv; /** What was chosen and why; `null` when the tool is not heavy. */ readonly resources: HeavyToolResourcePlan | null; } /** * Whether a canonical tool is heavy enough to need bounding. * * Heavy means "forks, and its memory is a product rather than a constant": * a test or build spawns workers, which is what makes the ceiling necessary. * `lint`, `typecheck`, `audit` and `security-scan` are single cheap processes — * bounding them costs a subprocess to guard nothing. * * @param canonical - the tool in question. * @returns `true` when the tool needs a worker cap, a memory ceiling and the * longer spawn deadline. All three must agree, which is why they read * this instead of each keeping a literal. * * @example * ```ts * isHeavyTool('test'); // true * isHeavyTool('typecheck'); // false * ``` */ export declare function isHeavyTool(canonical: CanonicalTool): boolean; /** * Whether a canonical tool's memory is bounded: a heap ceiling and workspace * concurrency in its spawn env, and a RAM-derived machine-wide slot count that * shrinks under pressure. * * A superset of {@link isHeavyTool}. `typecheck` and `lint` were treated as * cheap (`max(2, cpus/2)` slots, no ceiling) until T13123: nine concurrent * `tsc` runs of 2–5 GB each on an 18-core box is most of 48 GB. `audit` and * `security-scan` stay unbounded — they are network-bound and small. * * @param canonical - the tool in question. * @returns `true` for `test`, `build`, `typecheck` and `lint`. * * @example * ```ts * isMemoryBoundTool('typecheck'); // true * isMemoryBoundTool('audit'); // false * ``` * * @task T13123 */ export declare function isMemoryBoundTool(canonical: CanonicalTool): boolean; /** * The most workers a vitest run may use without overriding the project's own * limit, or `null` when it sets none (T14033). * * vitest 4 applies `VITEST_MAX_WORKERS` over the resolved config, including the * `maxWorkers = 1` that `fileParallelism: false` sets, so CLEO must not export * more than the project allows. Read from the command line * (`--no-file-parallelism`, `--maxWorkers N`) and from the literal settings of * every vitest config in `dir` and up to {@link CONFIG_SEARCH_DEPTH} levels * below it, so a workspace run (`pnpm -r test`, `pnpm --filter api test`) sees * its packages' configs. The lowest wins: one serial package makes the whole * run serial, since one environment variable reaches every package. A * computed setting (`maxWorkers: cpus / 2`) or a percentage is not read; the * CLEO plan applies there as before. * * @param dir - the directory the run starts in. * @param argv - the command line. @defaultValue [] * @returns the lowest worker limit found, or `null`. * * @example * ```ts * // apps/api/vitest.config.ts: test: { fileParallelism: false } * vitestWorkerCeiling('/repo'); // 1 * vitestWorkerCeiling('/other', ['vitest', 'run', '--maxWorkers=2']); // 2 * ``` * * @task T14033 */ export declare function vitestWorkerCeiling(dir: string, argv?: readonly string[]): number | null; /** * The project-file settings a heavy-run overlay overrides (T14038): a literal * vitest `maxWorkers` (or `fileParallelism: false`) that `VITEST_MAX_WORKERS` * changes, and a pnpm `workspace-concurrency` (`.npmrc`, or * `workspaceConcurrency` in `pnpm-workspace.yaml`) that the * `*_config_workspace_concurrency` variables change. A heap the project sets * (a vitest `execArgv`, an inline `NODE_OPTIONS` in a script, `.npmrc` * `node-options`) is applied after the inherited `NODE_OPTIONS`, so the * overlay never overrides it and it is not listed. * * @param dir - the directory the run starts in. * @param overlay - the overlay the run is spawned with. * @returns one entry per overridden setting, in file order. * * @example * ```ts * // apps/api/vitest.config.ts: test: { maxWorkers: 8 } * projectSettingOverrides('/repo', { VITEST_MAX_WORKERS: '3' }); * // [{ setting: 'vitest maxWorkers', file: 'apps/api/vitest.config.ts', projectValue: '8', * // variable: 'VITEST_MAX_WORKERS', appliedValue: '3' }] * ``` * * @task T14038 */ export declare function projectSettingOverrides(dir: string, overlay: Readonly>): ProjectSettingOverride[]; /** * Default worker count one heavy run plans for, given {@link GIB_PER_WORKER}: * {@link perRunBudgetShare} of the admission budget * ({@link admissionCapacityBytes}) — half on a shared machine, so a * whole-suite run never takes the budget other runs need (T13132), the whole * budget on a single-tenant CI runner. Fixed per machine and environment, so * the worker count — part of the tool cache key (T12989) — is stable. * * The planned count ({@link planHeavyToolEnv}) never exceeds this: a small * inherited heap does not buy extra workers, because each worker also costs * memory outside its heap. * * @param totalRamGib - total RAM in GiB; defaults to a live reading. * @param env - the environment ({@link perRunBudgetShare}). @defaultValue process.env * @returns a value in `[MIN_HEAVY_WORKERS, MAX_HEAVY_WORKERS]`. * * @example * ```ts * heavyToolWorkers(48, {}); // 3 (half of the 36 GiB budget) * heavyToolWorkers(16, {}); // 1 * heavyToolWorkers(16, { GITHUB_ACTIONS: 'true' }); // 2 (the whole 12 GiB budget) * ``` */ export declare function heavyToolWorkers(totalRamGib?: number, env?: NodeJS.ProcessEnv): number; /** * Default heap ceiling per Node process, in MiB: {@link HEAVY_TOOL_HEAP_MB}, * lowered to {@link HEAVY_HEAP_RAM_FRACTION} of RAM on a machine under 8 GiB * (never below {@link MIN_HEAVY_HEAP_MB}). * * @param totalRamGib - total RAM in GiB; defaults to a live reading. * @returns the default heap in MiB. * * @example * ```ts * defaultHeavyHeapMb(64); // → 4096 * defaultHeavyHeapMb(4); // → 2048 * ``` * * @task T13122 */ export declare function defaultHeavyHeapMb(totalRamGib?: number): number; /** * Default heap ceiling for a single-process tool (`typecheck`, `lint`), in MiB: * {@link defaultHeavyHeapMb}, but never above a quarter of RAM — V8's own * default old space on a machine under 16 GiB (measured 4288 MiB with no flags * on a 64 GiB host, the capped default) — so the plan never RAISES a `tsc`'s * ceiling on a small machine (review of #1810). Never below * {@link MIN_HEAVY_HEAP_MB}. * * @param totalRamGib - total RAM in GiB; defaults to a live reading. * @returns the default heap in MiB. * * @example * ```ts * defaultSingleProcessHeapMb(64); // → 4096 * defaultSingleProcessHeapMb(8); // → 2048 * ``` * * @task T13123 */ export declare function defaultSingleProcessHeapMb(totalRamGib?: number): number; /** * Heap budget for one heavy run, in MiB: the default worker count times the * default heap. `workspace concurrency × workers × heap` must fit in it. * * Since T13132 the worker count is half the admission budget's worth * ({@link heavyToolWorkers}); an inherited heap is planned against the budget * instead of multiplying it (T13122). * * @param totalRamGib - total RAM in GiB; defaults to a live reading. * @param env - the environment ({@link perRunBudgetShare}). @defaultValue process.env * @returns the budget in MiB. * * @example * ```ts * heavyRunBudgetMb(64, {}); // → 16384 (4 workers × 4096) * heavyRunBudgetMb(16, {}); // → 4096 (1 worker × 4096) * ``` * * @task T13122 * @task T13132 */ export declare function heavyRunBudgetMb(totalRamGib?: number, env?: NodeJS.ProcessEnv): number; /** * `NODE_OPTIONS` flags that set a V8 heap limit. `max-old-space-size` and its * percentage form decide the old-space ceiling; `max-semi-space-size` sizes the * young generation, three of which count toward the limit, and is capped at * {@link MAX_SEMI_SPACE_MB}. * * @task T12989 * @task T13122 */ export declare const HEAP_FLAG_NAMES: ReadonlySet; /** * The V8 heap flags a `NODE_OPTIONS` value sets, by name, with the value in * effect. * * V8 reads flags left to right and a later value replaces an earlier one, so * the LAST occurrence of each flag is the effective one. Underscores in a flag * name read as dashes, as they do to V8, and the space-separated spelling * (`--max-old-space-size 4096`) is read too. * * @param nodeOptions - A `NODE_OPTIONS` value, if any. * @returns flag name → effective value, in first-seen order. * * @task T12989 * @task T13122 */ export declare function parseHeapFlags(nodeOptions: string | undefined): ReadonlyMap; /** * The old-space heap ceiling a `NODE_OPTIONS` value asks for, in MiB, or * `null` when it sets none (or sets one Node could not use). * * `--max-old-space-size-percentage` wins over `--max-old-space-size` whatever * their order (measured on Node 24.21: `=10` beside `=2048` gave a 6745 MiB * limit on a 64 GiB machine either way round), and resolves against total RAM. * * @param nodeOptions - A `NODE_OPTIONS` value, if any. * @param totalRamMb - Total RAM in MiB, for the percentage form. * @returns the requested heap in MiB, or `null`. * * @example * ```ts * inheritedHeapMb('--max-old-space-size=8192', 65536); // → 8192 * inheritedHeapMb('--max-old-space-size-percentage=25', 65536); // → 16384 * inheritedHeapMb('--enable-source-maps', 65536); // → null * ``` * * @task T13122 */ export declare function inheritedHeapMb(nodeOptions: string | undefined, totalRamMb: number): number | null; /** * Set the old-space heap ceiling in a `NODE_OPTIONS` value: every existing * `--max-old-space-size` and `--max-old-space-size-percentage` is cut out and * one `--max-old-space-size=` appended. Every other byte survives — a * project may rely on `--experimental-*` or a quoted `--require` path there. * * Removing the percentage form is required, not tidiness: it outranks the size * form whatever the order, so leaving it would silently undo the ceiling. * * @param existing - current `NODE_OPTIONS`, if any. * @param heapMb - ceiling to apply. * @returns the rewritten value. * * @example * ```ts * withHeapCeiling('--enable-source-maps --max-old-space-size=8192', 4096); * // → '--enable-source-maps --max-old-space-size=4096' * ``` * * @task T13122 */ export declare function withHeapCeiling(existing: string | undefined, heapMb: number): string; /** * Cap the semi-space (young generation) flag of a `NODE_OPTIONS` value at * {@link MAX_SEMI_SPACE_MB}: a larger `--max-semi-space-size` is cut out and the * cap appended. A value at or under the cap is left alone. * * @param existing - `NODE_OPTIONS`, if any. * @returns The value to use, and the inherited semi-space it clamped (or `null`). * * @example * ```ts * withSemiSpaceCap('--max-semi-space-size=4096'); // → { value: '--max-semi-space-size=64', clampedFrom: 4096 } * ``` * * @task T13122 */ export declare function withSemiSpaceCap(existing: string | undefined): { value: string | undefined; clampedFrom: number | null; }; /** * Bound an inherited `MAKEFLAGS` to `workers` jobs. * * Absent: `-j` (GNU make sizes a bare `-j` off nproc). Carrying a * jobserver (`--jobserver-auth=` / `--jobserver-fds=`): kept, since inside a * `make` recipe the parent's jobserver already bounds the jobs. Otherwise any * `-j`, `-jN`, `--jobs[=N]`, a short cluster ending in j (`-sj18`) or make's * dash-less first word (`j18`) above `workers` (a bare one is unlimited) is * replaced by `-j`, keeping every other flag — a profile-wide * `export MAKEFLAGS=-j18` no longer outruns the plan. * * @param raw - The inherited `MAKEFLAGS`. * @param workers - The planned worker count. * @returns The value to set, or `null` to keep the inherited one. * @task T13122 */ export declare function boundMakeflags(raw: string | undefined, workers: number): string | null; /** * Remove npm's `Unknown env config` warnings from captured output. * * npm warns once per run for every `npm_config_*` variable it does not know, * including `npm_config_workspace_concurrency`, which the overlay sets for the * `pnpm -r` an `npm test` script may run. The warning would otherwise sit in * the stderr tail CLEO quotes when a tool fails and push the real error out of * it; the variable itself stays, because dropping it for npm unbounded those * child `pnpm -r` runs (review of #1808). * * @param text - Captured stderr. * @returns The text without those warning lines. * @task T13122 */ export declare function withoutNpmEnvConfigWarnings(text: string): string; /** * Append `--max-old-space-size` to an existing `NODE_OPTIONS`, or create it, * leaving an existing `--max-old-space-size` alone. * * @deprecated Since T13122 the heavy-tool overlay plans the heap against the * run's budget instead of letting any inherited value win; use * {@link planHeavyToolEnv} (or {@link withHeapCeiling} to set a ceiling). * Kept for SDK consumers. * * @param existing - current `NODE_OPTIONS`, if any. * @param heapMb - ceiling to apply. * @returns the merged value. */ export declare function mergeNodeOptions(existing: string | undefined, heapMb: number): string; /** * Plan a heavy or memory-bound tool spawn: the environment overlay, and the * resource plan behind it (T13122, T13123). * * The heap is chosen first — `CLEO_HEAVY_HEAP_MB`, else the inherited * `NODE_OPTIONS` heap when it fits the budget (clamped to it otherwise), else * {@link defaultHeavyHeapMb}. The worker count then derives from the heap * actually in effect, `⌊budget / (packages × heap)⌋`, never above * {@link heavyToolWorkers}, so `packages × workers × heap` stays within * {@link heavyRunBudgetMb}. Inherited worker counts and workspace concurrency * are kept at or below the plan and clamped above it; `CLEO_HEAVY_WORKERS` and * `CLEO_HEAVY_WORKSPACE_CONCURRENCY` override. * * A single-process memory-bound tool (`typecheck`, `lint`) gets the same heap * ceiling and workspace concurrency, and no worker variables: its plan is one * process (T13123). * * @param canonical - the canonical tool about to be spawned. * @param env - the environment the child would otherwise inherit. * @param totalRamGib - total RAM in GiB; injectable for deterministic tests. * @param maxWorkers - at most this many workers (a run that names its test * files needs no more than one per file, T13132). @defaultValue no limit * @returns the overlay and plan; an empty overlay and `null` plan for a tool * that is not memory-bound (`audit`, `security-scan`). * * @example * ```ts * // A shell profile exported an 8 GiB heap on a 64 GiB machine: * const { overlay, resources } = planHeavyToolEnv( * 'test', { NODE_OPTIONS: '--max-old-space-size=8192' }, 64); * overlay.VITEST_MAX_WORKERS; // → '2' (2 × 8192 = the 16384 MiB budget) * resources?.heapSource; // → 'inherited' * ``` * * @task T12096 * @task T13122 * @task T13123 * @task T13132 */ export declare function planHeavyToolEnv(canonical: CanonicalTool, env?: NodeJS.ProcessEnv, totalRamGib?: number, maxWorkers?: number): HeavyToolSpawnPlan; /** * Build the environment overlay for a heavy or memory-bound tool spawn: the * `overlay` of {@link planHeavyToolEnv}. * * Returns an empty object for a tool that is not memory-bound, so the caller * can merge unconditionally. * * @param canonical - the canonical tool about to be spawned. * @param env - the environment the child would otherwise inherit. * @param totalRamGib - total RAM in GiB; injectable for deterministic tests. * @returns variables to overlay; inherited values within the plan are kept. * * @example * ```ts * const overlay = heavyToolEnv('test', {}, 62); * // { NODE_OPTIONS: '--max-old-space-size=4096', * // VITEST_MAX_WORKERS: '6', * // npm_config_workspace_concurrency: '1', … } * heavyToolEnv('typecheck', {}, 62); * // { NODE_OPTIONS: '--max-old-space-size=4096', * // npm_config_workspace_concurrency: '1', pnpm_config_workspace_concurrency: '1' } * heavyToolEnv('audit', process.env, 62); // → {} * ``` * * @task T12096 * @task T13122 * @task T13123 */ export declare function heavyToolEnv(canonical: CanonicalTool, env?: NodeJS.ProcessEnv, totalRamGib?: number): HeavyToolEnv; //# sourceMappingURL=heavy-tool-env.d.ts.map