OpenRig 场景格式一致性适配指南:从 51-03 草稿到 51-02 真实运行器的逐项对齐

原创2026-10-03 13:45:461,721 阅读
文章标签:人工智能AI Agent多智能体Agent 编排代码智能体CLI

OpenRig 场景格式一致性适配指南:从 51-03 草稿到 51-02 真实运行器的逐项对齐

<output_article>

OpenRig 场景格式一致性适配指南:从 51-03 草稿到 51-02 真实运行器的逐项对齐

导读

本指南完整剖析 OpenRig 测试系统中 packages/test-system/FORMAT-CONFORMANCE-NOTES.md 记录的场景格式一致性(format-conformance)适配过程:当 51-03 场景套件最初按架构草图(arch-shape sketch)编写,而真实 51-02 运行器以锁定规格落地之后,二者之间出现了哪些格式差异、每个差异如何被逐项吸收、以及最终如何达成 11/11 场景通过真实校验器。读者将掌握 51-02 场景格式的真实形态(拓扑路径、env.queue 前置条件、expect 匹配字段、daemon:{op} 生命周期动词),以及这套“诚实台账(honest ledger)”方法论——路由(route)而非修补(shim) 的工程纪律。

本文以 FORMAT-CONFORMANCE-NOTES.md 为骨架,结合 packages/test-system/scenarios/ 下 11 个真实场景 YAML、packages/test-system/README.md 与 packages/test-system/ROUTING.md 中的依赖台账进行源码级印证。


一、背景:两份规格的碰撞

51-03 场景套件("the ten + one",即 11 个场景)最初是按架构形状草图(ARCH-SHAPE sketch fc30a736)编写的。随后,真实的 51-02 运行器(runner)在锁定规格 28dc80cf / 4180a007 下构建并通过 CLEARED 评审(verdict 57d5aa7d)。以 dev-driver 的权威示例场景(scenario-02-baton.yaml、scenario-10-one-view-state.yaml、topo-stub-baton.yaml)为基准,真实运行器的格式与草图编写的 11 个场景存在系统性差异,本文档即驱动对所有 11 个场景及配套 fixture 的一致性(conformance)改造。

从 packages/test-system/README.md 可以确认这套场景的定位:

"The eleven authored scenarios below are a broader target; they are not eleven admitted CI passes."

也就是说,11 个场景是更广的测试目标,它们并非 11 个已获准进入 CI 的通过项——这一表述本身就是“诚实台账”纪律的起点。

51-03 场景的历史定位

每个场景都固定锁定一个来自 0.4.8/0.5.0 马拉松(marathon)的具名缺陷类(defect class),并成为常驻基础设施,而不是手工搭建的一次性探针(probe)。场景格式遵循的是锁定的 51-02 规格(README b157f5bc68940963 / PRD 1bf3340188e39212),emit.behavior 词汇表则采用 51-01 的锁定四件套 {compaction, slow_output, mid_turn_death, restore}——usage_limit 是真实运行环境专属,在 stub 拓扑中必须响亮地失败于校验,因此没有任何场景引用它。

场景可断言的表面(surface)集合被严格限定为:ps · queue · stream · scope · pane · transcript · tui_socket · policy_provenance。其中 proof 被保留:运行器没有读取绑定,校验器直接拒绝。


二、九大格式差异(real runner form)逐项拆解

以下是 FORMAT-CONFORMANCE-NOTES.md 记录的真实运行器格式与草图格式之间的九项核心差异,每项都附真实场景 YAML 作为证据。

1. 拓扑路径:./<rig>.yaml 兄弟相对路径

差异:真实格式要求 topology: ./<rig>.yaml(场景文件同级的 ./ 相对路径),而不是草图中的 fixtures/<rig>.yaml。

在 INCREMENT 5(共置化改造)中,fixtures/ 目录被吸收进 scenarios/(扁平布局,与官方 scenario-02-baton.yaml 布局一致),所有 topology: 引用统一归一化为 ./。例如 queue-baton-survives-restart.yaml:

