@tea-agent/loop-agent 0.44.0-next.13 → 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 (145) hide show
  1. package/CHANGELOG.md +12 -0
  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 +685 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/pi-sdk-executor.js +3 -1
  13. package/dist/governance/harness.js +2 -7
  14. package/dist/governance/manifest-types.js +28 -9
  15. package/dist/governance/project-capability-scanner.js +263 -0
  16. package/dist/governance/project-config.js +96 -0
  17. package/dist/governance/resource-catalog.js +151 -0
  18. package/dist/infrastructure/console/app-data.js +6 -2
  19. package/dist/shared/operator/capabilities.js +2 -2
  20. package/dist/shared/package-metadata.js +23 -7
  21. package/dist/shared/pi-context-pressure/extension.js +23 -0
  22. package/dist/shared/pi-context-pressure/index.js +1 -0
  23. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  24. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  25. package/dist/shared/update/npm-client.js +49 -1
  26. package/dist/worker/cli.js +3 -5
  27. package/dist/worker/console/chat/browser-automation.js +114 -5
  28. package/dist/worker/console/chat/browser-routes.js +21 -0
  29. package/dist/worker/console/chat/chat-event-store.js +7 -6
  30. package/dist/worker/console/chat/context-insights.js +84 -19
  31. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  32. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  33. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  34. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  35. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  36. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  37. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  38. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  39. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
  40. package/dist/worker/console/chat/pi-runtime.js +7 -2
  41. package/dist/worker/console/chat/routes.js +43 -7
  42. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  43. package/dist/worker/console/chat/session-store.js +20 -3
  44. package/dist/worker/console/console-shutdown.js +34 -0
  45. package/dist/worker/console/server.js +2 -1
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  78. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  79. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  105. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  106. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  107. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  108. package/dist/worker/console/workspace-context.js +14 -0
  109. package/dist/worker/console/workspace-initialization.js +25 -16
  110. package/dist/worker/delivery/package.js +4 -1
  111. package/dist/worker/observe/static/operator-chrome.js +1 -42
  112. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  113. package/dist/workflows/dag/final-verification.js +45 -10
  114. package/dist/workflows/dag/hybrid/sources.js +14 -14
  115. package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
  116. package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
  117. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  118. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  119. package/dist/workflows/dag/skill-instructions.js +102 -32
  120. package/dist/workflows/dag/skill-snapshot.js +3 -0
  121. package/dist/workflows/dag/skills.js +7 -7
  122. package/dist/workflows/dag/validate.js +2 -2
  123. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  124. package/docs/architecture/runtime-boundaries.md +28 -22
  125. package/docs/init-surface.manifest.json +43 -295
  126. package/docs/templates/agent-dag.base.json +9 -9
  127. package/docs/templates/agent-dag.final-verification.json +6 -6
  128. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  129. package/docs/templates/frontend-implementation-dag.json +2 -2
  130. package/docs/templates/frontend-test-dag.json +2 -2
  131. package/docs/templates/hybrid-dag.json +9 -9
  132. package/docs/templates/knowledge-sync-dag.json +8 -8
  133. package/package.json +6 -2
  134. package/resource-catalog.v1.json +1973 -0
  135. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  136. package/resources/init-history/legacy-surface.v1.json +33 -0
  137. package/skills/init-update/SKILL.md +34 -0
  138. package/skills/init-update/agents/openai.yaml +6 -0
  139. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
  140. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
  141. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
  142. package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
  143. package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
  144. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
  145. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +0 -8
package/CHANGELOG.md CHANGED
@@ -1,9 +1,16 @@
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
+
7
14
  - Console Chat 浏览器控制补齐页面 JavaScript、登录态导入导出、带行上下文的元素查找、Markdown 内容抽取和外部窗口人工点选;条件等待可确认文本、元素或网址变化,多个对话保持各自选中的标签页,旧页面快照不会被误用于其他页面。
8
15
  - 浏览器截图可指定标签页和完整页面,作为图片发送给支持视觉的模型,并在工具详情中查看保存的截图;不支持视觉或图片过大时明确提示。右侧网页预览明确标注为独立页面,避免与实际受控窗口的登录态、操作混淆。
9
16
  - 登录态文件保存在仓库外,默认不把 cookie 值写入工具结果;导入显式合并 cookies/localStorage,保留未列出的现有数据。页面脚本超时后提示等待恢复或关闭该标签页;现有 Chrome 接管和 frontend-test 自动加载登录态仍为后续能力。
@@ -150,6 +157,9 @@
150
157
 
151
158
  ### 改进
152
159
 
160
+ - 项目初始化改为最小接入:不再改写 README、AGENTS,也不再复制整套文档、脚本和内置技能。目标项目只保留本机运行配置与一个外部智能体入口,通用能力会随 Loop Agent 程序更新,不需要逐个项目同步。
161
+ - 旧项目升级会清理仍未修改的初始化副本;用户改过的文档、脚本和技能会原样保留。升级前若还有任务运行或需要人工判断的内容,会停止写入并给出下一步,原有 DAG 使用方式不变。
162
+ - Console 对话浏览器新增 `LOOP_BROWSER_HEADLESS=1` 可选后台(headless)运行:不弹窗口、不抢焦点,导航、快照、点选、表单、截图、控制台/网络诊断等能力全部保留,右侧网页面板预览不受影响;后台模式下点选与「聚焦外部窗口」会明确提示原因。默认仍为可见窗口,人工点选开箱即用。同时不再向 Chrome 传递 `--no-sandbox`,启动时的「不受支持的命令行标记」黄条消失。
153
163
  - Console「浅色」主题改为 DeepSeek Harness 浅色家族:白底、侧栏冷灰、近黑正文、发送与主操作为 DeepSeek 蓝,用户气泡为淡蓝;默认暖纸和深色主题不变。
154
164
  - 对话时间线的「紧凑 / 完整」改为 DSH 文字 Tab:去掉小卡片边框与全局 34px 按钮高度,选中项用品牌色下划线。
155
165
  - DAG 运行时内部按职责拆开:模型配置、领域工具、分段会话、模板族、终态判定与检查点各自独立维护,测试也按行为分成可单独调度的文件。命令、任务流程与用户可见结果不变。本机默认测试并行下整池耗时未证实变快。
@@ -165,6 +175,8 @@
165
175
 
166
176
  ### 修复
167
177
 
178
+ - 修复旧项目升级在 Windows 执行 `npm` 验证时可能因命令 shim 启动方式报 `EINVAL` 的问题;升级器也能正确比较当前 beta/prerelease 与更新的稳定版本,不再把可用更新误判为“已是最新”。
179
+ - `agent-worker console` 第一次 Ctrl+C 仍会尝试正常关闭;若关闭卡住(例如主线程正在序列化超大会话),第二次 Ctrl+C 或约 2 秒超时会强制退出,不再出现连按几十次都不退、端口一直被占的情况。
168
180
  - Console Chat 的浏览器搜索工具在更多真实站点上可用:现代搜索框(`textarea`、`role=searchbox`、`name=search`)不再报“找不到搜索框”;对只在页面内更新结果、不发生跳转的搜索表单也不会超时失败;提交时只在搜索框所属表单内查找提交按钮,不再误点页面上无关的按钮导致“已输入但未提交”。
169
181
  - 浏览器导航在页面于加载完成后才发生跳转(如搜索引擎跳转到区域站点)时,不再把后续操作带入上一页;跳转后会等待短暂稳定再返回。
170
182
  - 浏览器搜索的默认搜索引擎改为 Bing(可从参数改写)。此前默认的百度对自动化已不可用(新版为文本框加验证码),模型不指定引擎时搜索会直接失败。
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.13",
4
- "gitSha": "03e91c507b0ab0f87e6dd376bb7bb735b8e5590e",
5
- "builtAt": "2026-09-18T01:29:28.992Z"
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
+ }