# 操作间数据传递去文件化方案

## 1. 问题边界：CLI 拦不住 Agent 写文件

宿主（WorkBuddy）在 Windows 上通过 PowerShell 调 CLI，典型写法：

```powershell
& $cli @a 2>&1 | Out-File "...\.wb_tmp\inv_raw.json" -Encoding UTF8
(Get-Item "...\inv_raw.json").Length | Out-File "...\size.txt" -Encoding UTF8
```

落盘发生在 **CLI 进程之外**，CLI 感知不到，也无法阻止。所以「从底层限制」必须拆成两个战场：

| 战场 | 手段 | 能做什么 |
| --- | --- | --- |
| CLI 侧 | 关闭「文件输入」这条路的可用性 | Agent 写了文件也喂不进来，被迫改道 |
| CLI 侧 | 提供比文件更好的替代通道 | 让文件变得不必要（疏导） |
| Skill 侧 | 在共享契约里写死红线 + 正确模板 | Agent 行为由 skill 文本驱动，这是唯一能直接约束它的地方 |

只做 CLI 侧会导致 Agent 报错后重试；只做 Skill 侧没有强制力。两者必须同时做。

## 2. 现状代码事实（2026-09-21）

- `--input <path>` 的解析**被复制进了 20 个业务模块**：34 处 `option('--input <path>')` 声明，每个模块自带 `inputFrom()` + `readStdin()` + `stripBom()`（`src/shortcuts/*/index.ts`）。
- `src/core/` 只有 `output.ts`，**没有对应的 `input.ts`**，因此不存在可以一刀切的开关点。
- 已具备的基础能力：`--input-json <json>`（内联）与 `--input -`（stdin）在所有模块都已实现；`--json` 输出 envelope 走 stdout，错误 envelope 走 stderr，退出码语义完整（`src/core/errors.ts`，含 `ExitCode.PolicyError = 6`）。
- `invoice` 已有 continuation 机制（HMAC 签名 token，`src/shortcuts/invoice/continuation.ts`），是「句柄代替数据」的现成雏形，但只用于 prepare/apply 写流程。

## 3. 目标通道模型

按优先级，Agent 应依次尝试：

1. **内联**（`--input-json`）——首选，跨 Windows/macOS 无差异。
2. **stdin**（`--input -`）——次选，需要 shell 能稳定传管道。
3. **引用句柄**（`--input-ref <id>`）——大结果专用，见 P1。
4. **文件**（`--input <path>`）——仅保留给豁免场景，默认告警，strict 下拒绝。

⚠️ **通道 1/2 的前提修正（2026-09-22 补充）**：宿主对 stdout 有截断行为——实测完整内联上限约 24-32 KB，超线后会被截成约 2048 B 预览，且截断是静默的（调用方不知道丢了什么）。这意味着超线结果「写 stdout 直传」这个选项实际上不存在；P1 的句柄化不是可选优化，而是大结果唯一可靠的通道。因此 P1 envelope 的设计必须满足：句柄与摘要控制在截断预算内自解释，且一次性读完（详见 5.1）。

## 4. P0：收口 + 硬开关（唯一能真正"限制"的一步）

### 4.1 新增 `src/core/input.ts`

```ts
export type FileInputPolicy = 'allow' | 'warn' | 'deny';

// 策略优先级：CLI flag > 环境变量 > config > 默认 warn
export function resolveFileInputPolicy(ctx: CliContext, opts: Record<string, any>): FileInputPolicy;
export function guardFileInput(ctx: CliContext, opts: Record<string, any>): void;
```

策略解析顺序：

1. `--allow-file-input` / `--no-file-input`（显式 flag 最高）
2. `WEAVER_WORK_CLI_FILE_INPUT=allow|warn|deny`
3. config `io.fileInput`（`ConfigStore` 已支持任意 key，`~/.weaver-work-cli/config.json`）
4. 默认 `warn`

### 4.2 行为定义

| 策略 | `--input <path>` 行为 |
| --- | --- |
| `allow` | 照旧 |
| `warn` | 正常执行，stderr 输出一行提示（不污染 stdout）：建议改用 `--input-json` 或 `--input -` |
| `deny` | 抛错，退出码 `6`（PolicyError），envelope `type=policy` / `subtype=file_input_denied` / `retryable=false`，message 给出可直接复制的改法 |