scenario: queue-baton-survives-restart
topology: ./dev-pair-stub.yaml

运行器将 topology: ./<rig>.yaml 解析为场景文件的同级兄弟,且需要 culture.md 与 agents/<seat>/agent.yaml 同置。因此在 scenarios/ 目录中可以看到 dev-pair-stub.yaml、rig-alpha-stub.yaml、rig-beta-stub.yaml、solo-stub.yaml、policy-seat-stub.yaml、mutation-pod-stub.yaml 等拓扑文件,以及它们引用的 agents/ 与 culture.md。

2. 队列建立 = env.queue 前置条件,而非 send 动作

差异:51-02 格式没有队列动作动词(动词集合为拓扑/座位生命周期 + daemon:{op})。被认领的接力棒(baton)必须在开始时通过 env.queue 前置条件建立:

env:
  queue:
    - id: baton-1
      source: harness@dev-pair-stub
      destination: dev-qa@dev-pair-stub
      summary: "51-03 baton — survives a restart"
      claim: true

流水线会在步骤执行之前先运行随附的 rig queue create/claim 写入。queue-baton-survives-restart.yaml 正是以这个真实形态编写的——草图中的 #2(用 send)和 #11(用 send)都必须把接力棒创建迁移到 env.queue。

这一差异在 ROUTING.md 的 R-17 中有更详细的记录:send 运行的是 rig send,它创建的是**流条目(stream item)**而不是队列条目;队列条目只能来自 env.queue 前置条件(在第一次 up 之后应用)。

3. expect queue 是列表,键名以真实发货字段为准

差异:expect queue 是一个列表,键名必须是真实发货的 qitemId、state、destinationSession,而不是草图中的 {items:[{destination,state,owner}]}:

- expect:
    surface: queue
    within: 20s
    match:
      - qitemId: baton-1
        state: in-progress
        destinationSession: dev-qa@dev-pair-stub

one-view-state-after-mutation-storm.yaml 与 queue-baton-survives-restart.yaml 均使用此形态。

4. equals 是在名义 surface 锚点下的列表

差异:schema 要求每个 expect 都必须有合法的 surface,因此 #10 以 surface: ps 作为锚点,并列出待比较的表面:equals: [tui_socket, ps, queue]——而不是草图中的 equals: {surfaces:[...]}:

- expect:
    surface: ps
    within: 30s
    equals:
      - tui_socket
      - ps
      - queue

这一形态对应 one-view-state-after-mutation-storm.yaml。ROUTING.md 的 R-13 进一步说明:声明式归一化器(normalizer)以按表面投影映射的形式发货(equals: {<surface>: {pluck, path, rig}},见 scenario-normalizer.ts),它是校验器唯一接受的形态。

5. "restart" = daemon: {op: restart}(已批准),而非座位级 restart:

差异:不存在无快照的按座位重启动词(F2),重启必须通过生命周期动词:

- daemon: { op: restart }

而 expect pane "restored" 这条断言腿(leg)被显式注释为 items-6-8 的延后(DEFERRED)断言,绝不会被静默丢弃。queue-baton-survives-restart.yaml 中保留了这条带注释的延后断言。

ROUTING.md 的 R-11 确认:daemon: {op: sigterm|restart} 步骤动词已进入修订后的 51-02 v1(A2 终稿 28dc80cf / 4180a007),操作符集合 DAEMON_OPS 定义在 scenario-schema.ts,由共享 env-helper 的生命周期表面承载。

6. 拓扑 fixture 形态:version: "0.2" + 无顶层 edges

差异:真实 fixture 形态为 version: "0.2"、name:、culture_file: culture.md、pods:[{id,label,members:[{id,agent_ref,profile,runtime:stub,cwd: .}],edges:[]}],没有顶层 edges。规范座位会话 = <pod>-<member>@<name>。同时需要 agents/ + culture.md 兄弟 fixture(dev-driver 的示例会带上它们)。

7. any_of(R-15)不是动词——会触发 UNKNOWN_STEP_VERB

