@tea-agent/loop-agent 0.36.3-beta.0 → 0.37.0

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 (115) hide show
  1. package/CHANGELOG.md +56 -55
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/task/contract/project.js +4 -0
  4. package/dist/task/source-prepare/completeness.js +36 -0
  5. package/dist/task/source-prepare/parse-intent.js +67 -11
  6. package/dist/task/source-prepare/prepare.js +102 -5
  7. package/dist/worker/console/chat/chat-event-store.js +91 -0
  8. package/dist/worker/console/chat/pi-runtime.js +25 -0
  9. package/dist/worker/console/chat/routes.js +7 -2
  10. package/dist/worker/console/chat/semantic-activity.js +7 -1
  11. package/dist/worker/console/chat/session-store.js +6 -1
  12. package/dist/worker/console/chat/turn-process.js +119 -12
  13. package/dist/worker/console/operator-actions.js +18 -1
  14. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +1 -0
  15. package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +1 -0
  16. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +36 -0
  17. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +132 -0
  18. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +10 -0
  19. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +1 -0
  20. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +1 -0
  21. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +1 -0
  22. package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +231 -0
  23. package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +15 -0
  24. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +88 -0
  25. package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +206 -0
  26. package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +1 -0
  27. package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +1 -0
  28. package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +1 -0
  29. package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +1 -0
  30. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +1 -0
  31. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +1 -0
  32. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +1 -0
  33. package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +166 -0
  34. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +62 -0
  35. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +321 -0
  36. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +4 -0
  37. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  38. package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +30 -0
  39. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +3 -0
  40. package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +24 -0
  41. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +24 -0
  42. package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +41 -0
  43. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +1 -0
  44. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +85 -0
  45. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +156 -0
  46. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +292 -0
  47. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +106 -0
  48. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +1 -0
  49. package/dist/worker/console/static/assets/index-DQxiso1w.js +325 -0
  50. package/dist/worker/console/static/assets/index-_j0Hzo69.css +1 -0
  51. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +2 -0
  52. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +1 -0
  53. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +70 -0
  54. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +139 -0
  55. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +89 -0
  56. package/dist/worker/console/static/assets/layout-D-LzfAck.js +1 -0
  57. package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +1 -0
  58. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +1 -0
  59. package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +308 -0
  60. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +96 -0
  61. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +1 -0
  62. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +1 -0
  63. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +39 -0
  64. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +7 -0
  65. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +1 -0
  66. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +84 -0
  67. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +40 -0
  68. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +162 -0
  69. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +1 -0
  70. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +1 -0
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +1 -0
  72. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +2 -0
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +8 -0
  74. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +120 -0
  75. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +34 -0
  76. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +78 -0
  77. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +7 -0
  78. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  79. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  80. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  81. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  82. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  83. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  84. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  85. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  86. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  87. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  88. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  89. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  90. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  91. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  92. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  93. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  94. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  95. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  96. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  97. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  98. package/dist/worker/console/static/index.html +2 -2
  99. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +25 -0
  100. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +37 -2
  101. package/dist/worker/observe/static/operator-chrome.js +4 -2
  102. package/dist/workflows/dag/failure-routing.js +4 -9
  103. package/dist/workflows/dag/init-hybrid.js +2 -47
  104. package/dist/workflows/dag/lifecycle.js +0 -4
  105. package/dist/workflows/dag/node-execution.js +6 -16
  106. package/dist/workflows/dag/report.js +0 -6
  107. package/dist/workflows/dag/retry-policy.js +0 -11
  108. package/dist/workflows/dag/runner.js +19 -3
  109. package/dist/workflows/dag/types.js +0 -2
  110. package/dist/workflows/dag/validate.js +2 -7
  111. package/docs/templates/frontend-task-constraints.md +7 -13
  112. package/package.json +6 -1
  113. package/skills/loop-agent/references/command-reference.md +1 -0
  114. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  115. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
package/CHANGELOG.md CHANGED
@@ -1,92 +1,93 @@
1
1
  # 更新日志
2
2
 
3
- ## [0.36.3-beta.0] - 2026-08-16
3
+ ## [Unreleased]
4
4
 
5
- > 前端 DAG Mock 治理收紧(beta,发布到 `beta` dist-tag)。
6
-
7
- ### 改进
8
-
9
- - 前端 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)
10
- - 前端任务生成期 Mock 预警:新增 `DagSpec.advisories`,任务源提到接口/API/Mock 但 auto 模式把策略收窄为 `not-needed` 时,`task advance` 的 `warnings` 会给出可操作提示(声明 `frontendMock.verifyCommands` 或 `policy: required` 后重新生成 DAG),把契约缺口提前到生成期暴露
11
-
12
- ## [0.36.1] - 2026-08-16
5
+ ## [0.37.0] - 2026-08-16
13
6
 
