@tea-agent/loop-agent 0.44.0-next.12 → 0.44.0-next.14

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 (188) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +686 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/dag-pi-executor.js +11 -6
  13. package/dist/executors/pi-sdk-executor.js +3 -1
  14. package/dist/executors/pi-writer-tool-policy.js +60 -4
  15. package/dist/executors/playwright-cli-launcher.js +31 -45
  16. package/dist/executors/shell-write-guard.js +24 -67
  17. package/dist/governance/exec-plans.js +10 -1
  18. package/dist/governance/harness.js +2 -7
  19. package/dist/governance/manifest-types.js +28 -9
  20. package/dist/governance/project-capability-scanner.js +263 -0
  21. package/dist/governance/project-config.js +96 -0
  22. package/dist/governance/resource-catalog.js +151 -0
  23. package/dist/infrastructure/console/app-data.js +6 -2
  24. package/dist/shared/operator/capabilities.js +2 -2
  25. package/dist/shared/package-metadata.js +23 -7
  26. package/dist/shared/pi-context-pressure/extension.js +23 -0
  27. package/dist/shared/pi-context-pressure/index.js +1 -0
  28. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  29. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  30. package/dist/shared/update/npm-client.js +49 -1
  31. package/dist/task/config-types.js +4 -2
  32. package/dist/task/source-prepare/build-draft.js +1 -0
  33. package/dist/task/source-prepare/path-policy.js +17 -7
  34. package/dist/worker/cli.js +3 -5
  35. package/dist/worker/console/chat/browser-automation.js +455 -41
  36. package/dist/worker/console/chat/browser-content.js +74 -0
  37. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  38. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  39. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  40. package/dist/worker/console/chat/browser-pick.js +113 -0
  41. package/dist/worker/console/chat/browser-routes.js +54 -5
  42. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  43. package/dist/worker/console/chat/browser-storage.js +89 -0
  44. package/dist/worker/console/chat/chat-event-store.js +7 -6
  45. package/dist/worker/console/chat/context-insights.js +84 -19
  46. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  47. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  48. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  49. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  50. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  51. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  52. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  53. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  54. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  55. package/dist/worker/console/chat/pi-runtime.js +8 -3
  56. package/dist/worker/console/chat/repo-browser.js +80 -2
  57. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  58. package/dist/worker/console/chat/routes.js +43 -7
  59. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  60. package/dist/worker/console/chat/session-mode.js +1 -1
  61. package/dist/worker/console/chat/session-store.js +24 -5
  62. package/dist/worker/console/chat/tools.js +12 -0
  63. package/dist/worker/console/console-shutdown.js +34 -0
  64. package/dist/worker/console/server.js +2 -1
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  97. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  98. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  124. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  125. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  126. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  127. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  129. package/dist/worker/console/workspace-context.js +15 -0
  130. package/dist/worker/console/workspace-initialization.js +25 -16
  131. package/dist/worker/delivery/package.js +4 -1
  132. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  133. package/dist/worker/observe/static/console-theme.css +33 -29
  134. package/dist/worker/observe/static/operator-chrome.js +1 -42
  135. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  136. package/dist/workflows/dag/backend-test-layout.js +27 -7
  137. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  138. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  139. package/dist/workflows/dag/final-verification.js +45 -10
  140. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  141. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  142. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  143. package/dist/workflows/dag/hybrid/sources.js +35 -16
  144. package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
  145. package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
  146. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  147. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  148. package/dist/workflows/dag/skill-instructions.js +102 -32
  149. package/dist/workflows/dag/skill-snapshot.js +3 -0
  150. package/dist/workflows/dag/skills.js +7 -7
  151. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  152. package/dist/workflows/dag/validate.js +24 -5
  153. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  154. package/docs/architecture/runtime-boundaries.md +28 -22
  155. package/docs/init-surface.manifest.json +43 -295
  156. package/docs/templates/README.md +4 -4
  157. package/docs/templates/agent-dag.base.json +9 -9
  158. package/docs/templates/agent-dag.final-verification.json +6 -6
  159. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  160. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  161. package/docs/templates/backend-test-dag.json +708 -708
  162. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  163. package/docs/templates/frontend-implementation-dag.json +2 -2
  164. package/docs/templates/frontend-test-case-checklist.md +2 -2
  165. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  166. package/docs/templates/frontend-test-dag.json +48 -39
  167. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  168. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  169. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  170. package/docs/templates/hybrid-dag.json +9 -9
  171. package/docs/templates/knowledge-sync-dag.json +8 -8
  172. package/package.json +12 -3
  173. package/resource-catalog.v1.json +1973 -0
  174. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  175. package/resources/init-history/legacy-surface.v1.json +33 -0
  176. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  178. package/skills/init-update/SKILL.md +34 -0
  179. package/skills/init-update/agents/openai.yaml +6 -0
  180. package/skills/playwright-cli/SKILL.md +1 -1
  181. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  182. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  183. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  185. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  186. package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
  187. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  188. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
package/CHANGELOG.md CHANGED
@@ -1,12 +1,28 @@
1
1
  # 更新日志
2
2
 
3
3
  ## [Unreleased]
4
+ - 修复 GitHub Actions Linux 上工作台浏览器起不来的问题:CI 没有可用 Chromium sandbox 时不再强制沙箱,桌面端仍保持沙箱以免出现 "unsupported command-line flag" 提示条。
5
+ - 对话预约漏填预算时,默认最多 5 次目标尝试、5 次任务执行和 4 次监督,执行窗口 240 分钟;确认卡会展示这些上限,第一次没通过还可以在预算内继续修。显式指定更小预算或更短窗口不会被抬高;写保护、无进展或监督决定停止时仍会停。已确认的旧预约不受影响。
6
+ - Operator Console 新增桌面宠物:移植 dsh-pet 的状态机、好感度、互动冷却、任务奖励与零食结算,宠物会跟随 Chat 的思考、工具调用、回复、完成和失败状态切换动画;支持拖动、摸摸、喂食及持久化;默认关闭,可在对话设置的「宠物」页开启。首个素材不复用 dsh 图片,改为仓库内脚本确定性生成的原创矢量橙色小精灵「小橘灵」:圆滚滚身体、小短手小短腿、叶子呆毛和火焰尾巴,嘴、手、尾巴、头部按对话状态做关节动画。
7
+ - 上下文洞察剪枝精确化:压缩/剪枝事件现在按 `shadowedSeqs` 或 `firstKeptEntryId` 锚点从活账本中精确移除对应消息(用户/助手/工具结果),并同步扣减组合占比与上下文浏览器行;事件卡新增 `removedIds`/`removalBasis`,趋势图在剪枝后反映真实净值而非按比例估算。同时引入 dsh 风格工具结果中段剪枝(overflow 时自动 armed,确定性保头尾),并修复洞察面板在部分事件序列下的类型问题。
4
8
 
5
9
  ### 新增
6
10
 
11
+ - 工作台浏览器预览新增「镜像外部窗口」视图:外部受控浏览器登录后,右侧预览直接展示同一页面的登录态(不再显示独立登录页),外部窗口的导航与操作实时同步到预览;预览里的地址栏、前进/后退/刷新也直接驱动外部窗口。镜像流只在页面变化时产生画面、只在浏览器已启动时工作,浏览器未运行时自动回退到原有的独立 iframe 预览(可点击工具栏按钮手动切换)。
12
+ - Context pressure 现在会在压力触发后对超大 Pi 工具结果做确定性的 DSH 风格头/标记/尾剪枝:按 Unicode 码点限制文本、保留图片等富内容顺序、原始会话记录不变,并通过 telemetry 记录剪枝条目与移除量;Console 与 DAG Pi executor 均支持配置开关和预算。
13
+
14
+ - Console Chat 浏览器控制补齐页面 JavaScript、登录态导入导出、带行上下文的元素查找、Markdown 内容抽取和外部窗口人工点选;条件等待可确认文本、元素或网址变化,多个对话保持各自选中的标签页,旧页面快照不会被误用于其他页面。
15
+ - 浏览器截图可指定标签页和完整页面,作为图片发送给支持视觉的模型,并在工具详情中查看保存的截图;不支持视觉或图片过大时明确提示。右侧网页预览明确标注为独立页面,避免与实际受控窗口的登录态、操作混淆。
16
+ - 登录态文件保存在仓库外,默认不把 cookie 值写入工具结果;导入显式合并 cookies/localStorage,保留未列出的现有数据。页面脚本超时后提示等待恢复或关闭该标签页;现有 Chrome 接管和 frontend-test 自动加载登录态仍为后续能力。
17
+ - Operator Chat 的「预览」面板支持图片预览:`.png` / `.jpg` / `.jpeg` / `.gif` / `.webp` / `.bmp` / `.ico` / `.svg` 直接在预览区展示,宽图按面板宽度缩放、超高图纵向滚动;位图不再误走文本解码、不再提示「binary files cannot be previewed」。SVG 可当图查看,也能切回 XML 源码;新窗口打开图片直接展示原图。
18
+ - 无渲染器的二进制容器(zip/mp4/pdf/docx 等常见格式)在预览区显示「此文件类型无法预览」空态说明,不再报错;未知后缀仍按文本读取,含二进制内容时保持明确报错。
19
+ - 前后端测试工作流的产物默认收进统一的运行时目录:后端测试默认写到运行时目录下的测试区,前端浏览器测试默认写到运行时目录下的测试区,不再混入项目根目录生成 `testcase/`;自定义目录仍可继续使用,已有产物不迁移不删除。新目录随运行时目录一起被 Git 忽略,测试运行仍会逐文件对比哈希来证明真实产出,不会因为目录被忽略而误判为“没有写入”;控制器自己的运行记录不受影响。
20
+ - 项目初始化生成的忽略规则新增 `.playwright-cli/`(浏览器测试工具缓存目录),避免工具缓存被误提交;已初始化的项目按统一更新机制刷新,用户自己写的忽略规则保持不变。 5642d882 (feat(test-workflows): 前后端测试产物默认收进运行时目录,init gitignore 增 .playwright-cli/)
7
21
  - 前端 DAG 的行为验证不再只认 Vitest:测试框架改为可插拔适配层,首批支持 Vitest、Jest 与 Playwright Test。三类项目都能用**真实报告**证明契约绑定的测试文件确实执行且通过,`npm`/`pnpm`/`yarn` 的常用测试入口走同一套识别逻辑,直接命令与 `package.json` 脚本不会再得出不同结论。
8
22
  - 未适配的测试框架(Cypress、`node --test`、Mocha、自定义脚本等)现在明确报「缺少适配能力」,并附上具体命令与原因;此前这类命令会被判为不支持,错误信息还暗示你「必须改用 Vitest」,把工具的能力缺口显示成了用户配置错误。
9
23
  - 行为验证保留「测试确实执行过」的校验:零测试、全部跳过、报告缺失、报告过期、契约绑定文件未执行都判失败,不会退回只看退出码(全部跳过的运行退出码是 0)。
24
+ - Console 对话中的浏览器工具支持原生下拉选择、显式勾选/取消,以及按标签页增量读取控制台日志和网络请求状态;诊断记录有容量上限并提示历史缺口,便于定位页面异常、接口失败与弹窗问题,无需打开额外诊断面板。
25
+ - 前端测试随 loop-agent 安装固定版本的 Playwright CLI,无需另装全局 CLI;运行时使用当前 loop-agent 安装的依赖,避免目标项目或 PATH 中的其他版本干扰。依赖缺失或版本不符时明确失败并提示重新安装;浏览器仍需单独准备,不在安装时自动下载。
10
26
 
11
27
  - main 上的 npm `next` 预发布改为跑默认测试(fast + integration),不再在每次发布时重复跑约 6 分钟的 I/O-heavy 池;完整三池仍由 PR、正式 release 线、本地推送前检查与发布前钩子承担。同时把该预发布任务的 I/O 并发从 1 调到 2,并把 npm 包可见性确认的等待窗口从约 3 分钟拉长到约 10 分钟,避免包已发出却因仓库异步处理未完成而被判失败。
12
28
 
@@ -27,7 +43,7 @@
27
43
 
28
44
  - Console 操作文案改为面向操作者的说明并给出下一步:等待超时不再伪装成「需要核对」,改为提示操作可能仍在后台运行;「源内容已变更」「前端拆分状态被并发修改」等原本直接展示英文原文的错误,改为说明原因与具体处理步骤;恢复页按钮与提示去掉 reconcile / supersede / orphan / Human Gate 等术语。
29
45
 
30
- - Operator Chat 新增对话驱动的真实浏览器自动化:右侧「浏览器」面板同步共享状态,登录态与下载隔离在仓库外;模型可操作同一浏览器完成导航、搜索、结构化快照、元素操作与截图,不读写仓库文件,也不暴露页面脚本执行。
46
+ - Operator Chat 新增对话驱动的真实浏览器自动化:右侧「浏览器」面板同步网址,登录态与下载隔离在仓库外;模型通过外部受控窗口完成导航、搜索、结构化快照、元素操作与截图,页面脚本执行由显式 browser_eval 提供。
31
47
  - 模型驱动的共享浏览器默认改为可见外部窗口,并保留右侧预览;地址、搜索与元素操作不再默认生成大截图,搜索逐字填写并提交真实表单,启动时降低自动化指纹;无法嵌入的页面提供外部打开与聚焦入口,同一持久配置可完成站点安全验证。
32
48
  - 修复观测页「执行历史」把仍在重试的节点误报为「最终失败」的问题:只要节点最后一个已记录的尝试失败,就会显示「N 次 · 最终失败」,即使该节点仍在运行、重试已经排队,会让人误判运行已结束。现在节点处于运行/已启动/暂停状态时改为「N 次 · 最近一次失败,仍在重试」,真正到达终态才显示「最终失败」;修复轮次的多轮标签同样处理。
33
49
 
@@ -141,6 +157,11 @@
141
157
 
142
158
  ### 改进
143
159
 
160
+ - 项目初始化改为最小接入:不再改写 README、AGENTS,也不再复制整套文档、脚本和内置技能。目标项目只保留本机运行配置与一个外部智能体入口,通用能力会随 Loop Agent 程序更新,不需要逐个项目同步。
161
+ - 旧项目升级会清理仍未修改的初始化副本;用户改过的文档、脚本和技能会原样保留。升级前若还有任务运行或需要人工判断的内容,会停止写入并给出下一步,原有 DAG 使用方式不变。
162
+ - Console 对话浏览器新增 `LOOP_BROWSER_HEADLESS=1` 可选后台(headless)运行:不弹窗口、不抢焦点,导航、快照、点选、表单、截图、控制台/网络诊断等能力全部保留,右侧网页面板预览不受影响;后台模式下点选与「聚焦外部窗口」会明确提示原因。默认仍为可见窗口,人工点选开箱即用。同时不再向 Chrome 传递 `--no-sandbox`,启动时的「不受支持的命令行标记」黄条消失。
163
+ - Console「浅色」主题改为 DeepSeek Harness 浅色家族:白底、侧栏冷灰、近黑正文、发送与主操作为 DeepSeek 蓝,用户气泡为淡蓝;默认暖纸和深色主题不变。
164
+ - 对话时间线的「紧凑 / 完整」改为 DSH 文字 Tab:去掉小卡片边框与全局 34px 按钮高度,选中项用品牌色下划线。
144
165
  - DAG 运行时内部按职责拆开:模型配置、领域工具、分段会话、模板族、终态判定与检查点各自独立维护,测试也按行为分成可单独调度的文件。命令、任务流程与用户可见结果不变。本机默认测试并行下整池耗时未证实变快。
145
166
  - Operator Chat 输入框下方的会话统计对齐 DSH:长串静态文字收敛为两个可点击摘要,可分别查看模型/工具用时、TTFT、TPS 与 Token 分项;详情卡互斥并支持外部点击或 Escape 关闭,窄屏打开时不会推动输入框或消息流。
146
167
  - Operator Chat 长会话新增完整轮次轨道与按需历史定位,可从任意用户消息精确新建会话;历史图片刷新后仍可预览,仓库文件链接可直接定位到行,最终回答显示单轮 token/耗时,并支持可持久化的紧凑/完整阅读模式。图片文件与消息记录分离保存,删除、分叉和失败回滚会同步清理,附件读取重新校验类型与引用。
@@ -154,7 +175,11 @@
154
175
 
155
176
  ### 修复
156
177
 
157
- - 前端 DAG 的验证失败不再把配置问题算到业务代码头上:报告缺失或过期、报告协议错误、命令或脚本哈希漂移、框架未适配都归为验证配置问题,不再触发一轮无意义的实现重写;测试断言失败仍归实现问题。
178
+ - 修复旧项目升级在 Windows 执行 `npm` 验证时可能因命令 shim 启动方式报 `EINVAL` 的问题;升级器也能正确比较当前 beta/prerelease 与更新的稳定版本,不再把可用更新误判为“已是最新”。
179
+ - `agent-worker console` 第一次 Ctrl+C 仍会尝试正常关闭;若关闭卡住(例如主线程正在序列化超大会话),第二次 Ctrl+C 或约 2 秒超时会强制退出,不再出现连按几十次都不退、端口一直被占的情况。
180
+ - Console Chat 的浏览器搜索工具在更多真实站点上可用:现代搜索框(`textarea`、`role=searchbox`、`name=search`)不再报“找不到搜索框”;对只在页面内更新结果、不发生跳转的搜索表单也不会超时失败;提交时只在搜索框所属表单内查找提交按钮,不再误点页面上无关的按钮导致“已输入但未提交”。
181
+ - 浏览器导航在页面于加载完成后才发生跳转(如搜索引擎跳转到区域站点)时,不再把后续操作带入上一页;跳转后会等待短暂稳定再返回。
182
+ - 浏览器搜索的默认搜索引擎改为 Bing(可从参数改写)。此前默认的百度对自动化已不可用(新版为文本框加验证码),模型不指定引擎时搜索会直接失败。
158
183
  - 已经自带 `--reporter`/`--outputFile` 的测试命令或脚本不会被悄悄覆盖;无法安全注入报告参数时在生成阶段直接报错,要求改用可注入的入口。
159
184
  - 修复 Playwright 项目在 Review 阶段被误判失败的问题:审查重读报告时没有带上协议,仍按 Vitest 解析,导致 Playwright 报告因缺少 `testResults` 报错,即使执行与 trace 已经通过。report 引用现在记录协议,review 按同一协议解析并校验一致性。
160
185
  - 修复 Playwright 报告路径按 `testDir` 解析错误的问题:报告中的文件路径相对 Playwright 自身的 `config.rootDir`,此前按进程 cwd 解析,配了 `testDir: "./tests"` 的项目会得到 `orders.spec.js` 而非 `tests/orders.spec.js`,契约目标永远无法匹配。
