@tea-agent/loop-agent 0.39.0-beta.5 → 0.39.0-beta.7

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 (130) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/client-recovery.js +3 -0
  4. package/dist/commands/task-advance.js +19 -0
  5. package/dist/executors/dag-pi-executor.js +138 -36
  6. package/dist/executors/pi-executor.js +7 -0
  7. package/dist/executors/pi-playwright-cli-tool.js +23 -16
  8. package/dist/executors/shell-executor.js +26 -9
  9. package/dist/shared/dag-failure-category.js +150 -0
  10. package/dist/task/config-types.js +6 -0
  11. package/dist/task/frontend-project-capability.js +19 -0
  12. package/dist/task/source-references.js +22 -1
  13. package/dist/worker/console/chat/operation-card.js +8 -1
  14. package/dist/worker/console/chat/routes.js +5 -3
  15. package/dist/worker/console/inspect-split.js +18 -0
  16. package/dist/worker/console/operation-run-facts.js +19 -3
  17. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-bPer1waP.js} +1 -1
  18. package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BLdkwucW.js} +1 -1
  19. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-Bw4wo4I1.js} +1 -1
  20. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-DUUkc0gr.js} +1 -1
  21. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-BC3O0nob.js} +1 -1
  22. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +1 -0
  23. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-Dd4g2nti.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-s2EDm7-n.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-GqtHM2yQ.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-BzRFrN8j.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-Bw9nFjCJ.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-B4n-03Qm.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-Dt75ZdBD.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-B_afMhHB.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-Bd8Rqqg9.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-9boOMji4.js} +1 -1
  33. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +1 -0
  34. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +1 -0
  35. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BtCycfCc.js} +1 -1
  36. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-eTOts9L1.js} +1 -1
  37. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Cibd2mjg.js} +1 -1
  38. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-CDjDZtDV.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-Btexn-JJ.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-Cyl8sdpM.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Ev-83E1t.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-41LqE0Mc.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-BC39T5R3.js} +1 -1
  44. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DzcNRuri.js} +1 -1
  45. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-B5Frvvnz.js} +1 -1
  46. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-BapxROIM.js} +1 -1
  47. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-Cd4d4wQ0.js} +1 -1
  48. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-CrL6buFW.js} +1 -1
  49. package/dist/worker/console/static/assets/index-DBh_x_Ek.js +406 -0
  50. package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
  51. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BrSiNxyd.js} +1 -1
  52. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-gvlvY-n9.js} +1 -1
  53. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-D-YaPzRQ.js} +1 -1
  54. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-BsmDv9E_.js} +1 -1
  55. package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-UWaoDN3h.js} +1 -1
  56. package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-BlvpGVG8.js} +5 -5
  57. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-CUCIP5CP.js} +1 -1
  58. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CB1aQUAK.js} +1 -1
  59. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-BwehHxhf.js} +1 -1
  60. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-CFz30APB.js} +1 -1
  61. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-BwNmw1i6.js} +1 -1
  62. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-jg-tPXlI.js} +1 -1
  63. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-BlyReDKl.js} +1 -1
  64. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DBUzW1f3.js} +1 -1
  65. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-BYcI2RAr.js} +1 -1
  66. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-D7AiyJdo.js} +1 -1
  67. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +1 -0
  68. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-BHGt7Kkv.js} +2 -2
  69. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +8 -0
  70. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-OPjooWcX.js} +1 -1
  71. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-ggsWDvrO.js} +1 -1
  72. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DjNe-BjZ.js} +1 -1
  73. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-Byp63Hei.js} +1 -1
  74. package/dist/worker/console/static/index.html +2 -2
  75. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  76. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  77. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  78. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  79. package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
  80. package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
  81. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  82. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  83. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  84. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  85. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  86. package/dist/worker/observability/read-model.js +22 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  88. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  89. package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
  90. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  91. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  92. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  93. package/dist/workflows/dag/failure-category.js +7 -128
  94. package/dist/workflows/dag/frontend-design-policy.js +31 -7
  95. package/dist/workflows/dag/frontend-implementation-contract.js +78 -3
  96. package/dist/workflows/dag/frontend-review-findings.js +238 -0
  97. package/dist/workflows/dag/frontend-shadow-dual-write.js +24 -10
  98. package/dist/workflows/dag/frontend-shape.js +5 -3
  99. package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
  100. package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
  101. package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
  102. package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
  103. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  104. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  105. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  106. package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
  107. package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
  108. package/dist/workflows/dag/init-hybrid.js +149 -15
  109. package/dist/workflows/dag/node-execution.js +18 -1
  110. package/dist/workflows/dag/report.js +37 -1
  111. package/dist/workflows/dag/rerun-feedback.js +101 -1
  112. package/dist/workflows/dag/retry-policy.js +31 -3
  113. package/dist/workflows/dag/runner.js +46 -1
  114. package/dist/workflows/dag/types.js +12 -0
  115. package/dist/workflows/dag/validate.js +32 -22
  116. package/docs/templates/agent-dag-report.schema.json +8 -2
  117. package/docs/templates/agent-dag.schema.json +1 -1
  118. package/docs/templates/frontend-test-case-checklist.md +2 -2
  119. package/package.json +1 -1
  120. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  121. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  122. package/skills/playwright-cli/SKILL.md +1 -1
  123. package/skills/playwright-cli-case-generator/SKILL.md +10 -8
  124. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
  125. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
  126. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
  127. package/dist/worker/console/static/assets/index-D5zGX6fG.js +0 -404
  128. package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
  129. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
  130. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