14
7
  ### 新增
15
8
 
16
- - cooperative live DAG interrupt and recovery UX
9
+ - markdown syntax highlight, math formulas and mermaid diagrams; operate entry lands on chat
10
+ - label rerun titles with round number from -rN id suffix
11
+ - enter-DAG action, artifact rail projection, zh changelog
12
+ - surface compaction progress and timeline summary
13
+ - floating auto-dismiss toasts replace in-flow error alerts
14
+ - add running spinners to session titles and stop button
15
+ - show context ratio, semantic cache icons, align process entry typography
16
+ - show context usage and semantic cache labels in usage bar
17
+ - stream live turn process in strict order
17
18
 
18
19
  ### 修复
19
20
 
20
- - move interrupt eligibility to observability to honor kernel import boundary
21
- - retry semantic intake across Pi tiers with 300s timeout
21
+ - isolate interrupt kernel behind worker adapter
22
+ - constrain session title width so hover tooltip engages
23
+ - backfill operation link for semantic activity refs
24
+ - fail fast on non-canonical newTask ids with suggested fix
25
+ - accept projected references and normalize PRD headings
22
26
 
23
- ## [0.36.1-beta.0] - 2026-08-16
27
+ ## [0.36.2] - 2026-08-16
24
28
 
25
- > 标准 DAG planner 输出超长紧凑重试(beta,发布到 `beta` dist-tag)。
26
-
27
- ### 改进
28
-
29
- - 标准 DAG 的 planner 节点(`contract-pi` / `plan-pi`)输出超长(`output-too-large`)时自动紧凑重试:新增 `PLANNER_OUTPUT_LIMIT_RETRY_POLICY`(默认重试集合 + `output-too-large`),紧凑重试指令按节点类型区分——结构化契约节点只返回单个紧凑 JSON,普通计划节点返回最小计划(有序步骤、writeSet 边界、验证命令),并丢弃上游原文引用与长证据摘录
29
+ ### 修复
30
30
 
31
- ## [0.35.4-beta.0] - 2026-08-15
31
+ - exempt changelog history from task-pool-root scan
32
+ - exempt gitignore managed-block doc references in task-pool-root check
32
33
 
33
- > frontend prewrite 阻断误路由修正 + Mock 契约文档落地(beta,发布到 `beta` dist-tag)。
34
+ ## [0.36.1] - 2026-08-16
34
35
 
35
- ### 改进
36
+ ### 新增
36
37
 
37
- - 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」这类场景给出可操作建议,而非误导性的「解除依赖」
38
+ - cooperative live DAG interrupt and recovery UX
38
39
 
39
- ### 文档
40
+ ### 修复
40
41
 
41
- - `docs/templates/frontend-task-constraints.md` Mock 约束从 TODO 改为可落地契约:要求从项目**探索**实际存在的确定性 Mock 验证命令(不硬编码命令名)并声明到 `task.json.frontendMock.verifyCommands`,说明生成期 `allowedMockStrategies` 来源、`mock-strategy-outside-allowed` 是生成期契约问题(修复 = 补契约 + 重新生成 DAG,plan-revision 无法修复)
42
- - `docs/runtime/frontend-implementation-workflow.md` task.json 输入标准补充 Mock 契约声明、命令探索与失败恢复指引
42
+ - move interrupt eligibility to observability to honor kernel import boundary
43
+ - retry semantic intake across Pi tiers with 300s timeout
43
44
 
44
45
  ## [0.36.0] - 2026-08-15
45
46
 
46
47
  ### 新增
47
48
 
48
- - add task journey activity rail
49
- - harden Operator Chat DAG recovery
50
- - confirm session deletion inline
51
- - supervise long-running DAG operations
52
- - polish slash command card grid
53
- - localize generated runtime state
49
+ - 新增任务旅程活动栏,把任务创建、人工门禁、多次运行、失败和恢复记录聚合在同一张卡片中
50
+ - 强化 Operator Chat DAG 恢复能力,运行失败后可以直接查看原因、恢复路径和后续动作
51
+ - 会话删除改为列表行内二次确认,减少误删,也不再被浏览器原生弹窗打断
52
+ - Operator Chat 可以持续监督长时间运行的 DAG,直到任务结束、失败或确实需要人工处理
53
+ - 优化斜杠命令面板的卡片布局和键盘操作,在较小窗口中也能完整浏览和选择命令
54
+ - 运行状态、恢复提示和关键操作文案统一为自然中文,减少内部状态码和生硬英文直接暴露给用户
54
55
 
55
56
  ### 修复
56
57
 
57
- - harden task advance supervision
58
- - runner heartbeat logs no longer wake operationWait; writers must use edit tools for markdown
59
- - require writer to sync catalog index companions in writeSet
60
- - accept zh-CN acceptance heading variants in PRD parse
61
- - writer must materialize plan and not claim blocked for untouched forbidden paths
62
- - parse dag report runs[] envelope in rerun eligibility
63
- - tell writers diff is measured from git status, not response text
64
- - allow custom operator tools in SDK session allowlist
65
- - fit slash palette to available height
66
- - close operator chat command gaps
58
+ - 任务推进期间的监督更加可靠,减少因短暂静默或状态更新延迟造成的误判
59
+ - 运行器心跳不再提前唤醒 `operationWait`;Markdown 写入统一使用安全编辑工具,降低误覆盖风险
60
+ - 写入目录文档时必须同步更新对应索引,避免内容已经生成却无法从目录入口找到
61
+ - PRD 验收标准支持更多常见中文标题写法,不再因括号、破折号或措辞差异漏掉验收条件
62
+ - 要求执行节点真正生成计划文件;没有触碰禁写路径时,不再错误声称任务受阻
63
+ - 恢复重跑可以正确读取 `dag report` 返回的运行列表,不再因为响应结构差异误判为不可恢复
64
+ - 变更范围统一以 Git 工作区的真实差异为准,不再根据模型回复文字推测是否修改过文件
65
+ - Operator 自定义工具可以正常进入 Pi SDK 会话,不再被工具白名单意外过滤
66
+ - 斜杠命令面板会根据实际可用高度自动调整,不再被窗口边缘裁切
67
+ - 补齐 Operator Chat 的本地命令处理、动态命令降级和错误提示等交互缺口
67
68
 
68
69
  ## [0.35.3] - 2026-08-15
69
70
 
70
71
  ### 新增
71
72
 
72
- - chat as default landing + analyze-skills-first new-task flow
73
- - guide plan verification targets to writeSet-internal files, command-level checks to static
74
- - console recovery display — surface frontend recovery lineage + outcome
75
- - frontend-implementation DAG phase 6 — recovery root manifest + statistics dedup
76
- - frontend-implementation DAG phase 5 — writer partial-write rollback + child recovery
77
- - frontend-implementation DAG phase 4 — writer transient rollback base
78
- - frontend-implementation DAG phase 3c — scheduler activation gate + recovery trigger
79
- - frontend-implementation DAG phase 3b — staged child-creation kernel
80
- - frontend-implementation DAG phase 3a — recovery state/result types + terminal recovery intent
81
- - producing-node contract self-check and run robustness hardening
82
- - frontend-implementation DAG phase 2 — unified writer helper + structured repair assessment
83
- - slim frontend-implementation DAG phase 1 — prewrite merge + writer admission gate
73
+ - Console 默认进入「对话」页面;创建新任务时先分析需求和依赖,再进入正式任务流程
74
+ - 计划中的文件验证会优先落到允许写入的范围内,命令级检查则使用稳定的静态验证入口
75
+ - Recovery 页面新增恢复链路和最终结果展示,能够看清一次失败是如何被接管和收敛的
76
+ - 前端实现 DAG 增加恢复根清单,并去除重复统计,恢复报告更清晰
77
+ - 写入节点支持部分写入回滚和子任务恢复,局部失败不再轻易污染后续执行
78
+ - 写入节点遇到短暂故障时可以回退到稳定基线,再决定重试或恢复
79
+ - 调度器增加激活门禁和恢复触发条件,只有满足执行条件的子任务才会进入运行
80
+ - 新增分阶段创建子任务的基础能力,为后续恢复和分批执行提供统一入口
81
+ - 补齐恢复状态、恢复结果和终态恢复意图的数据结构,前后端对失败状态的理解保持一致
82
+ - 产出节点新增契约自检,并强化长时间运行时的稳定性和异常收敛能力
83
+ - 统一写入节点的公共辅助逻辑,修复判断改为结构化结果,不再依赖模糊文本
84
+ - 精简前端实现 DAG 的第一阶段,在正式写入前完成结果合并和写入准入检查
84
85
 
85
86
  ### 修复
86
87
 
87
88
  - 规范证据面板不再单列 openspec,统一为「规范已读取/规范检索」
88
- - green full test suite — python3 fallback, build-stamp version normalization, DEP0205 suppression
89
- - update candidate-continuation fixtures for failureCode (rebase semantic fix)
89
+ - 恢复全量测试稳定性:没有 `python3` 时自动使用可用 Python,统一构建版本格式,并处理 Node 弃用警告造成的干扰
90
+ - 修正续跑候选测试数据,使 `failureCode` 与真实失败语义一致,避免变基后出现错误判断
90
91
 
91
92
  ## [0.35.2] - 2026-08-15
92
93
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.36.3-beta.0",
4
- "gitSha": "5baa4176d2b96e68c61f759934c6582911b4a42a",
5
- "builtAt": "2026-08-16T11:51:43.878Z"
3
+ "version": "0.37.0",
4
+ "gitSha": "7b2fcb1072e85a7dbe0d92311d4b457cc34e7735",
5
+ "builtAt": "2026-08-16T20:27:37.113Z"
6
6
  }
@@ -46,6 +46,10 @@ export function projectRequirementMarkdown(draft) {
46
46
  lines.push(`- ${item}`);
47
47
  }
48
48
  }
49
+ // References / Source Resolutions remain projected into 需求.md (durable
50
+ // provenance for human readers); the managed read-back validator accepts
51
+ // the projector's own canonical forms instead of rejecting them
52
+ // (2026-08-16 L1: writer/reader round-trip invariant).
49
53
  if (draft.sourceResolutions && draft.sourceResolutions.length > 0) {
50
54
  lines.push("", "## Source Resolutions", "");
51
55
  for (const res of draft.sourceResolutions) {
@@ -68,6 +68,15 @@ export function listPrepareGaps(input) {
68
68
  level: "blocking",
69
69
  field: "objective",
70
70
  message: "requirement.objective is required",
71
+ details: { headingAliases: ["目标", "objective", "业务目标", "需求概述"] },
72
+ selfHealing: {
73
+ action: "reimport-prd",
74
+ steps: [
75
+ "检查 PRD 是否含「## 目标」或「## Objective」章节;中文括号后缀(如「目标(概述)」)已被识别",
76
+ "若确实缺失:补全 PRD 后重新 importPrd(同 taskId),advance 会自动重投影",
77
+ "不要手改 source/需求.md(会触发 DIRTY_SOURCE);不要在 PRD 目录生成 sidecar draft JSON",
78
+ ],
79
+ },
71
80
  });
72
81
  }
73
82
  if (draft.requirement.acceptanceCriteria.length === 0) {
@@ -76,6 +85,25 @@ export function listPrepareGaps(input) {
76
85
  level: "blocking",
77
86
  field: "acceptanceCriteria",
78
87
  message: "at least one acceptance criterion is required",
88
+ details: {
89
+ recognizedHeadings: [
90
+ "验收标准",
91
+ "acceptance criteria",
92
+ "完成标准",
93
+ "完成定义",
94
+ "验收",
95
+ "dod",
96
+ ],
97
+ note: "标题含括号/破折号后缀会被自动归一;AC 需为列表项(- / 1.)或 AC-NNN 标题/行内格式",
98
+ },
99
+ selfHealing: {
100
+ action: "reimport-prd",
101
+ steps: [
102
+ "检查 PRD 验收章节标题(如「完成标准(验收)」已可识别)与列表格式:需为「- 项目」或「AC-001: 文本」",
103
+ "格式修正后重新 importPrd(同 taskId);advance 内置 semantic intake 兜底,文档语义完整时会自动结构化",
104
+ "若 advance 已返回 SEMANTIC_INTAKE_FAILED:补全 PRD 后重试,不要手改 source/ 需求.md",
105
+ ],
106
+ },
79
107
  });
80
108
  }
81
109
  if (input.acceptanceConflicts && input.acceptanceConflicts.length > 0) {
@@ -188,6 +216,14 @@ export function listPrepareGaps(input) {
188
216
  code: "DIRTY_SOURCE",
189
217
  level: "blocking",
190
218
  message: "existing non-empty or externally-modified source refuses --apply; use show/diff/adopt/recover or --force-overwrite-source",
219
+ selfHealing: {
220
+ action: "adopt-contract",
221
+ steps: [
222
+ "task status --json 读取 contract.observedCanonicalHash 与当前 revision",
223
+ "若变更确属预期:task contract adopt --task <id> --expected-revision <n> --expected-observed-hash <hash> --request-id <id> --request-payload-sha256 <sha>",
224
+ "若变更不是预期的:改 PRD 后重新 importPrd 走完整重投影;禁止手改 lifecycle.json / .transactions/ 绕过",
225
+ ],
226
+ },
191
227
  });
192
228
  risks.push({
193
229
  code: "DIRTY_SOURCE",
@@ -44,15 +44,6 @@ function stripFencedAndComments(text) {
44
44
  .replace(/~~~[\s\S]*?~~~/g, "\n")
45
45
  .replace(/<!--[\s\S]*?-->/g, "\n");
46
46
  }
47
- function normalizeHeadingText(raw) {
48
- return raw
49
- .trim()
50
- // Strip leading section numbers: "2. 业务目标" / "3.1 已确认范围" / "3.2、非目标"
51
- .replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "")
52
- .toLowerCase()
53
- .replace(/[::]\s*$/u, "")
54
- .replace(/\s+/g, " ");
55
- }
56
47
  function normalizeAcceptanceId(raw) {
57
48
  return raw
58
49
  .trim()
@@ -69,12 +60,67 @@ function parseEmbeddedAcHeading(headingText) {
69
60
  const text = match[2]?.trim() || id;
70
61
  return { id, text };
71
62
  }
63
+ function normalizeHeadingText(raw) {
64
+ return raw
65
+ .trim()
66
+ // Strip leading section numbers: "2. 业务目标" / "3.1 已确认范围" / "3.2、非目标"
67
+ .replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "")
68
+ .toLowerCase()
69
+ .replace(/[::]\s*$/u, "")
70
+ .replace(/\s+/g, " ");
71
+ }
72
+ /**
73
+ * Strip decorative suffixes that real PRD writers attach to section
74
+ * headings: "完成标准(验收)" → "完成标准", "Acceptance Criteria (DoD):" →
75
+ * "acceptance criteria", "验收标准 — 交付门槛" → "验收标准". Handles zh/en
76
+ * parens, dashes, colons and trailing separators before the suffix.
77
+ */
78
+ function stripHeadingDecorations(normalized) {
79
+ return normalized
80
+ .replace(/[((][^(())]*[))]\s*$/u, "")
81
+ .replace(/\s*[—–\-|]\s*[^—–\-|]+$/u, "")
82
+ .replace(/[::]\s*$/u, "")
83
+ .trim();
84
+ }
85
+ const HEADING_PREFIX_HINTS = [
86
+ { key: "objective", hint: "目标" },
87
+ { key: "objective", hint: "objective" },
88
+ { key: "scope", hint: "范围" },
89
+ { key: "scope", hint: "scope" },
90
+ { key: "nonGoals", hint: "非目标" },
91
+ { key: "nonGoals", hint: "out of scope" },
92
+ { key: "acceptance", hint: "验收" },
93
+ { key: "acceptance", hint: "acceptance" },
94
+ { key: "acceptance", hint: "完成标准" },
95
+ { key: "acceptance", hint: "完成定义" },
96
+ { key: "acceptance", hint: "dod" },
97
+ { key: "constraints", hint: "约束" },
98
+ { key: "constraints", hint: "constraints" },
99
+ { key: "openQuestions", hint: "待决问题" },
100
+ { key: "openQuestions", hint: "open questions" },
101
+ { key: "assumptions", hint: "假设" },
102
+ { key: "assumptions", hint: "assumptions" },
103
+ ];
72
104
  function classifyHeading(text) {
73
105
  const normalized = normalizeHeadingText(text);
74
106
  for (const [key, aliases] of Object.entries(HEADING_ALIASES)) {
75
107
  if (aliases.has(normalized))
76
108
  return key;
77
109
  }
110
+ // Decoration-stripped exact match: "完成标准(验收)" → "完成标准".
111
+ const stripped = stripHeadingDecorations(normalized);
112
+ if (stripped && stripped !== normalized) {
113
+ for (const [key, aliases] of Object.entries(HEADING_ALIASES)) {
114
+ if (aliases.has(stripped))
115
+ return key;
116
+ }
117
+ }
118
+ // Prefix-contains fallback: "验收标准与测试" starts with "验收"; keep the
119
+ // hint list narrow (whole-word/leading-substring) to avoid false hits.
120
+ for (const { key, hint } of HEADING_PREFIX_HINTS) {
121
+ if (stripped === hint || stripped.startsWith(hint))
122
+ return key;
123
+ }
78
124
  return "other";
79
125
  }
80
126
  function isAtxHeading(line) {
@@ -408,10 +454,20 @@ export function fillMissingRequirementFacts(base, supplemental) {
408
454
  export function parseVerifyFlag(raw) {
409
455
  const idx = raw.indexOf(":");
410
456
  if (idx <= 0 || idx === raw.length - 1) {
411
- throw new Error(`invalid --verify value "${raw}"; expected <label>:<command>`);
457
+ // Actionable error with concrete examples (2026-08-16 L3'-style):
458
+ // models were guessing flag names (--verify-label) and separator
459
+ // shapes; the example makes the contract self-evident.
460
+ const hint = !raw.includes(":")
461
+ ? `no label separator found; write it as <label>:<command>, e.g. --verify "typecheck:npm run typecheck"`
462
+ : `empty ${idx === 0 ? "label" : "command"}; write it as <label>:<command>, e.g. --verify "typecheck:npm run typecheck"`;
463
+ throw new Error(`invalid --verify value "${raw}"; expected <label>:<command> — ${hint}`);
464
+ }
465
+ const label = raw.slice(0, idx).trim();
466
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(label)) {
467
+ throw new Error(`invalid --verify label "${label}"; use short kebab-case letters/digits/hyphens (e.g. typecheck, unit-tests, build) — value was "${raw}"`);
412
468
  }
413
469
  return {
414
- label: raw.slice(0, idx).trim(),
470
+ label,
415
471
  command: raw.slice(idx + 1).trim(),
416
472
  };
417
473
  }
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { lstat, readFile } from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { ContractMutationError, applyTaskContract, logicalRevisionForState, observeTaskContractState, projectConstraintsMarkdown, projectRequirementMarkdown, sha256OfCanonicalJson, validateDraftForTask, } from "../contract/index.js";
4
5
  import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
5
6
  import { getTaskContractPaths } from "../contract/paths.js";
@@ -28,26 +29,114 @@ async function loadExistingTaskConfig(repoRoot, taskId) {
28
29
  return null;
29
30
  }
30
31
  }
32
+ /**
33
+ * Check whether a "## References" section in a managed 需求.md matches the
34
+ * projector's canonical form: every entry is `- [role] <manifest-path>` and
35
+ * every path exists in source-manifest.json. The projector itself writes
36
+ * this section, so matching entries are the writer's own round-trip output
37
+ * and must be accepted (2026-08-16 L1). Unknown/free-form entries stay
38
+ * blocking — they indicate hand-edited provenance.
39
+ */
40
+ function isProjectedReferencesSection(sectionBody, manifestPaths) {
41
+ const entryRe = /^- \[(?<role>[^\]]+)\] (?<ref>\S.*)$/;
42
+ const lines = sectionBody
43
+ .split(/\r?\n/)
44
+ .map((line) => line.trim())
45
+ .filter(Boolean);
46
+ if (lines.length === 0)
47
+ return true; // empty section: nothing to reject
48
+ return lines.every((line) => {
49
+ const match = entryRe.exec(line);
50
+ if (!match?.groups)
51
+ return false;
52
+ const ref = match.groups.ref?.replace(/\\/g, "/").trim() ?? "";
53
+ return manifestPaths.has(ref);
54
+ });
55
+ }
56
+ /**
57
+ * Source Resolutions canonical projector shape:
58
+ * `- <sourceRef>: <conflict> → <decision> (<reason>; decidedBy=<who>)`.
59
+ */
60
+ function isProjectedSourceResolutionsSection(sectionBody) {
61
+ const entryRe = /^- \S[^:]*: .+ → .+ \(.+; decidedBy=.+\)$/u;
62
+ const lines = sectionBody
63
+ .split(/\r?\n/)
64
+ .map((line) => line.trim())
65
+ .filter(Boolean);
66
+ if (lines.length === 0)
67
+ return true;
68
+ return lines.every((line) => entryRe.test(line));
69
+ }
70
+ /** Section titles the read-back validator may inspect (static whitelist). */
71
+ const MANAGED_PROJECTION_SECTION_TITLES = [
72
+ "References",
73
+ "Source Resolutions",
74
+ ];
75
+ /** Extract the body of the first `## <Title>` section, if present. */
76
+ function extractSectionBody(markdown, title) {
77
+ const re = new RegExp(`^##\\s+${title}\\s*$`, "im");
78
+ const match = re.exec(markdown);
79
+ if (!match)
80
+ return null;
81
+ const rest = markdown.slice(match.index + match[0].length);
82
+ const nextHeading = /^##\s+/m.exec(rest);
83
+ return nextHeading ? rest.slice(0, nextHeading.index) : rest;
84
+ }
85
+ /** Materialized paths registered in the task's source-manifest.json. */
86
+ async function loadSourceManifestPaths(requirementPath) {
87
+ const manifestPath = path.join(path.dirname(requirementPath), "source-manifest.json");
88
+ try {
89
+ const raw = JSON.parse(await readFile(manifestPath, "utf-8"));
90
+ return new Set((raw.documents ?? [])
91
+ .map((doc) => doc.materializedPath?.replace(/\\/g, "/"))
92
+ .filter((p) => Boolean(p)));
93
+ }
94
+ catch {
95
+ return new Set();
96
+ }
97
+ }
31
98
  async function loadManagedProjectionDraft(input) {
32
99
  const config = input.existingTaskConfig;
33
100
  if (!config) {
34
101
  return { ok: false, message: "managed task config could not be loaded" };
35
102
  }
36
103
  try {
37
- const [requirement, constraints] = await Promise.all([
104
+ const [requirementRaw, constraints] = await Promise.all([
38
105
  readFile(input.requirementPath, "utf-8"),
39
106
  readFile(input.constraintsPath, "utf-8"),
40
107
  ]);
41
- const requirementFacts = extractRequirementFactsFromMarkdown(requirement);
42
- const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
43
- if (/^##\s+(?:References|Source Resolutions)\s*$/im.test(requirement)) {
108
+ // L1 (2026-08-16): the requirement projector itself writes `## References`
109
+ // (from source-manifest) and `## Source Resolutions` sections into
110
+ // 需求.md. The read-back validator used to reject ANY such section —
111
+ // writer/reader contract drift that deadlocked every PRD import whose
112
+ // draft carried references. Now accept the projector's own canonical
113
+ // forms: References entries must all be `- [role] <path>` with paths
114
+ // present in source-manifest.json; Source Resolutions entries must
115
+ // match the projector's `- ref: conflict → decision (...)` shape.
116
+ // Free-form/hand-edited entries remain blocking.
117
+ const manifest = await loadSourceManifestPaths(input.requirementPath);
118
+ const referencesBody = extractSectionBody(requirementRaw, "References");
119
+ if (referencesBody !== null &&
120
+ !isProjectedReferencesSection(referencesBody, manifest)) {
121
+ return {
122
+ ok: false,
123
+ message: "managed requirement projection contains hand-edited or unknown References entries (not backed by source-manifest); use loop-agent task advance " +
124
+ input.taskId +
125
+ " --from-draft <reviewed-draft.json> --json",
126
+ };
127
+ }
128
+ const sourceResolutionsBody = extractSectionBody(requirementRaw, "Source Resolutions");
129
+ if (sourceResolutionsBody !== null &&
130
+ !isProjectedSourceResolutionsSection(sourceResolutionsBody)) {
44
131
  return {
45
132
  ok: false,
46
- message: "managed requirement projection contains unsupported References or Source Resolutions; use loop-agent task advance " +
133
+ message: "managed requirement projection contains hand-edited or malformed Source Resolutions entries; use loop-agent task advance " +
47
134
  input.taskId +
48
135
  " --from-draft <reviewed-draft.json> --json",
49
136
  };
50
137
  }
138
+ const requirementFacts = extractRequirementFactsFromMarkdown(requirementRaw);
139
+ const constraintFacts = extractRequirementFactsFromMarkdown(constraints);
51
140
  if (!requirementFacts.objective?.trim()) {
52
141
  return {
53
142
  ok: false,
@@ -517,6 +606,14 @@ export async function prepareTaskSource(input) {
517
606
  code: "MANAGED_PROJECTION_INCOMPLETE",
518
607
  level: "blocking",
519
608
  message: managedProjectionError,
609
+ selfHealing: {
610
+ action: "adopt-draft",
611
+ steps: [
612
+ "task status --json 读取 contract.observedCanonicalHash / observedSourceHashes,确认现有源文件状态",
613
+ "从 Console drafts(<app-data>/repos/<repo>/drafts/<taskId>.json)或 semantic intake 产物(artifacts/intake/semantic-draft.json 的 draft 字段)取回结构化 draft",
614
+ "补充缺失字段(scope/invariants 等)后:task advance --from-draft <reviewed-draft.json> --json",
615
+ ],
616
+ },
520
617
  });
521
618
  }
522
619
  // If --use-imported-prd and no parseable requirement and no AC from flags/text → blocking
@@ -152,6 +152,75 @@ export function projectOperationForChat(operation) {
152
152
  previewHash,
153
153
  };
154
154
  }
155
+ /** Action families that leave a user-inspectable durable outcome. */
156
+ const ARTIFACT_EMITTING_ACTIONS = new Set([
157
+ "taskAdvance",
158
+ "taskCreate",
159
+ "contractApply",
160
+ "dagReport",
161
+ "taskImportPrd",
162
+ "importPrd",
163
+ ]);
164
+ /**
165
+ * Project a terminal operation into an「产物」rail card (2026-08-16).
166
+ * Mirrors `ArtifactCardState` (schemaVersion 1) so the existing frontend
167
+ * parser renders it without any change. Best-effort: operations without a
168
+ * durable outcome (pure status reads) return undefined.
169
+ */
170
+ function artifactFromTerminalOperation(operation, projection) {
171
+ if (!ARTIFACT_EMITTING_ACTIONS.has(operation.action))
172
+ return undefined;
173
+ // Failed/interrupted actions leave nothing inspectable as a deliverable.
174
+ if (operation.state !== "succeeded")
175
+ return undefined;
176
+ const taskId = projection.taskId;
177
+ const kind = taskId
178
+ ? "contract"
179
+ : "operation";
180
+ const title = taskId ? `任务 ${taskId}` : `执行 ${operation.action}`;
181
+ const summary = projection.runSummary?.status
182
+ ? `运行 ${projection.runSummary.runId} · ${projection.runSummary.status}`
183
+ : projection.summaryPreview || undefined;
184
+ const reportLines = [];
185
+ if (projection.runSummary) {
186
+ const rs = projection.runSummary;
187
+ reportLines.push(`运行: ${rs.runId}`);
188
+ reportLines.push(`状态: ${rs.status}`);
189
+ if (rs.verdict)
190
+ reportLines.push(`结论: ${rs.verdict}`);
191
+ if (rs.failureCategory)
192
+ reportLines.push(`失败归类: ${rs.failureCategory}`);
193
+ if (Array.isArray(rs.nodes)) {
194
+ for (const node of rs.nodes.slice(0, 30)) {
195
+ reportLines.push(`- ${node.id}: ${node.status}`);
196
+ }
197
+ }
198
+ }
199
+ if (projection.stdoutPreview) {
200
+ reportLines.push("", "--- stdout 摘要 ---", projection.stdoutPreview);
201
+ }
202
+ const previewHash = createHash("sha256")
203
+ .update(JSON.stringify({
204
+ operationId: operation.operationId,
205
+ state: operation.state,
206
+ taskId: taskId ?? null,
207
+ runSummary: projection.runSummary ?? null,
208
+ stdoutPreview: projection.stdoutPreview ?? null,
209
+ }))
210
+ .digest("hex");
211
+ return {
212
+ schemaVersion: 1,
213
+ kind,
214
+ artifactId: `operation-${operation.operationId}`,
215
+ title,
216
+ ...(summary ? { summary } : {}),
217
+ ...(reportLines.length > 0
218
+ ? { reportText: reportLines.join("\n").slice(0, 8000) }
219
+ : {}),
220
+ inspectHref: projection.inspectHref,
221
+ previewHash,
222
+ };
223
+ }
155
224
  export function projectCompactSnapshot(value) {
156
225
  const record = value && typeof value === "object"
157
226
  ? value
@@ -254,6 +323,7 @@ export function projectChatToolEventPayload(input) {
254
323
  resultPreview: preview,
255
324
  previewHash,
256
325
  isError: Boolean(input.isError),
326
+ ...(input.operationId ? { operationId: input.operationId } : {}),
257
327
  };
258
328
  }
259
329
  export function projectOperationEventSummary(event) {
@@ -411,6 +481,27 @@ export function createChatOperationLinker(options) {
411
481
  },
412
482
  });
413
483
  }
484
+ // Artifact projection (2026-08-16): the sidebar「产物」tab was permanently
485
+ // empty because nothing in the server ever emitted `artifact-ref`. On a
486
+ // terminal operation with a durable outcome (task created / advanced /
487
+ // contract applied / report produced) project one artifact card so the
488
+ // user can inspect the deliverable straight from the rail.
489
+ if (isTerminalOperationState(operation.state)) {
490
+ const artifact = artifactFromTerminalOperation(operation, projection);
491
+ if (artifact) {
492
+ const artifactDuplicate = options.chatEvents
493
+ .snapshot(input.sessionId)
494
+ .some((event) => event.kind === "artifact-ref" &&
495
+ event.data
496
+ .artifact?.previewHash === artifact.previewHash);
497
+ if (!artifactDuplicate) {
498
+ options.chatEvents.append(input.sessionId, input.turnId, {
499
+ kind: "artifact-ref",
500
+ data: { artifact },
501
+ });
502
+ }
503
+ }
504
+ }
414
505
  if (isTerminalOperationState(operation.state)) {
415
506
  const key = keyFor(input.sessionId, input.operationId);
416
507
  followers.get(key)?.();
@@ -1963,6 +1963,31 @@ export class ConsolePiRuntime {
1963
1963
  if (usage)
1964
1964
  onEvent({ type: "usage", sessionId, usage });
1965
1965
  }
1966
+ // Context usage rides the usage event: refresh on agent_settled so the
1967
+ // bar reflects the post-turn context window fill (pi-web parity).
1968
+ if (event.type === "agent_settled") {
1969
+ try {
1970
+ const stats = session.getSessionStats?.();
1971
+ const contextUsage = stats?.contextUsage;
1972
+ if (contextUsage) {
1973
+ onEvent({
1974
+ type: "usage",
1975
+ sessionId,
1976
+ usage: {
1977
+ inputTokens: 0,
1978
+ outputTokens: 0,
1979
+ totalTokens: 0,
1980
+ ...(contextUsage.tokens === null
1981
+ ? {}
1982
+ : { contextUsage }),
1983
+ },
1984
+ });
1985
+ }
1986
+ }
1987
+ catch {
1988
+ // Stats getter is best-effort; never break the event stream.
1989
+ }
1990
+ }
1966
1991
  });
1967
1992
  options?.signal?.addEventListener("abort", onAbort);
1968
1993
  if (options?.signal?.aborted) {