Khorsheed/dsh-plugins--packages-eval ↗★ 0

@khorsheed/dsh-eval

The web-eval orchestrator: dataseek contract schemas, plan/condition validation, deterministic hashing, run-template generation from a suite manifest, the stage-one/two run loop (per-cell materialization, byte-exact delegation, submit/transition, judging, archive gate, bundle export), the two mechanical verdict sources (verify-layer probes writing script, de-fingerprinted double-sampled blind LLM judging writing llm-draft), and the bundle report (results.jsonl + summary.md behind the four-invariant gate) 适合需要对Agent进行自动化评测、方案校验和结果导出的用户。

パッケージ
@khorsheed/dsh-eval
互換性
未検証
Harness ピア範囲
^0.1.0-rc.6
Cordis ピア範囲
^4.0.1
バージョン
0.1.0-rc.1
ライセンス
MIT
最終更新
2026/09/28

インストール

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Khorsheed/dsh-plugins#548eeabe331c121621b2d08deab4278ad81d6312&path:packages/eval

ドキュメント

README 全文を読む ↗

dsh-eval

中文 | English

web-eval 编排器:dataseek 契约 schema、plan/condition 校验、条件与 scoped home 哈希、由题集 manifest 生成 run 模板、阶段一二的 run 循环(逐格物化、逐字节委派、提交推进、归档闸、bundle 导出)。 run 的发起是人的动作(/eval run,发起会话即所有委派的父会话);判定的两条机器通路(探针写 script、判官盲评写 llm-draft)随 T9 落地,human-final 仍归人。不依赖任何兄弟插件——四个上游服务(datasets / mission / localAgent / lab)在 run 时经 ctx.get 探测,缺哪个就拒绝并列出哪个,绝不炸启动;前三个每次 run 都要,lab 只在 plan 带 unit 段(容器路径)时才要——没有 unit 段就在宿主目录里跑,拒绝文案会说「挂上 dsh-lab 插件,或去掉 unit 段」。

给 agent 的模型工具是五个读工具、一个起草工具与一扇分析的门(见「模型工具」一节);run、finalize 与注解都不给模型。

report 把 mission export 的 bundle 变成 results.jsonl 与 summary.md(见「报告」一节),只读 bundle、不依赖宿主。

它管什么

三份契约 schema 与一条 lock 记录(全文见 dataset-authoring-protocol §6,代码里集中在 src/schema.ts 一个模块,测试钉住文档与代码不漂移):

schema是什么
dataseek.condition/1受试对象:harness、模型声明、permissions(按 harness 词表)、scoped home 哈希、env 键名
dataseek.plan/1一次 run 的全部输入:题 × 条件 × rep × 阶段 × 顺序 × 预算 × 判官 × 期望判定源
dataseek.verdict/1判定输出:探针与判官共同遵守的格式
dataseek.condition-lock/1条件哈希与 scoped home 的实物记录;plan 的 sha 从这里解析

校验器遵循 I1 定下的字段决定:condition 的可空字段(harness.version、model.declared、model.endpoint、home.sha)的 null 是「未解析」,validate 列为 warning;plan.conditions 写条件 id,sha 从 conditions/.lock.json 解析,缺 lock 即「未就绪」(同样是 warning——拦截归 run 前的就绪检查);judge 可缺省,缺省时 expectedNs 不得含 llm-draft;plan 不含 template 字段。

模板生成:manifest → run 模板

run 模板不由人或 agent 手写:它是题集 manifest.yml 的确定性函数(generateTemplate),测试逐项钉住与 I1 手写的 templates/bench-v1.json 等价:

pending → ws-ready → stage-1 → … → judged → archived → releasable → released
                          └→ halted(halt_on 命中)→ archived
  • 最早转移带 run-meta schema-check(schemas/run-meta.json:datasetId + commit),把快照钉进状态机;
  • 每个阶段的出边带该阶段 structured schema 的 schema-check(manifest output_schema..structured 指向的 schemas/.json;内联草记记法已废弃,遇到即拒绝);
  • 声明了 halt_on 的阶段生成 → halted 边,guard 指向约定文件 schemas/-halted.json(const 校验在 schema 文件里);
  • 进入 releasable 带 file-check(archive/workspace/、archive/verdicts/,目录须非空)。

阶段状态按 manifest 中的位次命名(stage1 → stage-1);格产出文件是 .json / .md(提示词里写明)。schemaPath 相对模板自身位置生成——编排器把模板写在 plan 旁(plans/ .template.json),与手写模板的 ../schemas/… 同一解析方式。

run 循环 v0(阶段一二;宿主目录或容器单元)

ctx.eval.run(planPath, options) 是本体;/eval run 是人的发起动作。流程:

  1. 校验先行:plan 有 error 即拒绝,什么都不执行;条件 lock 与现算哈希不符即拒绝(缺 lock 记 warning,用现算哈希——完整就绪检查归 I4 provision)。
  2. 就绪检查(T23):建 run 之前,对 plan 里每个条件跑一次最小委派——同一门面、同一 provider、同一条 cwd 规则,见「就绪检查」。任一条件失败即整 run 不启动;--ignore-readiness 才允许带着失败条件开跑,此时该条件的格子一律记 cell-skipped 并给出理由。
  3. 子集(T23):--only 与 --max-cells N 在随机顺序上取一部分。选择落进 run.meta.subset({only, maxCells, totalCells, selectedCells}),生成的模板只带被选中的 mission——ledger 里不会留下 run 永远不会驱动的格子。plan 契约不加字段:子集属于一次执行,不属于被审阅的那套程序。
  4. 快照:datasets.snapshot 钉 commit,进 run.meta。
  5. 模板 + 矩阵:生成模板写到 plan 旁;expandMatrix 展开(题 × 条件 × rep),orderCells 按 plan.order.seed 洗牌、interleave 时优先同条件不连续;顺序与并发数写进 run.meta。
  6. 逐格(并发缺省 1):格子独立目录 $DSH_HOME/state/eval/cells///attempt-/,物化该题 visible 层内容并写 materialization.json(排序逐文件 sha256 + 整体 sha,addArtifact kind materialization);每阶段一条 prompt = 题集 visible 层 prompts/.md 字节 + 一个换行 + 该题 task.md 字节,sha256 记入 orchestrator ns;编排器直接调 ctx.localAgent.start(首轮)/ resume(续轮),格子目录经委派 cwd 选项传给子代理(local-agent 家族的 cwd 支持,T11)。
  7. 推进:委派返回后从格子目录收 .json / .md,submit({to, json, files})(意向边预校验),transition(to);halt_on 命中走 halted。schema 违规不重试:记 {kind: 'submission-rejected', violations},格子停在当前态。
  8. 失败策略:委派启动失败、门面报错、超时取消 → retry(reason, 'infrastructure') 重做该格,预算缺省 1 次(--retries 可调);超限记 {kind: 'cell-skipped'} 并跳过。超时 = plan.budget.activeMinutes 的每格累计委派时长,到点 cancel(childSessionId)。
  9. 判定、归档、过闸:格子停在终态后先判(见下节),判定落 archive/verdicts/,再把格子目录拷到 archive/workspace/,transition(archived),然后推 releasable → released——T57 起这是缺省,逐格进行;file-check 要求 verdicts/ 非空:两条源里任一有产出即可过闸,两条都没有就如实记 finalize-refused 并停在 archived。--keep-units 是调试用的退出口:开了之后每格停在 archived、容器留着。
  10. 导出:结束时 export bundle 到所属实验的 exports/(旧 plan 路径起的 run 仍是 /exports/;--out 可改),只收 visible 层(modelFacing:true,无需泄题闸确认)。

run 开始时先为每格写一条锚点 {kind: 'cell', task, condition, conditionSha, rep}——在任何工作之前,所以连被跳过的格子也可归属;报告只认这条锚点定格子身份(bundle 里没有 labels,mission id 是有损的)。

每次委派在 orchestrator ns 记 {kind: 'delegation', stage, round, childSessionId, promptSha, startedAt, durationMs, usage, toolCalls?, cliVersion?, model: {declared, observed}}(toolCalls 与 cliVersion 只在本轮 settled 事件带了才写,缺位就是「这一轮没人报过」);observed 取自 T11 的回读——本轮 settled 事件优先,其次 delegationOf(childSessionId) 的记录。读记录要等:provider 在 settle 后的收尾遍里才并入观测,而门面在 result 落定的同一刻就清掉了带着 onProgress 的在跑记录,所以真实门面上 settled 事件根本到不了编排器、记录也要晚一拍才有值——run 结束即读会读空(第一次两格真跑的现场)。编排器因此在 settle 后有界地轮询记录(缺省 10s,readbackWaitMs 可调),等到与本轮开始前不同的观测即采用;等超时仍返回记录当前值(续轮跑的是同一模型时两者本就无从区分,记录本身的语义就是「该委派最近一次观测」),两处都没有才记 null。usage 只走 settled 事件:门面先清在跑记录的情况下这里拿不到,记 null 是诚实答案。回读到的模型与条件 model.declared 不符即当场失败(冻结决策 5:这次 run 归属错了),不按基础设施失败重试。

**声明的模型现在是「先请求,再核对」(T30b)。**从前 model.declared 只用来与回读比对,实际跑哪个模型全看 harness 自己的配置——判官条件声明 dsh v4-pro、实跑宿主默认 v4-flash,就绪检查因此拒(T22 第 5 步)。现在选手轮、判官委派、就绪探测三处都把非 null 的 model.declared 作为委派级 model 传给 local-agent,由它落成各家 CLI 的模型参数;声明为 null 就照旧不传。委派注解与 run.meta.readiness 各多一个 requestedModel,记的是「这一轮向 harness 要了什么」,与「读回来什么」并列。比对本身不变:请求了还回读到别的,仍是 MisattributedRun。resume 轮不带 model——local-agent 把首轮的请求记在委派记录里,之后每轮照它重发。

容器路径(I3·T20)

plan 里有 unit 段就走容器路径,没有就走宿主路径——后者与本节出现之前逐字节相同(pilot A 的 bundle 用本分支的 report 复算,results.jsonl 逐字节一致)。容器路径要 ctx.lab 在场;凭证不用另给——挂的是这台实例自己的作用域目录(见下)。

一格一单元,顺序固定(架构轨迹表第 11–17 步):

  1. acquire:镜像、网络、user、资源上限来自 plan 的 unit 段;挂载只有一条——该条件的凭证目录,bind 且可写(凭证续期要写回宿主,T17 的决定);env 只有该条件 unit.scopedHome.var = 容器内路径,dsh 再加 NODE_OPTIONS=--use-env-proxy;missionId 与 runId 都带上。workdir 是 /workspace,并要求 lab 把它交给单元自己的用户(ownWorkdir)——docker 建出来的 workdir 归 root,非 root 单元写不进去。
  2. 出网自检(T29d,plan 声明了 unit.egressCheck 才有):acquire 之后、populate 之前,经 lab.verify 在单元里跑一次 plan 声明的那条命令,退出码 0 才继续。先问再用的理由是实测出来的:eval-net 是 --internal 网络,出网全靠边车代理;边车停掉时 codex 在单元里正常起来、读到自己的模型与沙箱档位、跑满 230 秒,然后 task_complete 的 last_agent_message 是 null——一个空回答,没有一句网络错误。上面每一层只能读成「探针失败」,而同样的四分钟会在每一格重演。就绪探针的单元同样先问、且在委派之前问,所以出网断了是一次 EGRESS_UNAVAILABLE 拒绝,一次委派都不花。命令与目标写在 plan 里(与网络放在一起),编排器自己不持有任何地址;声明为空或含空词,run 以 EGRESS_CHECK_MALFORMED 拒绝。plan 有 network 却没声明自检时,run 在日志里说一次——能跑,但这一轮分不清「网络不通」与「模型没话说」。
  3. setRefs({resource, fingerprint}):编排器自己写,acquire 之后立刻。lab 也写,但那是 warn-and-skip 的尽力而为,而「环境一致」这条不变量不能建在允许跳过的写上——pilot A 的指纹列整轮是空的,报告整轮 unverifiable。
  4. populate:宿主物化目录 → /workspace,manifestPath 指向该 attempt 的 materialization.json,由 lab 算哈希、写文件、登记 artifact。编排器不再另算一份:两个同名 materialization.json 带两个不同哈希,比任一个单独存在都糟。工作区里因此只有题面字节——没有清单,也就没有宿主路径进单元。
  5. 每阶段一轮委派:exec = {container, workdir: '/workspace', env: {: }},不传 cwd(单元里宿主 cwd 没有意义;作用域目录变量不点名的话 T17 会当场拒绝这一轮)。
  6. 每阶段结束 checkpoint({name: })(ref 经 lab 进 mission),再 collect('/workspace' → 格子目录)。格子目录在容器路径上是工作区的宿主镜像:收产出、结构校验、submit、判官取材都在它上面,因此第 7、9 两步的代码两条路径共用。
  7. 判定在单元内:探针经 lab.verify 跑,判定材料走 verify 自己的 scratch(/run/dsh-lab/verify,跑完即删);--out 写到 /run/dsh-lab/verdicts//,不在 /workspace——归档是选手的产物,不该带判定输出。全部跑完一次 collect 把整棵 verdict 树收回该 attempt 的 probe-verdicts/,在宿主上解析、判定,再删掉单元内那个目录。退出码三态、task / by 先回填后校验的顺序,与宿主路径逐字相同。
  8. archive:lab 导出 workspace/ 与 manifest.json 到该 attempt 的 archive/;verdicts/ 早已在旁边(判在归档之前,闸要求它非空)。

