@tea-agent/loop-agent 0.39.0-next.2 → 0.39.0-next.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/dist/build-stamp.json +2 -2
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/executors/dag-pi-executor.js +42 -4
  5. package/dist/executors/shell-executor.js +269 -15
  6. package/dist/shared/operator/capabilities.js +149 -27
  7. package/dist/task/config-types.js +22 -0
  8. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  9. package/dist/worker/console/chat/pi-runtime.js +165 -83
  10. package/dist/worker/console/chat/repo-walk.js +10 -1
  11. package/dist/worker/console/chat/routes.js +19 -8
  12. package/dist/worker/console/chat/semantic-activity.js +17 -5
  13. package/dist/worker/console/chat/turn-process.js +17 -30
  14. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  15. package/dist/worker/console/operator-actions.js +128 -4
  16. package/dist/worker/console/operator-user-error.js +19 -0
  17. package/dist/worker/console/pi-readiness.js +8 -1
  18. package/dist/worker/console/security.js +20 -1
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-BmOEx0gA.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-BGiVVe8b.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-nDdu4rgJ.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-Bb5u0BWf.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BlhC0j94.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-Tl-FkUqr.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-Ai8_qQXI.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B9gyCjqX.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Bzbj3zGH.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BT415PvT.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-D_A5_5QL.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-srJkaP3f.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-UYGvEP4l.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-B8luBrrz.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-DN7wXZSq.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-DXKyMuKS.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BaehtRk4.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BaehtRk4.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-dqvoynXj.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2--PcyL34A.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DvWs_hTY.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-DxeIxbyO.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DPCG61Of.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CmuU50-U.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-B61XLD_v.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-BB7HTRRY.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-DvkboAIp.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BdJ_y1fL.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-9bUBZTRi.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-Dw4L4RBa.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-Jr82N_QP.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DAz9wFLr.js} +1 -1
  51. package/dist/worker/console/static/assets/index-B7Y-0g3J.css +1 -0
  52. package/dist/worker/console/static/assets/index-DkCrqaYg.js +325 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-CBB5Np7y.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-D8IOp94y.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-Dgd3AHGG.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-BmYOxLE9.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DfYTEF6J.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-BjuFPRqa.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-CLyUViGd.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BvX2Di4-.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-BFj0_OLv.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-BIUrD_hx.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-CHBB48fj.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-sJrYw2Sf.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BzHH6fPR.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-7QCnelCt.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-sDYSxn72.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-XpiUdwzm.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-eEhq2A8Y.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DvqgNPzD.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-la8AC7fo.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-2TyOYNx0.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-B-nM6wM2.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-B86wPdbr.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-Dr1C4AQz.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/app/console-types.js +39 -0
  78. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
  79. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  80. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  81. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  82. package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
  83. package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
  84. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  85. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  86. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  87. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  88. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  89. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  90. package/dist/workflows/dag/backend-test-layout.js +133 -0
  91. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  92. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  93. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  94. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  95. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  96. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  97. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  98. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  99. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  100. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  101. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  102. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  103. package/dist/workflows/dag/frontend-prewrite-gate.js +243 -8
  104. package/dist/workflows/dag/init-hybrid.js +753 -104
  105. package/dist/workflows/dag/node-execution.js +61 -2
  106. package/dist/workflows/dag/retry-policy.js +4 -0
  107. package/dist/workflows/dag/types.js +74 -0
  108. package/docs/templates/backend-test-dag.json +60 -35
  109. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  110. package/harness.json +3 -1
  111. package/package.json +1 -1
  112. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  113. package/skills/frontend-implementation/references/design-spec.md +16 -19
  114. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  115. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  116. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  117. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  118. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  119. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  120. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  121. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
package/CHANGELOG.md CHANGED
@@ -2,17 +2,88 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 变更
6
+
7
+ - npm `next` 渠道发布改为提交数节流:main push 仅当距上次 next 发布的 source commit ≥ 5 个 commit 时才自动发布(新增 `scripts/next-publish-gate.mjs`,读已发布 tarball build-stamp 对比 HEAD);不足阈值时 workflow 绿色跳过,手动 `workflow_dispatch` 不受限(切 release 线前强制出包 / 紧急补发)。发布文档真源 §4.1/§5.1/§7/§12 同步。
8
+
5
9
  ### 新增
6
10
 
