@tea-agent/loop-agent 0.38.0 → 0.38.1

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 (117) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/commands/init.js +14 -2
  5. package/dist/executors/dag-pi-executor.js +1 -0
  6. package/dist/governance/manifest-types.js +2 -0
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +65 -8
  10. package/dist/task/config-types.js +21 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  13. package/dist/worker/console/chat/pi-runtime.js +155 -78
  14. package/dist/worker/console/chat/repo-walk.js +10 -1
  15. package/dist/worker/console/chat/routes.js +19 -8
  16. package/dist/worker/console/chat/semantic-activity.js +17 -5
  17. package/dist/worker/console/chat/turn-process.js +17 -30
  18. package/dist/worker/console/harness-pi-model-matrix.js +146 -0
  19. package/dist/worker/console/operator-actions.js +123 -0
  20. package/dist/worker/console/operator-user-error.js +19 -0
  21. package/dist/worker/console/pi-readiness.js +7 -1
  22. package/dist/worker/console/security.js +20 -1
  23. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-Hfsot6rP.js} +1 -1
  24. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-YGwEASin.js} +1 -1
  25. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-CmOM0F92.js} +1 -1
  26. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DDLuaHC5.js} +1 -1
  27. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-B0Um43W4.js} +1 -1
  28. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +1 -0
  29. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DvBN4JZc.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B1UVgW4E.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Cf0GgXzX.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-DbNA24sj.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-Bi_0Be06.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-C8pPGvRd.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BitQY8Z-.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-U67GCiEo.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-IoJwXPBA.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-YDZ_UHgE.js} +1 -1
  39. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +1 -0
  40. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +1 -0
  41. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-CUZgsiRC.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-COmCh4pT.js} +1 -1
  43. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-Ta6FSm68.js} +1 -1
  44. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-UdcR-VW9.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DybcO3Et.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-wErDFWLw.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-DkVHZWqz.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DdqWs7QN.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-C9dMVmDo.js} +1 -1
  50. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-B2T2WcBK.js} +1 -1
  51. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-Bt9rjDFq.js} +1 -1
  52. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-U8ic_tvi.js} +1 -1
  53. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BfQJdQoO.js} +1 -1
  54. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CpMftH4O.js} +1 -1
  55. package/dist/worker/console/static/assets/index-BA5-N5ER.css +1 -0
  56. package/dist/worker/console/static/assets/index-DKPXBrdc.js +325 -0
  57. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-DzqWXNO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js} +1 -1
  59. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-ChPtKFLz.js} +1 -1
  60. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-PRe7wB5u.js} +1 -1
  61. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DpKX6QNp.js} +1 -1
  62. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-Dnl76b7Y.js} +5 -5
  63. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-S9PCXc3J.js} +1 -1
  64. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Du8e-FSX.js} +1 -1
  65. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CY6OmHTc.js} +1 -1
  66. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-C65GPhpj.js} +1 -1
  67. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-U6IUiTCL.js} +1 -1
  68. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D-I_9Qhi.js} +1 -1
  69. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-B57Fhed0.js} +1 -1
  70. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CJOtiw21.js} +1 -1
  71. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-FyX8zxSh.js} +1 -1
  72. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-eF-pbs9t.js} +1 -1
  73. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +1 -0
  74. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-CcfkA-Dq.js} +2 -2
  75. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +8 -0
  76. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DH8FjnOK.js} +1 -1
  77. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-beoTagJg.js} +1 -1
  78. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CQSfw4cK.js} +1 -1
  79. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-CQgzQ6jn.js} +1 -1
  80. package/dist/worker/console/static/index.html +2 -2
  81. package/dist/worker/console/static-src/app/console-types.js +51 -0
  82. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
  83. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  84. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  85. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  86. package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
  87. package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
  88. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  89. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  90. package/dist/workflows/dag/failure-routing.js +9 -4
  91. package/dist/workflows/dag/frontend-implementation-contract.js +158 -0
  92. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  93. package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
  94. package/dist/workflows/dag/init-hybrid.js +272 -27
  95. package/dist/workflows/dag/lifecycle.js +4 -0
  96. package/dist/workflows/dag/node-execution.js +51 -9
  97. package/dist/workflows/dag/report.js +6 -0
  98. package/dist/workflows/dag/retry-policy.js +15 -0
  99. package/dist/workflows/dag/types.js +50 -4
  100. package/dist/workflows/dag/validate.js +7 -2
  101. package/docs/templates/frontend-design-contract.md +40 -14
  102. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  103. package/docs/templates/frontend-task-constraints.md +40 -13
  104. package/docs/templates/frontend-task-requirement.md +54 -20
  105. package/docs/templates/harness.schema.json +8 -0
  106. package/harness.json +11 -2
  107. package/package.json +1 -1
  108. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  109. package/skills/frontend-implementation/references/design-spec.md +16 -19
  110. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  111. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  114. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  115. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  116. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 改进