`deny` 的错误对象在 core 内统一构造为 structured error（`{ type:'policy', subtype:'file_input_denied', message, retryable:false }`），这样各模块 `toErrorEnvelope()` 无需改动即可识别，不依赖每个模块自己的 `xxxValidationError`。

BOM 处理要保留并统一：Windows `Set-Content -Encoding UTF8` 会写 BOM，`JSON.parse` 不接受前导 BOM，`ehr` 模块的 `stripBom()` 是正确的，收口后所有模块共享这一份。

### 4.3 输入策略接入

当前先在 `createAction` 统一检查 `--input <path>`，不改各业务模块自己的 JSON 解析逻辑：

- 所有叶子命令统一注册 `--allow-file-input` / `--no-file-input`。
- action 在业务 handler 运行前执行 `guardFileInput()`；`--input -` 和业务附件 `--file` 不受影响。
- 默认 `warn` 先兼容旧调用；用户可用 config 或环境变量设为 `deny`，确认迁移完成后再考虑改变默认值。

### 4.4 灰度节奏

1. 第一版默认 `warn`，观察一轮 Agent 实际行为与告警命中率。
2. 确认主流路径都能走内联后，在 skill 安装/初始化时把 `io.fileInput: "deny"` 写进 config。
3. 保留 `allow` 作为显式逃生阀：`--allow-file-input`。

## 5. P1：结果引用（ref store）——让大结果不需要落盘

当单次 JSON 输出超过阈值（默认 24 KB，可用 `WEAVER_WORK_CLI_REF_THRESHOLD` 覆盖）：

- 完整数据写入 `~/.weaver-work-cli/state/refs/<date>/<id>.json`（CLI 自己的 state 目录，不在工作区、不进 Agent 上下文；权限限当前用户）
- stdout 只返回摘要 + 句柄：

```json
{
  "schemaVersion": 1,
  "ok": true,
  "operation": "invoice.list",
  "refId": "20260922-105401-invoice-list-a1b2c3d4",
  "resultPersisted": true,
  "outputBytes": 128743,
  "expiresAt": "2026-09-22T11:24:01.000Z",
  "previewTruncated": true,
  "largeOutputNotice": "结果已暂存……",
  "data": { "items": [ /* 压缩预览 */ ], "count": 137 }
}
```

- 下游用 `--input-ref <refId>` 消费；选单个值时加 `--pick-as <输入字段>`，例如 `--pick items[0].fid --pick-as fid`。CLI 从 state 目录读回（这是 CLI 内部 IO，不经过 Agent）
- ref 带 TTL 过期清理，不长期占用磁盘

结果引用独立使用 `src/core/refs.ts` 保存 SHA-256 校验、TTL 和容量索引；不复用 invoice/workflow 的 continuation，因为结果句柄用于读取业务结果，生命周期和用途不同。

可用 `refs show <refId>` 查看完整小结果；大结果请用 `refs get <refId> <json-path>` 读取需要的字段，避免再次触发宿主 stdout 截断。

### 5.1 落盘必须配「够用的精简版」（2026-09-22 补充，来自宿主截断实测）

只回指针是次优解（模型要多一次工具调用）；最优解是「落盘 + stdout 回精简版和前置索引」，让 90% 场景不需要读文件：

- **渲染层留在 stdout**：`finalMarkdown` 等模型唯一必须渲染的东西（通常几 KB）不落盘，移出的只是结构化明细。
- **移出清单时附紧凑索引**：字段清单被移出时，附一份 `writableFields` 紧凑索引（278 字段表单实测仅 3.7 KB），模型能直接构造 `data` 的键，不用读文件。
- **句柄前置（withLeadingMeta 思想）**：`refId` 排到 JSON 最前面——宿主截断只保留前 2048 B，排在后面等于没有。stdout 不返回绝对文件路径，避免 Agent 直接读取暂存文件。
- **契约明令一次读完、禁用 head/tail**：截断会破坏 JSON 结构，还会导致重复执行同一条命令。

据此 P1 的输出处理规则：

| 档位 | 条件 | 行为 |
| --- | --- | --- |
| 档 0 | ≤ 24 KB | 原样返回全量，不落盘 |
| 档 1 | > 24 KB | 完整结果落盘 + stdout 回前置 `refId` + 尽量压缩的原结构预览 |
| 档 2 | 压缩后仍超线 | stdout 回可解析的最小摘要和 `refId`，通过 `--input-ref` 或 `refs get` 取回数据 |

统一口径：`stdoutBytesOf(v) = JSON.stringify(v, null, 2)` 的 UTF-8 字节数，与 CLI 实际打印一致。

### 5.2 改造面：业务模块基本不用改

**关键代码事实（2026-09-22 核实）**：19 个业务模块的成功输出**全部**经过 `ctx.output.write(...)`（共 41 处调用），失败 envelope 走 `console.error`（stderr，体积小，不受影响）。也就是说输出只有一个出口——`src/core/output.ts` 的 `Output.write`。

因此 P1 **输出侧**的通用档可以做到 **零业务模块改动**：

| # | 文件 | 改动 | 是否碰业务模块 |
| --- | --- | --- | --- |
| 1 | `src/core/output.ts` | 主改动：阈值判断 → 精简/落盘 → 重建 envelope（句柄前置）→ 注入 `refId` / `outputBytes` / `largeOutputNotice` | 否 |
| 2 | 新增 `src/core/state.ts` | 统一 `stateRoot()`（现状：19 个模块各写一份）+ refs 目录 + TTL 清理，可复用现有 `continuation.ts` 的实现 | 否 |
| 3 | 阈值配置 | 复用 `ConfigStore`：`io.resultThreshold`（默认 24576）、`io.refTtlMs`；支持 env 覆盖 | 否 |
| 4 | 新增 `src/core/compact.ts`（可选） | 通用列式压缩 + 模块 `compact` 钩子注册表 | 否 |
| 5 | `src/core/output.ts` 内最终预算检查 | **必须做**：对附加 ref 信息后的整个 envelope 重新计字节；超限时继续精简，保证返回完整、可解析的 JSON | 否 |

**输入侧**（`--input-ref`）由注册层给叶子命令统一加选项，再在 action 入口转换成现有 `--input-json`，业务模块不需要逐个改动。`--pick` 选单个值时还要用 `--pick-as <字段名>` 映射成业务输入对象，例如 `{ "fid": "123" }`。

### 5.3 通用档与精品档

| 档 | 谁实现 | 效果 | 适用 |
| --- | --- | --- | --- |
| 通用档 | core 自动 | 落盘 + refId + 通用压缩 + refId 前置，零模块改动 | 全部 19 个模块，一次性生效 |
| 精品档 | 模块实现 `compact(operation, data)` 钩子 | 按业务语义压出「够用的精简版」（如 278 字段表单压到 3.7 KB 的字段索引） | 先给 invoice / workflow / ebuilder-form 等高频模块 |

建议先上通用档拿到全局收益，再给高频模块补精品档钩子。`finalMarkdown` / 字段索引这类精简版本质上依赖业务语义，core 只能做通用压缩，做不出「够用」的效果。

### 5.4 风险与回归

- **落盘位置必须是 CLI state 目录**（`~/.weaver-work-cli/state/...`），绝不能写 cwd、工作区或自定义 `--config` 文件旁。POSIX 文件权限设为当前用户私有；理由见 5.6。
- **回归开关**：提供 `WEAVER_WORK_CLI_REF_THRESHOLD=0`（或设为极大值）关闭落盘，用于对照回归。小结果（≤ 24 KB）走原路径不受影响，回归风险集中在有大结果的用例。
- 落盘文件需 TTL 清理，避免 state 目录无限增长。

### 5.5 会不会丢内容、能不能追溯、TTL 怎么清

#### 会不会丢：成功落盘时完整结果可取回；写盘失败会明确告知不可恢复

P1 的落盘发生在**写 stdout 之前**——写入成功时完整数据先落 state 目录，stdout 只回指针 + 精简版，因此完整数据不会被宿主截断。若暂存目录写入失败，CLI 会在 stderr 明确告警，并在 stdout 标记 `resultPersisted: false`；此时只有精简预览，完整结果不可恢复。

但仍有以下边界，必须逐个处理，否则会变成新的丢数据点：

| 风险 | 处理 |
| --- | --- |
| 精简版本身仍超输出预算 | 对最终 envelope（包含句柄、摘要和 warnings）计字节数，逐档精简；超预算时返回可解析的最小 envelope |
| 写盘失败（磁盘满 / 权限） | 必须显式报错或降级为「尽力压缩后全量输出 + `degraded` 标记」，**绝不静默截断** |
| ref id 冲突覆盖 | 用时间戳 + 随机串生成唯一 id，禁止按 operation 名复用固定文件名 |
| 模型读完整文件时被 Read 工具截断 | **模型不应直接读文件**。应提供 `--input-ref <id> --pick <jsonpath>` 让 CLI 代劳取子集，避免大文件读回时再次被截 |
| TTL 过期后引用 | 返回明确 `ref_expired` 错误（复用 continuation 的 `*_expired` 语义），而不是「文件不存在」 |