11
+ - backend-test 适配 A–D:可配置 `backendTest` 布局(`testRoot`/`markdownRel`/`scriptRel`)、用户公共前置文档引用与模块顶部 `user_shared_setup`(不写 conftest)、查询轴 Scenario Partitions 确定性展开与 fail-closed 门禁、`mode=gap-fill` 13 节点增量 DAG。Slot ID 以声明的 Partition ID 为真源(`TP-<Partition ID>-...`)。
12
+ - 前端 plan 唯一机器契约化(JSON-only):`frontend-plan-pi` / `frontend-plan-revision-pi` 输出收敛为「一行导语 + 唯一 fenced `json` 契约 + 唯一 `openspec-citations` 引用块」,彻底移除「可选 Markdown 说明」;plan.md(`contracts/frontend-plan.md`)改由 prewrite gate 用纯函数 `renderFrontendPlanMarkdown` 确定性渲染(不新增 DAG 节点/不新增 Pi 调用)
13
+ - 契约 schema 新增 5 个可选字段(`implementationSteps` / `stylingStrategy` / `dependencyPolicy` / `residualRisks` / `realIntegrationGap`),`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
14
+ - revision 输出改为 RFC 7386 merge-patch delta + 引用块:gate 配置新增 `revisionPatch`,生效时在 `planFallbackFromNodeIds[0]` 原始契约上应用 `applyFrontendImplementationContractPatch` 后进入既有分析/规范化/物化管线;旧 DAG 缺字段按整契约处理
15
+ - canonical contract 序列化(UTF-8)超过 64KB → `retryable-invalid` + 新增 `contract-too-large` failureCode(体积守卫兜底)
16
+ - `design-review` / `final-design-review` 提示改为消费契约 JSON(注入契约字段摘要),不再要求读取 plan prose
17
+ - Console「模型与认证」页新增模型矩阵编辑器:展示并允许编辑 `harness.json executors.pi` 的 LOW/MED/HIGH 三档模型;输入框从 readiness 结构化投影 `piModelMatrix` 预填(provider/model 或裸 model id),本地内联校验(空串 / `default` 哨兵 / 畸形 `provider/`、`/model`、`a//b`)非法时禁用保存;保存经既有 `POST /api/operator/v1/operations` 派发 `savePiModelMatrix`,复用 HTTP 层 `evaluateMutationGate`(boot capability cookie + `x-loop-console-confirmation` 头 + Origin/Host/Sec-Fetch-Site),读全文档后原子写 tmp+fsync+rename(保留 `$schema` 与其余字段),成功后自动重测 readiness 并对活动 Chat session 触发 `/sessions/:id/reload`(409 `PI_SESSION_BUSY` 给出可恢复提示)
18
+ - `GET /api/operator/v1/readiness` 的 probe 新增 `piModelMatrix` 结构化投影(向后兼容,不改动既有 `modelsMessage`)
19
+ - 组件选型规范合规(ADR 0016):契约 schema 新增可选 `uiComponentChoices[]`(`{ purpose, component, decision(specified|reuse-existing|new), specReference{path,section,line}|null, rationale }`),`specified` 必须给非空 `specReference`、`new` 不得携带、空串拒绝;`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
20
+ - `frontend-prewrite-gate` 新增三个 `retryable-invalid` failureCode:`component-spec-reference-invalid`(specReference.path 非受支持规范路径)、`component-spec-not-cited`(未在 openspec-citations 引用块引用或未真实读取)、`component-choices-missing`(组件/主题候选桶非空且契约有 UI 可见工作而 `uiComponentChoices` 缺失/空)
21
+ - gate 配置新增可选 `componentSpecCandidatePaths`(旧 DAG 缺字段兼容);`init-hybrid` 从 `designEvidence.classified.component ∪ theme ∪ rule.components` 冻结注入,capability context 强调组件/主题桶语义,plan/revision/design-review/final-design-review 提示加入硬规则与 BAD/GOOD 示例
22
+ - `frontend-plan.md` 新增 `Component Selection (spec conformance)` 章节(纯函数确定性渲染,缺省 `_(not specified)_`)
23
+
7
24
  - `frontend-prewrite-gate` 的 OpenSpec 读取校验支持 `task.json.frontendOpenspec` 配置:`policy` 缺省 `cited`(候选 = `requiredReadPaths` ∪ 任务源显式引用的 openspec 路径),`scan-strict` 保留止血任务后的全量必读语义;`requiredReadPaths` 中非 openspec 支持路径在生成期确定性失败
8
25
  - 确定性引用块协议:生效 plan 输出在 fenced `json` 契约块后追加 `openspec-citations` fenced 块(每行一个 JSON `{"path","section","line"}`);gate 解析并与生效 plan/review 节点的成功 read 事件核验,新增 `openspec-citation-block-unparseable` / `openspec-not-cited` / `openspec-citation-not-read` 三个 retryable-invalid failureCode(防捏造、防遗漏)
9
26
  - `src/shared/openspec-spec.ts` 新增 `extractTaskSourceOpenspecPaths` 纯函数(反引号内联、markdown 链接目标、裸路径 token → 归一化 → spec 过滤 → 治理子树排除 → 去重排序)
10
27
  - 生成期把 `openspecPolicy` 与候选来源元数据(`openspecCandidateSources.declared` / `taskSourceCited` / `scanStrict`)冻结进 prewrite gate 配置;`task advance` 的 advisories 说明当前模式与候选来源
11
28
 
29
+ ### 改进
30
+
31
+ - `frontend-plan-pi` 改为 runtime skeleton + editable RFC 7386 patch:`schemaVersion`、source binding、risk、writer target files 与生产 Mock 安全位由 DAG 冻结且不可被 plan/revision 覆盖;节点 validator 将补丁编译为 canonical 完整契约后再交给 review/prewrite,并在 `contracts/candidates/<nodeId>/` 保存每次 raw/extracted/normalized/validation 证据。任务提示与三类结构化重试(invalid-output / provider length 截断 / output-too-large)同步收敛为单一 patch + 唯一 `openspec-citations`;无法在声明文件中解析的可选 verification symbol 会被确定性移除并留下 normalization action,避免格式冲突、完整契约重复输出和说明性 symbol 延迟到 trace 才失败。
32
+ - Operator Chat 恢复工具对齐真实能力:系统提示改用 `dagReport` / `dagRerunPlan` / `dagRerun` / `standaloneTaskRerun` / `workerTaskRetry` 真名,不再写 `operator_*` 或把 `dagResume` 当成模型可执行项;`dagDoctor` / `dagStatus` / `workerPoolDoctor` 改为 typed `runId`/`taskId`,不再让模型猜 CLI `args[]`。ADR 0013 自定义 base prompt 上限由 1900 统一调整为 2500(2026-08-18,废止 0.8×2498 比例),预算用于写清节点续跑 / 整单重生 / Worker 重排队分流,以及多轮迭代恢复纪律(先一句话总结上一轮错误、修根因不修症状、禁止原样重交、同错两轮后停下报告)。
33
+ - Operator Chat 模型工具面收敛:`advanced` 分类的命令(workflow/loop/benchmark/knowledge-graph 等 8 个只读低频入口)不再进入模型工具白名单,模型注意力集中到高频工具;保留的 27 个 model-callable 官方 CLI 动作全部换上「做什么 + 何时用」语义描述,不再是无信息的 `Official CLI action coverage for ...` 模板;dispatcher 对官方尾部动作的识别从描述前缀匹配改为结构化 `OFFICIAL_TAIL_ACTION_IDS` 集合判断。审查收口:`knowledgeCurate` 纠正为 `mutation`/`advanced`(条件写盘,移出模型面,防经 read 通道传 `--output` 写盘);`promoteRun`/`closeoutTask` 描述去掉 dispatch 不保证的生命周期断言,如实标注为 task-advance 别名并提示先核 run/任务状态。工具清单渲染修复:promptSnippet 去掉重复工具名前缀(SDK 渲染自带 `- name:`,原实现名字出现两遍);`inspect`/`status`/`spineCheck`/`sourceInstructions` 四条占位描述换成语义描述;task-advance 家族(`newTask`/`dagRunTask`)标注与标准流 `taskAdvance` 的关系,避免模型在 5 个相似入口间选错。可调用性修复:`observeLink` 补上 4 个可选参数 schema(dagRunId/taskId/featureId/workerRunId,原空 schema + additionalProperties:false 导致模型传参必被拒、工具名存实亡);Pi 工具参数 schema 现在携带注册表里的参数描述(Type.String/Boolean/Number/Array 注入 `description`,原实现丢弃参数描述、模型只能看到参数名)。
34
+
35
+ - Operator Chat 对齐 pi-web:建会话不再传封闭 `tools` 白名单(避免 SDK 把扩展 MCP 工具滤出 registry),创建后 `bindExtensions` 发 `session_start`,再钉激活集。扩展注册的 MCP 工具可被模型调用;`apply_patch` / `full-tools` / `shell` / `coding-chat` 仍排除。
36
+ - 对话侧栏 Plugins 右侧新增只读 MCP 弹窗:展示 `mcp.json` 配置的服务,以及当前会话真正注册/激活的 MCP 工具;没有配置也没有工具时保持真实空态。
37
+ - 对话侧栏原 Packages 入口改名为 Plugins,与 pi-web / Pi 用户面一致;内部仍是 package inventory。
38
+ - 对话进行中底部上下文占用百分比保持常驻:token 用量事件不再冲掉上一帧的 contextUsage,并在回合中持续刷新。
39
+ - 上一轮大量 bash 结束后再发新消息,旧工具条不再整批挂到新气泡下面:无主工具不再丢进最新一组,历史按完整回合裁切,空助手气泡若还挂着工具会留下来当锚点。
40
+ - 侧栏文件树点击目录行直接插入 `@目录/` 引用(箭头单独负责展开/收起);输入框 @ 补全也列出目录,排在文件前面。
41
+ - 对话进行中点「停止」立即安静下来(乐观停止,对齐 pi-web):不再等 POST /abort 往返;后端 abort 等待从 8s 降到 2.5s,未终结时由状态对账收敛。
42
+ - 侧栏文件树实时刷新:agent 回合结束、浏览器标签页回到前台、手动点「刷新」时重拉根目录+全部已展开目录(此前手动刷新只拉根目录,子目录永远旧缓存)。
43
+ - Skills / Packages / Models 面板不再把 Pi 的 warning / collision 原文堆在列表上方:改成标题条一颗摘要芯片(如「14 条提示」),点开按「需要处理 / 名称不规范 / 同名未生效」分组,文案用自然语言,路径只显示末两段;名称不规范的技能在列表旁标「提示」,详情里说明「现在这样也能用」。加载失败仍单独成组,可点「再读一次」。
44
+ - Skills / Packages 双栏改为左右各自滚动;资源弹窗挂到 `document.body`,按顶部对话导航的实时高度在剩余视口居中,不再被 sticky 顶栏挡住上沿。
45
+
46
+ ### 修复
47
+
48
+ - Console 对话中的 Markdown 表格只约束横向滚动,不再截断纵向滚动链;鼠标或触控板停在表格上时仍可继续上下滚动对话。
49
+ - scenario-param 确定性 rewrite 的 Python 语法校验:Windows 优先 `python`,并把 Microsoft Store `python3` 空壳(非 0 且无语法诊断)视为解释器不可用后回退,避免误回滚合法 rewrite。
50
+ - `frontend-plan-revision-pi` 节点级校验与 merge-patch 输出协议矛盾修复:新增 schemaId `frontend-implementation-contract-revision-patch-v1` 与格式级宽松校验 `validateFrontendRevisionPatchNodeOutput`(至少一个 `openspec-citations` 块 + 剥离引用块后恰好一个 JSON 对象,不做完整契约 schema 校验),revision 节点 `structuredContractOutput.schemaId` 切换(plan-pi 仍为 v1);node-execution 的 invalid-output retry 指令按 schemaId 分支,patch 协议提示 merge-patch delta + 引用块,消除「prompt 要求 patch、retry 要求完整契约」的 retryable 死循环;gate 仍是唯一权威合并/校验点(零改动)。ADR 0015 补「节点级校验」决策 7
51
+ - `frontend-implementation-contract.schema.json` 的 `uiComponentChoices` items 补 `if/then` 决策语义(`specified` ⇒ `specReference` 必填非 null;`new` ⇒ `specReference` 为 null/缺失);`frontend-plan-render.ts` 空 `section` 不再渲染尾随 `#`;ADR 0016 增补「门禁边界」小节
52
+ - 前端 gate 内容驱动 revisionPatch 兼容 + verdict 归一化统一:旧 DAG(缺 `revisionPatch` 字段)但 revision 节点按协议输出 merge-patch 时,gate 按内容判定自动合并原始契约并走既有校验(不再因 `missing targets.files` 进入 retryable 死循环);patch 形态但无法合并(fallback 缺失/自身、原始节点不可读或非完整契约、apply 失败)→ `blocked` + 新增 `revision-patch-unmergeable`(错误信息含重新生成 DAG 指引);`normalizeVerdictCandidateLine` 增强 backtick 强调剥离,gate 的 `firstNonEmptyVerdictLine` 复用共享 normalize,runIf / output-protocol / gate 三处一致
53
+
12
54
  ### 文档