6
+
7
+ - 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 重排队分流,以及多轮迭代恢复纪律(先一句话总结上一轮错误、修根因不修症状、禁止原样重交、同错两轮后停下报告)。
8
+
9
+ - Operator Chat 对齐 pi-web:建会话不再传封闭 `tools` 白名单(避免 SDK 把扩展 MCP 工具滤出 registry),创建后 `bindExtensions` 发 `session_start`,再钉激活集。扩展注册的 MCP 工具可被模型调用;`apply_patch` / `full-tools` / `shell` / `coding-chat` 仍排除。
10
+ - 对话侧栏 Plugins 右侧新增只读 MCP 弹窗:展示 `mcp.json` 配置的服务,以及当前会话真正注册/激活的 MCP 工具;没有配置也没有工具时保持真实空态。
11
+ - 对话侧栏原 Packages 入口改名为 Plugins,与 pi-web / Pi 用户面一致;内部仍是 package inventory。
12
+ - 对话进行中底部上下文占用百分比保持常驻:token 用量事件不再冲掉上一帧的 contextUsage,并在回合中持续刷新。
13
+ - 上一轮大量 bash 结束后再发新消息,旧工具条不再整批挂到新气泡下面:无主工具不再丢进最新一组,历史按完整回合裁切,空助手气泡若还挂着工具会留下来当锚点。
14
+ - 侧栏文件树点击目录行直接插入 `@目录/` 引用(箭头单独负责展开/收起);输入框 @ 补全也列出目录,排在文件前面。
15
+ - 对话进行中点「停止」立即安静下来(乐观停止,对齐 pi-web):不再等 POST /abort 往返;后端 abort 等待从 8s 降到 2.5s,未终结时由状态对账收敛。
16
+ - 侧栏文件树实时刷新:agent 回合结束、浏览器标签页回到前台、手动点「刷新」时重拉根目录+全部已展开目录(此前手动刷新只拉根目录,子目录永远旧缓存)。
17
+ - Skills / Packages / Models 面板不再把 Pi 的 warning / collision 原文堆在列表上方:改成标题条一颗摘要芯片(如「14 条提示」),点开按「需要处理 / 名称不规范 / 同名未生效」分组,文案用自然语言,路径只显示末两段;名称不规范的技能在列表旁标「提示」,详情里说明「现在这样也能用」。加载失败仍单独成组,可点「再读一次」。
18
+ - Skills / Packages 双栏改为左右各自滚动;资源弹窗挂到 `document.body`,按顶部对话导航的实时高度在剩余视口居中,不再被 sticky 顶栏挡住上沿。
19
+
20
+ ### 新增
21
+
22
+ - 前端 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 调用)
23
+ - 契约 schema 新增 5 个可选字段(`implementationSteps` / `stylingStrategy` / `dependencyPolicy` / `residualRisks` / `realIntegrationGap`),`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
24
+ - revision 输出改为 RFC 7386 merge-patch delta + 引用块:gate 配置新增 `revisionPatch`,生效时在 `planFallbackFromNodeIds[0]` 原始契约上应用 `applyFrontendImplementationContractPatch` 后进入既有分析/规范化/物化管线;旧 DAG 缺字段按整契约处理
25
+ - canonical contract 序列化(UTF-8)超过 64KB → `retryable-invalid` + 新增 `contract-too-large` failureCode(体积守卫兜底)
26
+ - `design-review` / `final-design-review` 提示改为消费契约 JSON(注入契约字段摘要),不再要求读取 plan prose
27
+ - 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` 给出可恢复提示)
28
+ - `GET /api/operator/v1/readiness` 的 probe 新增 `piModelMatrix` 结构化投影(向后兼容,不改动既有 `modelsMessage`)
29
+ - 组件选型规范合规(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` 同步
30
+ - `frontend-prewrite-gate` 新增三个 `retryable-invalid` failureCode:`component-spec-reference-invalid`(specReference.path 非受支持规范路径)、`component-spec-not-cited`(未在 openspec-citations 引用块引用或未真实读取)、`component-choices-missing`(组件/主题候选桶非空且契约有 UI 可见工作而 `uiComponentChoices` 缺失/空)
31
+ - gate 配置新增可选 `componentSpecCandidatePaths`(旧 DAG 缺字段兼容);`init-hybrid` 从 `designEvidence.classified.component ∪ theme ∪ rule.components` 冻结注入,capability context 强调组件/主题桶语义,plan/revision/design-review/final-design-review 提示加入硬规则与 BAD/GOOD 示例
32
+ - `frontend-plan.md` 新增 `Component Selection (spec conformance)` 章节(纯函数确定性渲染,缺省 `_(not specified)_`)
33
+
34
+ - `frontend-prewrite-gate` 的 OpenSpec 读取校验支持 `task.json.frontendOpenspec` 配置:`policy` 缺省 `cited`(候选 = `requiredReadPaths` ∪ 任务源显式引用的 openspec 路径),`scan-strict` 保留止血任务后的全量必读语义;`requiredReadPaths` 中非 openspec 支持路径在生成期确定性失败
35
+ - 确定性引用块协议:生效 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(防捏造、防遗漏)
36
+ - `src/shared/openspec-spec.ts` 新增 `extractTaskSourceOpenspecPaths` 纯函数(反引号内联、markdown 链接目标、裸路径 token → 归一化 → spec 过滤 → 治理子树排除 → 去重排序)
37
+ - 生成期把 `openspecPolicy` 与候选来源元数据(`openspecCandidateSources.declared` / `taskSourceCited` / `scanStrict`)冻结进 prewrite gate 配置;`task advance` 的 advisories 说明当前模式与候选来源
38
+
39
+ ### 修复
40
+
41
+ - `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
42
+ - `frontend-implementation-contract.schema.json` 的 `uiComponentChoices` items 补 `if/then` 决策语义(`specified` ⇒ `specReference` 必填非 null;`new` ⇒ `specReference` 为 null/缺失);`frontend-plan-render.ts` 空 `section` 不再渲染尾随 `#`;ADR 0016 增补「门禁边界」小节
43
+
44
+ ### 文档
45
+
46
+ - 新增 `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`
47
+ - 新增 `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`
48
+ - 新增 `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`
49
+
50
+ ## [0.36.4-beta.0] - 2026-08-16
51
+
52
+ > 前端 DAG 规范上下文确定性落地(beta,发布到 `beta` dist-tag)。
53
+
54
+ ### 新增
55
+
56
+ - `discoverFrontendProjectCapability` 产出 `designEvidence.classified` 语义归类:按通用目录约定(`openspec/schemas/**`、`openspec/project-specs/templates/**`、`openspec/project-specs/rules/**`、`openspec/project-specs/ui/**`、`ai_workspace/**`)把规范路径分入稳定键 `schemas` / `codeTemplate` / `rule.*` / `theme` / `component` / `uiOther` / `advisoryOther`,并进入 zod schema 校验(分区恒等式:归类值集合 == `normativePaths` 集合),重复探测稳定,不硬编码具体项目文件名
57
+ - 前端 contract/scout/plan/design-review 节点的 `subtask_prompt` 注入归类后的规范路径(含角色语义)并明确要求报告适用规则、命中路径/章节/行号、冲突与缺失
58
+
59
+ ### 修复
60
+
61
+ - `frontend-prewrite-gate` 在 `openspecCandidatePaths` 非空时强制生效 plan/review 节点实际读取候选路径:未读 → `retryable-invalid` + 新增 `openspec-not-read` failureCode,错误信息列出未读路径;空候选(greenfield)不阻断。此前读取证据仅记录不阻断(advisory),本次真正 fail-closed
62
+ - OpenSpec 候选发现止血:默认排除治理子树(`harness.json.governanceRoot` → 缺省 `ai_workspace/loop-agent`),移除固定 `slice(0, 40)` 截断,`openspec/**` 排序优先于 `ai_workspace/**`,仅保留 ≥200 的显式安全上限(命中时写入 `DagSpec.advisories`,非静默丢弃);`openspec-not-read` 失败原因增加实际读取路径/候选总数/排除统计与「重新生成 DAG」归因,生成后已删除/改名的候选改为 `candidate-missing-drift` 诊断而非误报「未读」
63
+
64
+ ### 文档
65
+
66
+ - `docs/templates/frontend-task-constraints.md` 把裸 `TODO` 改写为「探索协议 + 结果 schema + 缺失/冲突 fail-closed」,不再出现具体业务组件名或项目专属路径/命令
67
+ - `docs/templates/frontend-task-requirement.md` 与 `docs/templates/frontend-design-contract.md` 把裸 `TODO` 改写为「编写协议」三要素(要写什么 / DAG 如何消费 / 缺失/冲突时 fail-closed),并新增 `test/frontend-template-no-todo.test.ts` 扫描三个前端模板断言无裸 `TODO`
68
+ - `docs/runtime/frontend-implementation-workflow.md` 的项目规范归类清单改为通用目录约定 + 归类规则,删除写死的示例项目文件名
69
+
70
+ ## [0.36.3-beta.0] - 2026-08-16
71
+
72
+ > 前端 DAG Mock 治理收紧(beta,发布到 `beta` dist-tag)。
73
+
74
+ ### 改进
75
+
76
+ - 前端 DAG 的 Mock 策略约束注入设计评审链:`mockContextBlock`(含冻结 allowlist 的「HARD CONSTRAINT」与修复路径)现在也注入 `frontend-design-review-pi` / `frontend-plan-revision-pi` / `frontend-final-design-review-pi`,消除「design-review 按项目规范要求 native、但生成期 allowlist 冻结为 not-needed、prewrite 硬阻断」的策略断环——评审链会明确提示这是生成期契约缺口(需声明 `frontendMock.verifyCommands` 或 `policy: "required"` 后重新生成 DAG)
77
+ - 前端任务生成期 Mock 预警:新增 `DagSpec.advisories`,任务源提到接口/API/Mock 但 auto 模式把策略收窄为 `not-needed` 时,`task advance` 的 `warnings` 会给出可操作提示(声明 `frontendMock.verifyCommands` 或 `policy: required` 后重新生成 DAG),把契约缺口提前到生成期暴露
78
+
5
79
  ### 新增
6
80
 
7
81
  - 版本发布改为三渠道:main 自动发布 npm `next`(`x.y.0-next.N`)、release 分支自动发布正式 patch 到 `release-x.y`、`latest` 仅由人工 GitHub Actions promotion 移动(支持 dry-run 与 CAS 校验)
@@ -49,6 +123,27 @@
49
123
  - move interrupt eligibility to observability to honor kernel import boundary
50
124
  - retry semantic intake across Pi tiers with 300s timeout
51
125
 
126
+ ## [0.36.1-beta.0] - 2026-08-16
127
+
128
+ > 标准 DAG planner 输出超长紧凑重试(beta,发布到 `beta` dist-tag)。
129
+
130
+ ### 改进
131
+
132
+ - 标准 DAG 的 planner 节点(`contract-pi` / `plan-pi`)输出超长(`output-too-large`)时自动紧凑重试:新增 `PLANNER_OUTPUT_LIMIT_RETRY_POLICY`(默认重试集合 + `output-too-large`),紧凑重试指令按节点类型区分——结构化契约节点只返回单个紧凑 JSON,普通计划节点返回最小计划(有序步骤、writeSet 边界、验证命令),并丢弃上游原文引用与长证据摘录
133
+
134
+ ## [0.35.4-beta.0] - 2026-08-15
135
+
136
+ > frontend prewrite 阻断误路由修正 + Mock 契约文档落地(beta,发布到 `beta` dist-tag)。
137
+
138
+ ### 改进
139
+
140
+ - prewrite 阻断导致的 writer `SKIPPED`(`skippedReason=frontend-prewrite-not-authorized`)失败路由从 `DependencyFailure`/`unblock dependency` 修正为 `ContractMismatch`/`fix-frontend-task-contract-and-regenerate-dag`;`dag report` / `dag doctor` 透传 `skippedReason`,对「Mock 策略契约缺陷需改 task.json + 重新生成 DAG」这类场景给出可操作建议,而非误导性的「解除依赖」
141
+
142
+ ### 文档
143
+
144
+ - `docs/templates/frontend-task-constraints.md` 的 Mock 约束从 TODO 改为可落地契约:要求从项目**探索**实际存在的确定性 Mock 验证命令(不硬编码命令名)并声明到 `task.json.frontendMock.verifyCommands`,说明生成期 `allowedMockStrategies` 来源、`mock-strategy-outside-allowed` 是生成期契约问题(修复 = 补契约 + 重新生成 DAG,plan-revision 无法修复)
145
+ - `docs/runtime/frontend-implementation-workflow.md` 的 task.json 输入标准补充 Mock 契约声明、命令探索与失败恢复指引
146
+
52
147
  ## [0.36.0] - 2026-08-15
53
148
 
54
149
  ### 新增
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.38.0",
4
- "gitSha": "cc01c2ec09b56080f4c32416bddcc911feaa0e7a",
5
- "builtAt": "2026-08-17T01:42:05.218Z"
3
+ "version": "0.38.1",
4
+ "gitSha": "0b7518c6f009d1fcf8776f757775bb447fd006ee",
5
+ "builtAt": "2026-08-18T22:33:47.281Z"
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(",")}`] : []),
@@ -1362,7 +1362,13 @@ async function readExistingSurfaceState(repoRoot) {
1362
1362
  }
1363
1363
  /** The IDE schema reference remains a structural update signal. */
1364
1364
  const HARNESS_STRUCTURAL_VALUE_KEYS = new Set(["$schema"]);
1365
- const PI_MODEL_ROUTING_KEYS = new Set(["LOW", "MED", "HIGH", "defaultModel"]);
1365
+ const PI_MODEL_ROUTING_KEYS = new Set([
1366
+ "LOW",
1367
+ "MED",
1368
+ "HIGH",
1369
+ "defaultModel",
1370
+ "modelOptions",
1371
+ ]);
1366
1372
  function isPiModelRoutingPath(path) {
1367
1373
  return path.length === 2 && path[0] === "executors" && path[1] === "pi";
1368
1374
  }
@@ -1727,7 +1733,13 @@ function assessHarnessModelMigration(harness) {
1727
1733
  const hasDefaultModel = "defaultModel" in pi;
1728
1734
  const hasTier = PI_MODEL_TIERS.some((tier) => tier in pi);
1729
1735
  if (["requiresApiKey", "provider", "model"].some((field) => field in pi) ||
1730
- Object.keys(pi).some((field) => !["description", "enabled", "defaultModel", ...PI_MODEL_TIERS].includes(field))) {
1736
+ Object.keys(pi).some((field) => ![
1737
+ "description",
1738
+ "enabled",
1739
+ "defaultModel",
1740
+ "modelOptions",
1741
+ ...PI_MODEL_TIERS,
1742
+ ].includes(field))) {
1731
1743
  return {
1732
1744
  kind: "ambiguous",
1733
1745
  reason: "executors.pi contains unknown or deprecated model semantics",
@@ -1043,6 +1043,7 @@ export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
1043
1043
  sdkAttempted: result.sdkAttempted,
1044
1044
  tokensUsed: result.tokensUsed,
1045
1045
  parsedEvents: result.parsedEvents,
1046
+ stopReason: readWriterThinkingExhaustionEvidence(result).stopReason,
1046
1047
  };
1047
1048
  }
1048
1049
  function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
@@ -78,6 +78,8 @@ export const executorManifestSchema = z
78
78
  LOW: executorTierModelSchema.optional(),
79
79
  MED: executorTierModelSchema.optional(),
80
80
  HIGH: executorTierModelSchema.optional(),
81
+ /** Console 模型矩阵下拉专用列表;仅供 UI 使用,不参与 DAG 模型路由。 */
82
+ modelOptions: z.array(z.string().min(1)).optional(),
81
83
  })
82
84
  .strict();
83
85
  export const workflowPolicyProfileNameSchema = z.enum([
@@ -1,13 +1,14 @@
1
1
  import { copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { assertHarnessWriteAllowed, } from "./completed-facts-guard.js";
4
+ let atomicWriteSequence = 0;
4
5
  async function writeAtomic(targetPath, content, options) {
5
6
  assertHarnessWriteAllowed(targetPath, options);
6
7
  const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
7
8
  ? path.resolve(options.repoRoot, targetPath)
8
9
  : path.resolve(targetPath);
9
10
  await mkdir(path.dirname(resolved), { recursive: true });
10
- const tempPath = `${resolved}.${process.pid}.tmp`;
11
+ const tempPath = `${resolved}.${process.pid}.${(atomicWriteSequence += 1)}.tmp`;
11
12
  try {
12
13
  await writeFile(tempPath, content, "utf-8");
13
14
  try {
@@ -14,6 +14,30 @@ export const OPENSPEC_SPEC_DIRS = [
14
14
  ];
15
15
  /** Allowed spec file extensions (case-insensitive). */
16
16
  export const OPENSPEC_SPEC_EXT_RE = /\.(md|mdx|json|yaml|yml)$/i;
17
+ /**
18
+ * Default governance subtree excluded from candidate discovery. Aligned with
19
+ * init.ts's DEFAULT_GOVERNANCE_ROOT; discovery derives the real root from
20
+ * harness.json.governanceRoot and falls back to this value.
21
+ */
22
+ export const DEFAULT_OPENSPEC_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
23
+ /**
24
+ * Explicit safety ceiling for auto-discovered openspec candidates (>=200).
25
+ * Reaching the cap is reported via DagSpec.advisories, never silently dropped.
26
+ */
27
+ export const MAX_OPENSPEC_CANDIDATE_PATHS = 200;
28
+ /**
29
+ * Returns `true` when `filePath` is the governance subtree itself or any path
30
+ * under it. `filePath` must be a repo-relative POSIX path (backslashes are
31
+ * normalised internally). Comparison is case-sensitive, matching the other
32
+ * predicates in this module.
33
+ */
34
+ export function isGovernanceSubtreePath(filePath, governanceRoot) {
35
+ const normalized = filePath.replaceAll("\\", "/");
36
+ const root = governanceRoot.replaceAll("\\", "/").replace(/\/+$/, "");
37
+ if (!root)
38
+ return false;
39
+ return normalized === root || normalized.startsWith(root + "/");
40
+ }
17
41
  /**
18
42
  * Returns `true` when `filePath` is inside one of the canonical frontend spec
19
43
  * directories. `filePath` must be a repo-relative POSIX path (backslashes are
@@ -47,3 +71,55 @@ export function isOpenspecSpecSearchTarget(value) {
47
71
  const normalized = value.replaceAll("\\", "/");
48
72
  return /(?:^|[^A-Za-z0-9_.-])(?:openspec\/(?:schemas|project-specs)|ai_workspace)(?:\/|$)/.test(normalized);
49
73
  }
74
+ /** Normalizes a free-form path token to a repo-relative POSIX path or null. */
75
+ function normalizeCitationToken(raw) {
76
+ let token = raw.trim();
77
+ token = token.replace(/^[<"'`(\[\s]+|[>"'`)\]\s]+$/g, "");
78
+ // Strip trailing punctuation that is not part of a filename.
79
+ token = token.replace(/[),.;:!?]+$/g, "");
80
+ const hashIndex = token.indexOf("#");
81
+ if (hashIndex >= 0)
82
+ token = token.slice(0, hashIndex);
83
+ token = token.replace(/\\/g, "/");
84
+ if (!token)
85
+ return null;
86
+ return token;
87
+ }
88
+ /**
89
+ * Deterministically extracts repo-relative openspec spec paths explicitly cited
90
+ * in task-source markdown (requirement + execution constraint). Pure function,
91
+ * no IO.
92
+ *
93
+ * Scanned sources, in order:
94
+ * - backtick inline code spans (`path`)
95
+ * - markdown link targets ([text](path))
96
+ * - bare path tokens under openspec/ or ai_workspace/
97
+ *
98
+ * Every token is normalized to POSIX, stripped of a trailing `#fragment`,
99
+ * filtered through `isOpenspecSpecFilePath`, and excluded when it falls inside
100
+ * the governance subtree (matching `listOpenspecDiscovery`'s exclusion口径).
101
+ * Results are deduped and sorted.
102
+ */
103
+ export function extractTaskSourceOpenspecPaths(markdown, governanceRoot = DEFAULT_OPENSPEC_GOVERNANCE_ROOT) {
104
+ if (!markdown)
105
+ return [];
106
+ const raw = new Set();
107
+ const collect = (token) => {
108
+ const normalized = normalizeCitationToken(token);
109
+ if (normalized)
110
+ raw.add(normalized);
111
+ };
112
+ for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
113
+ collect(match[1] ?? "");
114
+ }
115
+ for (const match of markdown.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) {
116
+ collect(match[1] ?? "");
117
+ }
118
+ for (const match of markdown.matchAll(/(?:^|[\s"'([>])((?:openspec|ai_workspace)\/[A-Za-z0-9._/-]+)/g)) {
119
+ collect(match[1] ?? "");
120
+ }
121
+ return [...raw]
122
+ .filter((token) => isOpenspecSpecFilePath(token) &&
123
+ !isGovernanceSubtreePath(token, governanceRoot))
124
+ .sort();
125
+ }
@@ -63,8 +63,6 @@ const officialAction = (action, cli, kind, coverage) => ({
63
63
  });
64
64
  const OFFICIAL_ACTIONS = [
65
65
  ...[
66
- ["dagDoctor", "loop-agent dag doctor", "read", "model-callable"],
67
- ["dagStatus", "loop-agent dag status", "read", "model-callable"],
68
66
  [
69
67
  "dagDecisionInspect",
70
68
  "loop-agent dag decision inspect",
@@ -284,7 +282,6 @@ const OFFICIAL_ACTIONS = [
284
282
  "long-running",
285
283
  "advanced",
286
284
  ],
287
- ["workerPoolDoctor", "agent-worker pool doctor", "read", "model-callable"],
288
285
  [
289
286
  "workerPoolMarkFailed",
290
287
  "agent-worker pool mark-failed",
@@ -1079,7 +1076,7 @@ export function buildOperatorCapabilitiesDocument() {
1079
1076
  "CONTROLLER_MISMATCH",
1080
1077
  "INVALID_INPUT",
1081
1078
  ],
1082
- description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor/operationWait.",
1079
+ description: "Consume a single-use executionId from prepareDagExecution and execute the reviewed DAG. Do not pass a DAG path and do not bash `task advance --approve-gate`. accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor/operationWait.",
1083
1080
  inputParams: [
1084
1081
  {
1085
1082
  name: "executionId",
@@ -1102,7 +1099,7 @@ export function buildOperatorCapabilitiesDocument() {
1102
1099
  resultSchemaVersion: 1,
1103
1100
  envelopeSchemaVersion: 1,
1104
1101
  requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1105
- description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint, unsafe shell, worker-managed, workspace/controller drift), do not force execute — Worker-owned runs use workerTaskRetry; standalone runs use standaloneTaskRerun or same-task advance after fixing the root cause (contract changes must be adopted first). Returns planHash required by execute.",
1102
+ description: "Read-only check: can this failed run continue from fromNode? Call after dagReport; pass primaryFailure.nodeId. Does not execute. eligible=true returns planHash for dagRerun. eligible=false: Worker/Task Pool Failed workerTaskRetry; standalone Console run standaloneTaskRerun after any required contract adopt. Do not call dagRerun or invent a new task.",
1106
1103
  inputParams: [
1107
1104
  {
1108
1105
  name: "runId",
@@ -1132,7 +1129,7 @@ export function buildOperatorCapabilitiesDocument() {
1132
1129
  "BINDING_DRIFT",
1133
1130
  "INVALID_INPUT",
1134
1131
  ],
1135
- description: "Execute R1 continuation from effective node. Requires planHash from a fresh dagRerunPlan (eligible=true). Prefer this over new task / standaloneTaskRerun for provider flake and safe downstream failures. Server enforces planHash/binding/fingerprint freshness.",
1132
+ description: "Continue the same DAG from fromNode. Requires a fresh dagRerunPlan with eligible=true and that exact planHash. Prefer this for provider flake / safe downstream failures. Not a full task regenerate (that is standaloneTaskRerun). Server rejects stale planHash or binding drift.",
1136
1133
  inputParams: [
1137
1134
  {
1138
1135
  name: "runId",
@@ -1498,7 +1495,7 @@ export function buildOperatorCapabilitiesDocument() {
1498
1495
  "INVALID_INPUT",
1499
1496
  "HUMAN_CONFIRMATION_REQUIRED",
1500
1497
  ],
1501
- description: "Full standalone task regenerate validate execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting. Regeneration reuses the CURRENT contract snapshot: if the failure requires contract changes (e.g. allowing a catalog index path), first update the contract (re-import PRD, or adopt externally-edited sources via `task contract adopt`) BEFORE calling this action, otherwise the regenerated DAG repeats the same failure.",
1498
+ description: "Regenerate the whole standalone DAG from the CURRENT contract, then validate and execute a new run (parent lineage kept). This is NOT continue-from-failed-node that is dagRerun. Server rejects the call when dagRerunPlan is eligible, when primaryRecovery.humanRequired=true, or when run facts do not recommend rerun-task. Adopt contract changes before calling or the new DAG repeats the same failure.",
1502
1499
  inputParams: [
1503
1500
  {
1504
1501
  name: "runId",
@@ -1542,7 +1539,7 @@ export function buildOperatorCapabilitiesDocument() {
1542
1539
  "INVALID_INPUT",
1543
1540
  "HUMAN_CONFIRMATION_REQUIRED",
1544
1541
  ],
1545
- description: "Requeue failed Task Pool task (Failed → Ready) via in-package pool store. Only when read-only validation confirms the task exists in Failed state; the server checks this before accepting.",
1542
+ description: "Requeue a Worker Task Pool task Failed → Ready. Not a DAG node rerun (use dagRerun) and not a standalone DAG regenerate (use standaloneTaskRerun). Confirm with workerPoolDoctor first; server accepts only Failed pool tasks. Pass featureId when known it must match the pool record.",
1546
1543
  inputParams: [
1547
1544
  {
1548
1545
  name: "taskId",
@@ -1568,6 +1565,66 @@ export function buildOperatorCapabilitiesDocument() {
1568
1565
  modelCallable: "always",
1569
1566
  humanConfirmation: "none",
1570
1567
  },
1568
+ {
1569
+ action: "dagDoctor",
1570
+ cli: "loop-agent dag doctor [--run-id <id>] --json",
1571
+ kind: "read",
1572
+ inputSchemaVersion: 1,
1573
+ resultSchemaVersion: 1,
1574
+ envelopeSchemaVersion: 1,
1575
+ requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
1576
+ description: "Diagnose DAG run health. Omit runId for a repo-wide inventory (active/paused/completed + health issues). Pass runId for one run's liveness, heartbeat, failure node, and recovery hints. Use this to supervise stalls; it does not mutate the run.",
1577
+ inputParams: [
1578
+ {
1579
+ name: "runId",
1580
+ type: "string",
1581
+ required: false,
1582
+ description: "optional dag run id; omit to inventory all runs, pass to diagnose one run",
1583
+ },
1584
+ ],
1585
+ modelCallable: "always",
1586
+ humanConfirmation: "none",
1587
+ },
1588
+ {
1589
+ action: "dagStatus",
1590
+ cli: "loop-agent dag status --run-id <id>",
1591
+ kind: "read",
1592
+ inputSchemaVersion: 1,
1593
+ resultSchemaVersion: 1,
1594
+ envelopeSchemaVersion: 1,
1595
+ requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
1596
+ description: "Read one DAG run's live status, current node, and liveness. Requires runId. Complements dagReport (terminal handoff) and dagDoctor (health diagnosis). Does not mutate the run.",
1597
+ inputParams: [
1598
+ {
1599
+ name: "runId",
1600
+ type: "string",
1601
+ required: true,
1602
+ description: "dag run id",
1603
+ },
1604
+ ],
1605
+ modelCallable: "always",
1606
+ humanConfirmation: "none",
1607
+ },
1608
+ {
1609
+ action: "workerPoolDoctor",
1610
+ cli: "agent-worker pool doctor --repo . --json",
1611
+ kind: "read",
1612
+ inputSchemaVersion: 1,
1613
+ resultSchemaVersion: 1,
1614
+ envelopeSchemaVersion: 1,
1615
+ requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
1616
+ description: "Read-only Task Pool inventory (legacy + v2). Use before workerTaskRetry to confirm the task exists and status is Failed. Optional taskId filters the returned inventory to that task.",
1617
+ inputParams: [
1618
+ {
1619
+ name: "taskId",
1620
+ type: "string",
1621
+ required: false,
1622
+ description: "optional task id to focus the inventory",
1623
+ },
1624
+ ],
1625
+ modelCallable: "always",
1626
+ humanConfirmation: "none",
1627
+ },
1571
1628
  ...OFFICIAL_ACTIONS,
1572
1629
  ];
1573
1630
  return {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { isOpenspecSpecFilePath } from "../shared/openspec-spec.js";
2
3
  export const taskFlowSchema = z.enum([
3
4
  "auto",
4
5
  "micro",
@@ -65,6 +66,24 @@ export const frontendMockConfigSchema = z.object({
65
66
  serviceRoot: z.string().min(1).optional(),
66
67
  verifyCommands: z.array(frontendMockVerifyCommandSchema).optional().default([]),
67
68
  });
69
+ /** OpenSpec gate policy for frontend prewrite: cited (default) vs scan-strict. */
70
+ export const frontendOpenspecPolicySchema = z.enum(["cited", "scan-strict"]);
71
+ /**
72
+ * Frontend OpenSpec gate configuration. `cited` (the task-config default) makes
73
+ * the prewrite gate verify contract-declared requiredReadPaths plus task-source
74
+ * citations instead of the full generation-time directory snapshot; `scan-strict`
75
+ * keeps the auto-discovered full-read semantics.
76
+ */
77
+ export const frontendOpenspecConfigSchema = z.object({
78
+ policy: frontendOpenspecPolicySchema.optional().default("cited"),
79
+ requiredReadPaths: z
80
+ .array(z
81
+ .string()
82
+ .refine((candidate) => !candidate.includes("\\") &&
83
+ isOpenspecSpecFilePath(candidate), "frontendOpenspec.requiredReadPaths entries must be repo-relative supported files under openspec/schemas/, openspec/project-specs/, or ai_workspace/"))
84
+ .optional()
85
+ .default([]),
86
+ });
68
87
  /** Batch limits and gate profile for the browser-driven frontend test DAG.
69
88
  * Token fields are post-case stop thresholds, not model-provider hard caps. */
70
89
  export const frontendTestConfigSchema = z.object({
@@ -175,6 +194,8 @@ const taskConfigObjectSchema = z.object({
175
194
  frontendMock: frontendMockConfigSchema.optional(),
176
195
  /** Frontend browser-test batch and post-case token-stop configuration. */
177
196
  frontendTest: frontendTestConfigSchema.optional(),
197
+ /** Frontend OpenSpec gate policy and contract-declared required read paths. */
198
+ frontendOpenspec: frontendOpenspecConfigSchema.optional(),
178
199
  notes: z.string().optional().default(""),
179
200
  });
180
201
  export const taskConfigSchema = z.preprocess((raw) => {