差异:any_of 不在锁定语法中(match/contains/equals 都是单结果判定),一旦使用会以 UNKNOWN_STEP_VERB 失败。这曾是 #5 的阻塞点,最终通过**年代拆分裁决(era-split ruling)**解决:GAP-7 折叠(d0668d5f)进入 main 之后,固定 tip 上的结果变得确定性,因此 #5 的 GREEN 腿固定"preseed 可达",seed_regression{gap7-home-divergence} 腿固定"失败在流上可见",不再需要 any_of。详见 home-divergence-preseed-visible.yaml。

8. expect 上的 select(R-16)会被静默忽略

差异:select 不是 schema 键,草图中 #9 的 -A/--rig 腿需要一个运行器认可的 ps 作用域选择器。核心泄漏捕获(默认 ps = 仅当前 rig,beta 缺席)现在即可表达;-A/--rig 确认性腿路由到 51-02(R-16)。#9 的最终形态(见 ps-scope-honesty.yaml)为:当前 rig 默认腿 LIVE,-A/--rig 作用域腿显式延后(注释标注),原因正是"daemon 可能在其投影中静默忽略选择器 → 现在断言会冒 FALSE GREEN(选择器被丢弃)的风险"。

9. seed_regression 保留在每个 51-03 场景上

差异:dev-driver 的 51-02 证据场景省略了 seed_regression(它们不是种子场景);但 51-03 的 mini-req 3 要求每个场景都保留它。因此全部 11 个场景都以 seed_regression: {class: <缺陷类>} 收尾。


三、种子契约(seed contract):可证伪 RED 的基石

README.md 对种子契约的定义是:每个场景都携带一个 seed_regression: {class: …} 步骤,并在头部有SEED 定义——精确说明该类的回归种下了什么、以及为什么场景的 expect 腿必须捕获它。验收标准是配对:发货 tip 上的 GREEN + 种子运行上的 RED,且运行器差异(expected vs last-observed)能点名该类。

"A scenario that cannot be shown RED is not delivered."(无法展示 RED 的场景不算交付。)

对可执行种子,步骤应放在受影响动作之前,并提供流水线的 deps.seedRegression 控制器:在种子运行中武装真实的本地故障,并在健康运行中显式记录该故障为禁用。配对 harness 会验证注入回执与具体的失败观察。历史标记(历史缺陷注释)本身不是可执行证明。

以 queue-baton-survives-restart.yaml 为例:

# Arm the paired harness BEFORE the affected action. Healthy runs record a
# disabled fault; seeded runs reset this exact claim while the daemon is down.
- seed_regression: {class: baton-drop}
- daemon: { op: restart }

种子运行使重启路径无法重新水合认领记录,post-restart 的 expect queue 必须捕获它——差异显示"期望 in-progress/owned vs 观察到 pending/unowned"。


四、真实读取表面的 --json 形态(INCREMENT 4,verify-at-source)

51-02 运行器是"哑读取器"(dumb-reader),它绑定的真实 --json 形态在 INCREMENT 4 中逐一落地:

表面 真实形态 说明
ps rig ps --json = 裸数组,按 rig 分组的 PsEntry {rigId,name,nodeCount,runningCount,activeCount,hasWorkCount,status:"running"|"partial"|"stopped",lifecycleState,uptime,…} 不是按座位(seats 走 --nodes = 不同端点/NodeEntry)。因此 #1/#9 断言的是按 rig 汇总而非按座位
queue rig queue list --json = 裸数组 QueueItem[](qitemId/state/destinationSession…) 紧凑模式下 body/summary 为空,需用 --full/show 读取
stream rig stream list --json = 裸数组 StreamItem {streamItemId,tsEmitted,sourceSession,body,format,hintType,…} 注意:这是 SEND/BROADCAST 协调流,不是 emit 行为写入的 activity/hooks 流
policy_provenance rig policy effective --json → {effective:{binding,resolvedScope}|null, posture:"known"|"unknown_posture", hint?} binding 携带 setBy/setAt/record.evidence_citation。因此 #3/#4 必须断言 {posture, effective:{binding:{…}}},而非草图的 {policy, source}
pane 捕获 → CaptureResult{content};contains 对 content 做子串匹配 #2/#6/#7/#8-pane 已正确
tui_socket UNIX socket,发送 state → {ok, instanceId, state:{screen, drill, filter, viewTab,…}} #10 的归一化读取
proof 无读取表面(FLAG-1,fail-closed UnboundSurfaceError) 没有任何场景断言它