13
55
 
56
+ - 新增 `docs/decisions/0016-frontend-component-conformance.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-implementation-contract.schema.json`、`skills/frontend-implementation/references/design-spec.md`、`skills/frontend-implementation/references/node-contracts.md`、`skills/frontend-design-review/references/review-checklist.md`、`CONTEXT.md`
57
+ - 新增 `docs/decisions/0015-frontend-plan-json-only-contract.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-implementation-contract.schema.json`、`skills/frontend-implementation/references/node-contracts.md`
14
58
  - 新增 `docs/decisions/0014-openspec-gate-claim-verification.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-task-constraints.md`、`docs/templates/frontend-design-contract.md`、`skills/frontend-implementation/references/node-contracts.md`
15
59
 
60
+ ## [0.37.1-beta.0] - 2026-08-16
61
+
62
+ ### 重点更新
63
+
64
+ - backend-test live campaign R115–R141 收口:全新 R141 达到 readiness PASS、23/23 correspondence exact、146/146 Test Points、77/77 scenario MATCH、traceability PASS,并唯一执行 82 个真实 API pytest body
65
+ - 强化 backend-test correspondence、scenario-param、Markdown/pytest writer、collection repair、日志审计和 Windows pytest argfile 执行链,稳定区分 controller TestBug、generated TestBug 与真实 API ProductBug
66
+
67
+ ### 改进
68
+
69
+ - backend-test correspondence 只分析目标请求实际绑定的 body/payload,忽略 `scenario` 元数据与 setup-only payload;对 `scenario["body"]` 保留字段 shape,并把 intentional invalid enum 交由 scenario-param 门禁判断;同时识别下划线前缀 pytest 参数集合与带说明的 `Payload Contract: none(…)`,避免零 eligible 的 payload/parameter 误阻断
70
+ - backend-test correspondence 继续解析模块内 transport wrapper 的位置 payload 与 wrapper 构造 body,并清理 `Payload Enum` 值后的中文说明,避免 PUT/lifecycle 合法载荷误报;Markdown/pytest writer 同步禁止臆测响应 ID 类型,并在路径 ID 类型未声明时使用 create-delete 派生的合法 missing ID,避免把测试资产缺陷误分类为产品 400/404 缺陷
71
+ - backend-test correspondence 可还原 `_apply_patch(_base_body(...), patch)` 等 transport-body 组合表达式,并把 setup wrapper payload 限定为有 body 契约时的 fallback;traceability 接受经递归脱敏和有界输出的结构化 request/response 日志,scenario-param 支持带显式例值的 string wrong-type;pytest writer/repair 同步禁止把 path/query/control `id` 混入 DTO patch
72
+ - backend-test correspondence 识别模块级 `_post_case`/`_put_case` 参数工厂、factory TP 字面量与通过 `payload["field"] = value` 构造的 DTO shape,并避免空列表全局变量吞并后续参数集合;writer/repair 要求每个变体展开为直接 `pytest.param(..., id="TP-...")`,scenario readiness 不再依赖隐藏的动态参数行;traceability 允许窄 `urllib.error.URLError`/`httpx.RequestError` DELETE cleanup
73
+ - backend-test correspondence 继续还原 `payload = _base_note_payload()` 形式的 builder-return dataflow,并把 intentional invalid enum 交给 scenario-param 而非 Case 聚合 payload gate;Markdown review 明确 GET/DELETE setup DTO 不得改写目标 no-body contract,pytest writer/repair 禁止把返回 scalar 的 helper 当 tuple 解包
74
+ - backend-test collection repair 把 upstream 的 `Repair paths: testcase/...` 直接视为完整权威路径证据并读取/编辑该文件;禁止猜测根级 `contracts/**` 或 DAG run 目录,也禁止 read tool 已成功返回文件后反事实声称路径不存在并提前 blocked
75
+ - backend-test 的 REPAIRABLE collection repair 现在强制 non-empty diff,禁止 `already-satisfied` 制造假修复;Markdown review 增加 `### 测试点` 与三类 binding union 的 exact-set 校验并删除 legacy alias;no-body Case 的 payload correspondence 忽略同一 journey 的 direct setup POST/PUT DTO
76
+ - backend-test generated-local collection repair 从 HIGH 路由降为 MED,在保留 write guard 与 requireChangedFiles 的同时避开连续多轮 grok repair hallucinated blocked/changed-without-write,改由稳定 writer 路由执行有界单文件修复
77
+ - writer outcome 协议确定性归一化显式中文 `实现结果:已变更|已满足|已阻塞` 到 canonical `IMPLEMENTATION_OUTCOME`,仍拒绝无标签 prose、未知值和冲突值,不根据 diff 猜测语义
78
+ - scenario-param 识别 `"x" * _TITLE_MAX_LENGTH` 等长度表达式与 helper 常量名相乘形式
79
+ - correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免把 DTO 外 helper 字段误判为 PAYLOAD_SHAPE_MISMATCH
80
+ - scenario-param 解析 `bound=LEN100` 机器行、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,并把 `NOMINAL` 长度样本从 min/max 纠正为 nominal
81
+
82
+ - scenario-param 观测识别 `_OMIT`/omit sentinel 为合法 `missing`,跳过 `"2xx"`/`"400"` 期望码位置参数,解码 Python `\t`/`\n` 空白字面量,识别 `_TITLE_LEN1` 等长度 helper 名,支持 `custom-literal:whitespace-only|whitespace-padded` 语义标签,`enum-invalid` 接受具体非法字面量,并在 `min`/`LEN1` 意图下优先读取 TP 编码长度与 `minLength` 边界
83
+ - scenario-param 兼容 `empty` 意图下的 omit/`missing-key`,规范化 `custom-literal` 外包引号与可见空白符,并把 `max+1`/`min`/`max` 的裸数字位置参数按长度 token 比较(含 oversized 数字本身);`custom-literal:trim` 视为 padded whitespace 语义标签
84
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量,禁止 `filter-active` 类描述性伪值
85
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
86
+
16
87
  ## [0.36.4-beta.0] - 2026-08-16