package/CHANGELOG.md CHANGED
@@ -2,11 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ - 前端 review `request_review_changes` 不再被静默当作成功(方向 A):`frontend-review-pi` 提交 typed `request_review_changes` 时,run 终态判定为「未通过」(`partial_failed` + `terminalReason=review-request-changes`,即使全部节点 FINISHED 也不显示成功),committed findings 自动渲染为 run-owned 下游任务 PRD 草稿 `contracts/frontend-review-findings-prd.md`(含 issueCategory/evidenceRefs/findings/restartPhase 指引),并生成 recovery trigger(issueCategory→failureOwner 映射,new-dag)。`deriveDagRerunFeedback` 新增 typed-fact 通道:无文本 verdict、无 convergence 的 frontend 场景也能把 committed `request_review_changes` findings 注入下一轮 `<rerun_feedback>`,问题不再丢失、下一轮从 restartPhase 定点修复而非重复流程。
6
+ - 前端 review findings PRD 草稿携带修复范围路由信号(方向 A 补充):`deriveFrontendReviewFixScope` 从 committed findings 确定性推导建议 shape(单文件无远程 API→micro、同目录集中多文件→small、跨目录/API 信号→standard)与建议 complexity、目标文件、建议 allowedPaths;PRD 标题改为「修复 review 发现(<shape>):<目标文件>」,正文新增「修复范围(供下一轮任务生成路由)」段(含 small 信号词,如「单文件局部修复」「无远程接口」),下一轮任务以该 PRD 生成 DAG 时,`resolveFrontendTaskShape` 能把小修复路由到 micro=7/small=10 节点拓扑,而非默认 11 节点 standard 全流程。
7
+ - 前端 shape 分类 API 否定词覆盖中文空格形式(冒烟暴露边界):`无 API/网络调用`、`无 API` 等表述不再被误判为远程 API 信号(原 `\b(...)\b` 对 CJK 开头无词边界),`resolveFrontendTaskShape` 与 `deriveFrontendReviewFixScope` 的 hasApiSignal 判定同步修正——带「无 API/无接口」声明的单文件小修复正确路由 micro/small,而非误升级 standard。
8
+ - 前端实现契约对 `decision=new` 的退化空 `specReference` 归一化兜底(冒烟暴露 plan invalid-output):模型偶发把 new 组件输出成 `specReference:{path:"",section:"",line:null}`(空对象而非 prompt 要求的 null),原校验直接判 `invalid-output` 触发完整 plan 重试;`frontendImplementationContractSchema` preprocess 新增 `normalizeNewComponentSpecReference`——new 选择携带空 path 的退化 specReference 时按 null 意图归一化为缺省(非空 path 的 `specified` 命中原样保留,真实 new+specReference 仍拒绝),一次格式滑动不再烧掉整个 plan attempt。
5
9
  - 深度合入 origin/main(source e68dfbe1):以 frontend-node typed-tools 拓扑为基底移植 main 硬化——canonical artifact 消费、fail-closed 无验证 marker、风险分级细化(DEPENDENCY_INSTALL_LANGUAGE)、`HARNESS_DAG_RERUN_PROMPT_OVERRIDE` prompt override、Pi SDK 运行时依赖 0.83.0 与 `engines node>=22.19.0`、next-release scripts 入包;废弃节点(verify-assess/reverify/repair-pi/plan-revision/final-design-review/lint-baseline shell)能力确认被当前流程覆盖后裁剪对应实现(repair-gate 机制、lint-baseline shell 类型),plan retry 文案对齐 committed ledger;affected 392 + M2-M9 303 测试全绿,fast 3909/3910、integration 397/398(各 1 个 fn 既有失败)。
6
10
 
7
11
  ### 重点更新
8
12
 
9
- - Console「设置」页新增「模型配置(models.json)」区块:以弹框形式增/改/删 Pi 的模型条目,支持 6 个定义字段(id、name、contextWindow、maxTokens、input、reasoning)。新增时 Provider 用下拉选择、其余字段自动补默认值;编辑时 Provider 只读且仅在原位置就地替换,只改表单内字段、不覆盖配置里页面未展示的其它字段(如 cost/compat);不读写 API key(凭据走 Pi CLI)。编辑已存在的模型不再被移到最后。
13
+ - 修复 Console 四处评审缺陷(合并前收口):① 向导「运行」进度块接通——`runDag` 只返回 operationId,原实现读的 `recovery.dagRunId` 在向导路径永远为空;现在确认页先快照基线 run、进入运行页后轮询 `observeLink`,以 (taskId, operationId) 任一变化为失效信号(operationId 覆盖「重新运行」按钮——它不改变向导步骤;taskId 覆盖运行步骤中手动改任务 ID 的场景,失效时基线一并重置,避免沿用上一任务的 run):新派发/切任务会清掉旧选中 run 并重新解析,**只接受与当前任务基线不同的新 run**;从 Recovery 带回的共享选中 run 先做归属校验——`GET /api/dag-runs/:id` 详情响应新增 `taskId` 投影(源自 run.json 的 `taskContractBinding`→`sourceBinding`→Hybrid 标题,与 observeLink/candidates 同一绑定真源,不受候选列表 limit 截断影响;404 视为失效选中、无绑定视为不可判定),非本任务的 run 不展示;校验全程由 effect 生命周期栅栏保护——任务/运行/选中在异步校验期间变化时,旧响应直接丢弃、不会改写新状态;校验暂不可用时按有界退避(2s→30s,约 2 分钟)自动重试而非被动等待状态变化,重试计划为纯函数并有行为测试;**展示层另有闸门**——进度面板与运行操作只消费「本解析器已采纳/已验证」的 run(组件 state),未验证期间(校验中、重试中、give-up 后)进度面板保持空缺,不会即时或永久显示其他任务的 DAG;「打开检视」「侧栏预览」两个入口同样双重门禁(按钮无已验证 run 即禁用 + 处理器复核 verifiedRunId),共享的 openInspectSplit 解析器不再能带出未验证的外来 run。解析决策本身抽取为纯状态机 `pages/tasks/run-id-resolution.ts`(首跑/重跑/切任务/外来选中/基线门控五类迁移),Effects 只做薄适配,主流程「确认页基线快照 → runDag 派发 → observeLink 轮询 → 新 run 采纳」有完整行为测试。基线快照为三态(成功/权威无运行/暂不可查),**查询失败不会被当成「没有历史运行」**——快照重试期间基线保持未知、探测轮询等待,不会把旧运行误判为新运行;新 run 创建失败或注册较慢时进度面板宁可空缺也不把上一次已终止的 DAG 当作当前运行(轮询上限 5 分钟后安静停止)。② 失败建议数据契约修正——旅程卡原先把原始 run 级 `failureCategory` 直接查归一化建议表(`missing-api-key`/`nonzero-exit` 等永远查不中,节点分类又在安全投影中被丢弃);现在纯分类核心下沉 `src/shared/dag-failure-category.ts`(console↔workflows 双向共享叶,不破坏 console 禁止 import workflows 的边界测试),服务端投影把 run 级与节点级分类都归一化输出(success/skipped 不出),卡片按「失败节点分类优先、run 级兜底」取值,失败卡始终有可操作建议。③ Recovery interrupt 资格轮询在运行中档位同步刷新 run 状态——原先状态只在选中时读一次,运行中的 run 结束后仍永久保持 4 秒快轮询;现在 4 秒档附带轻量 `GET /api/dag-runs/:id` 刷新 `effectiveStatus`,终态后自动退到 30 秒。④ Inspect 紧凑检视一次瞬时刷新失败不再永久显示错误——成功刷新回合会清除上一轮 error 横幅。
14
+ - Operator Chat 输入框两项交互增强:① **↑/↓ 历史输入召回**——每次发送的文本进入输入历史(连续重复折叠、上限 100 条),光标在首行按 ↑ 逐条向旧回溯、按 ↓ 向新前进,越过最新一条时恢复回溯前**未发送的草稿**(草稿不会被历史浏览冲掉);多行文本中部的方向键仍是普通光标移动,IME 组词与 @文件/命令面板打开时不抢占。② **运行中排队下一轮指令**——当前回合运行中 Enter 不再被吞掉,消息作为「排队消息」绑定当前 Turn(服务端 `PUT deferred-turn` 从仅限 stopping 放开为 running/stopping,错误码 `TURN_NOT_STOPPING`→`TURN_NOT_ACTIVE`),当前回合结束(正常完成或停止)后自动发送;排队条提供「立即发送」(停止当前回复并立即发出)、「编辑」(移回输入框改写后再排)、「删除」三种操作,运行中输入框右侧提供排队发送按钮(与停止按钮并列);排队条单行展示(状态 + 省略号预览 + 操作按钮一行),输入框默认一行高(自动增高上限 280px 不变)。
15
+ - 向导「运行」面板结构化与预期管理(A2/C9):新增实时运行进度块——节点完成计数与百分比进度条(含 progressbar 无障碍语义)、当前运行节点名称与已运行时长、失败节点与错误摘要,数据来自 `/api/dag-runs/:id` 每 5 秒轮询并在终态自动停止;`effectiveStatus=running-suspected-stall` 时显示橙色「疑似停滞」提示条(建议打开检视查看节点活动或到恢复页协作中止/诊断),把 20–40 分钟长跑的「日志噪音等待」变为有阶段感、可判断的等待。原始 SSE 事件日志默认折叠为「原始事件日志(最近 80 条)」,需要排查时再展开。
16
+ - Console 前端三项体验优化(P0/P2):① Recovery 页 DAG 紧凑检视侧栏(InspectSplitPanel)从一次性快照改为实时刷新——运行中每 5 秒自动拉取节点/事件并新增「进行中/已结束」刷新状态行,到达终态(finished/failed/partial_failed/superseded/abandoned)自动停止轮询,刷新轮不清空既有内容避免闪烁;② Chat 任务旅程卡失败态新增按归一化 failureCategory 的可操作「建议」行(validation/executor/write-guard/timeout/auth/human-required/human-rejected/shell-command/static-error/decision-envelope/unknown 各映射到具体下一步:从失败节点重跑、修正契约后重新生成、跳「模型与认证」页等),不再只报「原因分类为 X」;③ Recovery 页 interrupt 资格轮询退避:选中 run 已终态(含 interrupted)时从 4 秒降到 30 秒,运行中保持 4 秒。
17
+ - Console 对话页 Models 面板的模型管理增强:以弹框形式增/改/删 Pi 的模型条目,支持 6 个定义字段(id、name、contextWindow、maxTokens、input、reasoning)。新增时 Provider 用下拉选择、其余字段自动补默认值;编辑时 Provider 只读且仅在原位置就地替换,只改表单内字段、不覆盖配置里页面未展示的其它字段(如 cost/compat);不读写 API key(凭据走 Pi CLI)。编辑已存在的模型不再被移到最后。(该能力仅在对话页提供,设置页不再重复入口。)
18
+ - 修复 `markdown-traceability` pipeline 未将冻结 `backendLayout` 传给 `validateBackendMarkdownTraceability`,自定义 layout 下不再回落默认 `testcase/md` ENOENT;新增回归用例以非默认 nested layout(`testRoot=testcase/elb/remote`、`markdownRel=md`)执行真实 shell pipeline,traceability 从自定义 Markdown 目录读取并 PASS,默认 layout 行为不变。(验证:`npm run typecheck`、`npx vitest run test/backend-test-markdown-workflow.test.ts test/backend-test-layout.test.ts`、`npm run build`、`git diff --check`。)
19
+ - backend-test 快速补强(backend-test-fast-failclosed-attribution):两处 fail-open 缺口已 fail-closed。① 当前轮次 Markdown 作用域严格依赖唯一有效 `## Module Index`——README 缺失 / 不可读 / `## Module Index` 重复(或缺标题)/ 唯一但空或不可解析任一状态,`markdownFiles` 与 coverage `markdownModuleFiles` 均返回空作用域而非回退全目录扫描,历史 `.md` 不再渗入 Case/coverage/partition facts,`resolveBackendTestManifestModules` 新增可区分 `manifestStatus`(valid/missing/unreadable/duplicate/empty-unparsable)与 `moduleIndexHeadingCount`,`validateBackendMarkdownCases` 与 `analyzeBackendTestCaseCoverage` 为非法状态各推 FAIL finding,默认与自定义 `backendTest` 布局一致。② collection 分类器不再凭 `__NNN_*` 裸 import 名称单独判 REPAIRABLE:inventory 构建时预计算每个 mapped script 的裸本地 helper import 证据(`scriptBareImports`),只有「存在同名 generated-local helper asset 且 mapped script 源码确实 import 该裸模块」才判 REPAIRABLE 并给精确 repairPaths;无 helper asset / mapped script 无对应 import / 无法精确归因一律 `BLOCKED`(`missing-generated-local-helper-without-evidence`。真第三方模块缺失保持 BLOCKED,不通过 PYTHONPATH/sys.path/conftest/ImportError fallback 绕行。(验证:`npm run typecheck`、backend-test 四聚焦 vitest、`npm run build`、`git diff --check`。)
20
+ - backend-test 后续修复(backend-test-fast-manifest-import-fix):Markdown 用例校验与 coverage 分析改为按 README `## Module Index` 清单限定本轮有效模块资产——未声明旧 `.md`(如 legacy.md)不再进入 Case/coverage/partition 统计,报告改为显式非静默 unmanifested advisory;“清单声明但文件缺失” fail-closed 明确失败,默认与自定义 `backendTest` 布局统一生效。generated pytest 裸本地 helper import(`from __551_manager_helpers import ...`)在 writer completeness gate 判为 recoverable 并给出精确 mapped 脚本 target path;collection 分类器对可归因到 mapped 资产的裸本地 helper `ModuleNotFoundError` 判 REPAIRABLE 并提供精确 repairPaths,真正第三方依赖缺失仍保持 BLOCKED。不放宽 self-contained、writeSet、forbiddenPaths、skip/xfail、断言与单次业务执行约束(验证:`npm run typecheck`、backend-test 聚焦 vitest、`npm run build`、`git diff --check`)。
10
21
  - 前端风险分级细分 manifest 触及语义:`allowedPaths` 含 `package.json`/lockfile 或需求文本提到 package.json 不再单独触发 `new-dependency` high-risk(改一行 script、调构建参数的任务不再被顶到强制全门禁并误标高风险),只有任务文本同时出现安装语言(npm install / yarn add / pnpm add / 新增依赖等)才升级;仅路径触及时记 `new-dependency:manifest-path-without-install-language` 拒绝信号,审计可溯。
11
22
  - 前端验证 fallback 生成期诚实化:真实项目无可读取的 `package.json` scripts 且未探测到本地 tsc 时,不再注入必然失败的 `npm run typecheck` / `npm run build` / `npm test`(纯静态 / vanilla JS 站点原会在 verify 阶段假失败),改为执行显式 no-op 标记命令(`No frontend verification command configured`,进入冻结命令集可被 contract 引用,trace 如实记录 not-run),并写生成期 advisory 提示用 `--verify` 或执行约束显式声明验证命令后重新生成 DAG。无 repoRoot 的单测 fixture 保留历史 generic fallback。**标记不构成验证证据(fail-closed)**:verification trace 将绑定标记的 target 判为失败、只含标记的 static 证据判为 unavailable,repair 分类归为 non-repairable `contract`,verify 节点以 `frontend verification unavailable` 硬失败阻断 review/closeout——无真实验证命令的 run 无法走完绿灯链。
12
23
  - frontend-test `passed` 回执链改按 dispatch 序号判序(B-HASH-01):`summarizePlaywrightCliReceipts` 先按 `sequence` 排序再判 open→find→cleanup 连续链;并行 `find`/`screenshot` 按完成时间落盘导致的乱序不再误杀执行已通过的 case(Wave 1 实测 4 条 passed 被改判 `browser-command-evidence-missing`)。真 gap / 重复序号仍 fail-closed。
@@ -31,6 +42,7 @@
31
42
  ### 新增
32
43
 
33
44
  - Operator Chat 时间线左缘新增「用户消息锚点栏」:每个用户输入一枚横向小条(12×3px 微圆角),紧凑等距排列并在时间线高度内垂直居中;条数超出栏高时相邻锚点自动聚合为稍宽的聚合条(悬停展开「N 条聚合输入」逐条列表,时间 + 单行预览,逐条点击定位)。悬停时小条向对话一侧平滑伸长(12→18px)并变橙,浮窗右侧弹出(白底圆角,正文最多 3 行、超出省略号,附序号与时间);单击平滑滚动定位到对应消息(精确停靠在视口顶部偏移 8px);覆盖视口顶部的锚点保持高亮。空会话自动隐藏;历史分页/流式更新/窗口缩放时自动重算(rAF 节流)。
45
+ - frontend-test 产物根可配置:`task.json.frontendTest.testRoot` 缺省仍为 `testcase/frontend`,省略时生成的 DAG 路径与历史默认根一致;自定义根(如 `qa/frontend-test`)在编译期冻结并驱动 writer、gate、playwright 围栏与 `allowedPaths`。`.harness/**` 等隐藏/治理树在生成期拒绝。
34
46
  - 实验能力「编辑失败节点提示词后从节点重跑」(默认关闭):内部开关 `HARNESS_DAG_RERUN_PROMPT_OVERRIDE=1` 启用后,`dag rerun` 支持 `--prompt-override <nodeId>@<file>`(全文替换)与 `--prompt-override-append <nodeId>@<file>`(追加指令:保留冻结原提示词,末尾追加 `<operator_addendum>` 块)。仅限 ERROR 且位于 reset closure 内的节点;mode + base/最终 prompt sha256 计入 planHash(execute 漂移即 `PLAN_DRIFT`),父 run spec 冻结、provenance(含 mode)落 `continuation.promptOverrides`。Console 新增 `dagPromptOverrideConfig` 开关探测 action,`dagRerunPlan`/`dagRerun` 接受 `promptOverrides`(`{nodeId, prompt, mode?}`);Inspect DAG 节点 inspector 新增「提示词重启」tab——默认「追加指令」模式(只读底稿 + 追加编辑器),可切换「全文替换(高级)」,预检通过展示执行估算,提交后续跑并跳转新 run,全程不离开图页;开关开启时 `/inspect` 只注入 **scoped** 变体凭据(独立 cookie + `__INSPECT_MUTATION_TOKEN__`,与 Console 通用 token 互不可重放),mutations 路由对 inspect 凭据仅放行 `dagRerunPlan`/`dagRerun`(其余 action 403 `ACTION_NOT_IN_INSPECT_SCOPE`),Inspect 不获得通用写能力;开关关闭时不注入任何 token、tab 不出现,检视面保持严格只读。
35
47
  - 观察面 DAG Inspector 节点详情适配 Pi 扩展:执行器输入摘要展示节点声明的 `piExtensions` 短 id;新增「Pi 扩展运行事实」展示 `pi-extensions.json` 的 resolved(包名@版本)与 missing 降级原因(缺包降级不再静默);产物未生成/损坏时降级提示不阻断接口,绝对路径不外泄。
36
48
  - 后端实现 DAG(standard / review-gated / supervised)的 Pi 节点支持按桶显式加载本机 Pi 导航扩展:读侧节点(除 closeout)加载 `pi-codegraph` + `pi-lens`,写侧节点只加载 `pi-codegraph`(pi-lens 的 `ast_grep_replace` 可绕过 writeSet,不加载);前端/测试模板与省略字段节点保持扩展全关。扩展发现始终关闭,只经白名单解析 `~/.pi/agent` 与项目 `.pi` settings 的已装包(缺包降级不失败,产物 `pi-extensions.json` 记录 requested/resolved/missing);session 后 pin 工具面剔 builtin 同名、deny 面与写工具,writer 的 `edit`/`write` 仍为 policy-wrapped;`.codegraph/**` index 同步不再计入 write-guard 越界。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.39.0-beta.5",
4
- "gitSha": "0981b80311d973603a870fd80693331cb5e48335",
5
- "builtAt": "2026-08-23T11:38:56.113Z"
3
+ "version": "0.39.0-beta.7",
4
+ "gitSha": "7f2b8f56e95923c3bde69fffa0575cfeae065c42",
5
+ "builtAt": "2026-08-24T02:16:38.537Z"
6
6
  }
@@ -1356,12 +1356,15 @@ export async function runClientRecovery(input) {
1356
1356
  }
1357
1357
  const plugin = await installProjectOpenCodePlugin({
1358
1358
  repoRoot: input.repoRoot,
1359
+ onlyIfMissing: true,
1359
1360
  });
1360
1361
  const overflowPlugin = await installProjectOpenCodeOverflowCompactPlugin({
1361
1362
  repoRoot: input.repoRoot,
1363
+ onlyIfMissing: true,
1362
1364
  });
1363
1365
  const piExtension = await installProjectPiContextOverflowExtension({
1364
1366
  repoRoot: input.repoRoot,
1367
+ onlyIfMissing: true,
1365
1368
  });
1366
1369
  const pi = await applyProjectPiSettingsMerge({ repoRoot: input.repoRoot });
1367
1370
  if (mode !== "user") {
@@ -355,6 +355,25 @@ export async function runTaskAdvance(repoRoot, args) {
355
355
  intervalMs: options.progressIntervalMs,
356
356
  });
357
357
  }
358
+ // Idempotent client-recovery surface install before any DAG run: the Pi
359
+ // context-overflow extension / OpenCode plugins / project .pi settings
360
+ // are normally installed by `loop-agent init`, but an upgraded or
361
+ // non-init-created project may lack them. Installing here (missing-only)
362
+ // ensures the overflow compact+retry recovery path is always present,
363
+ // independent of how the project was created. Best-effort: a failure to
364
+ // install never blocks the run.
365
+ if (options.approveGate && !options.dryRun) {
366
+ try {
367
+ const { runClientRecovery } = await import("./client-recovery.js");
368
+ // mode "project" installs only project-owned surfaces
369
+ // (.pi/extensions + .opencode/plugins + .pi/settings.json),
370
+ // never the user home; installs are missing-only.
371
+ await runClientRecovery({ repoRoot, mode: "project" });
372
+ }
373
+ catch {
374
+ // best-effort; do not fail the run over a recovery-surface install
375
+ }
376
+ }
358
377
  const result = await advanceTaskLifecycle(toUseCaseInput(repoRoot, taskId, options, progress?.observer));
359
378
  const outcome = mapOutcome(result);
360
379
  const gateStop = result.lifecycleState === "awaiting-write-set-approval" &&
@@ -15,6 +15,7 @@ import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelai
15
15
  import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
16
16
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
17
17
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
18
+ import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
18
19
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
19
20
  /**
20
21
  * Writer classification for a length-stopped thinking-only attempt: the model
@@ -25,6 +26,15 @@ import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
25
26
  * recoverable partial-write-set (incomplete-write-set) upgrade.
26
27
  */
27
28
  export const WRITER_THINKING_EXHAUSTED_CATEGORY = "writer-thinking-exhausted";
29
+ /**
30
+ * The writer session burned an excessive token budget (a read-edit-test loop
31
+ * that never converged) and still failed. Distinct from empty-output so the
32
+ * report shows the real cause and recovery recommends a fresh compacted run.
33
+ * Not auto-retried by default; operators may rerun after a model switch.
34
+ */
35
+ export const WRITER_BUDGET_EXHAUSTED_CATEGORY = "writer-budget-exhausted";
36
+ /** Token ceiling for a single writer node before it is judged budget-exhausted. */
37
+ export const WRITER_TOKEN_BUDGET = 2_000_000;
28
38
  function readWriterThinkingExhaustionEvidence(result) {
29
39
  const wider = result;
30
40
  return {
@@ -324,6 +334,49 @@ export function scanReviewTerminalKindsFromSessionEvents(content) {
324
334
  }
325
335
  return kinds;
326
336
  }
337
+ /** Read-only discovery tool budget for frontend-plan-pi. Exceeding it means
338
+ * the plan re-read upstream outputs/sources instead of trusting typed facts,
339
+ * which blows up the context window (400 request-too-large). */
340
+ const PLAN_READ_TOOL_BUDGET = 40;
341
+ /** Deterministic read-burst guard for frontend-plan-pi: count read-only
342
+ * discovery tool calls (read/grep/ls/find) from the session log. Over budget →
343
+ * read-burst, retried with a reduced-reading instruction. Pure scan; a
344
+ * successful plan under budget is never blocked. */
345
+ export async function detectPlanReadBurst(input) {
346
+ const sessionEventsPath = path.join(input.runDir, input.nodeId, "session-events.jsonl");
347
+ let count = 0;
348
+ try {
349
+ const content = await readFile(sessionEventsPath, "utf8");
350
+ for (const line of content.split("\n")) {
351
+ if (!line.trim())
352
+ continue;
353
+ try {
354
+ const event = JSON.parse(line);
355
+ if (event.type === "tool_execution_start" &&
356
+ typeof event.toolName === "string" &&
357
+ (event.toolName === "read" ||
358
+ event.toolName === "grep" ||
359
+ event.toolName === "ls" ||
360
+ event.toolName === "find")) {
361
+ count += 1;
362
+ }
363
+ }
364
+ catch {
365
+ // skip unparseable line
366
+ }
367
+ }
368
+ }
369
+ catch {
370
+ // Missing/unreadable session log → no burst detection
371
+ return [];
372
+ }
373
+ if (count > PLAN_READ_TOOL_BUDGET) {
374
+ return [
375
+ `frontend plan read-burst: ${count} read-only tool calls (budget ${PLAN_READ_TOOL_BUDGET}). Trust the upstream contract/scout typed facts; do not re-read contract/scout outputs or source files already captured. Minimize discovery reads, commit record_* facts directly, then finalize_plan.`,
376
+ ];
377
+ }
378
+ return [];
379
+ }
327
380
  export const FRONTEND_DESIGN_TERMINAL_TOOL_NAMES = new Set([
328
381
  "approve_design",
329
382
  "request_design_changes",
@@ -654,7 +707,9 @@ export async function createFrontendPlanLedgerTools(input) {
654
707
  const optionalStringArray = Type.Optional(stringArray);
655
708
  const requirementSchema = Type.Object({
656
709
  id: Type.String({}),
657
- expectedOutcome: Type.String({}),
710
+ expectedOutcome: Type.Optional(Type.String({
711
+ description: "Optional. When omitted, the runtime derives it from the contract requirement. Prefer omitting it to keep this tool call small.",
712
+ })),
658
713
  implementationTargets: stringArray,
659
714
  verificationTargetIds: stringArray,
660
715
  evidenceGap: Type.Optional(Type.Object({
@@ -706,7 +761,7 @@ export async function createFrontendPlanLedgerTools(input) {
706
761
  commandLabel: Type.String({}),
707
762
  file: Type.String({}),
708
763
  symbol: Type.Optional(Type.String({
709
- description: "Real exported identifier or describe/it title in the target file. Forbidden for type=static (runtime strips it); required for non-static targets.",
764
+ description: "Optional. Real exported identifier or describe/it title in the target file. Omit it to keep the call small — the trace gate then only verifies the file exists and the command ran. Forbidden for type=static (runtime strips it).",
710
765
  })),
711
766
  requirementIds: stringArray,
712
767
  uiStates: stringArray,
@@ -795,9 +850,9 @@ export async function createFrontendPlanLedgerTools(input) {
795
850
  const recordTargetSurfaceTool = defineTool({
796
851
  name: "record_target_surface",
797
852
  label: "record_target_surface",
798
- description: "Record the plan target surface (routes + files) as an origin=plan target-surface fact.",
799
- promptSnippet: "Record the plan target surface fact.",
800
- parameters: Type.Object({ targets: Type.Optional(targetsSchema), files: stringArray }, { additionalProperties: false }),
853
+ description: "Record the plan target surface (routes only) as an origin=plan target-surface fact. files are owned by the runtime skeleton / scout — omit them.",
854
+ promptSnippet: "Record the plan target surface (routes) fact.",
855
+ parameters: Type.Object({ targets: Type.Optional(targetsSchema), files: optionalStringArray }, { additionalProperties: false }),
801
856
  async execute(_toolCallId, params) {
802
857
  const routes = stringList(params?.targets?.routes);
803
858
  const files = stringList(params?.files);
@@ -808,24 +863,28 @@ export async function createFrontendPlanLedgerTools(input) {
808
863
  const recordComponentChoiceTool = defineTool({
809
864
  name: "record_component_choice",
810
865
  label: "record_component_choice",
811
- description: "Record component choices and styling strategy as an origin=plan component-choice fact.",
812
- promptSnippet: "Record the plan component-choice fact.",
866
+ description: "Record ONE component choice (origin=plan component-choice fact). Only declare components you add or replace; reuse of existing components needs no entry. Omit rationale for reuse-existing; it is optional. Call once per component — one tool call per message. Optionally include stylingStrategy (set it once, on the first call).",
867
+ promptSnippet: "Record one component choice (one tool call per message).",
813
868
  parameters: Type.Object({
814
- uiComponentChoices: Type.Optional(Type.Array(uiComponentChoiceSchema)),
869
+ choice: uiComponentChoiceSchema,
815
870
  stylingStrategy: optionalString,
816
871
  }, { additionalProperties: false }),
817
872
  async execute(_toolCallId, params) {
818
- const uiComponentChoices = Array.isArray(params?.uiComponentChoices)
819
- ? params.uiComponentChoices
820
- : [];
821
- const components = uiComponentChoices
822
- .map((choice) => typeof choice?.component === "string" ? choice.component : "")
823
- .filter(Boolean);
873
+ const rawChoice = params?.choice;
874
+ if (!isRecordObject(rawChoice)) {
875
+ return planToolReceipt({
876
+ ok: false,
877
+ kind: "component-choice",
878
+ error: "record_component_choice requires a non-empty choice object",
879
+ });
880
+ }
881
+ const choice = rawChoice;
882
+ const components = typeof choice.component === "string" ? [choice.component] : [];
824
883
  const result = await adoptPlanFact("component-choice", `${attemptId}:record_component_choice:${randomUUID()}`, {
825
884
  kind: "component-choice",
826
885
  origin: "plan",
827
886
  components,
828
- uiComponentChoices,
887
+ uiComponentChoices: [choice],
829
888
  ...(params?.stylingStrategy
830
889
  ? { stylingStrategy: params.stylingStrategy }
831
890
  : {}),
@@ -934,8 +993,8 @@ export async function createFrontendPlanLedgerTools(input) {
934
993
  const recordPlanRequirementTool = defineTool({
935
994
  name: "record_plan_requirement",
936
995
  label: "record_plan_requirement",
937
- description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement; each entry carries id, expectedOutcome, implementationTargets, verificationTargetIds, and optional evidenceGap.",
938
- promptSnippet: "Commit one plan requirement entry.",
996
+ description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement; entry carries id, implementationTargets, verificationTargetIds, and optional expectedOutcome (omit it — the runtime derives the outcome text from the contract requirement). IMPORTANT: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
997
+ promptSnippet: "Commit one plan requirement entry (one tool call per message).",
939
998
  parameters: Type.Object({
940
999
  entry: requirementSchema,
941
1000
  }, { additionalProperties: false }),
@@ -956,8 +1015,8 @@ export async function createFrontendPlanLedgerTools(input) {
956
1015
  const recordPlanVerificationTargetTool = defineTool({
957
1016
  name: "record_plan_verification_target",
958
1017
  label: "record_plan_verification_target",
959
- description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Call once per target; entry carries id, type, commandLabel, file, symbol, requirementIds, uiStates.",
960
- promptSnippet: "Commit one plan verification target entry.",
1018
+ description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Call once per target; entry carries id, type, commandLabel, file, requirementIds, uiStates, and optional symbol (omit it — the trace gate verifies the file and command, not a symbol). IMPORTANT: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
1019
+ promptSnippet: "Commit one plan verification target entry (one tool call per message).",
961
1020
  parameters: Type.Object({
962
1021
  entry: verificationTargetSchema,
963
1022
  }, { additionalProperties: false }),
@@ -978,8 +1037,8 @@ export async function createFrontendPlanLedgerTools(input) {
978
1037
  const recordPlanEvidenceGapTool = defineTool({
979
1038
  name: "record_plan_evidence_gap",
980
1039
  label: "record_plan_evidence_gap",
981
- description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking.",
982
- promptSnippet: "Commit one plan evidence gap entry.",
1040
+ description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking. IMPORTANT: call this tool exactly ONE time per assistant message — never batch multiple record_* calls together in one message; emit one call, wait for its result, then call the next.",
1041
+ promptSnippet: "Commit one plan evidence gap entry (one tool call per message).",
983
1042
  parameters: Type.Object({
984
1043
  entry: evidenceGapSchema,
985
1044
  }, { additionalProperties: false }),
@@ -1949,10 +2008,11 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1949
2008
  throw new Error(`unknown command capability: ${capability}`);
1950
2009
  }
1951
2010
  if (capability === "playwright-cli") {
1952
- const caseId = resolveCaseIdFromWriteSet(input.task.writeSet) ??
2011
+ const layout = frontendTestLayoutFromSpec(meta.spec);
2012
+ const caseId = resolveCaseIdFromWriteSet(input.task.writeSet, layout) ??
1953
2013
  input.task.id;
1954
- const evidenceDir = resolveEvidenceDirFromWriteSet(input.task.writeSet) ??
1955
- `testcase/frontend/evidence/${caseId}`;
2014
+ const evidenceDir = resolveEvidenceDirFromWriteSet(input.task.writeSet, layout) ??
2015
+ `${layout.evidenceDir}/${caseId}`;
1956
2016
  playwrightToolContext = {
1957
2017
  repoRoot: input.cwd,
1958
2018
  runDir: meta.runDir,
@@ -1960,7 +2020,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1960
2020
  caseId,
1961
2021
  evidenceDir,
1962
2022
  baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir),
1963
- inputRoot: "testcase/frontend/fixtures",
2023
+ inputRoot: layout.fixturesDir,
2024
+ layout,
1964
2025
  };
1965
2026
  // Cleanup must be confirmed before a new case; otherwise default-session
1966
2027
  // isolation is unknown and no Pi invocation is permitted.
@@ -2272,6 +2333,23 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
2272
2333
  catch {
2273
2334
  // best-effort flush; missing ledger still fails at the node validator
2274
2335
  }
2336
+ // Read-burst guard: a plan that burned dozens of read/grep/ls/find
2337
+ // calls (re-reading upstream outputs and source files it should trust
2338
+ // from typed facts) blows up the context window and eventually fails
2339
+ // with 400 request-too-large. Detect it deterministically from the
2340
+ // session log and retry with a reduced-reading instruction.
2341
+ const readBurstIssues = await detectPlanReadBurst({
2342
+ runDir: meta.runDir,
2343
+ nodeId: input.task.id,
2344
+ });
2345
+ if (readBurstIssues.length > 0) {
2346
+ return {
2347
+ ...mapped,
2348
+ ok: false,
2349
+ failureCategory: "read-burst",
2350
+ stderr: [mapped.stderr, ...readBurstIssues].filter(Boolean).join("\n\n"),
2351
+ };
2352
+ }
2275
2353
  }
2276
2354
  if (!isWriteTask) {
2277
2355
  return mapped;
@@ -2612,6 +2690,12 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
2612
2690
  changeManifestChangedFiles !== undefined &&
2613
2691
  changeManifestChangedFiles.length === 0 &&
2614
2692
  writeToolCallCount === 0;
2693
+ // Budget-exhausted: the provider session consumed an extreme amount of
2694
+ // tokens (e.g. an unresolved read-edit-test loop) and still failed. Only
2695
+ // applies on a non-ok writer result; a successful write is never re-labeled.
2696
+ const writerBudgetExhausted = !mapped.ok &&
2697
+ typeof mapped.tokensUsed === "number" &&
2698
+ mapped.tokensUsed > WRITER_TOKEN_BUDGET;
2615
2699
  const targetCleanTransport = !mapped.ok &&
2616
2700
  isTargetTemplateTransientRetryNode(input.task) &&
2617
2701
  rawFailureCategory !== undefined &&
@@ -2655,17 +2739,35 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
2655
2739
  mapped.failureCategory === "invalid-output" ||
2656
2740
  mapped.failureCategory === WRITER_EMPTY_DIFF_RETRY_CATEGORY)
2657
2741
  ? INCOMPLETE_WRITE_SET_RETRY_CATEGORY
2658
- : // writer-thinking-exhausted: a length-stopped thinking-only attempt with
2659
- // zero write tool calls and zero attributed diff is terminal and
2660
- // non-retryable; recommend a model switch + fresh run. Only applies on
2661
- // the empty-output base category so provider/transport failures keep
2662
- // their original category, and only when the completeness gate did not
2663
- // upgrade to incomplete-write-set above.
2664
- writerThinkingExhausted
2665
- ? WRITER_THINKING_EXHAUSTED_CATEGORY
2666
- : writerCleanTimeout
2667
- ? WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY
2668
- : mapped.failureCategory,
2742
+ : // partial-success-with-context-overflow: the writer hit a context
2743
+ // overflow on its final provider call but had already persisted
2744
+ // real file changes (change-manifest non-empty). Classify it
2745
+ // distinctly so the report shows "code largely done, verification
2746
+ // incomplete" instead of a misleading empty-output / spec issue,
2747
+ // and recovery can rerun from verify/implement with a compacted
2748
+ // session rather than asking the operator for spec clarification.
2749
+ rawFailureCategory === "context-overflow" &&
2750
+ (changeManifestChangedFiles?.length ?? 0) > 0
2751
+ ? "partial-success-with-context-overflow"
2752
+ : // writer-thinking-exhausted: a length-stopped thinking-only attempt with
2753
+ // zero write tool calls and zero attributed diff is terminal and
2754
+ // non-retryable; recommend a model switch + fresh run. Only applies on
2755
+ // the empty-output base category so provider/transport failures keep
2756
+ // their original category, and only when the completeness gate did not
2757
+ // upgrade to incomplete-write-set above.
2758
+ writerThinkingExhausted
2759
+ ? WRITER_THINKING_EXHAUSTED_CATEGORY
2760
+ : writerCleanTimeout
2761
+ ? WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY
2762
+ : // writer-budget-exhausted: the provider session consumed an
2763
+ // excessive amount of tokens (a read-edit-test loop that never
2764
+ // converged) and still failed. Classify distinctly so the
2765
+ // report says the run burned its budget instead of a generic
2766
+ // empty-output, and recovery recommends a fresh compacted
2767
+ // run rather than spec-clarification.
2768
+ writerBudgetExhausted
2769
+ ? WRITER_BUDGET_EXHAUSTED_CATEGORY
2770
+ : mapped.failureCategory,
2669
2771
  durationMs: mapped.durationMs || Date.now() - started,
2670
2772
  };
2671
2773
  }
@@ -1012,6 +1012,13 @@ export function classifyPiFailure(input) {
1012
1012
  if (input.exitCode === 0 && input.assistantText.trim())
1013
1013
  return "success";
1014
1014
  const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
1015
+ // Context overflow (400 request-too-large / context window exceeded).
1016
+ // Must be detected BEFORE the generic rate-limit/quota phrases: an overflow
1017
+ // error often contains "too many tokens" / "maximum context" which would
1018
+ // otherwise be misread as rate-limit or quota. Kept in sync with
1019
+ // CONTEXT_OVERFLOW_PHRASES in commands/client-recovery.ts.
1020
+ if (/请求上下文过大|context_length_exceeded|context overflow|context length overflow|too many tokens|maximum context|prompt is too long|request_too_large|context window/.test(combined))
1021
+ return "context-overflow";
1015
1022
  // Prefer explicit rate-limit / HTTP 429 before generic "limit" quota phrases.
1016
1023
  // Chinese gateways often say "请求频率已达到限制" without English "rate limit".
1017
1024
  if (/rate.?limit|too many requests|\b429\b|请求频率|频率.*限制|rpm|tpm/.test(combined))
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
6
6
  import { resolvePlaywrightCliLauncher } from "./playwright-cli-launcher.js";
7
7
  import { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, parsePlaywrightCliFindMatchCount, } from "../shared/playwright-cli-command-policy.js";
8
+ import { defaultFrontendTestLayout, escapeRegexLiteral, } from "../workflows/dag/frontend-test-layout.js";
8
9
  export { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, };
9
10
  /** Logical capability name only; execution always uses the verified JS launcher. */
10
11
  export const PLAYWRIGHT_CLI_EXECUTABLE = "playwright-cli";
@@ -267,7 +268,8 @@ function validateOpenArgs(args, baseUrl) {
267
268
  }
268
269
  function resolveEvidenceRelative(rawPath, ctx) {
269
270
  const evidenceDir = normalizePosix(ctx.evidenceDir).replace(/\/$/, "");
270
- const evidencePrefix = `testcase/frontend/evidence/${ctx.caseId}`;
271
+ const layout = ctx.layout ?? defaultFrontendTestLayout();
272
+ const evidencePrefix = `${layout.evidenceDir}/${ctx.caseId}`;
271
273
  if (!(evidenceDir === evidencePrefix ||
272
274
  evidenceDir.startsWith(`${evidencePrefix}/`))) {
273
275
  throw new PlaywrightCliPolicyError("evidence-dir-invalid", `evidenceDir must be under ${evidencePrefix}`);
@@ -291,7 +293,7 @@ function resolveEvidenceRelative(rawPath, ctx) {
291
293
  if (candidate.startsWith(`${evidenceDir}/`) || candidate === evidenceDir) {
292
294
  return candidate;
293
295
  }
294
- if (candidate.startsWith("testcase/frontend/evidence/") &&
296
+ if (candidate.startsWith(`${layout.evidenceDir}/`) &&
295
297
  !candidate.startsWith(`${evidencePrefix}/`)) {
296
298
  throw new PlaywrightCliPolicyError("path-cross-case", "output path escapes current case evidenceDir");
297
299
  }
@@ -472,11 +474,12 @@ function assertInputPathAllowed(rawPath, ctx) {
472
474
  if (!isSafeRelativePosix(candidate)) {
473
475
  throw new PlaywrightCliPolicyError("input-path-unsafe", `unsafe input path: ${rawPath}`);
474
476
  }
477
+ const layout = ctx.layout ?? defaultFrontendTestLayout();
475
478
  const allowedPrefixes = [
476
- `testcase/frontend/evidence/${ctx.caseId}/`,
477
- "testcase/frontend/cases/",
478
- "testcase/frontend/rag/",
479
- "testcase/frontend/fixtures/",
479
+ `${layout.evidenceDir}/${ctx.caseId}/`,
480
+ `${layout.casesDir}/`,
481
+ `${layout.ragDir}/`,
482
+ `${layout.fixturesDir}/`,
480
483
  ];
481
484
  if (!allowedPrefixes.some((prefix) => candidate === prefix.slice(0, -1) || candidate.startsWith(prefix)) &&
482
485
  !candidate.startsWith(normalizePosix(ctx.evidenceDir) + "/")) {
@@ -629,8 +632,8 @@ async function validatePlaywrightCliOutputPath(command, args, ctx) {
629
632
  if (!outputFilename)
630
633
  return;
631
634
  const repoRoot = path.resolve(ctx.repoRoot);
632
- const caseEvidenceRoot = path.resolve(repoRoot, "testcase", "frontend", "evidence", ctx.caseId);
633
- const evidenceDir = path.resolve(repoRoot, ctx.evidenceDir);
635
+ const caseEvidenceRoot = path.resolve(repoRoot, ctx.evidenceDir);
636
+ const evidenceDir = caseEvidenceRoot;
634
637
  const target = path.resolve(repoRoot, outputFilename);
635
638
  if (!pathIsContained(repoRoot, caseEvidenceRoot) ||
636
639
  !pathIsContained(caseEvidenceRoot, evidenceDir) ||
@@ -1024,22 +1027,26 @@ export async function createPlaywrightCliTool(ctx) {
1024
1027
  export const PI_COMMAND_CAPABILITY_REGISTRY = {
1025
1028
  "playwright-cli": createPlaywrightCliTool,
1026
1029
  };
1027
- export function resolveCaseIdFromWriteSet(writeSet) {
1030
+ export function resolveCaseIdFromWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
1031
+ const prefix = layout.evidenceDir.replace(/\/$/, "");
1032
+ const escaped = escapeRegexLiteral(prefix);
1033
+ const exact = new RegExp(`^${escaped}/([^/]+)(?:/\\*\\*)?$`);
1034
+ const nested = new RegExp(`^${escaped}/([^/]+)/`);
1028
1035
  for (const entry of writeSet ?? []) {
1029
1036
  const normalized = normalizePosix(entry);
1030
- const match = normalized.match(/^testcase\/frontend\/evidence\/([^/]+)(?:\/\*\*)?$/);
1037
+ const match = normalized.match(exact);
1031
1038
  if (match?.[1] && match[1] !== "**" && match[1] !== "*") {
1032
1039
  return match[1];
1033
1040
  }
1034
- const nested = normalized.match(/^testcase\/frontend\/evidence\/([^/]+)\//);
1035
- if (nested?.[1])
1036
- return nested[1];
1041
+ const nestedMatch = normalized.match(nested);
1042
+ if (nestedMatch?.[1])
1043
+ return nestedMatch[1];
1037
1044
  }
1038
1045
  return undefined;
1039
1046
  }
1040
- export function resolveEvidenceDirFromWriteSet(writeSet) {
1041
- const caseId = resolveCaseIdFromWriteSet(writeSet);
1047
+ export function resolveEvidenceDirFromWriteSet(writeSet, layout = defaultFrontendTestLayout()) {
1048
+ const caseId = resolveCaseIdFromWriteSet(writeSet, layout);
1042
1049
  if (!caseId)
1043
1050
  return undefined;
1044
- return `testcase/frontend/evidence/${caseId}`;
1051
+ return `${layout.evidenceDir}/${caseId}`;
1045
1052
  }