这一表格直接决定了场景断言怎么写。例如 clean-lifecycle-no-residue.yaml 的注释明确写着:

Real rig ps --json = BARE ARRAY of per-RIG PsEntry — NOT the sketch {rigs:[{seats}]}.

相应地,其 expect ps 断言为 match: [{name: solo-stub, status: running}](裸数组上的子集匹配)。

两个新的可表达性发现(route,don't shim)

  • F-#1(空性):#1 的"settled → NO residue"需要一个空数组断言。但子集 match: [] 平凡通过(空子集 ⊆ 任何集合),contains/equals 也不断言空。格式缺少否定/空/长度断言形态。已路由到 51-02(match: [] 表示精确为空,或新增 absent/count 形态)。#1 保持延后。
  • F-#8(stream vs activity):emit slow_output 写入的是 ACTIVITY(/api/activity/hooks),不是 StreamItem。因此 #8 的"stream durable"前提需要决策:用 send(确实会创建持久 StreamItem)建模,还是确认运行器的 stream 表面是否也应读取 activity。路由到 dev-driver/orch,不猜测。

五、11 个场景的逐类对照(附真实 YAML 证据)

以下表格将 FORMAT-CONFORMANCE-NOTES.md 中的增量记录与 scenarios/ 目录下的真实文件一一对应:

编号 缺陷类 场景文件 核心形态
#1 install/up/down 残留 clean-lifecycle-no-residue.yaml up → expect ps(裸数组)→ down → seed_regression{install-up-down-residue};空性断言 F-#1 延后
#2 接力棒丢失/陈旧 activity/中途死亡 queue-baton-survives-restart.yaml env.queue 前置 + daemon:{op:restart} + 重启前后两次 expect queue(qitemId/state/destinationSession)
#3 Seam-B 姿态在结构编辑后丢失 policy-posture-survives-structural-edit.yaml policy:{op:attach} → expect policy_provenance(posture 形态)→ mutate:{op:add_member} → 再次断言不变
#4 fail-OPEN Locked→full-bypass 反转 locked-restore-never-permissive.yaml attach Locked → restore: lead → expect policy_provenance 仍为 Locked(fail-CLOSED)
#5 GAP-7 HOME 分歧 home-divergence-preseed-visible.yaml env.pre_existing_tmux: true + GREEN 腿 expect pane contains "trust reached" + seed_regression{gap7-home-divergence};any_of 已按年代拆分裁决移除
#6 acting-orch/错误座位 restore compaction-restore-resumes-role.yaml emit:{behavior: compaction} → emit:{behavior: restore} → expect pane contains "restore delivered"(座位键控)
#7 pane-render vs receipt(send --verify 诚实性) send-verify-means-rendered.yaml send:{verify: true} → expect pane contains "render-marker-7f3a"
#8 流脊梁持久 + 可重放 stream-emit-durable-replay-live.yaml emit:{behavior: slow_output} → expect stream(裸数组)→ expect pane contains "slow_output chunk 3/3"
#9 跨 rig 泄漏(ps 作用域诚实性) ps-scope-honesty.yaml 双 rig up(R-12)+ 默认作用域 expect ps(alpha 在、beta 缺席)+ seed_regression{across-rigs-leak};-A/--rig 腿 R-16 延后
#10 单视图状态不变量(tui_socket == ps == queue) one-view-state-after-mutation-storm.yaml env.tui: true + 有界突变风暴(send/mutate 交错)+ expect equals: [tui_socket, ps, queue]
#11 丢失执行闭合/executed-unclosed(RED-first) kill-daemon-mid-handoff.yaml send 驱动 handoff → daemon:{op:sigterm} → daemon:{op:restart} → post-restart queue/stream 断言;51-06 前记录为 expected-RED

两个已适配场景的详解

#2 queue-baton-survives-restart(重启后接力棒存活)

这是第一个适配到真实形态的场景。它复现的缺陷类来自马拉松:一次重启(崩溃/恢复)丢掉了进行中的队列认领,导致已属主的工作看起来无主,第二个座位重新捡起它。种子 baton-drop 使重启路径无法重新水合认领记录。完整步骤链:

  1. up(带 env.queue 前置:baton-1 已 claim)
  2. 重启前 expect queue:baton-1 in-progress + owned
  3. seed_regression: {class: baton-drop}(在受影响动作之前武装)
  4. daemon: { op: restart }(接力棒存活于 daemon SQLite,必须跨 daemon 生命周期存活)
  5. 重启后 expect queue:同一条目、仍 in-progress、仍 owned
  6. expect pane "restored" 腿以注释形式延后(items-6-8)
  7. down

#10 one-view-state-after-mutation-storm(突变风暴后的单视图状态)

复现 PIN-1 不变量:tui_socket == ps == queue 必须表达同一事实。种子 view-state-divergence 使一个表面(tui_socket)在风暴后滞后/遗漏。关键形态包括 env.tui: true(D7 增量:tui_socket 期望要求 TUI opt-in——流水线仅在声明时供应隔离 tmux server + scaffold socket)以及有界突变风暴(无循环动词,用显式序列):四次 send + 两次 mutate(add_member d → rename_member d→e)交错,风暴后 equals: [tui_socket, ps, queue] 交叉表面归一化断言。


六、运行诚实性:组成台账(composition ledger)与路由纪律

INCREMENT 5 在真实运行器(main 5dc70f0e,run-scenarios.mjs)上、密封环境(hermetic env)中运行了全部 11 个场景,得到如下组成台账:

  • 每个已绑定产品步骤都 GREEN(仅在路由的 seed_regression 处阻塞):#1、#2、#9
  • 表面可解析、内容标记路由到 51-01 stub 行为:#5(默认 stub 不发射 preseed "trust reached")、#7(stub 不回显发送文本到 pane;send 确实到达,pane 显示 "From: ")
  • 响亮未绑定动词(路由到 51-02 运行器绑定):#6/#8(emit)、#3/#4(policy;#4 还含 restore)、#10(mutate;其 sends 先组合完成)
  • 51-06 门控(expected-RED,绝不粉饰):#11(queue 形态已适配;R-17 send-vs-queue 前提 + R-20 51-06 闭合表面)

"GREEN through every bound product step + LOUD unbound verb + route the unbound runner verbs = the honest ledger"(review50-r1 确认:一个无种子承载的干净 PASS 预绑定就是 shim)。

绝不 shim 的路由缺口清单

ROUTING.md 按目标切片完整列出依赖:

目标 能力 状态
51-01 R-01 emit 四件套(launch 脚本已落地;步骤期 emit 仍未绑定,抛 UnboundActionError) 部分 LANDED
51-01 R-02 场景解析 stub 脚本(env.stub_scripts,主机模式 LANDED;容器模式拒绝 ScenarioModeUnsupportedError) LANDED(主机模式)
51-01 R-03 stub ctx% 资格(A5 item 5) 门控
51-02 R-10 运行器 + 密封 env-helper LANDED
51-02 R-11 daemon:{op:sigterm|restart} 动词(ARCH-RULING-51-09) LANDED in 51-02 v1
51-02 R-12 多 rig up(#9 第二 rig) OPEN
51-02 R-13 equals 跨表面归一化器 LANDED in format
51-02 R-14 env.pre_existing_tmux 密封 fixture(#5) OPEN(校验器接受、流水线忽略)
51-02 R-15 any_of(#5) RESOLVED:不采纳(年代拆分)
51-02 R-16 ps 作用域选择器(#9) OPEN
51-02 R-17 handoff/queue 完成动作(#11) OPEN
51-06 R-20 #11 GREEN 门 = 事务执行闭合 reconcile POOLED,不在 main

七、校验器语义边界与验收结论

结构校验 vs 运行时正确

51-02 的 validateScenario(topologyKind: stub)只检查结构:表面合法 / 单匹配模式 / 动词合法 / within。它不检查 match: 字段的内容。因此草图时代的 #1/#3/#4/#7/#8/#9/#11 可以"schema-PASS 但运行时形态错误"。文档反复强调:

"Schema-pass ≠ runtime-correct."(结构通过 ≠ 运行时正确。)

11/11 的时间线

  • 2026-08-06 首次跑真实校验器:10/11 PASS,唯一硬失败是 home-divergence-preseed-visible → UNKNOWN_STEP_VERB@steps[1].any_of(R-15)。
  • 同日 #5/#9 裁决落地:11/11 全部 schema-PASS。
  • INCREMENT 3(#6/#8 emit 场景组合)后:仍然 11/11(manifest YAML-only c0ba79aa)。
  • INCREMENT 5(共置化 + 运行时字段名通过 + 组成台账)后:在真实运行器上逐场景验证,未绑定动词一律 LOUD 失败并路由。

剩余工作被明确列出(下一增量):草图场景 #1/#3/#4/#7/#8/#11 的运行时字段名通过(queue→qitemId/state/destinationSession、ps 形态、#11 的 env.queue、stream/policy_provenance 形态)+ fixture 形态通过(culture_file/cwd:./agents)。#6/#8 还门控于 51-01 items 6-8(emit)。#11 的 GREEN 门 = 51-06(R-20)。


八、给场景作者的实践清单

把 FORMAT-CONFORMANCE-NOTES.md 的方法论沉淀为可操作的清单:

  1. 拓扑路径用 ./<rig>.yaml 兄弟相对路径,并在场景文件旁放置 culture.md + agents/<seat>/agent.yaml(flat 布局,fixtures/ 已被吸收)。
  2. 接力棒/队列条目一律用 env.queue 前置条件建立(id/source/destination/summary/claim),不要用 send——语法中没有队列动作动词。
  3. expect queue 键名用真实字段:qitemId、state、destinationSession。
  4. 每个 expect 都必须有合法 surface 锚点;equals 是锚点表面下的列表,不是对象。
  5. 重启 = daemon: {op: restart|sigterm},不是座位级 restart:;延后断言要显式注释,绝不静默丢弃。
  6. fixture 形态:version: "0.2"、name:、culture_file:、pods:[{id,label,members:[…],edges:[]}],无顶层 edges;规范座位会话 = <pod>-<member>@<name>。
  7. 不要用 any_of(校验器拒绝是正确的);用年代拆分裁决后的确定性单结果,或用 seed_regression 腿固定失败可见性。
  8. expect 上不要放 select——它会被静默忽略(R-16);只断言当前 rig 默认腿,作用域腿显式延后。
  9. 每个 51-03 场景保留 seed_regression: {class: …},种子放在受影响动作之前,并配套 deps.seedRegression 控制器。
  10. 结构通过 ≠ 运行时正确:断言字段名必须对齐运行器绑定的真实 --json 形态(裸数组、按 rig 汇总的 ps、bare-array QueueItem、bare-array StreamItem、{effective,posture} 的 policy_provenance)。

九、相关源码与文档导航

  • 格式一致性适配全文:FORMAT-CONFORMANCE-NOTES.md
  • 场景套件与种子契约:README.md
  • 依赖路由台账(51-01/51-02/51-06):ROUTING.md
  • 11 个场景 YAML(含拓扑、agents、culture):scenarios/
  • 场景运行器实现:run-scenarios.mjs
  • 密封 env-helper 与场景流水线:packages/daemon/test/helpers/scenario-*.ts、scenario-pipeline.ts、scenario-schema.ts、scenario-real-deps.ts、scenario-normalizer.ts(均在 packages/daemon/test/helpers/ 与 packages/daemon/src/ 下)
  • CI 门控说明:ci/README.md

</output_article>

登录后查看全文
openrig