17
88
 
18
89
  > 前端 DAG 规范上下文确定性落地(beta,发布到 `beta` dist-tag)。
@@ -407,6 +478,61 @@
407
478
  - 会话创建期间侧栏与 Landing 页共享 busy 状态反馈,抑制双击导致的重复创建
408
479
  - 删除正在初始化的会话不会被迟到的后台初始化任务复活,初始化失败统一返回 HTTP 503 错误码
409
480
 
481
+ ## [0.33.7-beta.2] - 2026-08-13
482
+
483
+ ### Fixed
484
+
485
+ - scenario-param 解析 `bound=LEN100`、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,识别 `"x" * _TITLE_MAX_LENGTH` 长度表达式
486
+ - correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免假 PAYLOAD_SHAPE_MISMATCH
487
+
488
+ ### Changed
489
+
490
+ - my-webapp 五轮 backend-test 全流程 live campaign 归档(R1–R5 均 finished;R5 correspondence PASS)
491
+
492
+ ## [0.33.7-beta.1] - 2026-08-12
493
+
494
+ ### Fixed
495
+
496
+ - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
497
+ - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量;max/min/max+1 应用长度表达式/helper
498
+
499
+ ### Changed
500
+
501
+ - 归档 R108–R110 live campaign 证据与 residual 收紧结果
502
+
503
+ ## [0.33.7-beta.0] - 2026-08-11
504
+
505
+ ### 重点更新
506
+
507
+ - backend-test 收口 source-fidelity P0:绑定 source coverage、有界 Markdown/pytest 同步、item-level eligibility 与单次 eligible 业务执行;my-webapp R107 16 节点 finished
508
+
509
+ ### 新增
510
+
511
+ - backend-test 新增绑定 source references 的来源覆盖事实、Markdown 一轮增量同步和 pytest correspondence 一轮有界修复,在业务测试体执行前保留 initial/effective/residual 证据
512
+ - backend-test 新增 item-level execution eligibility:只授权 payload-safe 的 pytest items,excluded items 与真实业务失败分开展示
513
+
514
+ ### 改进
515
+
516
+ - 将 backend-test Markdown/pytest map 的默认总 token 预算从 `600000` 提升到 `3000000`,降低多模块生成时过早触发 `dynamic map token budget exhausted` 的概率;仍保持预算耗尽 fail-closed
517
+ - Coverage Matrix 接受 `R400001` 等错误码 Rule Key,并保留单元格内 `ACTIVE|ARCHIVED` 未转义枚举管道符,避免合法行被拆成 10 列后整表丢弃
518
+ - scenario-param 按 TP 精确匹配多行 `场景意图`,保留 `ACTIVE` 大小写字面量,识别 `"a" * N` 长度表达式与空白 `empty`,request-level `nominal` 允许 `None` 查询过滤;避免把合法 payload 误判为 MISMATCH 并错误排除 eligible items
519
+ - backend-test 支持 evidence-only 元用例:Markdown 显式声明 `脚本/primary symbol=无` 且仅绑定 assertion/cross-cutting TP 时,不再强制 1:1 pytest 符号,也不再把其算作 MISSING_PYTEST/SCRIPT_MISMATCH
520
+ - generated pytest 内的 `ScopeMismatch`/setup fixture 失败若完全归因于 `testcase/**` 映射脚本,则归为 REPAIRABLE 并进入唯一一轮 collection repair,不再直接 BLOCKED
521
+ - scenario-param 观测跳过 `pytest.param("TP-...", value, id="TP-...")` 的前导 TP 标签,并支持长度意图用数字位置参数表达
522
+
523
+ ### 修复
524
+
525
+ - 修复 readiness 错误信息截断与无效 Coverage Matrix 状态语义,避免超长错误或 advisory 矩阵状态误阻断后续节点
526
+ - 修复 scenario 排除项未绑定 payload 字段、request-level 不确定性被误升级为硬失败的问题
527
+
528
+ ## [0.33.6-beta.0] - 2026-08-11
529
+
530
+ ### 修复
531
+
532
+ - 修复 backend-test correspondence scanner 将下一 Case 的分隔注释误归入前一个 pytest function region,造成虚假的 `MULTIPLE_PYTEST`、script mismatch 和额外 Test Point;现在按函数实际缩进边界隔离 body,并只从精确 `Case-ID:` 元数据或 primary symbol 绑定 Case
533
+ - 修复 backend-test 将 README 其他区域的 Markdown 链接误提取为业务模块、允许 `a401606` 等 opaque/hash-like stem 和异常多模块进入 map 的问题;现在只解析唯一 `## Module Index`,非法、mixed 或超过 8 个模块时 README-only 有界重试并由 manifest fail-closed
534
+ - 修复 backend-test map 总 token 预算耗尽后只生成部分 Markdown/pytest 资产但 parent 仍可能显示成功的问题;budget-blocked child 现在使 backend map 明确 ERROR,并保留 expected/finished/blocked/token 事实
535
+
410
536
  ## [0.33.6] - 2026-08-11
