docs(i18n): refresh Agent Note translations after merge

This commit is contained in:
Tianyi Cui
2026-07-23 00:10:53 +08:00
parent cd17f8d23a
commit 744d65d63c
325 changed files with 1535 additions and 1321 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-property-based-testing.md: 153584d3a2b77c8f2d103db02646f18a9d424b57
2026-06-11-property-based-testing.zh.md: e11d11f7db9eee97ab81bc678afab5d0fea36bf9
2026-06-11-property-based-testing.md: ac35591cb76c1d4243ecba153e8e143290716926
2026-06-11-property-based-testing.zh.md: 4a0def2d28ef828fd68b78e4c1e100ce7f85d4f0
@@ -1,4 +1,4 @@
# RFC: 对协议形态代码进行基于属性的测试
# Agent Note: 对协议形态代码进行基于属性的测试
Status: implemented
@@ -22,8 +22,8 @@ Status: implemented
## 后果
- 生成器质量是价值杠杆——生成器偏向小索引池和短字符串,使碰撞与交错频繁发生。
- **已经产出回报:** BlockAssembler 流测试发现了一个真实 bug——同一索引重复 `block-end` 覆盖了已刷出的块,导致流式前缀与最终 `blocks()` 不一致。已修复(首次关闭生效,与既有的滞后分片规则一致),并附带一个专门的回归测试。
- **已经带来回报:** BlockAssembler 流发现了一个真实 bug——同一索引重复 `block-end` 会改写已经完成的块。现已修复(首次关闭优先,与现有迟到项规则一致),并加入专用回归测试。
- 属性测试因超时而 flake 是一个发现,不应通过重试消除。循环属性测试在设计上是确定性的(通过 `agent/status` settle),因此挂起即为真实缺陷。
- 属性测试是对示例测试的补充而非替代;示例测试固定特定分支,服务于 100% 覆盖率门禁。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-19-acp-snapshot-tests.md: c336b4864b73b8db29c0a8bb983d974348a9515a
2026-06-19-acp-snapshot-tests.zh.md: bc9488562b0698b172ccffff74815a893acf722d
2026-06-19-acp-snapshot-tests.md: 43900632c4e5e3904c2d0e3b75f2a3fe3b3a50cc
2026-06-19-acp-snapshot-tests.zh.md: 9d9e49cf799ccc07fb9fbc4b13261b7d305d68dc
@@ -1,4 +1,4 @@
# RFC: ACP 快照测试——一次录制 / 确定性回放
# Agent Note: ACP 快照测试——一次录制 / 确定性回放
Status: implemented
@@ -6,19 +6,21 @@ Status: implemented
## 问题
单元测试无法覆盖完整的 ACPAgent Client Protocol)子进程 transcript(文本记录),而真实 API 测试既不确定又需要密钥。因此,面向编辑器的 `session/update` 输出可能在单元覆盖率全绿的情况下发生回归,正如 [default-export 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)所揭示的那样
单元测试不会覆盖完整的 ACPAgent Client Protocol)子进程 transcript(文本记录),而真实 API 测试不具确定性且受密钥门控。因此,即使单元覆盖率为绿色,面向编辑器的 `session/update` 输出可能回归,[默认导出事后分析](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)已经证明了这一点
全 transcript 测试的阻塞因素在于模型:agent 的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度与 fixture(测试前置数据)的确定性兼得。
全 transcript 测试的阻塞因素在于模型:agent(智能体)的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度与 fixture(测试前置数据)的确定性兼得。
RFC 记录了新增第三层测试——**快照测试**——的决策,以及使其具备确定性、CI 中无需密钥、维护成本低的设计选择。
Agent Noteagent 决策记录)记下了新增第三层测试——**快照测试**——的决策,以及让它具备确定性、CI 中无需密钥、维护成本低的设计选择。
## 决策
快照测试启动真实 ACP 示例,通过确定性脚本驱动其 stdio 协议,并将归一化后的输出与已提交的 golden 文件比对。一次从真实 API 录制的会话日志为后续所有模型流提供数据。fixture 就是产品正常持久化 JSONL。
快照测试启动真实 ACP 示例,通过确定性脚本驱动其 stdio 协议,并将规范化输出与已提交的预期输出比较。从真实 API 一次记录的 session log 为后续所有模型流提供数据。fixture 就是产品普通的持久化 JSONL。
### fixture 即持久化的会话 JSONL
每个场景的 `session.jsonl`一次真实运行中采集。`assistant/chunk` 事件现模型流;tool、message 和 boundary 事件捕获 harness 行为。一份普通的会话产物因此同时充当回放源和行为 golden
每个场景的 `session.jsonl` 从真实运行中采集。`assistant/chunk` 事件现模型流;工具、消息和边界事件捕获 harness 行为。因此,一份普通 session 产物同时充当重放来源和行为预期输出
当场景固定另一种物理存储布局时,其 fixture 会从真实的未打包对应项机械派生。场景测试要求包含每一种预期存储行类型,并在解码后逐事件精确相等;随后,普通重放与日志比较才会证明组合后的进程能够消费并复现该布局。
### 回放从日志推导模型脚本
@@ -30,7 +32,7 @@ Status: implemented
```
{ kind: 'chunks', chunks: StreamChunk[] }
| { kind: 'throw', chunks: StreamChunk[], message: string, code: string, status?: number }
| { kind: 'throw', chunks: StreamChunk[], message: string, code: string }
| { kind: 'hang' }
```
@@ -42,24 +44,24 @@ Status: implemented
### 录制采集日志;无密钥回放需要无提供方的配置
录制使用真实 `llm-deepseek` 适配器和 JSONL 持久化后端运行场景,然后将产出`.jsonl` 复制到场景目录。逐事件追加持久,但 harness 在采集前优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),确保最终事件已刷`llm-replay` 本身不做录制,它只负责放。
记录模式使用真实 `llm-deepseek` adapter 和配置为 `persistenceCompression: 'none'` JSONL 持久化后端运行场景,再把生成`.jsonl` 复制到场景目录。显式 raw 模式让已提交重放 fixture 保持逐行可读,而普通部署使用后端的压缩默认值。逐事件追加具有持久,但 harness 在采集前优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),确保最终事件已刷`llm-replay` 本身不执行记录——它只负责放。
放使用 `cordis.snapshot.yml` 覆盖配置,将真实适配器替换为 `llm-replay`,同时保留活跃的组合。录使用普通配置和 harness 提供的持久化根目录。放模式跳过 `.env` 加载,因此一个意外存在的 API key 不会触发实调用。见[单源配置 RFC](2026-07-04-single-source-acp-replay-config.md)。
放使用 `cordis.snapshot.yml` overlay,以 `llm-replay` 替换真实 adapter,同时保留实时组合。录使用普通配置和 harness 提供的持久化根目录。放模式跳过 `.env` 加载,因此意外存在的 API 密钥不会触发实调用。见[一来源配置 Agent Note](2026-07-04-single-source-acp-replay-config.md)。
### 两个表面:归一化后比对
快照运行断言**两个**归一化后的表面,因为 harness 的外部表面是不同的:
1. **stdout transcript**——编辑器看到的带帧 `session/update` JSON-RPC。捕获 ACP bridge 事件→update 转换(`streamSessionEventUpdate`的回归。与已提交的 `stdout.golden.jsonl`
2. **重新持久化的会话 JSONL**归一化后与 `session.jsonl`。同一 fixture 既是回放源也是预期日志。提示词文本被擦除;每个 header 类别一个场景固定可读 prompt 和 tool 内容,见 [header-pinning RFC](2026-07-06-pin-request-header-content-in-one-scenario.md)。覆盖场景的模型行为完全来自其伴随文件
1. **stdout transcript**——编辑器看到的、经过 framing 的 `session/update` JSON-RPC。用于捕获 ACP bridge 事件→更新转换(`streamSessionEventUpdate`)的回归。与已提交的 `stdout.expected.jsonl`
2. **重新持久化的 session JSONL**经过规范化后与 `session.jsonl`。同一 fixture 同时作为重放来源和预期日志。Prompt 文本会被清理;按照[请求头固定 Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md)所述,每种请求头类别一个场景固定可读 prompt 与工具内容。Override 场景仅从其 sidecar 派生模型行为
两个表面互补:stdout 覆盖 bridge 投影,JSONL 覆盖投影所省略的 loop、tool 和 boundary 结构。
归一化替换 session、cwd、protocol-id、时间戳、路径和进程相关的易变值,同时保留确定性序号。场景真实 bash 使用限制在稳定命令范围内。stdout golden 保持协议格式(wire format的 JSONL,每一行原始数据必须可解析为 JSON。Vitest 只更新 stdout golden;归一化后的会话相等性检查从不覆盖放 fixture。
规范化会替换 session、cwd、协议 id、时间戳、路径和进程易变值,同时保留确定性序号。场景真实 bash 使用限制在稳定命令。stdout 预期输出仍是线协议形状的 JSONL,每个原始行都必须可解析为 JSON。Vitest 只更新 stdout 预期输出;规范化 session 相等性检查从不覆盖放 fixture。
### 隔离:当前靠归一化,后续可加沙箱
工具确定性来自临时 cwd、擦除的环境变量、全新的非登录 shell、受限命令和归一化。它不声称具备操作系统级隔离。如果需要更强的隔离层级,可通过既有的[能力 seam](../architecture/2026-06-13-capability-seams.md) 将沙箱执行器替换本地后端。
工具确定性来自生成的 cwd、清理后的环境、全新的非登录 shell、受限命令和规范化。cwd 默认为平台临时目录;当临时目录是始终可写的策略根,而行为需要独立项目位置时,场景可以改为提供其父目录。并发重放运行各自拥有独立 cwd、持久化目录和由定长场景键区分的 spill 根目录,因此一个场景的拆除无法删除另一个场景仍在进行的完整输出恢复,同时真实路径预览预算保持稳定。该层不声称提供 OS 级隔离。如果需要更强层级,sandbox executor 可以通过现有[能力接缝](../architecture/2026-06-13-capability-seams.md)替换本地后端。
### 回放插件是独立的包
@@ -67,16 +69,16 @@ Status: implemented
### 两个子命令,回放在默认门禁中
`pnpm run test:snapshot` 无需密钥地回放已提交 fixture`test:snapshot:record` 使用真实 API 并重写采集到的会话日志和 stdout golden。fixture 缺失时立即报错。每个场景携带 `input.json``stdout.golden.jsonl``session.jsonl`无模型场景使用仅含 header 的日志。`replay.override.json` 仅在标记为 `overridden` 的场景中必需,因为它的存在会替换推导出的回放。fixture 守卫拒绝缺失、不匹配和遗留的文件。两个命令接受场景过滤器。
`pnpm run test:snapshot` 无需密钥即可重放已提交 fixture`test:snapshot:record` 使用真实 API并重写采集的 session log 与 stdout 预期输出。缺少 fixture 时会响亮失败。每个场景都包含 `input.json``stdout.expected.jsonl``session.jsonl`不调用模型的情况使用仅有请求头的日志。只有标记为 `overridden` 的场景才需要 `replay.override.json`,因为它一旦存在就会取代派生重放。Fixture 守卫拒绝缺失、不匹配和孤立文件。两个命令接受场景过滤器。
## 曾考虑的替代方案
- **手工编写模型 chunk `llm.json`**早期草案的做法。复用真实会话日志使 fixture 成为系统的真实产物而非手工构建的 mock,并兼作行为 golden
- **手工编写包含模型 chunk `llm.json`**——早期草案;复用真实 session log使 fixture 成为系统的真实产物而非手工构建的 mock,并让它同时充当行为预期输出
- **字节级 HTTP 录制库(Polly/nock/MSW)**:否决。与适配器耦合,处理流式 SSEServer-Sent Events)时笨拙,且层级低于被测对象。
- **从 `turn/end {kind:'error'|'aborted'}` 合成 throw/cancel 条目**:否决。这会将 `llm-replay` 耦合到 loop 内部的轮次关闭语义,且 `turn/end` 原因是有损的(无法区分抛出的 401 与 finish-error);显式的 `replay.override.json` 伴随文件是更清晰的 seam。
## 后果
新测试层为每个场景增加经评审的 input、session、stdout、可选 override 和可选 workspace fixture。workspace 种子在录制和回放时都会被复制到临时 cwd。作为回报,该层通过真实 Loader 和 tool 组合提供确定性的无密钥 transcript 覆盖。子进程、input、workspace、归一化和放 harness 可以支持 ACP 之外的示例。
新测试层为每个场景增加经评审的输入、session、stdout、可选 override 和可选 workspace fixture。记录与重放都会把 workspace seed 复制到生成的 cwd。作为回报,该层通过真实 Loader 和工具组合提供确定性的无密钥 transcript 覆盖。子进程、输入、workspace、规范化和放 harness 可以支持 ACP 之外的示例。
RFC 与[拟议的确定性 RFC](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md) 相关但不取代它:该提案的通用放 fixture在每次测试后重新推导会话的*消息历史*(一项内部一致性不变),而快照测试固定的是*外部协议输出*。二者互补:一个守护事件溯源不变,另一个守护面向编辑器的契约。
Agent Note 与[拟议的确定性 Agent Note](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md)相关但不取代它:该提案的通用放 fixture在每次测试后重新派生 session *消息历史*内部一致性不变),而快照测试固定*外部协议输出*。两者相互补充——一个守护事件溯源不变,另一个守护面向编辑器的契约。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-19-real-api-e2e-ci.md: cc3e14e2d411dfa4cc68132f649f4ed26ab1de1d
2026-06-19-real-api-e2e-ci.zh.md: 58a2a87541fd5b73b8272aa729f9dd09a429e4b4
2026-06-19-real-api-e2e-ci.md: a9289980ada8ab6390b5daa488f07ec258db1bd5
2026-06-19-real-api-e2e-ci.zh.md: 2e8f9fff5cdbada2b1ebfd45c4b3ebbc10630f09
@@ -1,4 +1,4 @@
# RFC: 在 CI 中对外部 DeepSeek API 运行真实 API e2e 测试
# Agent Note: 在 CI 中对外部 DeepSeek API 运行真实 API e2e 测试
Status: implemented
@@ -6,11 +6,11 @@ Status: implemented
## 问题
按照策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../testing.md) 论证了无密钥套件只能验证管道连通性而非产品本身,[ACP inject 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)是现成的证据——178 无密钥测试全绿,而真实编辑器会话一启动就崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)正是为弥合这一差距而存在的:它驱动 agent(智能体)对接实时 DeepSeek API——真实模型调用、真实 bash 工具、多轮次对话、恢复、ACP-over-stdio。
根据策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../../docs/testing.md) 指出,无密钥套件证明的是管线,而非产品[ACPAgent Client Protocolinject 事后分析](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)则是常设证据——178 无密钥测试保持绿色时,真实编辑器 session 却立即崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)的存在正是为弥合这一缺口:它针对实时 DeepSeek API 驱动 agent(智能体)——真实模型调用、真实 bash 工具、多 turn、恢复、ACP-over-stdio。
默认门禁([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml))刻意无密钥:不携带 secret,可供 fork 运行。`test:e2e` 在无密钥时自动跳过(`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`),因此将其加入该工作流只会报绿而不会真正执行真实套件。要让真实 API 覆盖率成为合并信号,需要一个独立的、携带 secret 的工作流。
RFC 记录的决策是:添加一个**第二个、消费 secret 的工作流**在 CI 中运行真实 API 套件。由于这是向一个未来可能公开的仓库引入个 CI secret属于安全/隔离决策,本文同时记录其依赖的威胁模型以及仓库公开的变
Agent Noteagent 决策记录)记下了新增**第二消费 secret 的工作流**在 CI 中运行真实 API 套件的决策;由于向未来可能公开的仓库引入第一个 CI secret 属于安全/隔离决策,本文记录其依赖的威胁模型以及仓库公开时需要做出的变
## 决策
@@ -22,7 +22,7 @@ ci.yml 的价值在于它无密钥、可 fork、始终为绿:任何贡献者
### 约束不是成本,而是可靠性
内部推理inference成本不是限制因素,因此工作流覆盖和信号优化目标。它在多触发条件和每个可信 PRPull Request)上运行所有匹配的 `*.e2e.ts` 文件,落实 [docs/testing.md](../../../testing.md) 的有密钥策略。
内部推理成本不是限制因素,因此工作流针对覆盖和信号优化。它在多触发条件和每个受信任 PRPull Request)上运行所有匹配的 `*.e2e.ts` 文件,落实 [docs/testing.md](../../../../docs/testing.md) 的有密钥策略。
### 触发条件:仅限可信事件
@@ -58,6 +58,8 @@ repo secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;映射到适配器和测试
job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和 schedule 运行完整执行以提供合并后信号。
DeepSeek 原生 `web_search` 探测已注册但会跳过。实时 Anthropic 兼容端点可能返回成功响应却没有结构化来源块,因此对来源存在性的正向断言不是可靠的合并信号;单元覆盖率仍会固定响应解析,但 CI 不会证明实时来源块的线协议形状。
## 安全性
仓库的首个 CI secret 需要一份记录在案的威胁模型,因为同仓库 PR、fork PR 和 Dependabot PR 的访问权限各不相同,且仓库公开后会发生变化。
@@ -95,6 +97,6 @@ job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属
新增一个 CI 工作流和仓库的首个需要维护的 secret。真实 API 套件现在作为合并门禁(可信 PR 上的合并前门禁、主分支上的合并后门禁)并每夜运行,因此 agent 与外部 API 交互中的真实故障会在 CI 中浮现,而非仅在开发者的本地运行中出现——代价是每个可信 PR 和合并都会产生真实的(但内部免费的)API 调用。preflight 使 secret 配置错误变为自我通告而非静默禁用安全网。
设计携带一个记录在案的约束面:`pull_request` 触发器密钥暴露权衡(移除以加固)、`if:` 门禁对基于作者的 Dependabot 判断的依赖,以及对 `pull_request_target`硬性禁止。上公开清单是运维伴侣——本 RFC 是未来维护者在更改触发器集合或翻转仓库可见性之前应重读的地方,而非从头重新推导 fork/secret 模型。
设计带有已记录的约束面:`pull_request` 触发器密钥暴露方面的取舍(删除它可加强防护)、`if:` 门禁对基于作者的 Dependabot 检查的依赖,以及对 `pull_request_target`严格禁止。上公开仓库检查清单是操作配套——未来维护者在更改触发器集合或切换仓库可见性之前应重新阅读本 Agent Note,而不是从头推导 fork/secret 模型。
schedule 触发器在仓库不活跃 60 天后会自动禁用(GitHub 行为);push/PR/dispatch 是后备,活跃的 monorepo 不会触及此限制。假设 runner 对 `https://api.deepseek.com` 有出站连通性——GitHub 托管的 `ubuntu-latest` 具备此条件;受出站限制的自托管 runner 需要在依赖每夜运行之前确认连通性。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-20-remove-redundant-snapshot-log-goldens.md: 18d0a4491eb10a3b4dc56d3d63285c219ba6a00a
2026-06-20-remove-redundant-snapshot-log-goldens.zh.md: 5675c69862b6052ed3f3e4710461cc1478b9fa7d
2026-06-20-remove-redundant-snapshot-log-expected-output.md: c2452f971d3cb76dceb766072dbc0a5c81465e78
2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md: b584bfa0154c71c1bbb683c3041dc7baf7a3548e
@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-remove-redundant-snapshot-log-expected-output.zh.md)
## Problem
Model-driving ACP snapshot scenarios ship both `session.jsonl` and `session.expected.jsonl`. For normal recorded scenarios, `session.jsonl` is the replay fixture harvested from a real run, and the replay test normalizes the newly persisted log and compares it to `session.expected.jsonl`. In the current fixtures, the two normalized logs are identical for ordinary recorded scenarios.
@@ -1,24 +1,24 @@
# RFC: 使用 `session.jsonl` 作为唯一的快照会话日志产物
# Agent Note: 使用 `session.jsonl` 作为唯一的快照会话日志产物
Status: implemented
[English](2026-06-20-remove-redundant-snapshot-log-goldens.md) | 中文
[English](2026-06-20-remove-redundant-snapshot-log-expected-output.md) | 中文
## 问题
模型驱动的 ACPAgent Client Protocol)快照场景同时包含 `session.jsonl``session.golden.jsonl`。对于普通录场景,`session.jsonl` 是从真实运行采集的放 fixture(测试前置数据),回放测试新持久化的日志做归一化后`session.golden.jsonl` 比较。在当前 fixture 中,普通录场景的归一化录制日志与归一化 golden 完全一致
驱动模型的 ACPAgent Client Protocol)快照场景同时包含 `session.jsonl``session.expected.jsonl`。对于普通录场景,`session.jsonl` 是从真实运行采集的放 fixture(测试前置数据);重放测试会规范化新持久化的日志,并将其`session.expected.jsonl` 比较。在当前 fixture 中,普通录场景的两份规范化日志完全相同
手工编写的覆盖场景(`error-finish``cancel`)目前使用 `replay.override.json` 驱动模型行为,并保留 `session.jsonl` 为最小占位 fixture,而 `session.golden.jsonl` 存放预期的持久化日志。覆盖文件是一个 `ReplayEntry` 对象的 JSON 数组:`{ "kind": "chunks", "chunks": StreamChunk[] }``{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string, "status"?: number }``{ "kind": "hang" }`。这种拆分同样是多余的:当覆盖 sidecar 存在时,`llm-replay` 会替换派生脚本,不需要从 `session.jsonl` 获取模型分片,因此 `session.jsonl` 仍可作为场景的预期会话日志产物。
手工编写的 override 场景(`error-finish``cancel`)目前使用 `replay.override.json` 驱动模型行为,并 `session.jsonl` 保留为最小 dummy fixture,而 `session.expected.jsonl` 存放预期的持久化日志。override 文件是 `ReplayEntry` 对象组成的 JSON 数组:`{ "kind": "chunks", "chunks": StreamChunk[] }``{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string }``{ "kind": "hang" }`。这种拆分同样没有必要:override sidecar 存在时,`llm-replay` 会替换派生脚本,不需要从 `session.jsonl` 取得模型 chunk,因此 `session.jsonl` 仍可作为场景的预期 session log 产物。
## 决策
彻底移除 `session.golden.jsonl` 概念。每个场景最多只有一个提交到仓库的会话日志产物,即 `session.jsonl`
彻底移除 `session.expected.jsonl` 概念。每个场景最多只有一个提交 session log 产物,即 `session.jsonl`
- 对于录制场景,`session.jsonl` 仍是原始采集的日志。回放仍从中派生模型分片,快照测试将回放运行归一化后的持久化日志与归一化后的 `session.jsonl` 进行比较。
- 对于手工编写的覆盖场景,`replay.override.json` 驱动模型行为,`session.jsonl` 存放预期产出的会话日志。当覆盖文件存在时,回放适配器不从 fixture 获取模型分片,因此同一个文件既可作为预期日志,又不影响回放行为。
- 对于无模型场景,`session.jsonl` 可保留为引导 `llm-replay` 所需的最小 fixture;除非场景创建了持久化会话,否则无需进行会话日志比较。
stdout golden 保持不变;它们是面向编辑器的投影,与会话 fixture 不构成冗余
Stdout 预期输出保持不变;它们是面向编辑器的投影,与 session fixture 并不重复
## 曾考虑的替代方案
@@ -26,11 +26,11 @@ stdout golden 保持不变;它们是面向编辑器的投影,与会话 fixtu
## 验证
`session.golden.jsonl`快照 harness、fixture、遗留文件守卫和文档中不再出现;快照测试对每个模型场景都从 `session.jsonl` 派生预期会话日志;手工编写 sidecar 场景预期产出的日志作`session.jsonl` 提交,并以 `replay.override.json` 作为模型行为覆盖;遗留 fixture 守卫知道每种场景类型需要哪些文件。[ACP 快照测试 RFC](../../implemented/testing/2026-06-19-acp-snapshot-tests.md) 描述了精简后的 fixture 集合。
快照 harness、fixture、孤立项守卫和文档中不再出现 `session.expected.jsonl`;对于每个模型场景,快照测试都从 `session.jsonl` 派生预期 session log;手工编写 sidecar 场景预期生成日志提交`session.jsonl`,并以 `replay.override.json` 覆盖模型行为;孤立 fixture 守卫知道每种场景类型所需的文件。[ACP 快照测试 Agent Noteagent 决策记录)](2026-06-19-acp-snapshot-tests.md)描述了精简后的 fixture 集合。
## 后果
评审者失去了一个预期持久化日志在视觉上与回放 fixture 分离的产物名。stdout golden 仍保护编辑器 transcript(文本记录),将回放输出与 `session.jsonl` 比较则在不重复文件的前提下保留循环/持久化回归检查。
评审者失去了一个能在视觉上区分预期持久化日志与重放 fixture 的产物名。stdout 预期输出仍然保护编辑器 transcript(文本记录),而将重放输出与 `session.jsonl` 比较,无需复制文件即可保留循环/持久化回归检查。
## 实现说明
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-22-fork-child-replay-seed-boundary.md: 28ce76309da2dca7076dd11211229a0631d11db3
2026-06-22-fork-child-replay-seed-boundary.zh.md: 92b60589b4cfe9668a1542405693cec8d29eceaf
2026-06-22-fork-child-replay-seed-boundary.md: d3cbbb1dae1d64a10973bd5895ccc47d877eba28
2026-06-22-fork-child-replay-seed-boundary.zh.md: 7137256f0e1f34a2da928b966d170d054f18ddde
@@ -1,4 +1,4 @@
# RFC: 持久化 seed 边界以确保 fork 子会话回放正确路由
# Agent Note: 持久化 seed 边界以确保 fork 子会话回放正确路由
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
[会话快照RFC](2026-06-22-subagent-snapshot-replay.md)快照层表达嵌套 agent(智能体)的形状:一个父会话加上每个进程内 subagent 一份已录制的日志,各自作为独立脚本回放、以调用方会话为键。该 RFC 指出(§ Scope 末尾条目)fork 快照是「一个平凡的后续补充,不是键控方案的缺口。这对 fork 子会话而言是错的——问题不在键控,而在*脚本推导*。
[ session 快照Agent Noteagent 决策记录)](2026-06-22-subagent-snapshot-replay.md)使快照层能够表达嵌套 agent 形状:一个父加上每个进程内 subagent 一份记录日志,每份日志都按调用 session 作为键,以独立脚本重放。它曾指出(§ 范围,最后一个项目符号),fork 快照“只是未来很容易添加的一项,并非键控缺口。这一判断对 fork 子而言是错的——问题不在键控,而在*脚本派生*。
subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-replay) 从已录制的会话日志推导:它按 `(turn, step)` 对日志中的 `assistant/chunk` 事件分组,每次 `stream()` 调用对应一条回放条目。对 **spawn** 子会话而言这是正确的,因为其日志只包含自身的模型调用。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-22-fork-snapshot-scenarios.md: a5324cbfa13b79c0ea60b74b689f1b19db99a725
2026-06-22-fork-snapshot-scenarios.zh.md: 543382db86eb50b5f278a99de74586a13bff9eb7
2026-06-22-fork-snapshot-scenarios.md: 46c688a4095a1d8af32b3b99887929f71a1526ce
2026-06-22-fork-snapshot-scenarios.zh.md: afeb495e480a601656fc551a478e0e40c579cfd5
@@ -1,4 +1,4 @@
# RFC: 记录 fork 与混合 spawn+fork 快照场景
# Agent Note: 记录 fork 与混合 spawn+fork 快照场景
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
[seed-boundary RFC](2026-06-22-fork-child-replay-seed-boundary.md) 使 fork 子会话的回放路由正确运作`dsh-llm-replay` 从子会话持久化 `seedLength` 边界处或之后的事件推导出子会话的脚本,因此 fork 子会话继承的父会话前缀不会被当作子会话自身的模型调用来回放。但该 RFC 交付时**没有记录 fork 场景**——该切片仅`llm-replay` 单元测试(一个合成的子会话 fixture(测试前置数据))和一个持久化往返测试覆盖。 transcript(文本记录)快照层(即启动真实 `acp-agent`放端到端嵌套 transcript 的那张网只有 spawn 子会话`subagent-spawn``subagent-multi`)。如果一个 fork 路由回归让单元测试保持绿色,它仍会逃过专为捕获 transcript 回归而建的一层。
[seed 边界 Agent Noteagent 决策记录)](2026-06-22-fork-child-replay-seed-boundary.md) fork 子项重放能够正确路由`dsh-llm-replay` 根据持久化 `seedLength` 边界处及其后的事件派生子项脚本,因此 fork 子继承的父前缀不会作为子项自身的模型调用放。但落地时**没有记录 fork 场景**——slice 只`llm-replay` 单元测试(合成子项 fixture(测试前置数据))和持久化往返测试覆盖。完整 transcript(文本记录)快照层——会启动真实 `acp-agent`放端到端嵌套 transcript 的那张网——只有 spawn 子`subagent-spawn``subagent-multi`)。如果 fork 路由回归没有让单元测试变红,它仍会逃过专为捕获 transcript 回归而建的一层。
表达 fork 场景所需的快照基础设施已经就位:两个进程内后端都在 `cordis.yml` / `cordis.snapshot.yml` 中以两个面向模型的工具接入(`subagent` → spawn、`subagent_fork` → fork),harness 会收集每个子会话的日志,回放按 `seedLength` 为键转发各子会话的 fixture。缺少的是一个*已记录的场景*来驱动 fork 子会话走完这条路径。
@@ -15,17 +15,17 @@ Status: implemented
针对真实 API 记录两个场景,均在默认门禁中以无密钥方式回放:
- **`subagent-fork`**:父会话完成一个轮次以建立一个事实,然后通过 `subagent_fork` 委派一个子任务。fork 子会话继承对话(其日志携带非零 `seedLength`),因此可以从父会话的上下文中作答。这是聚焦的回归守卫:子会话 fixture 的 `seedLength` 就是回放切片所依赖的边界,来自真实 fork 的记录而非手工合成。
- **`subagent-mixed`**:父会话完成一个轮次,然后在同一 transcript 中分别通过 `subagent`(全新 spawn 子会话`seedLength` 为 0 `subagent_fork`fork 子会话`seedLength` 非零)各委派一次。这是 seed-boundary 和 per-session-replay 两份 RFC 都列为后续补充的混合 spawn+fork 场景:一 transcript 同时覆盖两种传输方式和切片的两个分支(`seedLength` 0 = 无操作,`seedLength > 0` = 裁剪继承前缀),两个子会话`createdAt`为先 spawn 后 fork。
- **`subagent-mixed`**——父项完成一个 turn,随后在同一 transcript 中通过 `subagent` 委托一次(全新 spawn 子`seedLength` 为 0,再通过 `subagent_fork` 委托一次fork 子`seedLength` 非零)。这是 seed 边界与逐 session 重放 Agent Note 都点名作为未来新增项的 spawn+fork 混合场景:一 transcript 覆盖两种传输方式和 slice 的两个分支(`seedLength` 0 = 无操作,`seedLength > 0` = 裁剪继承前缀),两个子`createdAt`为先 spawn后 fork。
### 为什么需要一个已完成的第一轮次
fork 后端用父会话的**已完成轮次的平衡前缀**[`completedTurnPrefix`](../../../../packages/subagent/subagent-fork))来初始化子会话。如果父会话在第一轮次就 fork没有已完成的轮次可继承,seed 为空(等价于全新 spawn`seedLength` 为 0这不会覆盖切片逻辑。因此两个场景都使用双 prompt 输入:第一个 prompt 完成一个轮次(建立一个 codeword,子会话稍后被要求回忆它),第二个 prompt 委 fork。子会话 transcript 中回忆出的 codeword 只是模型行为的附带产物;真正承载验证的产物是子会话 fixture 中记录的 `seedLength`,回放切片消费的正是它
fork 后端使用父的**已配平完整 turn 前缀**为子项提供 seed。父项若在第一个 turn 就执行 fork,没有已完成 turn 可供继承,因此 seed 为空(全新 spawn`seedLength` 为 0——这不会覆盖 slice。因此两个场景都使用双 prompt 输入:第一个 prompt 完成一个 turn(建立稍后要求子项回忆的 codeword),第二个 prompt 委 fork。子 transcript 中回忆出的 codeword 只是模型行为的附带结果;承载关键约束的产物是子 fixture 中记录、由重放 slice 消费`seedLength`
## 后果
- fork 路由切片现在由全 transcript 层守卫,而不仅仅是单元测试。移除 `slice(seedLength)`(回放整个子会话日志)会让**两个**新场景变红——fork 子会话收到的是父会话记录的 chunk 而非自己的——证明守卫确实生效(场景落地时已验证红→绿)。
- `subagent-mixed` 是第一个在同一个 transcript 中驱动两种*不同* subagent 后端的快照场景,同时覆盖了跨 spawn 和 fork 子会话的 per-session 回放键控。
- 进程外(ACP)subagent 回放形态不同(每个子会话是独立进程、有自己的回放),仍以 `TODO(acp-subagent-replay)` 跟踪——本文场景仅限进程内。
- 进程外(ACPAgent Client Protocol)subagent 回放形态不同(每个子会话是独立进程、有自己的回放),仍以 `TODO(acp-subagent-replay)` 跟踪——本文场景仅限进程内。
- 重新录制(`pnpm run test:snapshot:record`)会从真实 API 重新生成全部四个 fork/spawn fixture;两个新场景在无密钥时自动跳过,与所有已录制场景一致。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-22-subagent-snapshot-replay.md: 89fc4e8d4d267fd4df373a7fd82b8c6e742be6ea
2026-06-22-subagent-snapshot-replay.zh.md: 7dc234ab9c6e3bb1facd78e98aad15005d158325
2026-06-22-subagent-snapshot-replay.md: 4aad8b6ddd3e4f8b8ad5565e10b263953d81ea31
2026-06-22-subagent-snapshot-replay.zh.md: 9d31e4d7f8e65b8442a94dee154ff1d3a5631651
@@ -1,4 +1,4 @@
# RFC: 嵌套 agent 的逐会话快照回放
# Agent Note: 嵌套 agent 的逐会话快照回放
Status: implemented
@@ -6,14 +6,14 @@ Status: implemented
## 问题
快照测试层(`pnpm run test:snapshot`)启动真实 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 回放录制的会话,并将归一化后的 stdout transcript(文本记录)重新持久化的会话日志对已提交的金标文件做 diff。是唯一一个端到端验证完整编辑器 transcript 的测试层。
快照层(`pnpm run test:snapshot`启动真实 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 重放已记录 session,并将规范化 stdout transcript(文本记录)+ 重新持久化的 session log 与已提交预期输出进行 diff。是唯一端到端覆盖完整面向编辑器 transcript 的测试层。
该层最初为每个进程只有一个会话而构建,这一假设硬编码在两处:
- **`dsh-llm-replay` 没有做任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent 和一个进程内 subagent 在同一个上下文上同时流式输出时,调用交错,单一游标会把子 agent 的脚本发给父 agent(反之亦然)。
- **`dsh-llm-replay` 没有做任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent(智能体)和一个进程内 subagent 在同一个上下文上同时流式输出时,调用交错,单一游标会把子 agent 的脚本发给父 agent(反之亦然)。
- **harness 只收集一份日志。** `findSessionLog` 遍历 sessions 根目录,返回找到的第一个 `.jsonl`。subagent 作为第二个 `Session` 运行,在同一个 cwd bucket 下有自己的日志,因此子 agent 的 transcript 被静默丢弃。
这就是 [subagent seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 中记录的 `TODO(subagent-snapshots)` 延期项:进程内后端(PR2)已有单元测试和 e2e 覆盖,但 transcript 快照层在本基础设施就绪之前无法表达嵌套 agent 的形态。本 RFC 即为该堆叠后续。
这就是 [subagent 接缝 Agent Noteagent 决策记录)](../feature/2026-06-21-subagent-capability-seam.md)中通过 `TODO(subagent-snapshots)` 推迟的工作:进程内后端(PR2落地时已有单元 + e2e 覆盖,但在这套基础设施落地前,完整 transcript 快照层无法表达嵌套 agent 形状。本 Agent Note 就是该堆叠后续工作
## 决策
@@ -55,4 +55,4 @@ Status: implemented
- `TODO(subagent-snapshots)` 延期项已解决:嵌套 agent 的 transcript 现在是快照层的一等形态。
- `GenerateOptions.sessionId` 是一个小而诚实的 core-seam 新增,在回放之外同样有用(遥测、请求路由)。
- `subagent` 工具绑定到单一提供方,因此 `subagent-multi` 中的两个子 agent 都是 spawn(全新创建)。键控按会话路由而非按后端路由,因此对 fork 同样正确。但脚本*派生*逻辑此前不正确:fork 子会话的日志以种子化的父前缀(父会话的 `assistant/chunk` 事件)开头,如果从完整日志派生脚本,就会把父 agent 的响应当作子 agent 的来回放。这一正确性缺口通过持久化种子边界来弥合——见 [Persist the seed boundary so fork-child replay routes correctly](2026-06-22-fork-child-replay-seed-boundary.md)——录制的 fork 与混合 spawn+fork 场景现在通过一份 transcript 同时验证两种传输方式(见 [Record fork and mixed spawn+fork snapshot scenarios](2026-06-22-fork-snapshot-scenarios.md))。
- 进程外(ACP)subagent 是完全不同的回放形态(每个子 agent 是自己的进程、有自己的回放),作为 `TODO(acp-subagent-replay)` 记录在 PR3 计划中。
- 进程外(ACPAgent Client Protocol)subagent 是完全不同的回放形态(每个子 agent 是自己的进程、有自己的回放),作为 `TODO(acp-subagent-replay)` 记录在 PR3 计划中。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-04-hook-snapshot-matrix.md: b365992c01e081e5698e81a9ff9682e9b8166ce6
2026-07-04-hook-snapshot-matrix.zh.md: bcb8e5ba55a14dd0c299dac161146a19f18201eb
2026-07-04-hook-snapshot-matrix.md: ceb91a70e1f9582cf2a7cb4cec0ba8aaf5699b8e
2026-07-04-hook-snapshot-matrix.zh.md: 0e2318d48dbd0200c04dd35decf8046ed6ecce9f
@@ -1,4 +1,4 @@
# RFC: Hook 快照矩阵——覆盖两种 bridge 的端到端 golden 测试
# Agent Note: Hook 快照矩阵——覆盖两种 bridge 的端到端 预期输出 测试
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
hook bridge——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)7 个 Claude Code hook 点)和 [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex)5 个 Codex 点)——外部 hook 命令映射到 harness 拦截 seam 上。它们有深的单元测试和 coverage-spec 覆盖率(每个决策分支、每种 payload 方言,均对 mock 的 seam 驱动),外加一个需要密钥的 e2e 测试`hooks.e2e.ts`一次真实的 `PreToolUse` 拦截)。但完整 transcript(文本记录)快照层:那张真正启动 `acp-agent` 子进程、无密钥回放录制会话、并将规范化 ACP stdout 重新持久化日志与已提交 golden 做 diff 的网,只覆盖了一个 hookClaude `UserPromptSubmit` 拦截`hook-cc-promptsubmit-block`)。
hook bridge——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)7 个 Claude Code hook 点)和 [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex)5 个 Codex 点)——外部 hook 命令映射到 harness 拦截接缝。它们有深的单元与覆盖率规格覆盖(每个决策分支、每种 payload dialect,针对 mock 接缝驱动),外加一个受密钥门控的 e2e`hooks.e2e.ts`实时 `PreToolUse` 阻止)。但完整 transcript(文本记录)快照层——会启动真实 `acp-agent` 子进程、无密钥重放已记录 session并将规范化 ACPAgent Client Protocolstdout + 重新持久化日志与已提交预期输出进行 diff 的那张网——只覆盖了一个 hookClaude `UserPromptSubmit` 阻止`hook-cc-promptsubmit-block`)。
这正是 mock 单元测试在结构上无法替代的层级:它验证的是真实 bridge 将真实 hook 进程的结果翻译到真实 seam 决策,再到真实 agent loop(智能体循环)的反应,渲染结果与编辑器看到的完全一致。一个 bridge 翻译或 loop 结构的回归,即使让所有单元测试保持绿色,也会在除那一个 hook 点之外的所有点上逃逸;而对于 Codex bridge,ACP 示例甚至没有加载它,因此没有任何 Codex hook 能端到端触发。
@@ -31,20 +31,22 @@ hook bridge——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)
每个 hook 命令只输出固定字面量字符串(无时间戳/pid/`$RANDOM`/cwd 回显);快照规范化器擦除 `hook/result` 携带的唯一不稳定字段(`durationMs`)。`Stop` 场景通过标记文件(`.stop_fired`)自限,使 force-continue 不会循环——`stop_hook_active` 循环守卫仍是 bridge 的一个 `TODO`,因此无条件的 Stop hook 会在每一步都 force-continue。
`PostToolUse` 阻止场景会在其证明的机制处自行限制。Claude hook 在首次拒绝后持久化一个 workspace 标记,因此允许一次恢复调用;Codex prompt 发起一次调用并报告注入结果。每份预期输出固定一次遭阻止调用,不会重复阻止/重试循环。
### 三个 hook 点被有意排除在快照之外
在构建矩阵过程中发现,记录于此是因为这些遗漏是决策而非疏忽:
- **`SessionStart` `SubagentStart`** 通过一个分离的、尽力而为的 `void runPoint(...).then(agent.inject())` 注入上下文,没有轮次绑定。由此产生的 `context/message` 与它先于的工作(首次模型请求/子 agent 的首轮)存在竞争,落在日志中的位置不确定。录制的 golden 甚至无法在自身回放中复现——10 次放稳定性检查对两者均 10/10 失败。它们留在 bridge 的单元覆盖率中,单元测试直接驱动 seam 而无时序竞。(如果注入将来变为轮次绑定且确定性——`TODO(session-start-gating)` 所指方向——它们就可以纳入快照。)
- **`SubagentStop`** 是纯观察性的:其 `subagent/end` 处理器不传递轮次(因此 `hook/*` 日志事件)、不做注入。它 transcript 写入任何内容,因此 golden 与无 hook 运行逐字节一致,永远无法证明失败——一道永远不会触发的守卫。它留在单元覆盖率`bridge.spec.ts` 已断言了纯观察调用)。
- **`SessionStart` `SubagentStart`** 通过脱离且尽力而为的 `void runPoint(...).then(agent.inject())` 注入上下文,没有 turn 绑定。由此产生的 `context/message` 与它先于的工作(首次模型请求 / 子项的第一个 turn)竞速,并落在不确定的日志位置。记录的预期输出甚至无法在自己的重放中复现——对两者执行 10 次放稳定性检查,结果均为 10/10 失败。它们继续留在 bridge 的单元覆盖率中,那里会直接驱动接缝而不存在时序竞。(如果注入未来改为绑定 turn 且具备确定性——`TODO(session-start-gating)` 所指方向——它们就能接受快照测试。)
- **`SubagentStop`** 只观察:其 `subagent/end` handler 不传递 turn(因此没有 `hook/*` 日志事件),也不执行注入。它不会向 transcript 写入任何内容,因此预期输出会与无 hook 运行逐字节相同,永远无法证明失败——一道咬不住问题的守卫。它继续由单元覆盖率负责`bridge.spec.ts` 已断言观察调用)。
因此,该矩阵覆盖了所有具有确定性、可观测 transcript 足迹的 hook 点,涵盖两种方言。
## 后果
- 每个具有可观 transcript 的 bridge seam 映射现在都在完整 transcript 层级、在真实应用中、对两种方言受到守护——包括此前完全没有端到端覆盖的 Codex bridge。录制的 golden 捕获模型对 deny/block/force-continue 轮次的真实反应,这是手工编写的 transcript 只能猜测
- block 场景无需密钥(无模型轮次);其余场景从录制的 fixture(测试前置数据)无密钥放。`pnpm run test:snapshot:record`实 API 重新生成录制的 fixture密钥时自跳过,与所有录制场景一致
- prove-red 纪律成立:篡改 hook 配置输出(例如修改 deny 原因)会使其场景在放时变红——hook 进程在放期间真实运行(只有模型被放),因此 golden 守护的是实际 hook→seam→loop 路径,而非它的 mock。
- 现在,两种 dialect 中每个具有可观 transcript 的 bridge 接缝映射,都在真实应用的完整 transcript 层受到守护——包括此前完全没有端到端覆盖的 Codex bridge。记录的预期输出捕获模型对遭拒绝/遭阻止/强制继续 turn 的真实反应,手工编写的 transcript 只能猜测这种反应
- `UserPromptSubmit` 阻止场景无需密钥即可编写(没有模型 turn);其余场景从已记录 fixture(测试前置数据)无密钥放。`pnpm run test:snapshot:record` 从实 API 重新生成记录式 fixture并像所有记录场景一样在缺少密钥时自跳过。
- 证明会变红的准则仍成立:篡改 hook 配置输出(例如改变拒绝理由)会让相应场景在放时变红——hook 进程在放期间真实运行(只有模型被放),因此预期输出守护的是实际 hook→接缝→循环路径,而非 mock。
- `acp-agent` 演示现在加载了一个通常会无操作的 Codex bridge(典型项目中没有 `codex-hooks.json`),这正是预期的柔性失败行为,而非代价。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-04-single-source-acp-replay-config.md: 51cbd54d45408df1548c9cc2522b07b5ffaac110
2026-07-04-single-source-acp-replay-config.zh.md: 2aec0e0e46007be243c0386fc0fca92065ce3c9e
2026-07-04-single-source-acp-replay-config.md: f270d70feca184217503c472c1cb7c536187a249
2026-07-04-single-source-acp-replay-config.zh.md: 86eb2d3e15c4939d1de3a78150cd0536d46e10f6
@@ -1,4 +1,4 @@
# RFC: 将 acp-agent 回放配置改为单一来源
# Agent Note: 将 acp-agent 回放配置改为单一来源
Status: implemented
@@ -6,13 +6,13 @@ Status: implemented
## 问题
`examples/acp-agent` 曾维护两份手写配置:`cordis.yml`正式运行树)和 `cordis.snapshot.yml`(逐条镜像前者,仅替换 LLM(大语言模型)后端)。去掉注释后,全部差异是八行 `llm-deepseek` 段落换成两行 `llm-replay` 段落。每次应用结构变更都要改两遍,没有门禁保障对称性:一旦两份副本漂移,快照层会悄悄测试一个与实际交付不同的应用——正是快照层本要消除的["单元测试全绿、产品却坏了"这类缺口](../../../postmortem/0001-acp-default-export-drops-inject.md),在上一层重新引入,唯一的防线是评审者的警觉
`examples/acp-agent` 发布了两份手工维护的配置:`cordis.yml`实时树)和逐条镜像它、只替换 llm 后端的 `cordis.snapshot.yml`——去除注释后,两者的全部差异是八行 `llm-deepseek` stanza 与两行 `llm-replay` stanza。每次应用形状变化都必须修改两遍,没有任何机制约束对称性:如果副本发生漂移,快照层会悄然覆盖与已发布应用不同的应用——快照层本就是为了弥合[单元测试绿色,产品损坏”这类缺口](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)如今同类缺口在上一层重新出现,只能依靠评审者警惕
## 决策
`cordis.snapshot.yml` include 正式配置,通过 id 和 name 禁用指定的 DeepSeek 适配器,并插入回放适配器。其余所有条目因此来自正式运行树。回放时选择 overlay;录制仍然启动 `cordis.yml`,加载守卫允许被有意禁用的条目。
overlay 依赖一 vendor 插件事实,这是有意为之include 加载文件时应用 `patches`,其 `refresh()`/`internal/update` 路径重读时不会重新打补丁这恰好满足一次性放启动的需要(回放应用不加载 `hmr`也没有东西在运行中改写配置)。快照套件即为证明:所有场景在 overlay 上原样通过,包括逐字节一致的 golden 文件
overlay 有意依赖一 vendored 插件事实:include 加载文件时应用 `patches``refresh()`/`internal/update` 路径会重新读取但不重新打补丁——这恰好足以满足一次性放启动(重放应用不加载 `hmr`运行中也没有内容重写配置)。快照套件就是证明:所有场景都能在 overlay 上原样通过,包括逐字节相同的预期输出
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-06-pin-request-header-content-in-one-scenario.md: 0166b459fbb8d883f07fb195bdd5e025d70349de
2026-07-06-pin-request-header-content-in-one-scenario.zh.md: 1ca7df68fc743b919c6769ae8fa40ea16eb3d88a
2026-07-06-pin-request-header-content-in-one-scenario.md: bca6d9eb943e758d68efaf3a76ec367179cd15fd
2026-07-06-pin-request-header-content-in-one-scenario.zh.md: 9637602aca34977bee7c0efd5dc57b848ed93e2c
@@ -1,4 +1,4 @@
# RFC: 在单个快照场景中固定请求头内容
# Agent Note: 在单个快照场景中固定请求头内容
Status: implemented
@@ -10,11 +10,11 @@ Status: implemented
## 决策
个 header 组合类别恰好有一个场景标记为 `pinsHeader`。其目录按评审格式拆分固定内容:`system-prompt.golden.md` 以普通 Markdown 存放归一化后的组合提示词,`tool-schemas.golden.json` 以结构化 JSON 存放完整的初始 schema 及后续 schema 变更,而 `session.jsonl` 保留 config、reason 及任何模型可见前缀,同时将 `header.system` `header.tools` 存为 `"{{system}}"` / `"{{tools}}"`。其余所有 JSONL 使用相同的提示词和工具 token,并同样对会话前缀内容 token 化处理。固定机制实现在 [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md),其套件工厂强制每类别有一个固定场景。
种请求头组合类别恰好有一个场景标记为 `pinsHeader`。其目录按评审格式拆分固定内容:`system-prompt.expected.md` 以普通 Markdown 包含规范化的完整 prompt 序列;`tool-schemas.expected.json` 以结构化 JSON 包含对应的完整 schema 序列;`session.jsonl` 保留 config、reason 和所有模型可见前缀,同时将 `header.system` `header.tools` 存为 `"{{system}}"` / `"{{tools}}"`。其他每份 JSONL 使用相同的 prompt 与工具 token,并同样将 session 前缀内容 token 化。固定机制位于 [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md),其套件 factory 强制每类别恰好有一个固定场景。
粹的 `scrubSystemPrompts``scrubToolSchemas` 归一化器应用于每个存储的会话 fixture(测试前置数据),独立地对初始 header 内容和 header-delta 批量内容做 token 化。`scrubRequestHeaders` 还为非固定场景的会话前缀内容 token 化,同时保留结构性事实:system-delta 的位置与数量、新增/移除/变更的工具名称、前缀消息数量、字段存在性、configreason。record 与 refresh 的回写操作在写入 JSONL 前应用相应的 scrub,并从归一化后的实时 header 和 delta 重新生成两个 sidecar 文件,因此两条路径都不会把提示词/schema 批量内容重新引入 JSONL,也不会评审产物变陈旧
`scrubSystemPrompts``scrubToolSchemas` 规范化器会分别将每个存储完整请求头 token 化。`scrubRequestHeaders`为非固定场景把 session 前缀内容 token 化,同时保留请求头数量、字段存在性、configreason 和前缀消息数量。记录与刷新写回会在写入 JSONL 前应用适当清理,并根据规范化的实时完整请求头序列重新生成两个 sidecar,因此两条路径都无法把大段 prompt/schema 重新引入 JSONL,也不会留下陈旧的评审产物。
守卫机制使这拆分自我强制。在磁盘上每个 `session*.jsonl` 都是提示词和 schema 两个 scrubber 的不动点;只有非固定 fixture 还必须是完整 header scrub 的不动点;两个 sidecar 文件恰好存在于固定 fixture 旁,采用规范的换行终止格式;每类别有且仅有一个固定场景。在运行时:由 parent、spawn 子会话、fork 子会话、初始请求或 resume 产生的每个 `request/header`在经过易变值归一化后必须与重建的固定内容匹配;固定运行的提示词和 schema delta 也必须与其 sidecar 匹配。如果 header 没有字符串类型的 prompt、没有数组类型的工具列表,或包含未声明的 `request/header-delta`,则立即失败并报错
守卫使这拆分能够自我强制。在磁盘上每个 `session*.jsonl` 都是 prompt 和 schema 清理器的固定点;只有非固定 fixture(测试前置数据)必须是完整请求头清理的固定点;两个 sidecar 恰好位于固定 fixture 旁,采用规范、以换行符结尾的格式;每类别有一个固定场景。在实时运行中,由父项、spawn 子、fork 子、初始请求、恢复或实例内变化产生的每个 `request/header`都必须在易变值规范化后与重建的类别序列匹配。请求头若没有字符串 prompt、没有数组工具列表,或超过固定场景声明的变更请求头数量,就会响亮失败
一个固定场景覆盖整个套件,因为每个会话(parent、spawn 子会话、fork 子会话)组合出的工具列表完全相同、提示词除 cwd 外完全相同,而一致性守卫会在这一前提不再成立时立即使套件失败。如果 header 组合将来在设计上变为会话相关的(例如受限的 subagent 工具集),那么分歧的形态将获得自己的固定场景。
@@ -24,11 +24,11 @@ Status: implemented
- **仅在比较时 scrubfixture 保持原始内容**:比较能通过,但已提交的 fixture 保留着陈旧的重复内容,下次录制时会整体重写。存储 token 诚实地表明每个 JSONL 没有固定什么。
- **全部 scrub,不做任何固定**:丢失了组合 header 实际发送内容(提示词组装、已注册工具顺序、完整 schema)的唯一端到端记录。生成的工具目录只孤立地记录每个工具;只有真实 fixture 才能固定组合后的完整集合。
- **将完整固定内容全部保留在 JSONL 中**:消除了套件范围的重复,但提示词和 schema 变更仍然是一行转义文本。Markdown 和结构化 JSON 为每种内容提供其自然的评审格式,同时不削弱重建 header 的断言。
- **精简会话日志本身(记录内容摘要,将 header 存放在别处**违反可重建性契约:产品日志必须逐位重现每个请求([可重建请求 RFC](../architecture/2026-07-05-reconstructable-requests.md))。header 体积是测试产物问题,在测试归一化中解决;线上日志不受影响
- **收窄 session log 本身(记录内容 digest,把请求头存到其他位置**——违反可重建性契约:产品日志必须逐 bit 复现每个请求([可重建请求 Agent Noteagent 决策记录)](../architecture/2026-07-05-reconstructable-requests.md))。请求头体积是测试产物问题,在测试规范化中解决;实时日志保持不变
## 验证
套件针对拆分后的固定内容放每个场景。单元测试覆盖率盖独立 scrubber 和完整 scrubber、两种 sidecar 格式、record/refresh 重新生成、归一化提示词/schema 提取、不动点强制、必需文件对称性、重建 header 一致性以及 delta 拒绝。
套件针对拆分后的固定内容放每个场景。单元覆盖率会覆盖独立与完整清理器、两种完整请求头 sidecar 格式、记录/刷新重新生成、规范化 prompt/schema 提取、固定点强制、必需文件对称性、重建请求头一致性以及变更请求头数量拒绝。
## 后果
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-08-shared-acp-snapshot-package.md: c378222804251761a1b04f59c35799a97a1525f1
2026-07-08-shared-acp-snapshot-package.zh.md: cda7a578d4643800736ff159a6427d3a0e3e0fae
2026-07-08-shared-acp-snapshot-package.md: 3e5a2b12114d535490a17361128862f6d1c09a73
2026-07-08-shared-acp-snapshot-package.zh.md: 63daf0bd8b18a5afb44161b1621ab16ff7285ba2
@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-08-shared-acp-snapshot-package.zh.md)
## Problem
The ACP snapshot tier ([snapshot Agent Note](2026-06-19-acp-snapshot-tests.md)) was built from three modules living inside one example's test directory: `snapshot-harness.ts` (boot the real bin subprocess, drive it over ACP JSON-RPC, harvest the persisted logs), `snapshot-normalize.ts` (the pure expected-output normalizers), and the ~150-line scenario body plus fixture guards in `acp.snapshot.ts` (record/replay modes, the stdout expected-output and log comparisons, the pinned-header uniformity guard, the orphan/required-file/single-pin meta-tests).
@@ -1,4 +1,4 @@
# RFC: 将 ACP 快照套件提取为支持包
# Agent Note: 将 ACP 快照套件提取为支持包
Status: implemented
@@ -6,33 +6,35 @@ Status: implemented
## 问题
ACP 快照层([快照 RFC](2026-06-19-acp-snapshot-tests.md))由位于个示例测试目录的三个模块构`snapshot-harness.ts`(启动真实 bin 子进程,通过 ACP JSON-RPC 驱动它,集持久化日志)、`snapshot-normalize.ts`(纯粹的 golden 规范化器),以及 `acp.snapshot.ts` 中约 150 行的场景主体 fixture(测试前置数据)守卫(record/replay 模式、stdout-golden 与日志比对、pinned-header 一致性守卫、orphan/required-file/single-pin 元测试)。
ACPAgent Client Protocol)快照层([快照 Agent Noteagent 决策记录)](2026-06-19-acp-snapshot-tests.md))由位于个示例测试目录的三个模块构`snapshot-harness.ts`(启动真实 bin 子进程,通过 ACP JSON-RPC 驱动它,集持久化日志)、`snapshot-normalize.ts`(纯预期输出规范化器),以及 `acp.snapshot.ts` 中约 150 行的场景主体 fixture(测试前置数据)守卫(记录/重放模式、stdout 预期输出与日志比较、固定请求头一致性守卫、孤立项/必需文件/单一固定项元测试)。
第二个 ACP 示例只能复制 record、规范化和收集逻辑,而这些逻辑必须保持一致。`examples/` 下的代码也不在包(package)覆盖率门禁范围内,且原始 harness 只能取消权限请求。共享包使这些机制纳入度量,并允许场景脚本化地提供审批答案
第二个希望获得快照覆盖的 ACP 示例——直接消费者是 sandbox/approval 组合——只能复制这些模块,恰好分叉了绝不能漂移的逻辑:记录写回、请求头清理、子 session 采集顺序。spawn/client 胶水也在 `acp.e2e.ts``hooks.e2e.ts` 和 harness 中重复三份。文件位置决定了测试严格度:逐文件 100% 覆盖率门禁只测量 `packages/*/*/src`,因此这些机制完全未被测量——正是同一种缺口,曾推动 `dsh-llm-replay``examples/` 移入 [packages/support](../../../../packages/support/README.md)。此外,harness 的 ACP client 硬编码 `requestPermission → cancelled`,因此 approval 往返——sandbox 组合的主打行为——完全无法在快照层表达
## 决策
这些机制位于 [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md)`@deepseek-ai/dsh-acp-snapshot`);示例的 `*.snapshot.ts` 只包含场景表、agent 路径和一次工厂调用,依赖自己的 `snapshots/` fixture 与 `cordis.snapshot.yml` overlay[单源 replay 配置](2026-07-04-single-source-acp-replay-config.md))。读取 `DSH_SNAPSHOT` 留在边缘层——库接收的是已解析的 `mode`
**`src/harness.ts`** 提供 `runScenario` 及其脚本/结果类型,以 agent 的 bin 和配置路径为参数。权限答案构成一个 FIFO 队列,以稳定的 option kind(而非随机的 option id)为键。缺少答案时取消该请求;不可用的 kind 取消 agent 请求并使场景失败
**`src/launcher.ts`**——`launchAcpTestAgent` 拥有通用的未构建进程边界:绝对 tsx loader 解析、`TSX_TSCONFIG_PATH`、隔离的 harness home、stdio 接线、原始字节 stdout tee、stderr 与更新捕获、失败关闭的权限后备、更新 waiter,以及优雅或信号式关闭。快照场景和普通 e2e 套件提供相同的 `AgentUnderTest``binScript``configPath``tsconfigPath`);扮演用户的测试只提供其权限 handler。ACP 与 hook e2e 套件以及 sandbox/approval e2e 套件都使用该 launcher,而不再重新构建 SDK client 边界
**`src/harness.ts`**——`runScenario` 和输入脚本/结果类型在 launcher 之上叠加确定性步骤、临时 workspace、快照环境和持久化日志采集。其 `session/request_permission` handler 消费可选的 `InputScript.permissionAnswers` FIFO 队列,每个条目按选项**类型**进行选择(id 是 agent 生成的随机值,已提交脚本无法预知;类型是 ACP 稳定词汇,会在回答时映射到已提供的 `optionId`);队列不存在或耗尽时回答 `cancelled`,若请求从未提供某种类型则拒绝该次运行——agent 自身收到的回答是 `cancelled`,因此场景 bug 会使 harness 失败,而不会被吸收为 agent 侧拒绝。由此,approval 套件可以根据 `input.json` 确定性地驱动允许/拒绝往返。
**`src/normalize.ts`** 是纯规范化器,按策略不含钩子:当未来某个事件携带新的易变字段(例如审批耗时),共享规范化器在同一个变更中学会它,保持「规范化」的含义只有一个归属,而非各套件各自扩展清洗逻辑。
**`src/suite.ts`** 提供 `Scenario` 类型 `defineAcpSnapshotSuite(options)`,注册场景比对、record/refresh 的 fixture 回写、header pin 及其实时一致性守卫,以及 fixture 守卫块(无 orphan 场景目录、必需文件齐全、每个 class 恰好一个 pin、每 JSONL 是 `scrubSystemPrompts` 的不动点、非 pinning fixture 也是 `scrubRequestHeaders` 的不动点)。pinned-header 契约([pinned-header RFC](2026-07-06-pin-request-header-content-in-one-scenario.md))按套件划分:每个 header class 恰好标记一个 `pinsHeader` 场景,其 `system-prompt.golden.md` JSONL 工具列表组合后的 header 拆分为可评审产物;一致性守卫将二者与该 class 中每个实时 header 进行比对。纯辅助函数(`childFixturePaths``fixtureContext``normalizedHeaders``normalizedSystemPrompts``formatSystemPromptSnapshot``headerDeltaCount`)从模块导出,以便直接进行单元覆盖。
**`src/suite.ts`**——包含 `Scenario` 类型 `defineAcpSnapshotSuite(options)`,注册场景比较、记录/刷新 fixture 写回、带实时一致性守卫的请求头固定项,以及 fixture 守卫块(没有孤立场景目录、必需文件存在、每种类别恰好一个固定项、每 JSONL `scrubSystemPrompts` 固定点、非固定 fixture 同时也是 `scrubRequestHeaders` 固定点)。刷新会先展开打包的计时信封,再对齐现有易变事件时间,因此在打包与未打包布局之间切换不会移动后续记录;全新的 chunk fragment 数组仍为权威,因为其边界属于重放行为。场景目录中的 `session.jsonl` 加连续的 `session.<n>.jsonl` 同级文件构成有序主项/子项清单,因此场景表可以声明策略而不重复子项数量。固定请求头契约([固定请求头 Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md))按套件生效:每种请求头类别恰好标记一个 `pinsHeader` 场景,其 `system-prompt.expected.md` JSONL 工具列表组合请求头拆成可评审产物;一致性守卫会将两者与该类别的每个实时请求头比较。固定场景可以声明任何合法的变更请求头数量,其 Markdown 产物记录每个完整的已变 prompt。纯辅助函数(`sessionFixtureNames``fixtureContext``normalizedHeaders``normalizedSystemPrompts``formatSystemPromptSnapshot``headerChangeCount`)从模块导出,以便直接进行单元覆盖。
## 曾考虑的替代方案
- **模块复制到每个示例中**正是本 RFC 要防止的 fork。record/守卫逻辑恰是必须在各套件间保持逐字节一致的代码,而示例不在覆盖率门禁范围内,因此每份副本也无法被度量。
- **模块复制到每个示例中**——这正是本 Agent Note 要防止的分叉:记录/守卫逻辑恰是必须在各套件间保持逐字节相同的代码,而示例位于覆盖率门禁之外,所以每份副本也无法量。
- **在 `examples/` 下建共享模块目录**:代码仍在覆盖率门禁之外,且需要跨示例边界的相对导入,违反包名导入约定;`examples/` 的叶子节点按设计应保持轻薄。
- **`dsh-acp-demo``/testing` 子路径导出**:将测试基础设施耦合到产品包的对外服务接口与依赖集中;`packages/support/` 的存在正是为了真实但兼容性承诺较低的开发/测试包,`dsh-llm-replay` 是先例,本包与之配套。
- **导出原始测试体函数而非套件工厂**:每个示例将重新拥有 `describe`/`it` 骨架(每套件约 80 行注册样板),却无灵活性收益;工厂使消费方只需一张场景表加一次调用,而导出的纯辅助函数在工厂设计内保留了可单元测试性。
- **可注入 ACP `Client` 工厂,而非声明式 `permissionAnswers`**灵活性最大,但 SDK 客户端构造泄给每个消费,并在正统一的层重新引入逐示例漂移;声明式队列使 `input.json` 为唯一脚本化界面,且可被 golden 规范化。
- **使用可注入 ACP `Client` factory 代替声明式 `permissionAnswers`**——灵活性最大,但会把 SDK client 构造泄给每个消费,并恰好在正统一的层重新引入逐示例漂移;声明式队列 `input.json` 保持为唯一脚本表面,并与预期输出规范化兼容
- **泛化到 ACP 之外(传输无关的快照 harness)**:不存在第二种传输方式;harness 端到端都是 ACP 形态(SDK 客户端、JSON-RPC 帧、`session/update` 等待器),推测性的抽象将是一个超前于任何消费方的 seam 拆分。
## 测试
提取保留了所有既有 ACP golden 字节。包的 `src/` 通过脚本化的 ACP 子进程达到逐文件 100% 覆盖率:harness 测试覆盖每个步骤操作、两条预期错误分支、权限选择/回退/不可能选项、环境变量转发、workspace 种子注入、收集排序/噪声/回退;suite 测试对已提交合成 fixture 执行 replay,并对临时副本执行 record,同时覆盖纯辅助函数。两个结构上不可达的守卫保留了有理由的覆盖率排除。fake agent 将 `session/new` 的 cwd 替换到日志中,包括 Darwin `/var` realpath 行为,与真实 bin 一致
提取一致性得到机械证明:迁移后,`pnpm run test:snapshot` 的结果与基准提交匹配,`examples/acp-agent/tests/snapshots/` 下没有任何字节变化。包的 `src/` 在门禁单元运行中保持逐文件 100% 语句/分支/函数/行覆盖,并通过脚本化 fake ACP bin`tests/fixtures/fake-acp-agent.ts`,每个场景由 fixture 旁的 `behavior.json` 编排行为)经过真实 launcher 驱动:`harness.spec.ts` 直接覆盖 launcher 默认值、捕获、更新等待、关闭以及环境/配置变体,随后覆盖每种场景 step 操作、两个 expect-error 分支、权限队列(选择、后备、不可能点击)、workspace seed,以及采集顺序/噪音/后备分支;`suite.spec.ts` 在收集时真实运行 factory——一个针对已提交合成 fixture 的重放套件和一个针对临时副本的记录套件(写回从不触及已提交树;`ACP_SNAPSHOT_SPEC_BOOTSTRAP=1` 会重新引导它)——并包含纯辅助函数的直接用例。fake bin 会把 `session/new` cwd 而非 `process.cwd()` 代入脚本化日志,与真实 bin 请求头携带的内容一致(darwin 会将 `/var/folders/…` realpath `/private/var/folders/…`
## 后果
新示例只需一张场景表加 fixture 即可获得完整快照层——sandbox 分支从 master 合入后添加自己的套件(自己的 pin 场景、自己的 overlay、通过 `test:snapshot:record` 生成 fixture、通过 `permissionAnswers` 提供审批答案)。代价:`suite.ts` 导入 vitest,因此包只能在 vitest 运行中导入——这是其他包没有的形态,已在其 README 中声明;每个套件 pin 自己约 8 KB 的 header fixture(真正不同的组合值得拥有自己的 pin;相同组合会被该套件的一致性守卫捕获)e2e launcher 的重复仍然存在(`TODO(acp-test-harness)`)——当该迁移落地时,harness 即为提取目标
新示例通过场景表加 fixture 即可获得完整快照层,普通 ACP e2e 则通过一次 launcher 调用获得同一条经过测试的进程/client 边界。代价`suite.ts` 导入 vitest,因此包入口只能在 vitest 运行中导入——其他包没有这种形状,其 README 已说明;每个套件还要固定自己约 8 KB 请求头 fixture(真正不同的组合理应拥有自己的固定项;相同组合会被该套件的一致性守卫捕获)。