#!/usr/bin/env node
import { type Brain, type OnAsk, type TaskSpec } from "@sema-agent/core";
import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
import { type ServiceConfig } from "./config.js";
import { type Logger } from "./observability/logger.js";
/** Parsed CLI invocation. */
export interface RunLocalArgs {
/** The task objective (the first positional argument). */
objective: string;
/** `--root
` — the local config/data root (overrides CONFIG_LOCAL_DIR / AGENT_DATA_DIR). */
root?: string;
/** `--json` — print the full TaskResult JSON instead of just the final assistant text. */
json: boolean;
/** `--scenario ` — pick a capability bundle (default: config.defaultScenario). */
scenario?: string;
/** `--session ` — REUSE a session across invocations (durable multi-turn). Default: a fresh id (the
* session + its events still persist to disk, just not continued). */
session?: string;
/** `--workspace ` — the FIXED dir the agent's tool calls operate in (CC-like). Default: the cwd. The
* agent's file edits PERSIST there (never deleted) — this is the user's own directory, not a sandbox. */
workspace?: string;
/** `--user ` — the local memory/identity scope (so long-term memory accumulates per user). Default "local". */
user?: string;
}
/** Injectable side-effects so {@link runLocal} is deterministic + testable (no real process I/O). */
export interface RunLocalDeps {
/** Override the Brain (a mock in tests). When absent, the real `createBrain(config, { fetchImpl })` is used. */
brain?: Brain;
/** Forwarded into `createBrain` when no `brain` is given — lets a test fake the gateway wire. */
fetchImpl?: typeof fetch;
/** Structured logger (defaults to the real one at config.logLevel). */
logger?: Logger;
/** stdout sink for the human/JSON result (defaults to console.log). */
print?: (line: string) => void;
/** stderr sink for the "you still need to set …" guidance (defaults to console.error). */
printErr?: (line: string) => void;
/** [ref] 件三: override the TTY detection behind the {@link createLocalApprover} seat
* (defaults to `process.stdin.isTTY === true`) — lets a test drive BOTH arms deterministically. */
isTty?: boolean;
/** [ref] 件三: the TTY arm's prompt (defaults to a readline `y/N` on stdin/stderr);
* `signal` = core `OnAsk` 的取消(EOF/abort 决断臂见 {@link promptYesNoOnTty})。 */
askYesNo?: (prompt: string, signal?: AbortSignal) => Promise;
}
/** Parse argv (everything AFTER `node run-local.js`). Throws a clear Error on a missing objective. */
export declare function parseArgs(argv: string[]): RunLocalArgs;
/** A bad-usage error → print to stderr + exit 2 (distinct from a task failure exit 1). */
export declare class UsageError extends Error {
}
/**
* The model GATEWAY pre-flight (mirrors the doctor message): the engine cannot run a real
* task without (a) a gateway base URL that ends in `/v1` and (b) the gateway/model API key resolvable from
* env. We surface a "you still need to set: …" list rather than letting the first brain call crash with a
* cryptic auth/connect error. Returns a list of missing items (empty = OK).
*
* NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips
* this check in that case — the check is about a REAL run, not the assembly.
*/
export declare function missingGatewayRequirements(config: ServiceConfig): string[];
/** 本腿读的部署配置切面(`ServiceConfig` 结构满足;两个构造口各自的切面之和)。 */
export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView {
}
/**
* [ref] 件三:run-local 腿的**部署治理链装配**([ref])。
*
* 与另外两条腿(HTTP 的 `resolveSpec`、durable park 的赎回腿)装的是**同一条**链,经**同一个**构造口:
* 审批基线作 base 的 `toolPolicy` 座,再经**折叠属主** `applyRuntimeGovernance`(→ core `tightenTaskSpec`)
* 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
* 自己合成。
*
* · **审批基线 = adjudicated allow-all**:一次性 CLI 没有 durable 审批设施(没有 `/decide` 腿可赎回),
* 所以 `createApprovalBaselinePolicy` 走 durable 缺席那一臂。它是一条**在场的** effect-aware 策略
* (满足 core 的 `hasEffectAwareGate`),运维照旧用 `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
* · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
* (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
* 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
* ✅ **旧残余面已收口(core 5.19.0 [ref],2026-08-08 校正)**:`rootPath` 是装配期的**静态**值,而 core
* 的写工具解析相对形用的是被 Bash `cd` 就地改写的活 cwd —— 于是 `cd .git/hooks` 之后
* `Write("pre-commit")` 落进守卫段却判不出来(两条 host 腿同形,本腿与 resolve-spec)。5.19.0 起守卫
* 按 `ToolCallRequest.cwd`(引擎每次调用从同一只 tracked cwd 盖戳)解析写目标,本腿零改动即得保护;
* **不对称是 core 故意的**:目标跟活 cwd 走,守卫自己配置的目录仍锚静态根,`cd` 搬不动栅栏。全文与
* 证据见 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 顶注,端到端正控钉见
* test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
* · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
*/
/** @param engineDataRoot 本腿已铸的引擎记忆根(`memoryEngineBackendFor(config, root)?.root`,S-133 单一写者);`undefined` = 引擎未接。 */
export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string, engineDataRoot: string | undefined): TaskSpec;
/** {@link createLocalApprover} 的 io 面(全部可注入,所以两条臂都测得动)。 */
export interface LocalApproverIo {
/** 这次调用是不是接在一个人面前(生产里 = `process.stdin.isTTY`)。 */
readonly isTty: boolean;
/** TTY 臂的问人器(默认 {@link promptYesNoOnTty} 的 readline y/N;`signal`=core 取消透传)。 */
readonly askYesNo: (prompt: string, signal?: AbortSignal) => Promise;
/** 非 TTY 臂的告知面(stderr)。 */
readonly printErr: (line: string) => void;
}
/**
* [ref] 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
*
* 🔴 为什么这个座位是接治理链的**同窗必做项**(成因,留档):彼时 run-local 的主 runner 带着
* checkpointStore(file backend),于是 `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进
* `irreversibleTools` ⇒ park lane effective ⇒ **每一次 shell 调用都 durable park 成一张永远没人 resume
* 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里没有任何赎回腿,那不是「等人来批」,是卡死。
* core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk` 早退(prepare-task dist 亲读),ask 于是落回
* 本回调 —— 把 park 换成「问人 / 当场拒」的正当接法。
*
* 🔴 **本席不是那颗雷的唯一防线,也从来不该是**(B-060 批 codex 交叉复审 [medium] 的教训):它只挡
* **审批**那条车道;core 的**平台/资源停驻**(`suspendForPlatformLimit`)由「店在不在」一位武装,
* 压根不经过 onAsk。所以自 B-060 起本腿的结构性防线是**整条腿不接 checkpointStore**(装配点在
* `createSharedRunnerDeps` 调用处,理由全文在那里),本席退回它本来的职责:**给 TTY 前的人一次真的
* 问答**。两道并存不是冗余 —— 一道管「谁来答」,一道管「答不了时会不会留下无人兑付的行」。
*
* ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
* 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
*
* 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
* `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
* 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。
*/
export declare function createLocalApprover(io: LocalApproverIo): OnAsk;
/** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。
* streams 可注入=EOF/abort 两臂测得动(默认 process.stdin/stderr)。
*
* 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
* **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
* 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
* · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
* · readline `close`(输入流 end/close 时 readline 自关);
* · `question` 自身 reject(abort throw / 流错误)。 */
export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
input: NodeJS.ReadableStream;
output: NodeJS.WritableStream;
}): Promise;
/**
* Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
* code (0 = completed, 1 = the task did not complete / a fatal error, 2 = bad usage / unmet env). Pure of
* `process.exit` so a test can assert the code; `main()` applies it.
*/
export declare function runLocal(argv: string[], deps?: RunLocalDeps): Promise;
//# sourceMappingURL=run-local.d.ts.map