411
537
 
412
538
  ### 重点更新
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "version": "0.37.0",
4
- "gitSha": "4e502c40653f25c38302f1104e367d49bc950791",
5
- "builtAt": "2026-08-17T08:09:21.899Z"
4
+ "gitSha": "a2ca9f2fc2fa0e3eee17e28fd1ad187819d922b7",
5
+ "builtAt": "2026-08-19T15:13:16.805Z"
6
6
  }
@@ -938,6 +938,80 @@ function changedWorkspacePaths(before, after) {
938
938
  .filter((relativePath) => before[relativePath] !== after[relativePath])
939
939
  .sort();
940
940
  }
941
+ /**
942
+ * Generated-output prefixes treated as verification noise when git ignore
943
+ * rules are unavailable (non-git target). In a git repo the authoritative
944
+ * filter is `.gitignore` via `git check-ignore`; this list only covers the
945
+ * common case where a verification command such as `npm run build` writes
946
+ * untracked artifacts (build/dist/coverage/...) into the workspace.
947
+ */
948
+ const WORKSPACE_NOISE_PREFIXES = [
949
+ "node_modules/",
950
+ "build/",
951
+ "dist/",
952
+ "coverage/",
953
+ ".turbo/",
954
+ ".next/",
955
+ ".nuxt/",
956
+ ".cache/",
957
+ ".parcel-cache/",
958
+ ".vite/",
959
+ "out/",
960
+ "target/",
961
+ "__pycache__/",
962
+ ];
963
+ function isWorkspaceNoisePath(relative) {
964
+ if (relative === ".git" || relative.startsWith(".git/"))
965
+ return true;
966
+ if (relative === RUN_ROOT || relative.startsWith(`${RUN_ROOT}/`))
967
+ return true;
968
+ return WORKSPACE_NOISE_PREFIXES.some((prefix) => relative === prefix.slice(0, -1) || relative.startsWith(prefix));
969
+ }
970
+ /**
971
+ * Filters changed workspace paths down to meaningful (non-generated) changes
972
+ * so a verification command that legitimately writes git-ignored artifacts is
973
+ * not misjudged as an unauthorized workspace mutation. Git ignore rules are
974
+ * authoritative when the target is a git repo; otherwise the built-in
975
+ * generated-output prefixes apply.
976
+ */
977
+ async function filterWorkspaceNoise(repoRoot, relativePaths) {
978
+ const builtinFiltered = relativePaths.filter((relative) => !isWorkspaceNoisePath(relative));
979
+ if (builtinFiltered.length === 0)
980
+ return builtinFiltered;
981
+ const ignored = await gitIgnoredPaths(repoRoot, builtinFiltered);
982
+ if (ignored === null)
983
+ return builtinFiltered;
984
+ return builtinFiltered.filter((relative) => !ignored.has(relative));
985
+ }
986
+ /**
987
+ * Returns the subset of paths matched by git ignore rules, or null when the
988
+ * target is not a git repository (or git is unavailable), in which case the
989
+ * caller falls back to the built-in noise prefixes.
990
+ */
991
+ async function gitIgnoredPaths(repoRoot, relativePaths) {
992
+ return await new Promise((resolve) => {
993
+ execFile("git", ["-C", repoRoot, "check-ignore", ...relativePaths], {
994
+ timeout: 15_000,
995
+ maxBuffer: 4 * 1024 * 1024,
996
+ windowsHide: true,
997
+ }, (error, stdout) => {
998
+ if (error) {
999
+ // `git check-ignore` exits 1 when no path is ignored: that is a
1000
+ // successful lookup with an empty result set. Any other failure
1001
+ // (e.g. 128 outside a repo) means the git filter is unavailable.
1002
+ if (error.code === 1 && stdout.length === 0)
1003
+ resolve(new Set());
1004
+ else
1005
+ resolve(null);
1006
+ return;
1007
+ }
1008
+ resolve(new Set(stdout
1009
+ .split("\n")
1010
+ .filter(Boolean)
1011
+ .map((entry) => entry.split(path.sep).join("/"))));
1012
+ });
1013
+ });
1014
+ }
941
1015
  function isFrozenVerificationSurface(value) {
942
1016
  return (isRecord(value) &&
943
1017
  value.schemaVersion === 1 &&
@@ -1587,7 +1661,7 @@ async function validateContinueMerge(directory, state, anchor) {
1587
1661
  await assertSafeMergePath(state.repoRoot, allowedPath);
1588
1662
  }
1589
1663
  const current = await snapshotWorkspace(state.repoRoot);
1590
- const changed = changedWorkspacePaths(authoritativeGuard.baselineWorkspace, current);
1664
+ const rawChanged = changedWorkspacePaths(authoritativeGuard.baselineWorkspace, current);
1591
1665
  const pending = await readPendingAcceptance({
1592
1666
  directory,
1593
1667
  state,
@@ -1602,6 +1676,10 @@ async function validateContinueMerge(directory, state, anchor) {
1602
1676
  pending,
1603
1677
  })
1604
1678
  : undefined;
1679
+ // Only the out-of-bound drift check treats git-ignored/generated-output
1680
+ // changes as noise; allowed-path changes are merge content and must stay
1681
+ // intact even when the target is git-ignored (e.g. `.agents/skills/*`).
1682
+ const changed = await filterWorkspaceNoise(state.repoRoot, rawChanged);
1605
1683
  const unexpected = changed.filter((relativePath) => {
1606
1684
  if (allowed.has(relativePath))
1607
1685
  return false;
@@ -1611,7 +1689,7 @@ async function validateContinueMerge(directory, state, anchor) {
1611
1689
  if (unexpected.length > 0) {
1612
1690
  throw new Error(`init upgrade merge write-guard blocked out-of-bound workspace changes: ${unexpected.join(", ")}`);
1613
1691
  }
1614
- const changedAllowed = changed.filter((relativePath) => allowed.has(relativePath));
1692
+ const changedAllowed = rawChanged.filter((relativePath) => allowed.has(relativePath));
1615
1693
  if (changedAllowed.length === 0) {
1616
1694
  if (pending) {
1617
1695
  throw new Error("pending merge acceptance cannot authorize a merge without an allowed workspace change");
@@ -1834,16 +1912,8 @@ async function createVerificationWriteAudit(input) {
1834
1912
  const note = (scope, file) => {
1835
1913
  events.push(`${scope}=${file.split(path.sep).join("/")}`);
1836
1914
  };
1837
- const ignoredWorkspacePath = (candidate) => {
1838
- const relative = toRepoPath(canonicalRepoRoot, candidate);
1839
- return (relative === ".git" ||
1840
- relative.startsWith(".git/") ||
1841
- relative === "node_modules" ||
1842
- relative.startsWith("node_modules/") ||
1843
- relative === RUN_ROOT ||
1844
- relative.startsWith(`${RUN_ROOT}/`));
1845
- };
1846
- const install = async (scope, directory, ignore) => {
1915
+ const ignoredWorkspacePath = (candidate) => isWorkspaceNoisePath(toRepoPath(canonicalRepoRoot, candidate));
1916
+ const install = async (scope, directory, ignore, scopeRoot) => {
1847
1917
  const canonical = await realpath(directory);
1848
1918
  if (watched.has(canonical))
1849
1919
  return;
@@ -1867,11 +1937,11 @@ async function createVerificationWriteAudit(input) {
1867
1937
  // traversal opens an existing child directory. Nested watchers own real
1868
1938
  // mutations below that child; persistent changes are also snapshot-checked.
1869
1939
  if (!existingDirectory) {
1870
- note(scope, path.relative(directory, candidate) || ".");
1940
+ note(scope, path.relative(scopeRoot ?? directory, candidate) || ".");
1871
1941
  }
1872
1942
  // A parent rename can introduce a new directory. Installing its watcher
1873
1943
  // closes the delayed-descendant gap even when the container event is noise.
1874
- void install(scope, candidate, ignore).catch((error) => {
1944
+ void install(scope, candidate, ignore, scopeRoot).catch((error) => {
1875
1945
  const code = error.code;
1876
1946
  if (code !== "ENOENT" && code !== "ENOTDIR") {
1877
1947
  failures.push(`watcher refresh failed for ${scope}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1891,10 +1961,10 @@ async function createVerificationWriteAudit(input) {
1891
1961
  const child = path.join(canonical, entry.name);
1892
1962
  if (ignore?.(child) || !entry.isDirectory() || entry.isSymbolicLink())
1893
1963
  continue;
1894
- await install(scope, child, ignore);
1964
+ await install(scope, child, ignore, scopeRoot);
1895
1965
  }
1896
1966
  };
1897
- await install("workspace", canonicalRepoRoot, ignoredWorkspacePath);
1967
+ await install("workspace", canonicalRepoRoot, ignoredWorkspacePath, canonicalRepoRoot);
1898
1968
  await install("controller", runRoot(input.repoRoot));
1899
1969
  await install("controller-anchor", anchor.anchorRoot);
1900
1970
  const piHomeEntry = await lstat(piHome).catch((error) => {
@@ -2194,17 +2264,25 @@ async function runVerificationCommand(input) {
2194
2264
  if (audit.failures.length)
2195
2265
  throw new Error(audit.failures.join("; "));
2196
2266
  await assertFrozenVerificationSurface(repoRoot, surface);
2197
- const changedWorkspace = changedWorkspacePaths(workspaceBefore, await snapshotWorkspace(repoRoot));
2267
+ const changedWorkspace = await filterWorkspaceNoise(repoRoot, changedWorkspacePaths(workspaceBefore, await snapshotWorkspace(repoRoot)));
2198
2268
  const changedController = changedWorkspacePaths(controllerBefore, await snapshotDirectory(runRoot(repoRoot)));
2199
2269
  const changedAnchors = changedWorkspacePaths(anchorBefore, await controllerAnchorSnapshot(repoRoot, state));
2200
2270
  const changedPiHome = changedWorkspacePaths(piHomeBefore, await snapshotOptionalDirectory(path.join(os.homedir(), ".pi")));
2201
- if (descendantsObserved || audit.events.length || changedWorkspace.length || changedController.length || changedAnchors.length || changedPiHome.length) {
2271
+ // Workspace watcher events are repo-root relative (see scopeRoot in
2272
+ // createVerificationWriteAudit); drop git-ignored/generated-output noise
2273
+ // before deciding whether the subprocess wrote the workspace.
2274
+ const workspaceEvents = audit.events.filter((event) => event.startsWith("workspace="));
2275
+ const remainingEvents = [
2276
+ ...audit.events.filter((event) => !event.startsWith("workspace=")),
2277
+ ...(await filterWorkspaceNoise(repoRoot, workspaceEvents.map((event) => event.slice("workspace=".length)))).map((relative) => `workspace=${relative}`),
2278
+ ];
2279
+ if (descendantsObserved || remainingEvents.length || changedWorkspace.length || changedController.length || changedAnchors.length || changedPiHome.length) {
2202
2280
  return {
2203
2281
  ...result,
2204
2282
  ok: false,
2205
2283
  stderr: boundedOutput(`${result.stderr}\nverification write guard blocked changes: ${[
2206
2284
  ...(descendantsObserved ? ["process-tree=descendant"] : []),
2207
- ...(audit.events.length ? [`events=${audit.events.join(",")}`] : []),
2285
+ ...(remainingEvents.length ? [`events=${remainingEvents.join(",")}`] : []),
2208
2286
  ...(changedWorkspace.length ? [`workspace=${changedWorkspace.join(",")}`] : []),
2209
2287
  ...(changedController.length ? [`controller=${changedController.join(",")}`] : []),
2210
2288
  ...(changedAnchors.length ? [`controller-anchor=${changedAnchors.join(",")}`] : []),
@@ -10,6 +10,7 @@ import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation
10
10
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
11
11
  import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
12
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
13
+ import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
13
14
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
14
15
  /**
15
16
  * Writer classification for a length-stopped thinking-only attempt: the model
@@ -808,6 +809,13 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
808
809
  isBackendTestCompletenessRetryCandidate(input.task)) {
809
810
  try {
810
811
  const writerRole = backendTestWriterProgressRoleForTask(input.task.id);
812
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
813
+ ? {
814
+ testRoot: meta.spec.backendTestLayout.testRoot,
815
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
816
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
817
+ }
818
+ : undefined);
811
819
  const progress = isBackendTestShardChildTask(input.task)
812
820
  ? await assessBackendTestShardChildCompleteness({
813
821
  workspaceRoot: input.cwd,
@@ -815,12 +823,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
815
823
  writeSet: input.task.writeSet ?? [],
816
824
  })
817
825
  : isBackendTestMdPlanTask(input.task)
818
- ? await assessBackendTestMdPlanCompleteness(input.cwd)
826
+ ? await assessBackendTestMdPlanCompleteness(input.cwd, backendLayout)
819
827
  : isBackendTestPytestPlanTask(input.task)
820
- ? await assessBackendTestPytestPlanCompleteness(input.cwd)
828
+ ? await assessBackendTestPytestPlanCompleteness(input.cwd, backendLayout)
821
829
  : writerRole === "pytest-generate"
822
- ? await assessBackendTestPytestWriterCompleteness(input.cwd)
823
- : await assessBackendTestMdWriterCompleteness(input.cwd);
830
+ ? await assessBackendTestPytestWriterCompleteness(input.cwd, backendLayout)
831
+ : await assessBackendTestMdWriterCompleteness(input.cwd, backendLayout);
824
832
  await writeBackendTestWriterProgressArtifacts({
825
833
  runDir: meta.runDir,
826
834
  progress,
@@ -977,6 +985,15 @@ function parseWriterImplementationOutcome(text) {
977
985
  const lines = text.split(/\r?\n/);
978
986
  const candidates = [];
979
987
  for (const [lineIndex, line] of lines.entries()) {
988
+ const localizedOutcome = localizedWriterImplementationOutcome(line);
989
+ if (localizedOutcome) {
990
+ candidates.push({
991
+ lineIndex,
992
+ value: localizedOutcome,
993
+ outcome: localizedOutcome,
994
+ });
995
+ continue;
996
+ }
980
997
  const nextLine = lines[lineIndex + 1];
981
998
  const normalized = normalizeProtocolLine(line, WRITER_OUTCOME_PROTOCOL_LINE, nextLine);
982
999
  if (normalized === undefined)
@@ -1005,6 +1022,26 @@ function parseWriterImplementationOutcome(text) {
1005
1022
  candidates,
1006
1023
  };
1007
1024
  }
1025
+ function localizedWriterImplementationOutcome(line) {
1026
+ const normalized = normalizedProtocolValue(line);
1027
+ const match = /^(?:实现|实施)结果\s*[::]\s*(已变更|已更改|已修改|已满足|无需变更|已阻塞|阻塞)$/u.exec(normalized);
1028
+ if (!match)
1029
+ return undefined;
1030
+ switch (match[1]) {
1031
+ case "已变更":
1032
+ case "已更改":
1033
+ case "已修改":
1034
+ return "changed";
1035
+ case "已满足":
1036
+ case "无需变更":
1037
+ return "already-satisfied";
1038
+ case "已阻塞":
1039
+ case "阻塞":
1040
+ return "blocked";
1041
+ default:
1042
+ return undefined;
1043
+ }
1044
+ }
1008
1045
  function isWriterImplementationOutcome(value) {
1009
1046
  return (value === "changed" || value === "already-satisfied" || value === "blocked");
1010
1047
  }
@@ -1043,6 +1080,7 @@ export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
1043
1080
  sdkAttempted: result.sdkAttempted,
1044
1081
  tokensUsed: result.tokensUsed,
1045
1082
  parsedEvents: result.parsedEvents,
1083
+ stopReason: readWriterThinkingExhaustionEvidence(result).stopReason,
1046
1084
  };
1047
1085
  }
1048
1086
  function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {