@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.
- package/CHANGELOG.md +12 -0
- package/README.md +12 -12
- package/dist/adapters/loop-agent.js +34 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +5 -5
- package/dist/cli/program.js +1 -4
- package/dist/commands/init-legacy-audit.js +330 -0
- package/dist/commands/init-upgrade.js +644 -104
- package/dist/commands/init.js +685 -330
- package/dist/commands/operator-guide.js +99 -0
- package/dist/commands/operator.js +16 -3
- package/dist/executors/pi-sdk-executor.js +3 -1
- package/dist/governance/harness.js +2 -7
- package/dist/governance/manifest-types.js +28 -9
- package/dist/governance/project-capability-scanner.js +263 -0
- package/dist/governance/project-config.js +96 -0
- package/dist/governance/resource-catalog.js +151 -0
- package/dist/infrastructure/console/app-data.js +6 -2
- package/dist/shared/operator/capabilities.js +2 -2
- package/dist/shared/package-metadata.js +23 -7
- package/dist/shared/pi-context-pressure/extension.js +23 -0
- package/dist/shared/pi-context-pressure/index.js +1 -0
- package/dist/shared/pi-context-pressure/telemetry.js +4 -0
- package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
- package/dist/shared/update/npm-client.js +49 -1
- package/dist/worker/cli.js +3 -5
- package/dist/worker/console/chat/browser-automation.js +114 -5
- package/dist/worker/console/chat/browser-routes.js +21 -0
- package/dist/worker/console/chat/chat-event-store.js +7 -6
- package/dist/worker/console/chat/context-insights.js +84 -19
- package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
- package/dist/worker/console/chat/pet/pet-persist.js +109 -0
- package/dist/worker/console/chat/pet/pet-registry.js +142 -0
- package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
- package/dist/worker/console/chat/pet/pet-routes.js +125 -0
- package/dist/worker/console/chat/pet/pet-service.js +262 -0
- package/dist/worker/console/chat/pet/pet-state.js +99 -0
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
- package/dist/worker/console/chat/pi-runtime.js +7 -2
- package/dist/worker/console/chat/routes.js +43 -7
- package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
- package/dist/worker/console/chat/session-store.js +20 -3
- package/dist/worker/console/console-shutdown.js +34 -0
- package/dist/worker/console/server.js +2 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
- package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
- package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static/pets/ember/pet.json +167 -0
- package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
- package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
- package/dist/worker/console/workspace-context.js +14 -0
- package/dist/worker/console/workspace-initialization.js +25 -16
- package/dist/worker/delivery/package.js +4 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -42
- package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
- package/dist/workflows/dag/final-verification.js +45 -10
- package/dist/workflows/dag/hybrid/sources.js +14 -14
- package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
- package/dist/workflows/dag/skill-instructions.js +102 -32
- package/dist/workflows/dag/skill-snapshot.js +3 -0
- package/dist/workflows/dag/skills.js +7 -7
- package/dist/workflows/dag/validate.js +2 -2
- package/dist/workflows/dag/workspace-checkpoint.js +13 -1
- package/docs/architecture/runtime-boundaries.md +28 -22
- package/docs/init-surface.manifest.json +43 -295
- package/docs/templates/agent-dag.base.json +9 -9
- package/docs/templates/agent-dag.final-verification.json +6 -6
- package/docs/templates/agent-dag.supervised-implementation.json +12 -12
- package/docs/templates/frontend-implementation-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/docs/templates/hybrid-dag.json +9 -9
- package/docs/templates/knowledge-sync-dag.json +8 -8
- package/package.json +6 -2
- package/resource-catalog.v1.json +1973 -0
- package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
- package/resources/init-history/legacy-surface.v1.json +33 -0
- package/skills/init-update/SKILL.md +34 -0
- package/skills/init-update/agents/openai.yaml +6 -0
- package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
- package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
- package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
- 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 . --
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
`init model-catalog` 只返回可用模型的必要元数据与就绪状态,不输出原始配置或凭据。已有 LOW/MED/HIGH 只要仍可用就保持不变;缺失档位才需要重选。`--profile` 和 `--client-recovery` 已移除,传入时会给出明确错误;客户端适配不再随初始化隐式安装。
|
|
60
|
+
|
|
61
|
+
控制器升级后,可先只读检查目标项目:
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
|
-
loop-agent init
|
|
64
|
+
loop-agent init check-update --repo-root . --json
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
144
|
-
- `.agents/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;
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
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
|
|
238
|
-
usage: "init [instructions|model-catalog|doctor|check-update|update|reconcile|upgrade] [--
|
|
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: "
|
|
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)));
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
+
}
|