/** * Heavy-command planner for the provider hook (`cleo hook heavy-command`). * * An agent harness hands the hook the shell command line an agent is about to * run. This module finds the heavy commands in it (test runners, compilers, * builds, installs) and rewrites each one to `cleo run --wait --class --`, * so it is admitted through the machine-wide ResourceGovernor like every other * heavy job. Recognition is {@link looksHeavy} / {@link resolveRunClass} from * the dependency-free `run-class.ts` (the same code `cleo run` uses to infer * its class); this module only adds the shell structure around them. * * The rewrite never changes what the line does. It wraps the heavy simple * command IN PLACE and leaves every operator to the agent's own shell, so * pipelines, `pipefail`, `|&`, `&>`, globs and the `time` keyword keep their * meaning in whatever shell runs the line (zsh, bash, dash): * * - `pnpm test | tail` → `cleo run … -- pnpm test | tail` * - `pnpm test &> out.log` → `cleo run … -- pnpm test &> out.log` * - `cd x && pnpm test` → `cd x && cleo run … -- pnpm test` (the `cd` still * moves the agent's shell) * - `CI=1 pnpm test` → `CI=1 cleo run … -- pnpm test` (the assignment reaches * `cleo run`, which passes its environment on) * * The prefix asks `cleo run` for `--passthrough`: the child keeps the line's * stdin, stdout and stderr, and its exit code comes back unchanged. Only the * prefix is inserted; every other byte of the line stays as written. * * A heavy command is only reported (`warn`), never rewritten, when it runs in * a subshell, inside `$(…)` or backticks, as a background job (`&`), inside a * compound command (`if`, `for`, `while`, `case`, `{ …; }`, `!`), reads a * heredoc, or shares a pipeline with another heavy command (both would start * at once and could wait on each other through the pipe). * * What counts as heavy is entirely `run-class`'s call: only the command word * counts (`git commit -m vitest` and `grep -rn tsc .` never do), and * `--version`/`--help` and watch/dev/serve modes are never heavy, because a * job that never exits would hold a slot forever. * * @module resources/heavy-command * @task T12983 * @epic T12978 */ import type { HeavyCommandHookMode, HeavyCommandPlan } from '@cleocode/contracts'; /** * The `cleo run` flag that keeps the child's stdio and exit code (#1777). The * hook's rewrite depends on it: without it `cleo run` prints its own envelope * on stdout, which a pipe or redirection would capture. */ export declare const CLEO_RUN_PASSTHROUGH_FLAG = "--passthrough"; /** Options for {@link planHeavyCommand}. */ export interface HeavyCommandPlanOptions { /** The directory the command line starts in (class inference). */ readonly cwd: string; /** * `cleo run --wait --timeout` in seconds: how long a governed command may * queue before it gives up with `E_RESOURCE_DEFERRED`. Omitted: `--wait` * with `cleo run`'s own default. */ readonly waitTimeoutSec?: number; } /** * Decide what the hook does with one shell command line. * * @param command - the command line as the agent wrote it. * @param opts - where it starts, and how long a governed command may queue. * @returns `none`, a `rewrite` with every heavy command governed in place, or * a `warn` when a heavy command sits where a rewrite could change behaviour. * * @example * ```ts * planHeavyCommand('cd pkg && pnpm vitest run a.test.ts', { cwd: '/repo' }); * // { action: 'rewrite', * // command: 'cd pkg && cleo run --wait --passthrough --class test -- pnpm vitest run a.test.ts', … } * planHeavyCommand('npx tsc --noEmit 2>&1 | head -50', { cwd: '/repo', waitTimeoutSec: 300 }); * // command: 'cleo run --wait --passthrough --timeout 300 --class build -- npx tsc --noEmit 2>&1 | head -50' * planHeavyCommand('for f in a b; do pnpm test $f; done', { cwd: '/repo' }).action; // 'warn' * ``` */ export declare function planHeavyCommand(command: string, opts: HeavyCommandPlanOptions): HeavyCommandPlan; /** * Whether a Claude Code Bash rule pattern (the text inside `Bash(…)`) matches * one subcommand's text, as the permissions reference specifies: `*` stands for * any text including spaces, a trailing `:*` equals a trailing ` *`, a trailing * ` *` that is the rule's only wildcard also matches the bare command, and a * rule without `*` matches one exact command. The bare `Bash` rule is `*`. * * @param pattern - the rule pattern, e.g. `pnpm test *` or `npm run test:*`. * @param text - one subcommand as written. * * @example * ```ts * claudeBashRuleMatches('pnpm test *', 'pnpm test'); // true * claudeBashRuleMatches('pnpm test:*', 'pnpm test --run'); // true * claudeBashRuleMatches('ls *', 'lsof'); // false * ``` */ export declare function claudeBashRuleMatches(pattern: string, text: string): boolean; /** Options for {@link claudePreApproval}. */ export interface ClaudePreApprovalOptions { /** The directory the command line starts in. */ readonly cwd: string; /** * Claude Code's primary working directory (`CLAUDE_PROJECT_DIR`). A `cd` * and the read-only commands are accepted only inside it. */ readonly workingDir: string; } /** Whether Claude Code would run a command line without a prompt. */ export type ClaudePreApproval = { readonly approved: true; } | { readonly approved: false; readonly reason: string; }; /** * Whether Claude Code would run `command` WITHOUT a prompt because the user's * Bash allow rules approve it: every subcommand (split at `&&`, `||`, `;`, * `|`, `|&` and newlines, as Claude Code splits) matches an allow rule, or is * a narrow read-only form Claude Code runs unprompted (`cd` into the working * directory, `tail -50`, `grep -v x`, …). * * The heavy-command hook uses this to rewrite a command in Claude Code's * default, acceptEdits, dontAsk and auto modes with `permissionDecision: * "allow"`: the user already approved the command, and the rewrite only makes * it queue for the machine-wide budget. So this check must never approve more * than Claude Code would. It is deliberately narrower: no wrapper stripping * (`timeout`, `nice`), no leading assignments, no expansions, no * substitutions, subshells, compound commands or background jobs, no * redirection except to `/dev/null` or a file descriptor, and both the stage * as written and its words alone must match the same rule. Anything else is * "not pre-approved" and the hook falls back to a warning. Deny and ask rules * are not checked here (the hook checks them, and Claude Code enforces them * on the rewritten command regardless). * * @param command - the command line as the agent wrote it. * @param allowPatterns - the patterns of the user's `Bash(…)` allow rules (`*` for a bare `Bash`). * @param opts - where the line starts, and Claude Code's working directory. * @returns approved, or the first reason it is not. * * @example * ```ts * claudePreApproval('pnpm test 2>&1 | tail -50', ['pnpm test *'], { cwd: '/repo', workingDir: '/repo' }); * // { approved: true } * claudePreApproval('pnpm test && rm -rf dist', ['pnpm test *'], { cwd: '/repo', workingDir: '/repo' }); * // { approved: false, reason: '`rm` is not approved by an allow rule' } * ``` */ export declare function claudePreApproval(command: string, allowPatterns: readonly string[], opts: ClaudePreApprovalOptions): ClaudePreApproval; /** * Whether `value` (trimmed, any case) names a hook mode. The hook reads the * config only when the environment variable does not. * * @param value - e.g. `process.env.CLEO_HEAVY_COMMAND_HOOK`. */ export declare function isHeavyCommandHookMode(value: string | undefined): boolean; /** * The hook mode: the `CLEO_HEAVY_COMMAND_HOOK` environment variable, else the * `resources.heavyCommandHook` config value, else `rewrite`. Unknown values * are ignored. * * @param envValue - `process.env.CLEO_HEAVY_COMMAND_HOOK`. * @param configValue - the resolved `resources.heavyCommandHook`. * @returns the effective mode. */ export declare function resolveHeavyHookMode(envValue: string | undefined, configValue: string | undefined): HeavyCommandHookMode; /** * `resources.heavyCommandHook` from the merged global + project config, or * `undefined` when it is unset, invalid or unreadable. `cleo init` and * `cleo upgrade` install (or, for `off`, remove) the hook from this value * alone; the environment variable only switches the hook at run time. * * @param projectRoot - the project whose `.cleo/config.json` to read. */ export declare function configuredHeavyHookMode(projectRoot: string): Promise; /** * The project whose config the hook reads: the nearest directory at or above * `cwd` holding a `.cleo/` directory, so an opt-out in the project config * still applies when the agent's shell sits in a subdirectory. Falls back to * `cwd` (the global config still applies there). Never walks into `$HOME`. * * @param cwd - the directory the command line starts in. */ export declare function heavyHookProjectRoot(cwd: string): string; /** * Whether `name` resolves to an executable on `pathValue` (a `PATH` string). * A rewrite to `cleo run` only helps if the agent's shell can find `cleo`. * * @param name - the executable name, e.g. `cleo`. * @param pathValue - the `PATH` to search. */ export declare function executableOnPath(name: string, pathValue: string | undefined): boolean; /** * One context line when the machine is under pressure (`hold` = yellow, * `backoff` = red), or `null` when it is fine or the sample fails. Takes one * bounded sample through the platform backend (one `sysctl` exec on macOS, * `/proc` reads on Linux). * * @param timeoutMs - give up and return `null` after this long. */ export declare function heavyPressureNotice(timeoutMs?: number): Promise; //# sourceMappingURL=heavy-command.d.ts.map