「环境一致」比的是环境类

refs.fingerprint 记的不是单元的完整指纹,而是环境类:单元的复合指纹分量减去该条件自有的那几项——它自己的作用域目录挂载 target、指向它的变量名、该 harness 的额外变量(dsh 的 NODE_OPTIONS)、以及条件 env.keys 里声明的键。剩下的是计划声明的那部分:image、资源上限、network、user、计划级挂载与 env 键。

不这么做的话这条不变量对本 profile 存在的理由恰好不成立:四家各挂自己的凭证目录、各设自己的变量(CODEX_HOME / CLAUDE_CONFIG_DIR / KIMI_CODE_HOME / DSH_HOME),同一个 run 必然四个指纹,报告按既有规则记 violated 并拒绝比较——而那四个环境在比较关心的每一个意义上都是同一个。

减法是照着造 spec 的那段代码反着读出来的,不是从分量里猜的:acquireSpecFor 放进去哪几项,conditionOwnedComponents 就取回哪几项。哈希规则不复刻——环境类经 lab.fingerprintOf(components) 算,与 acquire 用的是同一个函数,因此派生出来的类与单元自己的指纹天然可比。标签仍是 lab-env:,分量 version 不变:它只是同一套算法作用在少了几项的分量上。

单元自己的完整指纹并没有丢:它记在该格 orchestrator ns 的 unit 注解里(连同被排除的项),也在 lab 写的 archive manifest.json 里。它不进 refs——mission 的 refs 只有 resource / fingerprint / sessions 三个键,加第四个是 mission 的改动,不属于本包。报告在不变量那一行下面把每格的单元指纹与排除项逐格列出,读者能看见差在哪儿,而不用信。

判定目录按题库的真实相对路径物化