结论：完整数据不丢；模型"看不到"的只是明细，且可用 `--pick` 按需取回。相比现状的静默截断，可观测性大幅提升。

#### 追溯：必须建索引，否则比 `.wb_tmp` 还难查

现状的 `.wb_tmp/时间戳/inv_raw.json` 散落在工作区，事后无法对应到具体哪次调用、什么入参。P1 必须配套可追溯设计：

- **ref id 自带语义**：`20260922-104135-invoice.list-a1b2c3.json`，按日期分目录 `refs/2026-09-22/`
- **索引** `refs/index.jsonl`：保存 `{ refId, operation, createdAt, expiresAt, bytes, sha256 }`；不保存业务入参，避免把敏感信息复制进索引
- **envelope 内自证**：`refId`、`outputBytes`、`sha256`——拿到 stdout 就能定位与校验；绝对路径只由显式的 `refs path` 管理命令显示，不进入业务结果
- **查询命令**：`weaver-work-cli refs list` / `refs show <refId>` / `refs get <refId> <json-path>` / `refs prune ...`；大结果只能用 `refs get` 按需查看

这样追溯链路是：命令输出 → `refId` → `refs show` 查看小结果或 `refs get` 查看子集。索引和业务输出中都不暴露暂存文件的绝对路径。

#### TTL 清理：自动为主，手动为辅，不引入后台进程

CLI 是「一次一进程」模型，没有常驻进程可跑定时任务，因此用**惰性 GC**：

| 机制 | 做法 |
| --- | --- |
| 惰性清理 | 每次 CLI 命令开始时扫描 refs 目录，清理过期项。成本仅一次 `readdir` + 时间戳比较 |
| 容量上限 | 除时间维度外加容量维度：保留最近 N 个（默认 50）或总大小上限（默认 200 MB），新结果写入后再清理旧项；若单个新结果已超过上限，保留本次结果并在 envelope 明确告警 |
| 手动命令 | `refs prune --older-than 7d` / `refs prune --all`，用于用户主动清理 |
| 不要引入 daemon / cron | Windows 无 cron，跨平台成本高。惰性 GC 与 CLI 的进程模型天然契合 |

实现注意：

- **并发安全**：CLI 可能被并行调用。删除用 `unlink`；索引追加一行，重建索引时用「临时文件 + rename」；不要引入锁文件（跨平台问题多）。
- **Windows 占用**：文件被占用时删除会失败，`catch` 忽略即可，下次 GC 再清。
- **过期时间要告诉模型**：envelope 顶层带 `expiresAt`，让模型知道句柄的大致可用窗口，避免隔很久再用已过期的 ref。

结果引用独立实现 TTL（默认 30 分钟）与 `ref_expired` 错误，不复用 invoice/workflow 的 continuation；两者语义和生命周期不同。

### 5.6 为什么必须落 CLI state 目录，不能落工作区

不是「不能」，而是工作区落盘会让 P1 的多项收益同时失效。五条理由按重要性排序：

| # | 理由 | 说明 |
| --- | --- | --- |
| 1 | **GC 直接失效（决定性）** | 惰性 GC 只能扫当前 cwd。调用①在 A 目录写了文件，调用②的 cwd 变成 B，GC 永远扫不到 A 的文件 → 只能永久残留。state 目录是固定路径，任何 cwd 下都能命中 |
| 2 | **跨调用不可达** | 工作区路径绑定 cwd，换一次目录 ref 就找不到；`--input-ref <id>` 要求在任何目录都能解析，只有固定 state 目录能满足 |
| 3 | **敏感数据有入库风险** | 工作区常常是 git 仓库、网盘同步目录或备份范围内。发票数据、OCR 原文、E10 业务明细落在那里，可能被误提交或同步出去。state 目录在用户主目录；POSIX 系统设置为当前用户可读写，Windows 继承用户目录 ACL |
| 4 | **污染用户工作区** | 写文件会让 `git status` 变脏、触发 IDE 文件监听与构建工具 watch 重启；工作区还可能只读或权限受限，写盘失败率更高，一旦失败就触发降级分支 |
| 5 | **会重新打开要关闭的通道** | 工作区路径对 Agent 是可见的、可直接 Read 的。落盘到工作区等于把「直接读文件」这条便捷通道又打开了，P1 想要的「模型不读文件、用 `--input-ref` 按需取」会被绕过。业务 envelope 只返回 `refId`，不暴露 state 文件路径 |