package/README.md CHANGED
@@ -48,37 +48,36 @@ npm install -g @tea-agent/loop-agent@0.37.5 # 精确、可复现
48
48
  ```bash
49
49
  loop-agent init instructions --repo-root .
50
50
  loop-agent init model-catalog --repo-root . --json
51
- loop-agent init --repo-root . --profile full --merge \
51
+ loop-agent init --repo-root . --merge \
52
52
  --low-model <provider/model> --med-model <provider/model> --high-model <provider/model>
53
53
  loop-agent init doctor --repo-root .
54
54
  loop-agent inspect --repo-root .
55
55
  ```
56
56
 
57
- `init model-catalog` 会通过 Pi runtime 与 OpenCode 的非 verbose 列表发现本机可用模型,只输出白名单模型元数据、Pi setup 状态和 provider 凭据就绪状态,不输出原始配置或凭据。已有 LOW/MED/HIGH 只要仍存在于 Pi 就保持不变;只有已移除或缺失的档位才由模型结合项目、能力、成本和上下文窗口重选。对于 `wizard-local` provider,目录会识别 `aicoder-default < aicoder-plus < aicoder-pro < aicoder-max` 的家族内相对能力顺序,但不会把它机械绑定为固定三档。如果 Pi 没有模型定义,初始化模型会先向用户补齐 provider/model、API/base URL、上下文窗口、最大输出等无法推断的信息并协助配置;如果只是凭据缺失,则保留 harness 选择并修复认证。初始化会保留已有用户内容,并补充 `AGENTS.md`、`harness.json`、`ai_workspace/loop-agent/`、`.agents/skills/`、`.harness/` 和验证脚本等治理入口。
57
+ 初始化不会改写项目的 `README.md` 或 `AGENTS.md`,也不会复制整套治理文档、脚本、模板和内置 Skills。它只维护本机 `harness.json`、最小 `.gitignore` 块、`.harness/init-surface.json`,以及供外部 Agent 发现 loop-agent 的 `.agents/skills/loop-agent/SKILL.md` 桥。DAG 所需的通用 Skills、模板和规则直接从当前 npm 包读取;项目自己的 `AGENTS.md` 和自定义 Skills 保持原样。
58
58
 
59
- 控制器升级后,旧项目可能仍使用过期的 repo-local skills、模板、managed blocks 和治理说明。普通安全仓库命令结束时会自动检测目标项目是否需要对齐:非 TTY 只向 stderr 输出提示且不写入目标;TTY 且无 human decisions、无活跃 DAG/Worker 时,可经明确 `y/yes` 同意后应用 deterministic safe actions;也可手动运行统一入口:
59
+ `init model-catalog` 只返回可用模型的必要元数据与就绪状态,不输出原始配置或凭据。已有 LOW/MED/HIGH 只要仍可用就保持不变;缺失档位才需要重选。`--profile` 和 `--client-recovery` 已移除,传入时会给出明确错误;客户端适配不再随初始化隐式安装。
60
+
61
+ 控制器升级后,可先只读检查目标项目:
60
62
 
61
63
  ```bash
62
- loop-agent init reconcile --repo-root .
64
+ loop-agent init check-update --repo-root . --json
63
65
  ```
64
66
 
65
- `init reconcile` 在 surface 缺失、存在 human decisions、存在活跃 DAG/Worker 或无法确认 Worker 状态时零写入,并返回 `needs-baseline`、`needs-human-decision` 或 `blocked-active-runtime`;其余情况复用 `init update --apply-safe` 的安全动作并复查返回 `clean`、`needs-model-merge` 或 `needs-safe-update`。
66
-
67
- 写入型自然语言请求「`loop-agent初始化更新`」或「`loop-agent 初始化更新`」必须进入可恢复的统一闭环,而不是只做检查:
67
+ 旧版完整初始化项目使用可恢复升级入口:
68
68
 
69
69
  ```bash
70
70
  loop-agent init upgrade --repo-root . --json
71
- # controller 返回当前单文件 merge task 后(其余任务已在同一次计划中排队):
72
71
  loop-agent init upgrade --repo-root . --run-id <run-id> --continue --json
73
72
  ```
74
73
 
75
- 它在 `.harness/init-upgrades/<run-id>/` 保存身份、一次性模型目录与 surface 计划、动作收据、合并队列、验证与报告。固定投影文件按 recorded ownership 确定性新增、更新或安全删除;用户改过的文件和项目自适应内容才进入模型合并。多个合并仍逐文件限制 `allowedPaths`,但接受后直接激活下一项,不重复版本发现和全量规划;全部处理完只执行一次最终验证。`harness.json` 按属性结构新增/删除字段并保留项目值,同时同步 schema。已完成的 `--status` / `--report` 只读取控制器哈希锚定的证据,不重新执行项目脚本。默认管理项目 `.opencode/plugins/`、`.pi/extensions/` 和 `.pi/settings.json`,不会默认写 `~/.pi/agent/settings.json`。旧项目首次执行后需 reload 宿主会话。明确「检查初始化更新」「只检查,不要修改」仍只运行 `init check-update`。
74
+ 升级会删除仍与控制器记录完全一致的旧投影;用户改过的 docs、scripts 和 Skills 会保留并解除初始化所有权。README/AGENTS 只移除精确 managed block,块外内容不变。升级完成后 DAG 的命令与运行事实路径不变,内置能力改从当前 package 加载。存在运行中或可恢复任务、身份不一致或需要人工取舍时,升级会停止写入并返回下一步。
76
75
 
77
76
  完整说明见[初始化目标项目](website/docs/quick-start/init-target-project.md)。
78
77
 
79
78
  ### 运行第一个任务
80
79
 
81
- 初始化后,在已初始化的目标项目中你可以直接对宿主 agent 说「**loop-agent 帮我完成 XXX 需求**」(或「帮我实现 / 帮我修复 / 帮我开发 XXX」「使用 loop-agent 完成 XXX」「按 loop-agent 流程处理 XXX」),初始化写入的 `AGENTS.md` 与 `.agents/skills/loop-agent/` 会把这类通用需求表达确定性地路由进 Agent DAG,**无需**追加额外的 `cli` 关键词。主会话只负责编排与验证,业务实现由受治理 DAG writer 完成。
80
+ 初始化后,在目标项目中可以直接对宿主 Agent 说「**loop-agent 帮我完成 XXX 需求**」。`.agents/skills/loop-agent/SKILL.md` 只负责发现和路由;完整工作流规则、内置 Skills 与模板由当前 npm 包提供。主会话只负责编排与验证,业务实现由受治理 DAG writer 完成。
82
81
 
