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进行自动化评测、方案校验和结果导出的用户。
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Khorsheed/dsh-plugins#548eeabe331c121621b2d08deab4278ad81d6312&path:packages/evalREADME
Read the full 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 是人的发起动作。流程:
- 校验先行:plan 有 error 即拒绝,什么都不执行;条件 lock 与现算哈希不符即拒绝(缺 lock 记 warning,用现算哈希——完整就绪检查归 I4 provision)。
- 就绪检查(T23):建 run 之前,对 plan 里每个条件跑一次最小委派——同一门面、同一 provider、同一条 cwd 规则,见「就绪检查」。任一条件失败即整 run 不启动;
--ignore-readiness才允许带着失败条件开跑,此时该条件的格子一律记cell-skipped并给出理由。 - 子集(T23):
--only与--max-cells N在随机顺序上取一部分。选择落进run.meta.subset({only, maxCells, totalCells, selectedCells}),生成的模板只带被选中的 mission——ledger 里不会留下 run 永远不会驱动的格子。plan 契约不加字段:子集属于一次执行,不属于被审阅的那套程序。 - 快照:
datasets.snapshot钉 commit,进 run.meta。 - 模板 + 矩阵:生成模板写到 plan 旁;
expandMatrix展开(题 × 条件 × rep),orderCells按plan.order.seed洗牌、interleave 时优先同条件不连续;顺序与并发数写进 run.meta。 - 逐格(并发缺省 1):格子独立目录
$DSH_HOME/state/eval/cells///attempt-/,物化该题 visible 层内容并写materialization.json(排序逐文件 sha256 + 整体 sha,addArtifact kindmaterialization);每阶段一条 prompt = 题集 visible 层prompts/.md字节 + 一个换行 + 该题task.md字节,sha256 记入 orchestrator ns;编排器直接调ctx.localAgent.start(首轮)/resume(续轮),格子目录经委派cwd选项传给子代理(local-agent 家族的 cwd 支持,T11)。 - 推进:委派返回后从格子目录收
.json/.md,submit({to, json, files})(意向边预校验),transition(to);halt_on命中走halted。schema 违规不重试:记{kind: 'submission-rejected', violations},格子停在当前态。 - 失败策略:委派启动失败、门面报错、超时取消 →
retry(reason, 'infrastructure')重做该格,预算缺省 1 次(--retries可调);超限记{kind: 'cell-skipped'}并跳过。超时 =plan.budget.activeMinutes的每格累计委派时长,到点cancel(childSessionId)。 - 判定、归档、过闸:格子停在终态后先判(见下节),判定落
archive/verdicts/,再把格子目录拷到archive/workspace/,transition(archived),然后推releasable → released——T57 起这是缺省,逐格进行;file-check 要求 verdicts/ 非空:两条源里任一有产出即可过闸,两条都没有就如实记finalize-refused并停在 archived。--keep-units是调试用的退出口:开了之后每格停在 archived、容器留着。 - 导出:结束时 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 步):
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 单元写不进去。- 出网自检(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 在日志里说一次——能跑,但这一轮分不清「网络不通」与「模型没话说」。 setRefs({resource, fingerprint}):编排器自己写,acquire 之后立刻。lab 也写,但那是 warn-and-skip 的尽力而为,而「环境一致」这条不变量不能建在允许跳过的写上——pilot A 的指纹列整轮是空的,报告整轮unverifiable。populate:宿主物化目录 →/workspace,manifestPath指向该 attempt 的materialization.json,由 lab 算哈希、写文件、登记 artifact。编排器不再另算一份:两个同名materialization.json带两个不同哈希,比任一个单独存在都糟。工作区里因此只有题面字节——没有清单,也就没有宿主路径进单元。- 每阶段一轮委派:
exec = {container, workdir: '/workspace', env: {: }},不传cwd(单元里宿主 cwd 没有意义;作用域目录变量不点名的话 T17 会当场拒绝这一轮)。 - 每阶段结束
checkpoint({name: })(ref 经 lab 进 mission),再collect('/workspace' → 格子目录)。格子目录在容器路径上是工作区的宿主镜像:收产出、结构校验、submit、判官取材都在它上面,因此第 7、9 两步的代码两条路径共用。 - 判定在单元内:探针经
lab.verify跑,判定材料走 verify 自己的 scratch(/run/dsh-lab/verify,跑完即删);--out写到/run/dsh-lab/verdicts//,不在/workspace——归档是选手的产物,不该带判定输出。全部跑完一次collect把整棵 verdict 树收回该 attempt 的probe-verdicts/,在宿主上解析、判定,再删掉单元内那个目录。退出码三态、task/by先回填后校验的顺序,与宿主路径逐字相同。 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 这一面不释放任何容器,并且每次都