题集级 verify 层是真目录(datasets//verify/),因此落在 /verify/…。item 的层是两种布局分岔的地方,错一层的代价正好是一个目录:

  • 约定式:文件在 items//verify/,display 路径相对它(probes/x.mjs);
  • register:descriptor 把 item 目录里的自由文件归位到层角色,display 路径是 item 相对的(checks/probes/x.mjs),文件真的在 items//checks/probes/x.mjs。

两种都按 items//verify/ 物化,register 的题就深了一层,../../../../verify/helpers/lib 落到判定根之外——题内探针只能自带一份题集级 lib 的副本。现在每个文件落在它在题库里的真实相对目录:哪些 display 是被 register 归位过的,从 datasets.show 顺带返回的 descriptor 里读 register 判断(面上是可选字段,不报的门面退回约定式,与从前相同)。探针的 by 仍是 display 路径——那是判定的出处,不是磁盘上的位置。

探针的 cwd 是该题 checklist 所在的目录:约定式是 items//verify,register 归位后是 items//checks,两种布局下共享探针读到的都是这道题自己的 ./checklist.yml。没有 checklist 的题回退到约定根。

一份物化哈希,两条路径

materialization.json 两条路径都由编排器按同一套算法算(排序后逐文件 sha256 再整体 sha256),因此同题同 commit 在宿主与容器里得到同一个数。此前容器路径记的是 lab populate 的 manifest 哈希,「题面一致」这条不变量只能在一条路径内部回答。lab 自己那份哈希没有丢,它换了个名字单独存在该 attempt 的 populate-manifest.json 里——那是另一个主张(「拷进单元的是这份」),不是同一个数的第二种写法。

目录与凭证约定

  • 容器路径挂的是评测实例自己的作用域目录:local-agent 为该 harness 建的 /,bind 到该条件 unit.scopedHome.container 声明的容器内路径,可写。凭证由这台实例上的 / login 写进去,不 stage 副本;续期失败或目录被清空,就在这台实例上重登。
  • 必须是同一个目录,不能是副本:容器轮的 CLI 把 rollout / session 写进被挂进去的那个目录,而委派回读按 homeDir(家名) 去找。指到两处不报错——回读永远为空,model.observed 每轮都是 null,就绪检查报 ready, model —,报告的「受试对象一致」停在 ⚠️。这正是 T20 用独立凭证树时的行为,pilot B 付过一次账。
  • 每条件一份作用域目录:条件可选的顶层字段 scope(只允许 [a-z0-9-],是名字不是路径)把该条件的挂载源与委派换成 /@——与缺省目录同级、各自登录的另一份。缺省即不写,行为与本字段出现之前逐字节相同。scope 进条件哈希:两条只差 scope 的条件是两个受试对象(登录的是两个账号),所以同一家在一次 run 里可以比两个账号。就绪检查按各自的 scope 探各自的目录,容器格挂各自的目录,run.meta.unit.scopedHomes 每条记下它的 scope(宿主路径照旧不进 bundle——那是运维事实)。
  • 编排器只检查这个目录存在、非空、属主,绝不读内容。空目录的意思就是「这台实例上还没人登录过这一家」——开跑前拒绝,比二十四格各自 401 便宜。
  • attempt 的运行数据目录多两样:materialization.json(编排器算的物化清单)与 probe-verdicts/(探针在单元内产出的原始判定)。工作区的宿主镜像不进 attempt 目录,位置记在 workspace-mirror.json 里。

销毁路径唯一

run.ts 里只有一个 destroyUnit,lab.release 只在它里面出现:

  • 过闸即销毁:archived → releasable 一过就销毁,然后才 → released。位置是刻意的:isReleasable 读的是当前状态,而模板的 releasableStates 只有 releasable——先走到 released 再销毁,闸会答否,于是每个容器都活着。finalize 走的是同样三步、同样顺序,事后回收容器才成为可能。
  • 过闸是缺省(T57):它曾经要显式开,因为 v0 没有判官、填不满闸要的 verdicts/。自从每格归档前都先判,旧缺省在实践中只意味着一件事:每个容器都活过它那一格,计划的第 4 格撞上 lab 的 maxConcurrentUnits 起不来——于是跑循环的缺省悄悄决定了矩阵能有多大。想要旧行为(留个容器进去看)就用 --keep-units(CLI 旗标,以及批准对话框的「保留单元」)。
  • 闸拒绝即留下:开了 --keep-units、或 verdicts 为空导致 releasable 过不去,销毁照样问一次、照样被拒,容器留着并在格子上记一条 {kind: 'unit-retained', reason}。没人看过的格子,现场比容器数值钱。
  • 异常路径先归档再释放:格子抛错(基础设施失败、schema 拒绝、归属错误)时先 collect 再 archive 再走同一条闸——不归档就 release 等于毁证据(lab README「失败恢复循环」)。同样不带 force,所以失败的格子会留着它的容器。
  • force 在本文件里只有一处:就绪检查的探活单元。它不绑 mission,因而没有闸;lab 要求这种销毁必须显式 force,正是为了让「无闸销毁」是一句判决而不是一个默认。

已知取舍

  • 串行。容器路径固定 concurrency: 1(显式给 >1 会被拒),并发单元归 I4。acquire 撞上 maxConcurrentUnits 当缺陷报出来,不排队——而且拒绝原文点名占着名额的是谁:lab 只说「先释放一个」,不说是哪个,于是编排器补上每个占着名额的单元的 run id、单元 id、容器名与格子,外加能结束它们的两条命令。这份名单加在这边而不是 lab 里,因为 lab 不知道什么叫一次 run。
  • 多家横比的指纹问题已由环境类解决(见上):refs.fingerprint 记环境类,单元自己的指纹记在 unit 注解与归档 manifest 里。
  • 物化哈希两条路径统一(见上):materialization.json 由编排器按同一算法算,lab 的那份另存 populate-manifest.json。报告仍兼容旧 bundle 的 sha 字段。

就绪检查:每个条件一次真委派

/ status 回答的是形状问题——scoped home 里有没有一份形状对的凭据记录。一份过期且刷不动的记录,读起来和一份能用的一模一样。pilot A 就照单全收了这个答案:claude-code status 报 authenticated: yes,而同一时刻每一次委派都 401,24 格里 6 格在开跑前就注定全废,而第一份证据要到第一次委派才出现。

判官条件同样在内。判官也是一次会失败的真委派,而失败的代价更大:一个选手条件挂了只损失它自己的格子,判官挂了损失的是整轮的 llm-draft 判定——pilot B 的两个判官样本全掉,run 却一路走到 released,llm-draft 命名空间是空的。所以判官按同一条规则探、同一条规则拒,结果以 role: 'judge' 记进 run.meta.readiness。判官在宿主上委派(判定是编排器发起的,不是格子发起的),因此容器路径下它也在宿主上探——探一个它根本不会遇到的环境毫无意义。

所以开跑前的检查不问,只花一次委派。runCreate 之前,对 plan 里每个条件用逐字节确定的一句话提示词 READINESS_PROMPT(回一个 READY,不用工具、不写文件)跑一次委派——与正式格子同一门面、同一 provider、同一条按目录给 cwd 的规则,不另开后门,所以探针证明的就是格子将要遇到的。条件只有在委派既起得来又返回 stopReason: 'completed' 时才算就绪;探针请求条件声明的那个模型(与格子将要请求的同一个),并回读实际模型,与 model.declared 不符即当场判该条件不就绪(冻结决策 5,在开跑前拦下,而不是等到第一个阶段轮次)。

每条判定是一条 {kind: 'readiness', condition, harness, provider, ok, startedAt, durationMs, childSessionId, declaredModel, requestedModel, observedModel, reason?} 记录,既进 run.meta.readiness,也作为 orchestrator ns 注解写到该条件的每一格上——问「这格为什么没产出」的人,在格子上就能看到答案。任一条件失败即整 run 不启动并打印原因(打印的是那句 401,而不是「有个条件失败了」);--ignore-readiness 才允许开跑,此时该条件的格子一律记 cell-skipped 并给出理由,一次委派都不发。

能力面:preset 从声明变成事实

条件文档里的 preset 一直是一句没有对应物的话。它进条件哈希,所以两条只差 preset 的条件在账面上是两个受试对象;但从来没有任何东西把 preset 写到哪里去过,也就没有任何东西能与它不符。T32 给了它对应物:

  • 谁可以声明。 只有 dsh。它的作用域目录里那份子 profile 是评测实例自己写的,preset roster 是那份 patch 的一层。三家外部 CLI 跑厂商自己的编排,本家族组不了——给它们写 preset 由 validate 报 error(PRESET_NOT_FOR_HARNESS),只能写 null。它们那侧的等价物是 skills.pack,同样尚未落地(I6)。
  • provision 先把 preset 组进 scope。 条件声明了 preset 时,conditions provision 在哈希作用域目录之前调 localAgent.provisionScope(harness, scope, { preset }):preset 是受试对象的属性,做主的该是条件文档,而不是一个实例只有一份的插件设置(pilot D 之前只能手工把 roster 层追进每个 scope 的 patch,而那一层会被下一次重启静默抹掉)。顺序不是随意的——scope 自带的那份 preset 副本就在作用域目录里面,先哈希再组进去会锁下一个描述不存在目录的 home.sha。门面没有这个动词就退回从前:有什么读什么。
  • provision 写下实物。 conditions provision(T31)在 provisioned 里多记两项:preset 从写出去的子 profile 回读,capabilities.sha 是 capability-catalog 对那份已配好的环境算出的能力哈希(规范形取技能的 name/source/正文 sha 与工具的 name/channel/parameters——描述措辞不进,改一次文案不该换一个受试对象)。测量由实例内的探针做(T32b,见下);没有 catalog 的组合里探针不在,条件照样落 lock 但不带能力记录,并按名报 CAPABILITIES_UNMEASURED——之后就绪检查再拒一次。
  • 就绪检查还比新鲜度。 lock 记的是 provision 当时的哈希;run 起来时就绪检查再量一次,两者不等即判该条件不就绪、点名「preset 在 provision 之后变了」。少了这一步,lock 会永远读作「已核对」而它量的那个 preset 在底下被改——而改技能正文不会动任何别的已记哈希(home.sha 只哈希配置类文件,不含 SKILL.md),只有这一步看得见。量不出来(catalog 不在、preset 挂不起来)时,原记录照旧算数:测不到是关于 catalog 的证据,不是关于受试对象的。改的若是 scope 自带那份副本,还有第二道、且不需要 catalog 的检查:重算 capabilities.snapshot.sha,不等即拒。validate 与 conditions list 也核这一条(在实例里跑、拿得到作用域目录解析器时)——T32b 记下的「离线读到 ready、run 起来才被拒」就此对上;能力面本身仍然要活的 catalog 才量得到,纯 CLI 的 validate 因此仍看不见它。
  • 就绪检查核对。 声明了 preset 而 lock 里没有能力记录,或记录取自另一个 preset——在花掉任何一次委派之前就判该条件不就绪。preset 进哈希却没人量过,等于两个纸面上的受试对象、事实上的一个。

编排实例自己的能力哈希另算一回事:它记在 run.meta.orchestrator.capabilities 里,是取证。报告的「程序一致」把它列出来,不做任何比较——编排器不回答题库的任何一道题,把它做成通过/不通过的输入,等于「我们升级了规划 agent」就判一条不变量违反。组合里没挂 capability-catalog 就不记这一行。

finalize:跑完之后的再入口

run 自己那次过闸发生在每格跑完的当下,而「判官与终评」是归档之后的工作。pilot A 因此人手把十二格逐个 dsh-mission transition 推过去。/eval finalize 就是这条路,机械化:对 run 内每个 archived 的格子走同一条闸(archived → releasable → released,含归档闸对 verdicts/ 非空的 file-check),非 archived 的格子逐格列出状态并跳过。它是 --keep-units 之后、取消之后、以及闸拒了而后来有人修好之后的再入口。

它不 force。闸拒绝按格记 {kind: 'finalize-refused', from, error} 到 orchestrator ns,格子停在闸拦下它的地方——闸正是「归档」这两个字有意义的原因。它也不碰没走到 archived 的格子:pending 或半途的格子是没做完的工作,不是没释放的工作。跳过按 already-released / interrupted / not-started 归类,一整个 run 一行就能说清。

它也回收容器(T57)。每个过闸的格子,单元在两次 transition 之间销毁——同一个位置、同一个顺序、同样不 force,与跑循环一致。在此之前这条路只动账本:一次容器 run 事后 finalize,会走到 released 而每个容器还 Up 着,且再没有任何一道闸能放行它们(T39 · G18)。报告写清销毁了哪些、还剩哪些,逐条给原因:闸拒了它的格子;格子已经过闸,只剩 dsh-lab release --force,那是人的决定;或者格子根本没跑完。单元面是可选的——组合里没有 lab 就把名单报成未知,绝不报 0。

进程外没有 mission 服务,所以 dsh-eval finalize 以子进程调用 dsh-mission 的 CLI(--data-dir、--mission-cli、$DSH_MISSION_CLI)——正是人手工用的那条缝。eval 仍然不 import mission 包的任何东西。那个子进程是账本不是 lab,所以 CLI 这一面不释放任何容器,并且每次都