**判据**：文件的所有权归谁？

- **临时传递介质**（上一步的输出、给下一步的输入）→ 归 CLI → state 目录 → 可 GC、全局可达、强制走句柄
- **交付给用户的产物**（下载附件、导出报表、生成的文件）→ 归用户 → 工作区或用户指定路径

后者正是 5.4 与第 10 节的豁免场景（`invoice.download --output`、`export`、用户提供的 `--file` 入参），两者不矛盾。

## 6. P2：进程内 pipeline

```bash
weaver-work-cli chain --steps '[
  {"op":"invoice.list","input":{"page_size":10}},
  {"op":"invoice.get","input":{"fid":"$.steps[0].data.items[0].fid"}}
]'
```

多步在单个 Node 进程内执行，中间数据是内存里的 JS 对象，只输出最后一步结果。附带收益：省掉每次调用重复的 auth 解析与会话建立（现在每次 CLI 调用都要重走一遍）。

## 7. P3：MCP stdio server（2026-09-22 细化）

`weaver-work-cli mcp serve` 把 operations 暴露为 MCP tools，输入输出走 JSON-RPC over stdio，协议层天然无文件。

### 7.1 为什么现在不做（六条理由）

1. **MCP 只换管道，不换数据量问题。** 工具返回值照样进上下文，超线照样被截断或撑爆上下文。P1 的精简版 + 句柄是任何管道都需要的地基。没解决「传多少」就先换「怎么传」，等于白改一轮。
2. **P1 的产物是 P3 的原材料。** 统一 envelope（`refId` / `outputBytes` / `largeOutputNotice` / `persistedResult`）就是 MCP tool 返回值的形状，`--input-ref` 句柄就是大入参的引用方式。契约定稿后 MCP 只是换个出口；反过来先做 MCP，契约一改工具定义全部返工。
3. **常驻进程引入新的风险面。** 要处理 E10 会话生命周期（token 过期/刷新）、并发、错误隔离（单个 operation 崩溃不能拖垮 server）。CLI 现有「一次一进程」模型下，这些问题被进程退出天然兜住。
4. **宿主依赖不在我们手里。** 需要宿主支持配置 stdio server、Windows 下正确 spawn、用户手动 Trust。P1 纯在本仓库内，收益确定、不依赖外部排期。
5. **提速收益被高估。** MCP 省的是每次调用的固定开销（Node 启动 + E10 鉴权，百毫秒级），而 E10 业务网络往返通常才是主导。业务请求 500 ms 时约省 30%，2 s 时只省约 10%；相比之下 P1 省的是整轮 LLM（数秒级）。
6. **验证成本更高。** MCP 调试链路长（握手 → 工具发现 → 宿主集成），CLI 直接跑命令即可验证。

### 7.2 项目特有的约束：tool 数量与体积

当前全仓 **136 个 operation**，每个 manifest 已自带完整 JSON Schema（`inputSchema` / `outputSchema` / `risk`）。全量暴露的后果：

- `tools/list` 响应达数百 KB
- 136 个 tool 描述长期占据上下文，严重拖慢工具选择

所以 MCP 必须先解决「按需暴露」，而这依赖的是 P1 的控量思想，不是协议本身能给的。

### 7.3 实现清单

**利好**：tool 的 `inputSchema` 可以直接复用 manifest 的 `inputSchema`（已是标准 JSON Schema，含 `type`/`enum`/`description`/`default`/`pattern`），`risk` 可映射为 MCP annotation（`readOnlyHint` / `destructiveHint`）。不需要手写 136 份 schema。

| # | 事项 | 说明 | 预估 |
| --- | --- | --- | --- |
| 1 | 传输层 | stdio + JSON-RPC 2.0。建议引 `@modelcontextprotocol/sdk`（`Server` + `StdioServerTransport`），手写协议易漏 initialize 协商/capabilities/cancellation 等细节 | 0.5-1 天 |
| 2 | 协议方法 | 最小集：`initialize`、`tools/list`、`tools/call`；可选 `resources/list`（通过 P1 的 refId 按需读取） | 含在上项 |
| 3 | 模块 dispatcher | 需要一个可枚举的模块注册表（manifest + execute + host + continuation 四件套），当前 20 个模块入口分散，`src/shortcuts/index.ts` 仅 42 行不足以支撑 | 1-2 天 |
| 4 | tools 生成 | manifest → tool；**必须按需暴露**：方案 A 启动时指定 `--modules invoice,workflow`；方案 B 暴露 3 个 meta-tool（`list_operations` / `describe_operation` / `run_operation`），tool 数恒定、体积可控，且与现有 `weaver-work-cli invoice schema` 的心智一致 | 0.5-1 天 |
| 5 | 认证常驻化 | 缓存 E10 凭据与会话；401/会话过期要有刷新或结构化错误（复用 `ExitCode.AuthError=3` 语义）；`--profile` / `--is-connector` 在 serve 启动时固定；token 不得出现在任何输出中 | 1-2 天 |
| 6 | 输出侧 | **直接复用 P1 的 envelope**，CLI 与 MCP 两条通道行为一致，测试与文档只写一份 | 依赖 P1 |
| 7 | 宿主配置 | `~/.workbuddy/mcp.json` 增加 `mcpServers.weaver-e10`，`command` 为 CLI 可执行文件（Windows 下注意 `.cmd` 与 PATH），随后需用户在连接器管理页手动 Trust 才生效 | 1 天+（外部依赖） |
| 8 | 测试 | MCP inspector 或自写 stdio 客户端，跑 initialize → tools/list → tools/call 三轮；补一个大结果走 `refId` 的用例 | 0.5 天 |

合计约 1 周，第 3、5 项不确定性最大。

### 7.4 启动 P3 的判据（需同时满足）

- P1 输出侧已稳定：envelope 契约定稿、阈值与精简版跑通
- 宿主明确支持 MCP stdio server（可配置 + Trust + Windows spawn 正常）
- 有明确的高频多步场景，且**实测**瓶颈确实在进程启动/鉴权而非网络往返

第三条若不成立（大概率不成立），MCP 的价值就只剩「更结构化」，优先级应继续后移。

## 8. 便宜但关键：envelope 自带 next 指引

在成功 envelope 的 `meta` 里返回下一步建议：

```json
"meta": {
  "status": "COMPLETE",
  "next": [
    { "op": "invoice.get", "input": { "fid": "12345" }, "why": "查看详情" }
  ]
}
```

Agent 之所以要暂存中间结果，多半是因为不确定下一步怎么接。给出 `next` 能显著减少自发的落盘行为，且实现成本极低。

## 9. Skill 文档侧改造（必须同步）

`skills/weaver-e10-shared/references/json-output-contract.md` 现在第 79-93 行**主动教 Agent 写临时文件**，且第 113 行、120 行鼓励「超长 JSON 先落盘」。需要：

1. 把「复杂 JSON 用临时文件」降级为最后手段，主推 `--input-json` 与 `--input -`。
2. 新增红线：**禁止把上一步的 stdout 落盘再喂回下一个 operation**；要用变量传递。
3. 给出 Windows PowerShell 正确模板：

```powershell
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$out = (& $cli --json invoice run invoice.list --input-json '{"page_size":10}') -join "`n"
if ($LASTEXITCODE -ne 0) { throw $out }            # 判退出码，不判文件大小
$fid = ($out | ConvertFrom-Json).data.items[0].fid  # 数据留在变量里
& $cli --json invoice run invoice.get --input-json "{""fid"":""$fid""}"
```

要点：不要用 `2>&1`（会把 stderr 混进 JSON）；`Out-File -Encoding UTF8` 在 PS 5.1 会加 BOM，导致下游 `JSON.parse` 失败；`&` 返回的是行数组，`ConvertFrom-Json` 前需要 `-join`。

### 9.1 结果超过预算时：Windows PowerShell 与 macOS/Linux 两套写法

大结果会返回 `refId`，下游直接引用，不必落盘中转。Windows PowerShell：

```powershell
$out = (& $cli --json invoice run invoice.list --input-json '{"page_size":200}') -join "`n"
if ($LASTEXITCODE -ne 0) { throw $out }
$refId = ($out | ConvertFrom-Json).refId
& $cli --json invoice run invoice.get --input-ref $refId --pick items[0].fid --pick-as fid
```

macOS/Linux（bash/zsh）：

```bash
out="$(weaver-work-cli --json invoice run invoice.list --input-json '{"page_size":200}')"
ref_id="$(node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);process.stdout.write(j.refId||"")})' <<<"$out")"
weaver-work-cli --json invoice run invoice.get --input-ref "$ref_id" --pick items[0].fid --pick-as fid
```

两套示例都必须给出：Windows PowerShell 用 `Get-ChildItem` 而非 `ls`；macOS/Linux 侧才使用 `printf`、`mktemp` 等写法。

shared 的 SKILL.md / references 不属于各业务模块 source manifest 的源资料或产物基线；只有某个 manifest 明确列入这些文件时，才需要更新对应指纹。

## 10. 必须保留的文件 IO 豁免

以下场景文件是业务产物而非传递介质，不受限制：

- `invoice.download` 的 `--output <path>`（附件下载）
- `invoice.upload` / `invoice.import.prepare` 的 `--file <path>`（本地发票文件是真实输入源，不是上一步的输出）
- 导出类 operation（blog export、报表导出）

判断标准：**文件是用户提供的输入或交付给用户的产物 → 放行；文件是上一步 CLI 的输出 → 限制。**

### 5.7 实现状态（2026-09-22 已落地）

P1 输出侧与引用输入、P0 文件输入策略已接入共享 action 层，业务模块零改动；文件输入默认 `warn`，严格拒绝需显式设置 `deny`。

| 文件 | 状态 | 说明 |
| --- | --- | --- |
| `src/core/input.ts` | 新增 | `--input <path>` 策略解析与告警/拒绝；不替换业务模块内的 JSON 解析 |
| `src/core/state.ts` | 新增 | `stateRoot` / `refsDir` / `refsIndexPath`；自定义 `--config` 不会把业务结果改存到项目目录 |
| `src/core/refs.ts` | 新增 | 写入 / 校验 / 读取 / 列表 / 惰性 GC / `--pick` 与 `--pick-as` / `RefError` |
| `src/core/output.ts` | 改造 | 阈值判断 → 精简 → 落盘 → refId 前置并限制最终 envelope 大小；`persist:false` 逃逸开关 |
| `src/cmd/action.ts` | 改造 | 每条命令开始时 GC；`--input-ref` + `--pick` / `--pick-as` 翻译成等价 `--input-json` |
| `src/cmd/register.ts` | 改造 | 递归给**叶子命令**注入引用参数与 `--allow-file-input` / `--no-file-input` |
| `src/cmd/refs/index.ts` | 新增 | `refs path` / `list` / `show` / `get` / `prune` |
| `test/core/refs.test.ts` | 新增 | 24 个用例，含 Windows 文件名安全与防回归用例 |

可调参数（env > config `io.*` > 默认值）：

| 变量 | config 键 | 默认 |
| --- | --- | --- |
| `WEAVER_WORK_CLI_REF_THRESHOLD` | `io.resultThreshold` | 24576（0 表示关闭落盘） |
| `WEAVER_WORK_CLI_REF_TTL_MS` | `io.refTtlMs` | 30 分钟 |
| `WEAVER_WORK_CLI_REF_MAX_COUNT` | `io.refMaxCount` | 50 |
| `WEAVER_WORK_CLI_REF_MAX_BYTES` | `io.refMaxBytes` | 200 MB（新结果优先保留；单个结果超限时会明确告警） |

超预算时 stdout 的 envelope（句柄在最前，整个 envelope 会保持在输出预算内）：

```json
{
  "refId": "20260922-111549-result-e71c6a88",
  "resultPersisted": true,
  "outputBytes": 174055,
  "sha256": "cd97...",
  "expiresAt": "2026-09-22T03:45:49.855Z",
  "previewTruncated": true,
  "largeOutputNotice": "结果已暂存。使用 --input-ref ... 引用…",
  "schemaVersion": 1,
  "ok": true,
  "operation": "invoice.list",
  "data": { "…": "精简版，结构不变、明细裁剪" },
  "warnings": []
}
```

实现中踩到并已修掉的两个坑：

1. **只压 `data` 不够**：`invoice schema` 这类命令的 envelope 没有 `data` 键，大字段是 `operations`，只压 data 等于没压。按 envelope 字段通用压缩后，再计算包含 refId 和提示的最终 JSON 字节数；仍超预算时降级为可解析的最小 envelope。
2. **父命令会吃掉子命令的选项**：给中间命令（如 `invoice`）也加 `--input-ref` 时，Commander 的 `parseOptions` 会先把它当自己的选项消费，叶子命令 `run` 的 `opts()` 为空。改为只给叶子命令注入。

已知限制：

- 阈值只按序列化后的字节数判断，不做业务语义压缩（精品档的 `compact` 钩子尚未实现，见 5.3）。
- 索引 `refs/index.jsonl` 只记录元信息，不记录入参（避免 OCR 原文等敏感数据入索引）。

## 11. 选型结论（2026-09-22）

### 11.1 先纠正直觉：真正的瓶颈不是磁盘 IO

按成本从高到低，Agent 多步任务的耗时构成是：

1. **额外的模型轮次**（读一次文件 = 一轮 LLM，数秒级）
2. **因截断丢数据导致的重跑**（一次完整业务请求 + 一轮 LLM，最贵）
3. **上下文膨胀**（大 JSON 进上下文后每轮推理都变慢，且持续到任务结束）
4. 进程启动 + E10 鉴权解析（百毫秒级）
5. 磁盘 IO（毫秒级）

所以「提升速度」的主战场是**减少模型轮次与重跑**，不是消灭文件 IO 本身。相应地，「保证完整」的最大威胁也不是数据没存下来，而是**静默截断 + 部分读取**（`head/tail` 会破坏 JSON 结构）。

这两点恰好都被 P1 直接命中，因此：

### 11.2 结论：以 P1 为主轴，P0 配套，P2 限场景，P3 暂缓

| 方案 | 速度 | 完整 | 成本 | 结论 |
| --- | --- | --- | --- | --- |
| **P1 句柄 + 精简版** | 高 | 高 | 中 | **主轴**，必须做 |
| P0 strict IO | 无提升（短期可能因重试略降） | 间接 | 极低 | 配套做，用于封死工作区落盘 |
| P2 进程内 chain | 最高 | 中 | 中高 | 限确定性链路，锦上添花 |
| P3 MCP | 最高 | 高 | 极高 | 暂缓，P1 的 envelope 契约是它的前置 |

**P2 的隐藏代价**：把多步塞进一个进程后，模型看不到中间结果，动态决策链路（"先看列表再决定看哪条"）用不了——而 Agent 场景大量是动态决策。它只对确定性链路（`prepare → apply`、固定组合）有效，且最终输出超线时仍需 P1 兜底。

**P1 必须对称做完整**：只做输出侧落盘而不做 `--input-ref`，模型要把大载荷回传给下一步时，仍会撞上命令行长度上限（Windows 约 8 KB）或退回落盘到工作区——半套方案会退化回原问题。

### 11.3 落地顺序

1. **P1 输出侧**（1-2 天）：统一 envelope + 24 KB 阈值分级 + 落盘配精简版（5.1 四条规则），提进 `src/core/output.ts`。直接消灭截断丢数据与重跑。
2. **P1 输入侧**（0.5-1 天）：`--input-ref` 消费句柄，让大载荷回传不必内联。
3. **P0**（收口时顺手做）：`src/core/input.ts` + `warn/deny` 策略，默认 `warn` 观察一轮。
4. **P2**（按需）：仅对高频确定性链路加 `chain`。
5. **P3**：待前四步稳定、宿主 MCP 支持明确后再评估。

### 11.4 不该做的事

- 不要让模型自己筛字段来规避超线——会漏字段，且要多轮试错。
- 不要默认全量落盘——多一次读文件轮次，比内联更慢。精简版是关键。
- 不要先做 P3——P1 定义的 envelope 契约是 P3 的前置，跳过会让 MCP 的工具定义反复返工。

### 11.5 验收指标（可度量）

取「多步任务」样本，改造前后对比：

- 平均模型轮次数（目标：多步任务减少 1-2 轮）
- 因截断导致的命令重跑率（目标：降到 0）
- 工作区 `.wb_tmp` 临时文件产生次数（目标：降到 0）
- 单次任务上下文 token 峰值（目标：大结果场景显著下降）

## 12. 验收清单

- [ ] `src/core/input.ts` 的策略已由 action 入口统一接入，叶子命令有策略选项
- [ ] `WEAVER_WORK_CLI_FILE_INPUT=deny` 下，`--input <path>` 退出码 6 且 envelope subtype 为 `file_input_denied`
- [ ] `warn` 模式下提示只写 stderr，不影响 stdout 的 `JSON.parse`
- [ ] stdout 在 `--json` 下保持无 BOM、无额外日志
- [ ] 下载/上传/导出类 operation 不受策略影响
- [ ] shared 文档红线已更新
- [ ] `npm run build` 与 `npm test` 通过