83
82
  ```bash
84
83
  # 标准单任务路径:唯一 mutation 入口 task advance + 可选只读 task status
@@ -140,8 +139,9 @@ Console Chat 支持直接用自然语言预约工作流,例如:“请预约3
140
139
  ## 核心边界
141
140
 
142
141
  - `.harness/` 保存任务、DAG run、one-shot run、可选 Task Pool 与 `init-upgrades/` 可恢复升级运行事实。
143
- - `ai_workspace/loop-agent/` 保存目标项目的长期治理资料。
144
- - `.agents/skills/` 保存目标项目可审计的 repo-local skills。
142
+ - `harness.json` 保存本机模型与运行设置;可提交的项目覆盖位于可选 `.loop-agent/project.json`。
143
+ - `.agents/skills/loop-agent/SKILL.md` 是外部 Agent 桥;内置 Skills、模板和通用治理资料由 npm 包提供。
144
+ - 项目自己的 `AGENTS.md`、文档、脚本和自定义 Skills 仍由项目维护,不属于 init surface。
145
145
  - `agent-worker` 通过已发布的 `loop-agent` 子进程执行 DAG,不维护第二套 runtime kernel。
146
146
  - 当前不内置远程 Task Pool、云 Worker 集群、自动 push、自动创建或合并 PR、生产凭据管理。
147
147
 
@@ -160,6 +160,40 @@ function commandFromScript(repoRoot, label, command) {
160
160
  function getManifestVerifyCommands(repoRoot, preset) {
161
161
  try {
162
162
  const manifest = JSON.parse(readFileSync(path.join(repoRoot, 'harness.json'), 'utf-8'));
163
+ let tracked = {};
164
+ try {
165
+ tracked = JSON.parse(readFileSync(path.join(repoRoot, '.loop-agent', 'project.json'), 'utf-8'));
166
+ }
167
+ catch {
168
+ // Tracked overrides are optional. Invalid JSON is rejected by manifest loading.
169
+ }
170
+ const commandsFor = (source, key) => {
171
+ const value = source?.[key];
172
+ return Array.isArray(value)
173
+ ? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
174
+ : [];
175
+ };
176
+ const unique = (items) => [...new Set(items)];
177
+ const quickV2 = unique([
178
+ ...commandsFor(tracked.verification, 'quick'),
179
+ ...commandsFor(manifest.verification, 'quick'),
180
+ ]);
181
+ const standardV2 = unique([
182
+ ...commandsFor(tracked.verification, 'standard'),
183
+ ...commandsFor(manifest.verification, 'standard'),
184
+ ]);
185
+ const fullV2 = unique([
186
+ ...commandsFor(tracked.verification, 'full'),
187
+ ...commandsFor(manifest.verification, 'full'),
188
+ ]);
189
+ if (quickV2.length + standardV2.length + fullV2.length > 0) {
190
+ const selected = preset === 'quick'
191
+ ? quickV2
192
+ : preset === 'standard'
193
+ ? [...quickV2, ...standardV2]
194
+ : [...quickV2, ...standardV2, ...fullV2];
195
+ return selected.map((command) => commandFromScript(repoRoot, 'project verification', command));
196
+ }
163
197
  const quick = typeof manifest.scripts?.quickVerify === 'string'
164
198
  ? manifest.scripts.quickVerify
165
199
  : undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.44.0-next.12",
4
- "gitSha": "91181001a8cbed75a00a8a6d8e76ae5fab889b36",
5
- "builtAt": "2026-09-17T10:04:11.141Z"
3
+ "version": "0.44.0-next.14",
4
+ "gitSha": "f4063ecbd9640174f9975da1096408f432aa7c1b",
5
+ "builtAt": "2026-09-19T10:49:42.503Z"
6
6
  }
@@ -149,7 +149,7 @@ const DAG_SUBCOMMANDS = [
149
149
  "decision",
150
150
  ];
151
151
  const TASK_SUBCOMMANDS = ["advance", "status", "source"];
152
- const OPERATOR_SUBCOMMANDS = ["capabilities"];
152
+ const OPERATOR_SUBCOMMANDS = ["capabilities", "guide"];
153
153
  const WORKFLOW_SUBCOMMANDS = [
154
154
  "list",
155
155
  "inspect",
@@ -234,8 +234,8 @@ export const COMMAND_DEFINITIONS = [
234
234
  name: "init",
235
235
  adapter: "none",
236
236
  tier: "primary",
237
- intent: "Initialize a target repository with loop-agent harness capabilities. Run INSIDE the target repo, or pass the global --repo-root <path> BEFORE this subcommand (e.g. `loop-agent --repo-root . init --profile full --merge`).",
238
- usage: "init [instructions|model-catalog|doctor|check-update|update|reconcile|upgrade] [--profile full|minimal] [--merge] [--model provider/model | --low-model provider/model --med-model provider/model --high-model provider/model] [--json|--markdown] [--bootstrap-surface|--apply-safe] [--client-recovery=auto|project|user|off] [--run-id <id> --status|--continue|--report]",
237
+ intent: "Initialize a target repository with the minimal loop-agent runtime surface. Run inside the target repo, or pass the global --repo-root <path> before this subcommand.",
238
+ usage: "init [instructions|model-catalog|doctor|check-update|update|reconcile|upgrade] [--merge] [--model provider/model | --low-model provider/model --med-model provider/model --high-model provider/model] [--json|--markdown] [--bootstrap-surface|--apply-safe|--legacy-audit] [--run-id <id> --status|--continue|--report]",
239
239
  subcommands: [...INIT_SUBCOMMANDS],
240
240
  handler: async ({ repoRoot, subcommand, rest }) => {
241
241
  await runInit(repoRoot, [subcommand, ...rest].filter(Boolean), { readRuntimeActivity: readInitRuntimeActivity });
@@ -288,10 +288,10 @@ export const COMMAND_DEFINITIONS = [
288
288
  },
289
289
  {
290
290
  name: "operator",
291
- adapter: "required",
291
+ adapter: "none",
292
292
  tier: "operator",
293
293
  intent: "Operator machine envelope capabilities registry for Console/CLI negotiation.",
294
- usage: "operator capabilities [--json]",
294
+ usage: "operator <capabilities|guide> [--json|--markdown]",
295
295
  subcommands: [...OPERATOR_SUBCOMMANDS],
296
296
  handler: async ({ repoRoot, subcommand, rest }) => {
297
297
  await runOperator(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
@@ -667,8 +667,6 @@ export function buildLoopAgentProgram(options) {
667
667
  .allowExcessArguments(true)
668
668
  .argument("[args...]", "init arguments")
669
669
  .option("--project-name <name>", "target project name")
670
- .option("--governance-root <path>", "governance root")
671
- .option("--profile <profile>", "full|minimal", "full")
672
670
  .option("--merge", "merge existing files")
673
671
  .option("--no-merge", "skip existing files")
674
672
  .option("--provider <provider>", "provider paired with a bare --model (otherwise use provider/model)")
@@ -679,8 +677,7 @@ export function buildLoopAgentProgram(options) {
679
677
  .option("--json", "print JSON")
680
678
  .option("--markdown", "print Markdown")
681
679
  .option("--bootstrap-surface", "write an inferred .harness/init-surface.json baseline")
682
- .option("--apply-safe", "apply deterministic safe init updates")
683
- .option("--client-recovery <mode>", "auto|project|user|off — OpenCode transient+overflow plugins, Pi overflow extension, optional Pi user retry config", "auto");
680
+ .option("--apply-safe", "apply deterministic safe init updates");
684
681
  command.action(async (args, _options, actionCommand) => runInitCommand(args, actionCommand, options.defaultRepoRoot));
685
682
  addStandaloneSubcommands(command, entry.subcommands ?? [], (args, actionCommand) => runInitCommand(args, actionCommand, options.defaultRepoRoot));
686
683
  program.addCommand(command);
@@ -0,0 +1,330 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, readFile, realpath, unlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ const SHA256 = /^[a-f0-9]{64}$/;
7
+ const CATALOG_PATH = "resources/init-history/legacy-surface.v1.json";
8
+ const catalogEntrySchema = z.object({
9
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
10
+ path: z.string().min(1),
11
+ mode: z.enum(["generated", "copied", "managed-block"]),
12
+ releaseFamily: z.string().min(1),
13
+ templateIdentity: z.string().min(1),
14
+ rawSha256: z.array(z.string().regex(SHA256)).default([]),
15
+ canonicalTextSha256: z.array(z.string().regex(SHA256)).default([]),
16
+ retirement: z.enum(["remove", "replace", "strip-block"]),
17
+ }).strict();
18
+ const catalogSchema = z.object({
19
+ schemaVersion: z.literal(1),
20
+ catalogVersion: z.number().int().positive(),
21
+ entries: z.array(catalogEntrySchema),
22
+ }).strict().superRefine((catalog, ctx) => {
23
+ const ids = new Set();
24
+ const paths = new Set();
25
+ for (const [index, entry] of catalog.entries.entries()) {
26
+ if (!isSafeRelativePath(entry.path)) {
27
+ ctx.addIssue({ code: "custom", path: ["entries", index, "path"], message: "legacy path must be a normalized repo-relative file path" });
28
+ }
29
+ if (ids.has(entry.id))
30
+ ctx.addIssue({ code: "custom", path: ["entries", index, "id"], message: "duplicate legacy id" });
31
+ if (paths.has(entry.path))
32
+ ctx.addIssue({ code: "custom", path: ["entries", index, "path"], message: "duplicate legacy path" });
33
+ ids.add(entry.id);
34
+ paths.add(entry.path);
35
+ }
36
+ });
37
+ function sha256(value) {
38
+ return createHash("sha256").update(value).digest("hex");
39
+ }
40
+ function isSafeRelativePath(value) {
41
+ if (!value || value.includes("\\") || path.posix.isAbsolute(value))
42
+ return false;
43
+ const parts = value.split("/");
44
+ return parts.every((part) => part !== "" && part !== "." && part !== ".." && !part.includes("\0"));
45
+ }
46
+ function canonicalTextSha256(bytes) {
47
+ const text = bytes.toString("utf8");
48
+ if (!Buffer.from(text, "utf8").equals(bytes))
49
+ return undefined;
50
+ return sha256(Buffer.from(text.replace(/\r\n/g, "\n"), "utf8"));
51
+ }
52
+ function candidateId(catalogVersion, entry, rawSha256) {
53
+ return `legacy-v${catalogVersion}-${sha256(`${entry.id}\0${entry.path}\0${rawSha256}`).slice(0, 24)}`;
54
+ }
55
+ async function loadCatalog(assetRoot) {
56
+ const file = path.join(assetRoot, ...CATALOG_PATH.split("/"));
57
+ const parsed = JSON.parse(await readFile(file, "utf8"));
58
+ return catalogSchema.parse(parsed);
59
+ }
60
+ function execGit(repoRoot, args) {
61
+ return new Promise((resolve, reject) => {
62
+ execFile("git", ["-C", repoRoot, ...args], { encoding: "buffer", windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
63
+ if (error)
64
+ reject(error);
65
+ else
66
+ resolve(stdout);
67
+ });
68
+ });
69
+ }
70
+ function parseObjectInventory(bytes) {
71
+ const result = new Map();
72
+ for (const record of bytes.toString("utf8").split("\0")) {
73
+ if (!record)
74
+ continue;
75
+ const tab = record.indexOf("\t");
76
+ if (tab < 0)
77
+ continue;
78
+ const metadata = record.slice(0, tab).trim().split(/\s+/);
79
+ const file = record.slice(tab + 1).replaceAll("\\", "/");
80
+ const treeRecord = metadata[1] === "blob";
81
+ const hash = treeRecord ? metadata[2] : metadata[1];
82
+ const stage = treeRecord ? undefined : metadata[2];
83
+ if (hash && (treeRecord || stage === "0"))
84
+ result.set(file, hash);
85
+ }
86
+ return result;
87
+ }
88
+ function parseChangedPaths(bytes, candidates) {
89
+ const changed = new Set();
90
+ for (const record of bytes.toString("utf8").split("\0")) {
91
+ if (!record)
92
+ continue;
93
+ for (const candidate of candidates) {
94
+ if (record === `? ${candidate}` || record.endsWith(` ${candidate}`) || record.endsWith(`\t${candidate}`))
95
+ changed.add(candidate);
96
+ }
97
+ }
98
+ return changed;
99
+ }
100
+ async function gitStates(repoRoot, paths) {
101
+ const result = new Map();
102
+ if (paths.length === 0)
103
+ return result;
104
+ try {
105
+ const pathArgs = ["--", ...paths];
106
+ const [headBytes, indexBytes, statusBytes] = await Promise.all([
107
+ execGit(repoRoot, ["ls-tree", "-rz", "HEAD", ...pathArgs]),
108
+ execGit(repoRoot, ["ls-files", "--stage", "-z", ...pathArgs]),
109
+ execGit(repoRoot, ["status", "--porcelain=v2", "-z", "--untracked-files=all", ...pathArgs]),
110
+ ]);
111
+ const head = parseObjectInventory(headBytes);
112
+ const index = parseObjectInventory(indexBytes);
113
+ const changed = parseChangedPaths(statusBytes, paths);
114
+ for (const candidatePath of paths) {
115
+ const headBlob = head.get(candidatePath);
116
+ const indexBlob = index.get(candidatePath);
117
+ const tracked = Boolean(headBlob && indexBlob);
118
+ const clean = tracked && headBlob === indexBlob && !changed.has(candidatePath);
119
+ result.set(candidatePath, {
120
+ tracked,
121
+ clean,
122
+ ...(headBlob ? { headBlob } : {}),
123
+ ...(indexBlob ? { indexBlob } : {}),
124
+ ...(!clean ? { status: changed.has(candidatePath) ? "changed" : tracked ? "index-head-mismatch" : "untracked-or-missing-index" } : {}),
125
+ });
126
+ }
127
+ }
128
+ catch (error) {
129
+ const status = `git-state-unavailable: ${error instanceof Error ? error.message : String(error)}`;
130
+ for (const candidatePath of paths)
131
+ result.set(candidatePath, { tracked: false, clean: false, status });
132
+ }
133
+ return result;
134
+ }
135
+ export async function inspectContainedRegularFile(repoRoot, relativePath) {
136
+ if (!isSafeRelativePath(relativePath))
137
+ return { conflict: "candidate path is not a normalized repo-relative file path" };
138
+ const absolute = path.join(repoRoot, ...relativePath.split("/"));
139
+ const info = await lstat(absolute).catch((error) => error.code === "ENOENT" ? undefined : Promise.reject(error));
140
+ if (!info)
141
+ return undefined;
142
+ if (!info.isFile() || info.isSymbolicLink())
143
+ return { conflict: "candidate is not a regular non-symlink file" };
144
+ let current = repoRoot;
145
+ for (const segment of relativePath.split("/").slice(0, -1)) {
146
+ current = path.join(current, segment);
147
+ const parent = await lstat(current);
148
+ if (parent.isSymbolicLink())
149
+ return { conflict: "candidate path crosses a symlink or junction" };
150
+ }
151
+ const [repoReal, fileReal] = await Promise.all([realpath(repoRoot), realpath(absolute)]);
152
+ const relative = path.relative(repoReal, fileReal);
153
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
154
+ return { conflict: "candidate resolves outside repoRoot" };
155
+ }
156
+ return { bytes: await readFile(absolute) };
157
+ }
158
+ export async function removeContainedRegularFileWithSha256Cas(input) {
159
+ const inspected = await inspectContainedRegularFile(path.resolve(input.repoRoot), input.relativePath);
160
+ if (!inspected)
161
+ return "missing";
162
+ if ("conflict" in inspected || sha256(inspected.bytes) !== input.expectedSha256) {
163
+ return "rejected";
164
+ }
165
+ await unlink(path.join(path.resolve(input.repoRoot), ...input.relativePath.split("/")));
166
+ return "removed";
167
+ }
168
+ function hasLegacyProvenance(surface, catalog) {
169
+ if (!surface)
170
+ return false;
171
+ const catalogPaths = new Set(catalog.entries.map((entry) => entry.path));
172
+ if (Object.keys(surface.files).some((file) => catalogPaths.has(file)))
173
+ return true;
174
+ if ((surface.dispositions ?? []).some((receipt) => (receipt.disposition === "removed" || receipt.disposition === "detached-as-project-content") &&
175
+ catalogPaths.has(receipt.path)))
176
+ return true;
177
+ return (surface.actionReceipts ?? []).some((receipt) => (receipt.type === "remove-owned-file" ||
178
+ receipt.type === "migrate-owned-file" ||
179
+ receipt.type === "replace-owned-file") &&
180
+ catalogPaths.has(receipt.path));
181
+ }
182
+ async function trackedTextFiles(repoRoot) {
183
+ try {
184
+ return (await execGit(repoRoot, ["ls-files", "-z"]))
185
+ .toString("utf8")
186
+ .split("\0")
187
+ .filter((file) => isSafeRelativePath(file.replaceAll("\\", "/")))
188
+ .map((file) => file.replaceAll("\\", "/"));
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ }
194
+ async function referenceGraph(input) {
195
+ const result = new Map(input.candidates.map((candidate) => [candidate.path, []]));
196
+ const candidatesByPath = new Map(input.candidates.map((candidate) => [candidate.path, candidate]));
197
+ const files = await trackedTextFiles(input.repoRoot);
198
+ const nestedWorkspaceRoots = files
199
+ .filter((file) => /^[^/]+\/harness\.json$/.test(file))
200
+ .map((file) => `${file.slice(0, -"harness.json".length)}`);
201
+ for (const sourcePath of files) {
202
+ if (sourcePath.startsWith(".git/") || sourcePath.startsWith(".harness/init-upgrades/"))
203
+ continue;
204
+ if (nestedWorkspaceRoots.some((root) => sourcePath.startsWith(root)))
205
+ continue;
206
+ const absolute = path.join(input.repoRoot, ...sourcePath.split("/"));
207
+ const info = await lstat(absolute).catch(() => undefined);
208
+ if (!info?.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024)
209
+ continue;
210
+ const bytes = await readFile(absolute);
211
+ if (bytes.includes(0))
212
+ continue;
213
+ const text = bytes.toString("utf8");
214
+ for (const candidate of input.candidates) {
215
+ if (!text.includes(candidate.path) && !text.includes(candidate.path.replaceAll("/", "\\")))
216
+ continue;
217
+ const sourceCandidate = candidatesByPath.get(sourcePath);
218
+ if (sourceCandidate && input.initiallyAutomatic.has(sourceCandidate.path))
219
+ continue;
220
+ result.get(candidate.path).push(sourcePath);
221
+ }
222
+ }
223
+ for (const consumers of result.values())
224
+ consumers.sort();
225
+ return result;
226
+ }
227
+ export async function auditLegacyInitSurface(input) {
228
+ const repoRoot = path.resolve(input.repoRoot);
229
+ const catalog = await loadCatalog(input.assetRoot);
230
+ const provenanceConfirmed = hasLegacyProvenance(input.surface, catalog);
231
+ const candidates = [];
232
+ const paths = [];
233
+ const retainedReceipts = new Map((input.surface?.dispositions ?? [])
234
+ .filter((receipt) => receipt.disposition === "retained-project-content" || receipt.disposition === "preserved-unproven-legacy" || receipt.disposition === "detached-as-project-content")
235
+ .map((receipt) => [receipt.path, receipt]));
236
+ for (const entry of catalog.entries) {
237
+ const inspected = await inspectContainedRegularFile(repoRoot, entry.path);
238
+ if (!inspected)
239
+ continue;
240
+ paths.push(entry.path);
241
+ if ("conflict" in inspected) {
242
+ candidates.push({ candidateId: candidateId(catalog.catalogVersion, entry, sha256(entry.path)), catalogEntryId: entry.id, path: entry.path, rawSha256: sha256(entry.path), evidenceClass: "path-conflict", git: { tracked: false, clean: false, status: inspected.conflict }, consumers: [], reason: inspected.conflict });
243
+ continue;
244
+ }
245
+ const raw = sha256(inspected.bytes);
246
+ const canonical = canonicalTextSha256(inspected.bytes);
247
+ const receipt = retainedReceipts.get(entry.path);
248
+ const receiptMatches = receipt && (receipt.resultSha256 === raw || receipt.recordedSha256 === raw);
249
+ const fingerprintMatch = entry.rawSha256.includes(raw) || Boolean(canonical && entry.canonicalTextSha256.includes(canonical));
250
+ const evidenceClass = receiptMatches
251
+ ? "project-content"
252
+ : fingerprintMatch && provenanceConfirmed
253
+ ? "historical-template-match"
254
+ : fingerprintMatch
255
+ ? "legacy-shaped-unproven"
256
+ : "recorded-owned-modified";
257
+ candidates.push({
258
+ candidateId: candidateId(catalog.catalogVersion, entry, raw),
259
+ catalogEntryId: entry.id,
260
+ path: entry.path,
261
+ rawSha256: raw,
262
+ ...(canonical ? { canonicalTextSha256: canonical } : {}),
263
+ evidenceClass,
264
+ git: { tracked: false, clean: false, status: "git-state-pending" },
265
+ consumers: [],
266
+ ...(receiptMatches ? { terminalDisposition: receipt.disposition === "preserved-unproven-legacy" ? "preserved-unproven-legacy" : "retained-project-content" } : {}),
267
+ reason: receiptMatches
268
+ ? "a hash-bound disposition already retains this file as project content"
269
+ : fingerprintMatch && provenanceConfirmed
270
+ ? "path and historical fingerprint match, with independent legacy initialization evidence"
271
+ : fingerprintMatch
272
+ ? "historical bytes match, but independent legacy initialization provenance is unavailable"
273
+ : "catalog path exists but bytes differ from every historical template fingerprint",
274
+ });
275
+ }
276
+ const states = await gitStates(repoRoot, paths);
277
+ for (const candidate of candidates)
278
+ candidate.git = states.get(candidate.path) ?? candidate.git;
279
+ const initiallyAutomatic = new Set(candidates
280
+ .filter((candidate) => candidate.evidenceClass === "historical-template-match" && candidate.git.clean && !candidate.terminalDisposition)
281
+ .map((candidate) => candidate.path));
282
+ const references = await referenceGraph({ repoRoot, candidates, initiallyAutomatic });
283
+ for (const candidate of candidates) {
284
+ candidate.consumers = references.get(candidate.path) ?? [];
285
+ if (candidate.terminalDisposition)
286
+ continue;
287
+ if (candidate.evidenceClass === "historical-template-match" && candidate.git.clean && candidate.consumers.length === 0) {
288
+ candidate.automaticAction = "remove-owned-file";
289
+ }
290
+ else if (candidate.evidenceClass === "historical-template-match" && !candidate.git.clean) {
291
+ candidate.evidenceClass = "recorded-owned-modified";
292
+ candidate.reason = `historical template match is not clean in Git (${candidate.git.status ?? "unknown"}); preserve until explicitly decided`;
293
+ }
294
+ else if (candidate.consumers.length > 0) {
295
+ candidate.reason = `external consumers must be migrated before retirement: ${candidate.consumers.join(", ")}`;
296
+ }
297
+ }
298
+ const automatic = candidates.filter((candidate) => candidate.automaticAction).length;
299
+ const retained = candidates.filter((candidate) => candidate.terminalDisposition).length;
300
+ const pending = candidates.length - automatic - retained;
301
+ const candidateSetSha256 = sha256(JSON.stringify(candidates.map((candidate) => ({ id: candidate.candidateId, path: candidate.path, rawSha256: candidate.rawSha256, evidenceClass: candidate.evidenceClass, consumers: candidate.consumers }))));
302
+ const migrationState = pending > 0
303
+ ? "review-required"
304
+ : automatic > 0
305
+ ? "in-progress"
306
+ : retained > 0
307
+ ? "retained"
308
+ : "clean";
309
+ return {
310
+ schemaVersion: 1,
311
+ catalogVersion: catalog.catalogVersion,
312
+ candidateSetSha256,
313
+ provenanceConfirmed,
314
+ migrationState,
315
+ candidates,
316
+ summary: { classified: candidates.length, automatic, retained, pending },
317
+ };
318
+ }
319
+ export function legacyAuditReceipt(report, removedCount) {
320
+ return {
321
+ catalogVersion: report.catalogVersion,
322
+ completedAt: new Date().toISOString(),
323
+ candidateSetSha256: report.candidateSetSha256,
324
+ classifiedCount: report.summary.classified,
325
+ removedCount,
326
+ retainedCount: report.summary.retained,
327
+ pendingCount: report.summary.pending,
328
+ migrationState: report.summary.pending > 0 ? "review-required" : report.summary.retained > 0 ? "retained" : report.summary.automatic > removedCount ? "in-progress" : "clean",
329
+ };
330
+ }