@tea-agent/loop-agent 0.10.0-alpha.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +39 -47
  3. package/README.md +33 -6
  4. package/dist/application/dag/args.js +2 -3
  5. package/dist/application/dag/generate-task-dag.js +5 -14
  6. package/dist/cli/command-definitions.js +44 -5
  7. package/dist/cli/program.js +37 -3
  8. package/dist/cli/update/notifier.js +117 -0
  9. package/dist/cli/update/npm-client.js +151 -0
  10. package/dist/cli/update/policy.js +58 -0
  11. package/dist/cli/update/state.js +68 -0
  12. package/dist/cli.js +33 -0
  13. package/dist/commands/init.js +432 -58
  14. package/dist/commands/plan.js +50 -0
  15. package/dist/governance/exec-plans.js +545 -0
  16. package/dist/governance/manifest-types.js +0 -5
  17. package/dist/task/config-types.js +0 -1
  18. package/dist/worker/observe/static/app.js +326 -45
  19. package/dist/worker/observe/static/styles.css +1 -0
  20. package/dist/workflows/dag/governance-profile.js +0 -10
  21. package/dist/workflows/dag/init-hybrid.js +5 -201
  22. package/dist/workflows/dag/sdd-embedded.js +128 -0
  23. package/dist/workflows/dag/skill-instructions.js +5 -4
  24. package/docs/README.md +1 -0
  25. package/docs/agent-dag-runner.md +2 -2
  26. package/docs/architecture/runtime-boundaries.md +3 -0
  27. package/docs/design/README.md +1 -0
  28. package/docs/development-principles.md +1 -1
  29. package/docs/exec-plans/active/README.md +2 -2
  30. package/docs/exec-plans/completed/README.md +6 -0
  31. package/docs/feature-workflow.md +27 -21
  32. package/docs/harness-methodology-debugging.md +1 -1
  33. package/docs/harness-methodology-tdd.md +3 -3
  34. package/docs/init-surface.manifest.json +23 -50
  35. package/docs/loop-agent-harness.md +8 -3
  36. package/docs/progress/README.md +6 -0
  37. package/docs/reports/README.md +10 -0
  38. package/docs/templates/project-start-checklist.md +2 -2
  39. package/harness.json +1 -3
  40. package/package.json +3 -3
  41. package/skills/frontend-implementation/SKILL.md +3 -0
  42. package/skills/loop-agent/references/command-reference.md +6 -0
  43. package/skills/loop-agent/references/docs-converge.md +5 -5
  44. package/skills/loop-agent/references/task-workflow.md +1 -1
  45. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +0 -131
  46. package/docs/templates/backend-test-dag.json +0 -213
  47. package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -128
  48. package/docs/templates/backend-test-dag.review-cases.prompt.md +0 -85
package/AGENTS.md CHANGED
@@ -71,7 +71,7 @@
71
71
  - `src/`:loop-agent 运行时代码
72
72
  - `test/`:Vitest 测试套件
73
73
  - `bin/loop-agent.js`:CLI 可执行入口
74
- - `skills/`:loop-agent DAG 模板引用的本地 skill 指令与参考资料;`init --profile full` 还会把同一份 skills 镜像到目标项目的 `.agents/skills/`(外部 agent 兼容路径,包内仍只带 `skills/`)。初始化还会向目标项目 `.gitignore` 合并 loop-agent managed block,忽略 `.harness/tasks/*`、`.harness/dag-runs/*`、`.harness/runs/*`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.worktrees/` 等运行态事实,但保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`。
74
+ - `skills/`:loop-agent 源仓库和 npm 包内置 skill 指令与参考资料;目标项目初始化后只生成 `.agents/skills/`,不再生成根 `skills/`。初始化还会向目标项目 `.gitignore` 合并 loop-agent managed block,忽略 `.harness/tasks/*`、`.harness/dag-runs/*`、`.harness/runs/*`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.worktrees/` 等运行态事实,但保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`。
75
75
  - `.harness/`:任务、DAG、run、cache 和 live state 等运行态目录
76
76
  - `docs/`:治理文档、计划、报告和模板
77
77
  - `website/`:Docusaurus 用户文档站
@@ -85,7 +85,7 @@
85
85
  - 长期决策写入 `docs/`,不要只留在聊天里。
86
86
  - 面向使用者的新增、修改、删除或修复,应同步更新根目录 `CHANGELOG.md`;保持版本级摘要即可,不写过细技术细节。
87
87
  - 面向用户的中文更新日志、README 和说明文档应使用自然、结果导向的表达:先说明用户能获得什么或问题如何改善,保留必要的命令和产品术语,避免逐字翻译、内部实现细节和无意义的中英混杂。
88
- - 涉及 `loop-agent init` 或目标项目投影的改动,必须同步考虑目标项目生成物:`AGENTS.md`、`README.md`、`harness.json`、`docs/`、`scripts/`、`skills/`、`.agents/skills/`(skills 镜像)、`.harness/prompts`、`.gitignore`(loop-agent runtime managed block)和 npm 包内置 assets
88
+ - 涉及 `loop-agent init` 或目标项目投影的改动,必须同步考虑目标项目生成物:`AGENTS.md`、`README.md`、`harness.json`、`ai_workspace/loop-agent/`、`scripts/`、`.agents/skills/`、`.harness/prompts`、`.gitignore`(loop-agent runtime managed block)和 npm 包内置 assets;目标项目根 `docs/` 和根 `skills/` 的旧投影需要由 `init update --apply-safe` 安全迁移或退役。
89
89
  - 涉及初始化能力演化时,按 `docs/init-surface.manifest.json` 与 `scripts/check-init-evolution-needed.sh` 分级处理:小改 advisory,中等 surface-check,高影响才需要模型审查;不要把所有小改动升级成重流程。
90
90
  - 通用脚本、skill 和模板可以复制或投影;项目相关 README、验证命令、发布/维护脚本必须基于模板和目标项目真实文件生成,不假定目标项目是 TypeScript、Node、前端、后端或工具项目。
91
91
  - 涉及 CLI command、skill entry、runtime boundary、import 方向或发布包范围时,同步更新对应文档、catalog/definition、治理脚本和测试,避免 README、skill reference、CLI help、npm 包内容互相漂移。
package/CHANGELOG.md CHANGED
@@ -6,80 +6,72 @@
6
6
 
7
7
  ### 新增
8
8
 
9
+ - 新增版本更新提醒。发现新版本时可以直接确认升级;忽略某个版本后不会重复提醒,后续新版本仍会正常提示。可用 `LOOP_AGENT_DISABLE_UPDATE_CHECK=1` 关闭检查。
10
+ - 新增 `loop-agent plan create`、`plan complete` 和 `plan check`,统一维护执行计划及其索引,并在生成 DAG 前发现遗漏或不一致。
11
+ - 目标项目可以选择复用本地 SDD skills,为需求、设计、实现和评审节点补充方法指引;未安装时保持原有流程。
9
12
  - 新增 `docs/architecture/` 全景、DAG 执行、Worker/Feature、运行态事实与演进五篇主题文档及目录索引,并在 npm package(不投影到 init)内可达。
10
- - 文档站新增功能导览、架构导读与当前规划入口,intro 改为概念地图和 30 分钟路径;`skills/loop-agent` 增加 docs converge 检查表,Markdown 断链在构建期失败,便于变更后收敛站上文档。
11
- - `agent-worker` 现在会把实际执行的 `loop-agent` controller 冻结为 schemaVersion 1 identity:记录绝对 launch spec、入口 SHA-256、package version,以及覆盖 `package.json`、`bin/**`、`dist/**`、`skills/**` 的 portable package fingerprint。`feature run`、`feature verify-final` `batch run-ready` 可用 expected version/fingerprint 在任何目标仓库写入前 fail-fast,identity 会进入 Worker、Task Pool、batch/Feature、QA 与最终验证证据。
12
- - DAG run 在首个节点执行前生成 run-owned resolved skill profile snapshot。普通节点、dynamic map childloop body、approve 与 resume 都继续使用同一份原始 bytes hash;live skill 后续被修改、删除或补建不会改变当前 run,snapshot 或 binding 被篡改时不会回退实时解析。
13
- - 源码仓库新增 `npm run self-host:canary -- --deterministic`。它把候选 tarball 安装到隔离 slot,通过候选包绝对入口验证两个 CLI、full init、目标项目治理、Feature dry-run 与 static/shell DAG,并输出包含 tarball hash、package fingerprint、入口 identity、PATH trap 和逐项检查结果的 JSON 证据;维护脚本会独立计算 canonical fingerprint、交叉核对候选实现,并为所有子进程设置硬超时。默认零 Pi,`--live` 尚未开放。
14
- - 新增可选的 `skills/agent-worker/` outer-loop operator skill,用于 Feature Packet、TaskSpec、Task Pool、controller pinning、自举 candidate canary 和 Worker failure recovery。该 skill 随发布包和 full init 投影到 `skills/` / `.agents/skills/`,不会成为 DAG executor、kernel 或默认 role skill。
13
+ - Worker DAG 运行会固定本次使用的 loop-agent 版本与 skill 配置,避免执行过程中因环境变化产生不一致结果。
14
+ - 新增 `npm run self-host:canary -- --deterministic`,可在隔离环境中检查候选发布包是否能够接管常用 CLI、初始化和 DAG 流程。
15
+ - 新增可选的 `agent-worker` skill,为 FeatureTask、交付和失败恢复提供统一的操作指引。
16
+ - 文档站新增功能导览、架构导读与当前规划入口,并把文档收敛检查固化到 `skills/loop-agent`,减少站上文档与仓库治理文档漂移。
17
+ - 前端 DAG 的 repo-local skills 补齐节点输入、输出、证据和失败规则;前端规范会优先查知识库,失败时回退到当前项目 `openSpec/`。
18
+ - 目标项目旧布局迁移:`init check-update`(只读)报告旧根 `docs/` / `skills/` 可安全迁移的 loop-agent owned 文件、空目录和 `harness.json` 中已废弃的模型字段;`init update --apply-safe` 迁移确认未修改的文件、清理空目录并清理废弃字段,本地修改过、来源不明或目标冲突的文件只进入人工决策,不会被覆盖或删除。
15
19
 
16
20
  ### 变更
17
21
 
18
22
  - 根 README 在「核心概念」后增加极简「仓库地图」,方便一眼区分 `src/`、`skills/`、`.harness/`、`docs/` 等职责;细节仍以 `AGENTS.md` 与 `docs/README.md` 为准。
19
23
  - 文档治理收敛:站上「当前规划」与 active plan 同步(无进行中计划时明确写出);`docs/README` 只索引核心契约与产物目录入口,progress/report 全量列表下沉到子目录 README;Dynamic Workflow 路线图迁入 `docs/design/`;补齐 design 漏索引与 DAG recovery playbook 入口。
20
24
  - 活文档校准:Dynamic Workflow 路线图页首标明已落地 vs 设计输入(Pi-only);六个月规划第 1–2 月改为 archive/reports 指针;`repository-analysis` 冻结为基线快照,新增 `docs/reports/current-capability-summary.md`;落地 ADR 0001–0003(Pi-only、Task Pool 根、双树/docs-converge)。
21
- - 本仓库样板 Feature Packet `F-2026-001` 从 `dogfood/features/` 迁到根目录 `features/`,与 `F-2026-002` 及产品线约定路径对齐;`scripts/check-product-line-docs.sh` 现与 init 投影脚本一致,扫描 `features/`、`product/features/`、`dogfood/features/`。
22
- - `features/` Feature Packet 的标题与描述性文案改为中文;YAML key、id、枚举、路径与验证命令保持原样。
23
- - `dogfood/profile-filter` 的 README、QA 文档、注释与测试标题改为中文;运行时错误串、CLI Usage、fixture 数据与包脚本保持原样,以免破坏断言与 `--test-name-pattern`。
25
+ - 统一 Feature Packet 的推荐目录和中文说明,相关检查同时兼容现有产品线目录。
26
+ - 目标项目初始化布局收敛:默认把 loop-agent 治理资料生成到 `ai_workspace/loop-agent/`,不再在目标项目根生成 `docs/`;`--profile full` 只把 repo-local skills 写到 `.agents/skills/`,不再生成根 `skills/`。
24
27
  - `new-task` 不再预种 L1 空报告模板(`分析报告.md` / `实现计划.md` / `复盘报告.md`);任务 `artifacts/` 默认只保留 `修改记录.md` 与 `验证结果.md`,由 `promote-run` 从 completed run 回填。
25
- - run 启动时仍沿用既有 skill 解析优先级,但解析结果从此属于该 run;任务若把 skill 本身作为交付物修改,新的指令只会在下一次 DAG run 生效。
26
- - 前端 DAG 的四个 repo-local skills 现在明确规定 contract、scout、plan、implement、design gate、review 和 closeout 的输入、输出、证据与失败规则;组件规范和设计规范预留可追溯的知识库接入 TODO,未接入时只允许使用仓库证据并明确披露缺口。
27
- - 前端组件与设计规范现在按固定来源链解析:优先连接知识库,未配置、连接/查询失败或无匹配结果时必须检索当前项目 `openSpec/`,命中内容作为当前项目规范并携带路径与章节证据。
28
+ - 运行期间修改 skill 不会影响当前任务,新配置会从下一次运行开始生效。
29
+ - Observe 自动刷新时会保留依赖图、检查器和日志的阅读位置,查看长时间任务时不再被频繁打断。
28
30
 
29
31
  ### 修复
30
32
 
31
33
  - 前端 DAG 从 `需求.md` / `执行约束.md` 读取验证命令时不再交给 shell 解释执行;命令会先解析为受限参数列表并拒绝 shell 特殊语法,避免任务文本触发额外命令。
34
+ - 改进 Windows 下 Git Bash、npm 和集成测试的运行方式,减少环境识别错误与资源竞争导致的误失败。
32
35
 
33
36
  ## [0.10.0] - 2026-07-12
34
37
 
35
38
  ### 破坏性变更
36
39
 
37
- - 受治理 Agent runtime 硬切为 Pi-only:DAG / Loop / Delegate / Worker 不再支持 `executor: "cursor"`、`implement-cursor` / `repair-cursor`、Cursor worker 或 `cursor-fix`。
38
- - Cursor DAG、`executors.cursor`、`entrypoints.cursorExecutorUsage`、task `executor` / `cursorModel`、`loopAutoWritePolicy` 会 fail-fast;需要重新生成 Pi-only DAG,并把自动执行策略改为 `loopAutoExecutionPolicy`。
39
- - 删除 `--no-cursor`、`--allow-cursor-fix`、`cursor-worker` Delegate `--executor` / `--no-auto-run`。Delegate 默认只建 worktree;显式 `--auto-run` 才生成并执行 Pi-only DAG。
40
- - `@cursor/sdk` 仅可由 `cursor-prompt` sidecar 动态加载;普通 doctor / init / DAG 路径不检查、不要求 Cursor。
41
-
42
- ### 修复
43
-
44
- - `cursor-prompt --stream` 超时或异常时会取消仍在运行的 Cursor 请求并释放一次性 Agent,避免残留连接或进程;Delegate `--auto-run` 增加工作树执行链路回归覆盖。
40
+ - DAGLoopDelegate Worker 统一使用 Pi,不再支持 Cursor executor 及相关旧配置。
41
+ - Cursor 仍可通过 `loop-agent cursor-prompt` 手动调用,但不再参与自动执行和受治理写入。
42
+ - Delegate 默认只创建 worktree;需要自动生成并执行 DAG 时必须显式使用 `--auto-run`。
45
43
 
46
44
  ### 迁移提示
47
45
 
48
- 1. 删除 harness / task 中的 Cursor runtime 字段,只保留 `executors.pi`。
46
+ 1. 删除 harness task 中的 Cursor executor 配置,只保留 `executors.pi`。
49
47
  2. 把 `loopAutoWritePolicy` 改为 `loopAutoExecutionPolicy`(`off` | `approval-required` | `enabled`)。
50
- 3. 重新 `dag run-task` 生成并 `dag validate`;writer 应为 `implement-pi` / `repair-pi`。
51
- 4. 人工干预继续用 `loop-agent cursor-prompt`;其成功不等于任务完成,仍需显式 shell verification。
48
+ 3. 重新运行 `dag run-task` 并使用 `dag validate` 校验 DAG。
49
+ 4. 需要 Cursor 人工辅助时使用 `loop-agent cursor-prompt`,完成后仍需运行项目验证命令。
52
50
 
53
51
  ### 新增
54
52
 
55
- - 严格初始化演化检查现在接受 `Head` 为当前 `HEAD` 可解析祖先提交的报告:只要 `Base` 精确匹配 `--base`、`Head` 是 `HEAD` 或其祖先,且 `reportHead..HEAD` 区间没有新的 `model-review` 高影响路径即可通过。仅 `advisory` `surface-check` 的后续变化不会让已完成的高影响审查失效;`working tree` 等不可解析 Head 与无关历史报告仍被拒绝。
56
-
57
- - 新增只读的 `agent-worker feature review`。用户无需翻查原始日志,即可查看 Feature 当前状态、主要阻塞、required AC 覆盖、证据位置和下一条建议命令;`--json` 可供脚本稳定消费。
58
- - 新增 `agent-worker feature run`。它会先校验 Feature 和目标仓库,再复用现有 Worker pipeline 推进一个 Ready Task,随后刷新晨报、Observe snapshot 和 Feature review;`--dry-run` 可在零写入下预览步骤与产物。
59
- - 新增 ProductBug Follow-up 人工接续流程。`agent-worker task draft-followup` 生成可审查、可去重的修复草稿;`agent-worker feature approve-followup --dry-run` 可先检查变更计划,批准后再新增 FIX Task、重连下游依赖并进入 Ready,同时保留原失败证据。
60
- - Follow-up 现已覆盖全部失败分类:可执行类生成严格 TaskSpec,Spec/Risk/Human 类生成行动卡,EnvFailure 只有连续两次失败后才建议环境检查任务;审批事务增加多阶段回滚验证,避免 Packet、Ready state、approval 或 index 半写。
61
- - `agent-worker feature run --git-mode checkpoint` 新增显式授权的本地 Feature 分支事务。成功任务形成带 Feature/Task/AC/run 信息的 checkpoint commit;失败任务保存 patch 与 untracked 证据后恢复 clean。默认仍不 commit,也不会 push、stash、merge 或创建远程 PR。
62
- - 新增 `agent-worker feature verify-final`、`feature delivery` 与 Feature Closeout。用户可在 clean Delivery HEAD 上生成 canonical QA/最终验证证据,再从 checkpoint、成功 run 和 required AC 形成带 hash 的 Delivery Package、Acceptance Coverage 与 PR 草稿;Closeout 默认预览,只有显式 `--apply --owner` 才原子写回并保留幂等、stale facts 与回滚保护。
63
- - Morning report 与 Observe 首页新增共享的 Feature Decision Summary,优先显示状态、下一步、原因、证据和 required AC。新增 `agent-worker report metrics --month YYYY-MM`,同时输出带分母、样本量、时间窗口和缺失数据说明的 JSON/Markdown。
64
- - Observe 总览新增仓库横幅,显示仓库名与绝对路径;KPI 按主要执行链路排列为“Worker 执行”在前、“DAG 运行”在后,DAG 组明确展示活跃运行、执行中/等待节点、暂停和需处理运行,“活跃 Task”更名为“活跃 Worker Task”,每张卡片新增可键盘聚焦的问号帮助,解释数据来源与统计口径。
65
- - Observe 总览的 Feature 决策仅在存在“运行中”或“需处理”的 Feature 时显示;只有可执行、待验收、已交付或已关闭记录时不再占用总览空间。
53
+ - `agent-worker feature review` 可汇总 Feature 状态、阻塞、验收覆盖、证据和下一步;`--json` 可供脚本读取。
54
+ - `agent-worker feature run` 可检查并推进一个就绪任务,`--dry-run` 可在不写入的情况下预览执行结果。
55
+ - 新增失败接续流程,可根据失败类型生成修复任务或人工行动建议,并在批准前保留原始失败证据。
56
+ - `feature run --git-mode checkpoint` 可为成功任务创建本地检查点,并在任务失败后恢复干净工作区;不会自动 push、merge 或创建远程 PR。
57
+ - 新增 `feature verify-final`、`feature delivery` `feature closeout`,覆盖最终验证、交付材料和显式确认后的收尾操作。
58
+ - 晨报、Observe `report metrics` 会集中展示 Feature 状态、下一步、验收覆盖和统计口径。
59
+ - 初始化演化检查可以复用仍然有效的历史审查结果,减少低影响后续改动造成的重复评审。
66
60
 
67
61
  ### 修改
68
62
 
69
- - 根目录 `design/` 已并入 `docs/design/`,视觉参考与产品线设计笔记现在统一从治理文档目录查找,相关索引和引用已同步更新。
70
- - Agent DAG Pi/Cursor 节点现在默认使用简体中文输出说明、分析、报告和文档;可通过 `workflowPolicy.dag.outputLanguage: "en"` 切换英文,代码、命令、路径、JSON 字段和固定协议 token 不会被翻译。
71
- - Dashboard 将“运行活性”调整为更直观的“执行器状态”,并明确区分执行器正常、心跳中断、已经退出、其他主机运行以及旧记录没有心跳等情况。
72
- - 修复 Dashboard 综合 DAG 状态使用中文文案后丢失徽章颜色的问题;完成、失败、暂停、中断和任务已另行完成现在恢复对应的语义颜色。
73
- - Dashboard 节点耗时达到 60 秒后改为按“分钟 + 秒”显示,例如 `2min 5s`,长任务更容易快速阅读。
74
- - Dashboard 的 DAG 节点模型列现在会显示 Pi/Cursor 节点实际路由到的模型;历史运行从 `run.json` 模型矩阵推导,新运行会直接把解析后的模型写入节点状态,Shell/Static 节点仍显示 `-`。
75
- - Dashboard 现在优先展示由生命周期、原始状态和运行活性共同判定的 DAG 当前状态,并单独保留原始记录;新增 `dag reconcile-run`,可在确认执行器已停止后将历史失联 DAG 显式标记为“任务已另行完成”或“已放弃”,同时保存收口前证据。
76
- - Observe DAG 详情页主信息区精简为单一“状态”,已结束 DAG 不再展示执行器心跳等无意义内部维度,状态一致或不允许恢复/收口时也不再显示“一致”或“否”;不一致、可恢复或可收口的警告与操作能力仍在主视图可见,原始状态、生命周期、活性、状态一致性与恢复资格折叠在“诊断信息”中保留。
77
- - DAG 运行现在记录本机执行器 PID、主机和持续心跳;`dag status` / `dag doctor` 可区分活跃、节点长时间无活动、心跳停滞、执行器已退出和远端状态未知,Observe 也不会再把已知孤儿或 paused 运行计为活跃 DAG。
78
- - 节点检查器支持拖动左边缘调整宽度,长 Markdown 表格和输出更容易查看;点击外部空白区域可平滑收起面板。
79
- - 执行过程改为更清晰的事件轨道:工具调用、AI 回复和 Agent 生命周期更容易区分;没有时间记录时会按采集顺序展示,连续协议事件会自动合并,减少无关噪声。
80
- - Observe 风险区拆为“需处理 DAG”与“Worker 风险”两个子块;DAG 按运行去重,不会因同一运行同时存在多个异常信号而重复计数,也不会让历史失败记录长期占用当前风险;Snapshot 投影失败不再静默显示为全零健康状态,而是返回安全错误摘要并在总览提示“指标暂不可用”,不含堆栈或敏感信息。
81
- - 修复旧版(未写 runner 心跳元数据)的运行中 DAG 被 Observe 误判为不活跃的问题:`effectiveStatus=unknown` 只表示活性证据不足,不再覆盖 `lifecycle=active` 且原始 `status` 为 `running`/`pending` 的运行事实,也不再掩盖存在 RUNNING/PENDING 节点的运行。服务端健康计数(活跃运行、执行中节点、等待节点)与前端进行中 DAG 列表现在使用一致的判定语义;`paused`、`terminal`、`stale`、`orphaned` 等明确非活跃事实仍优先于原始 `running`。
82
- - `npm test` 会先运行可并行的纯测试,再串行运行会启动 CLI 或子进程的集成测试。纯测试会根据机器可用并行度自动选择 1 至 4 个 Worker,低配和高配环境都能在稳定性与速度之间取得更合适的平衡。
63
+ - `design/` 已并入 `docs/design/`,设计资料统一从治理文档目录查找。
64
+ - Agent DAG 默认使用简体中文输出;可通过 `workflowPolicy.dag.outputLanguage: "en"` 切换为英文。
65
+ - Observe 和 Dashboard 更集中地展示运行状态、风险、模型、耗时和下一步,并减少已经结束任务中的无关诊断信息。
66
+ - 新增 `dag reconcile-run`,可在执行器已经停止后安全收尾历史失联的 DAG
67
+ - DAG 详情的节点检查器、事件记录和风险区域更易阅读,长内容和异常状态也能得到清楚提示。
68
+ - 测试会根据机器资源安排并行和串行任务,降低资源竞争造成的不稳定。
69
+
70
+ ### 修复
71
+
72
+ - 修复 `cursor-prompt --stream` 超时或异常后可能残留连接或进程的问题。
73
+ - 修复部分运行中 DAG 被误判为不活跃、状态颜色丢失以及风险统计重复的问题。
74
+ - 运行快照暂时无法生成时会明确提示指标不可用,不再显示具有误导性的全零状态。
83
75
 
84
76
  ## [0.9.0] - 2026-07-12
85
77
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `loop-agent` 是面向 AI coding agent 的仓库级任务运行时和治理工具。它把一次研发任务组织成可生成、可校验、可执行、可恢复、可交接的 Agent DAG,并用 `.harness/`、`docs/` 和 shell verification 记录执行事实、长期治理资料和完成依据。
4
4
 
5
- 它可以作为任意目标项目的稳定控制器:初始化目标项目后,项目会获得 repo-local skills、治理文档、验证脚本、任务运行态目录和模型执行指引,使 agent 在目标项目里的工作体验尽量与本仓库对齐。
5
+ 它可以作为任意目标项目的稳定控制器:初始化目标项目后,项目会获得 `.agents/skills/`、`ai_workspace/loop-agent/` 治理资料、验证脚本、任务运行态目录和模型执行指引,使 agent 在目标项目里的工作体验尽量与本仓库对齐。
6
6
 
7
7
  ## 快速开始
8
8
 
@@ -20,6 +20,22 @@ loop-agent --version
20
20
  loop-agent --help
21
21
  ```
22
22
 
23
+ ## 自动更新提醒
24
+
25
+ 通过 npm 全局安装的 `loop-agent` 会在普通交互式命令成功结束后检查 `@tea-agent/loop-agent` 是否有新版本。提醒只写入 `stderr`,不会污染命令原本的 `stdout`;失败命令、CI、管道/重定向、`--help`、`--version`、JSON/Markdown 输出以及 DAG/Loop/Delegate/Pi/Cursor 等 controller-sensitive 路径都会跳过。
26
+
27
+ 如果你拒绝版本 A,当前系统用户下不会再提醒 A;之后发布版本 B 时会继续提醒。确认更新时,CLI 会先证明当前安装来自同一 npm global root,然后安装刚确认的精确版本,例如:
28
+
29
+ ```bash
30
+ npm install -g @tea-agent/loop-agent@0.12.0
31
+ ```
32
+
33
+ 需要完全关闭自动检查时设置:
34
+
35
+ ```bash
36
+ LOOP_AGENT_DISABLE_UPDATE_CHECK=1
37
+ ```
38
+
23
39
  检查当前项目的 loop-agent 配置:
24
40
 
25
41
  ```bash
@@ -38,7 +54,7 @@ loop-agent inspect
38
54
 
39
55
  然后运行 `loop-agent init instructions --repo-root .`,按指引使用 full + merge 初始化。需要选择 provider/model,或涉及凭据、成本、部署副作用时先问我;其他能安全默认的选项直接继续。
40
56
 
41
- 初始化后请立刻探索当前项目的 README、manifest/build/config 文件和源码目录,补全根 README 的项目概览、技术栈/目录结构、开发与验证命令,并同步更新 `docs/verification-matrix.md` 和必要的 `scripts/ci-tests.sh`。
57
+ 初始化后请立刻探索当前项目的 README、manifest/build/config 文件和源码目录,补全根 README 的项目概览、技术栈/目录结构、开发与验证命令,并同步更新 `ai_workspace/loop-agent/verification-matrix.md` 和必要的 `scripts/ci-tests.sh`。
42
58
 
43
59
  最后运行 `loop-agent init doctor --repo-root .`、`loop-agent inspect --repo-root .`、`loop-agent docs audit --repo-root .`、`bash scripts/check-repo.sh`,如项目测试入口可识别也运行 `bash scripts/ci-tests.sh` 或 `bash scripts/ci.sh`,并汇报结果、假设和剩余风险。
44
60
  ```
@@ -51,7 +67,7 @@ loop-agent init --repo-root <target-repo> --profile full --merge
51
67
  loop-agent init doctor --repo-root <target-repo>
52
68
  ```
53
69
 
54
- `init instructions` 会输出给模型/Agent 执行完整初始化的指引包,不要求目标项目已有 `harness.json`。默认初始化会 merge 已有 `AGENTS.md`、`harness.json` 和 `docs/`,复制 repo-local `skills/` 并同步镜像到 `.agents/skills/`(agent 兼容路径,如 OpenCode 自动发现),生成语言无关的治理脚本矩阵、中文根 README 入口、目标项目版治理文档、`harness.json` IDE schema 指引和 `.harness/` 骨架;已有 README 会保留用户正文并插入/更新 loop-agent managed block。初始化还会向 `.gitignore` 合并一个 loop-agent managed block(`# LOOP_AGENT_INIT_START/END`),把 `.harness/tasks/*`、`.harness/dag-runs/*`、`.harness/runs/*`、`.harness/live/`、`.harness/cache/`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.worktrees/` 等个人/会话运行态事实忽略掉,同时保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`,也不会覆盖用户已有的 ignore 规则。
70
+ `init instructions` 会输出给模型/Agent 执行完整初始化的指引包,不要求目标项目已有 `harness.json`。默认初始化会 merge 已有 `AGENTS.md`、`harness.json` 和 loop-agent 治理资料,生成语言无关的治理脚本矩阵、中文根 README 入口、`ai_workspace/loop-agent/` 目标项目治理资料、`.agents/skills/` repo-local skills、`harness.json` IDE schema 指引和 `.harness/` 骨架;不会在目标项目根目录生成 `skills/`,也不会把 loop-agent 生成的治理资料写到根 `docs/`。已有 README 会保留用户正文并插入/更新 loop-agent managed block。初始化还会向 `.gitignore` 合并一个 loop-agent managed block(`# LOOP_AGENT_INIT_START/END`),把 `.harness/tasks/*`、`.harness/dag-runs/*`、`.harness/runs/*`、`.harness/live/`、`.harness/cache/`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.worktrees/` 等个人/会话运行态事实忽略掉,同时保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`,也不会覆盖用户已有的 ignore 规则。
55
71
 
56
72
  新初始化会写入 `.harness/init-surface.json`,记录当前 controller 版本、初始化投影文件 hash 和 manifest hash。已用旧版本初始化的目标项目,可以用下面的维护入口对齐新版本初始化能力:
57
73
 
@@ -64,7 +80,7 @@ loop-agent init update --repo-root <target-repo> --apply-safe
64
80
 
65
81
  `check-update` 只读报告 deterministic actions、model merge tasks、human decisions 和 recommended next。`update --bootstrap-surface` 为旧项目补 inferred baseline;`update --apply-safe` 只补缺失文件、目录和 managed block(包括过期的 `.gitignore` managed block),不覆盖已有但无法确认来源的本地文件。
66
82
 
67
- 当初始化由模型/Agent 执行时,它应把初始化当成一个自动化闭环:确认真正不能安全默认的 provider/model、治理根目录或凭据/成本问题后,运行 deterministic init,随后立刻读取目标项目真实文件,补全根 README 的项目概览、技术栈/目录结构、开发与验证命令,并同步适配 `docs/verification-matrix.md` 和必要的 `scripts/ci-tests.sh`。
83
+ 当初始化由模型/Agent 执行时,它应把初始化当成一个自动化闭环:确认真正不能安全默认的 provider/model、治理根目录或凭据/成本问题后,运行 deterministic init,随后立刻读取目标项目真实文件,补全根 README 的项目概览、技术栈/目录结构、开发与验证命令,并同步适配 `ai_workspace/loop-agent/verification-matrix.md` 和必要的 `scripts/ci-tests.sh`。
68
84
 
69
85
  初始化生成的 `scripts/ci-tests.sh` 不假定目标项目是 TypeScript、Node、前端或后端项目。它会保守探测 `package.json`、`Makefile`、`go.mod`、`Cargo.toml`、Python 测试配置、Maven、Gradle、.NET 等常见入口,只运行实际存在且工具可用的命令;探测不到时会清楚提示需要由初始化模型或用户按目标项目实际技术栈补充。
70
86
 
@@ -83,6 +99,16 @@ loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .
83
99
 
84
100
  `<temp-dir>` 表示平台原生临时目录;也可以省略 `--output`,再使用命令 JSON 输出里的 `outputPath`。
85
101
 
102
+ 非微小工作需要 exec-plan 时,使用确定性生命周期命令维护计划与索引;`new-task` 不会自动创建计划:
103
+
104
+ ```bash
105
+ loop-agent plan create <plan-id> "<title>"
106
+ loop-agent plan check
107
+ loop-agent plan complete <plan-id> --summary "<summary>"
108
+ ```
109
+
110
+ `plan create` 优先复用目标项目模板并回退到发布包内置模板,create/complete 失败时会回滚多文件修改。`dag run-task` 在生成 DAG 草稿前运行同源索引检查,避免遗漏登记直到末端 verify 才暴露。
111
+
86
112
  一次性只读评审或有边界写入:
87
113
 
88
114
  ```bash
@@ -139,7 +165,8 @@ nightly wrapper 按 feature 互斥,保留批次/超时退出码,并输出 mo
139
165
  - **Agent DAG**:把一次任务拆成 contract、scout、plan、implement、verify、closeout 等可审查节点。
140
166
  - **`.harness/`**:记录 task、DAG run、one-shot run、cache 和 live state 等运行态事实。
141
167
  - **`harness.json`**:描述项目名、治理根目录、模型路由、executor 和验证脚本;`docs/templates/harness.schema.json` 为 IDE 提供补全和字段说明,运行时仍由 Zod schema 校验。
142
- - **repo-local skills**:目标项目本地的 `skills/`(loop-agent 主路径)优先于发布包内置 skills,便于项目定制 agent 行为;`init --profile full` 还会把同一份 skills 镜像到 `.agents/skills/`,让外部 agent(如 OpenCode)也能自动发现。DAG skill 解析顺序为:用户配置目录 → `skills/` → `.agents/skills/` → 发布包内置。
168
+ - **repo-local skills**:目标项目本地 skills 统一放在 `.agents/skills/`,便于项目定制 agent 行为并让外部 agent 自动发现。DAG skill 解析顺序为:用户配置目录 → `.agents/skills/` → 发布包内置 `skills/`。
169
+ - **可选 SDD skill 嵌入**:如果目标项目在 `.agents/skills/` 中提供 `SDD-requirement-analysis`、`SDD-design-analysis`、`SDD-implementation-test-review`,`dag run-task` 会把它们作为知识与方法补充追加到对应的 Contract、Plan、Implement/Repair、Verify、Review 节点。loop-agent 仍控制 DAG、状态、写入边界、验证和收口;不会自动运行 SDD 初始化/扫描 skill,也不会推进 `ai_workspace` 状态或归档。没有这些 repo-local skills 时,生成结果保持原有默认流程。
143
170
  - **run-owned skill snapshot**:新 DAG run 会在任何节点执行前,把本次实际注入 prompt 的 resolved skill profiles 冻结到 run 自己的 `.runtime/skill-snapshot.json`。后续节点、dynamic child、approve/resume 都使用同一份 hash-anchored snapshot;run 内修改 skill 只会从下一次 run 生效。
144
171
  - **controller identity**:`agent-worker` 把一次 Feature/batch 实际使用的发布包、入口、启动参数和 package 内容 fingerprint 固定下来,并把 identity 传播到 Worker、Task Pool、batch/Feature 与最终验证证据。
145
172
  - **`agent-worker` operator skill**:`skills/agent-worker/` 只负责 Feature Packet、TaskSpec、Task Pool、自举 release train 和失败恢复的外层路由;单个 DAG 实现、DAG kernel 修复和节点执行仍由 `loop-agent` 负责,该 skill 不进入默认 DAG role skills。
@@ -253,7 +280,7 @@ Windows 上运行 `scripts/*.sh` 时使用 Git Bash 或已配置的兼容 Bash
253
280
 
254
281
  `docs/progress/`、`docs/reports/`、`docs/exec-plans/`、`docs/decisions/` 等目录下的任务正文是目标仓库实时生成或历史事实;npm 包只携带这些目录的 README,不携带本仓库已有历史记录。
255
282
 
256
- DAG skill 指令优先从目标项目或用户配置目录解析;目标项目未提供本地 `skills/` 时,CLI 会回退到 npm 包内置的 `skills/`。因此普通项目不需要复制 loop-agent 仓库历史文档或内置 skills 才能获得默认 DAG 能力。
283
+ DAG skill 指令优先从用户配置目录和目标项目 `.agents/skills/` 解析;目标项目未提供本地 skill 时,CLI 会回退到 npm 包内置的 `skills/`。因此普通项目不需要复制 loop-agent 仓库历史文档或根 `skills/` 目录才可获得默认 DAG 能力。
257
284
 
258
285
  ## 发布前检查
259
286
 
@@ -148,11 +148,10 @@ function parseDagRunTaskProfile(value) {
148
148
  value === "minimal" ||
149
149
  value === "standard" ||
150
150
  value === "reviewed" ||
151
- value === "supervised" ||
152
- value === "backend-test") {
151
+ value === "supervised") {
153
152
  return value;
154
153
  }
155
- throw new Error(`dag run-task --profile must be one of: auto, minimal, standard, reviewed, supervised, backend-test; got ${value}`);
154
+ throw new Error(`dag run-task --profile must be one of: auto, minimal, standard, reviewed, supervised; got ${value}`);
156
155
  }
157
156
  export function parseDagValidateArgs(args) {
158
157
  if (args.length === 0) {
@@ -4,6 +4,7 @@ import { resolveShellCommands } from "../../executors/shell-executor.js";
4
4
  import { parseDagSpec } from "../../workflows/dag/types.js";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
6
  import { loadHarnessManifest } from "../../governance/harness.js";
7
+ import { assertExecPlanIndexConsistent } from "../../governance/exec-plans.js";
7
8
  import { defaultHybridDagOutputPath, initHybridDagFromTask, } from "../../workflows/dag/init-hybrid.js";
8
9
  import { loadTaskConfig } from "../../task/runtime.js";
9
10
  import { validateDagUseCase } from "./validate-dag.js";
@@ -197,9 +198,12 @@ async function resolveProfileRouting(repoRoot, parsed, candidateProfile) {
197
198
  }
198
199
  export async function generateTaskDagUseCase(input) {
199
200
  const { repoRoot, ...parsed } = input;
201
+ // Deterministic exec-plan preflight: fail closed on real index drift
202
+ // before any expensive DAG generation or execution. Empty/consistent
203
+ // repos stay compatible so the default DAG flow is unblocked.
204
+ await assertExecPlanIndexConsistent(repoRoot);
200
205
  const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
201
206
  const isFrontendImplementationTask = taskConfig.taskKind === "frontend-implementation";
202
- const isBackendTestTask = taskConfig.taskKind === "backend-test";
203
207
  const candidateResult = await initHybridDagFromTask(repoRoot, parsed.taskId, {
204
208
  outputPath: parsed.outputPath,
205
209
  template: "standard-dag",
@@ -226,19 +230,6 @@ export async function generateTaskDagUseCase(input) {
226
230
  profileRouting.selectedByProfile = parsed.profile;
227
231
  }
228
232
  }
229
- if (isBackendTestTask) {
230
- profileRouting.selectedTemplate = "backend-test-dag";
231
- profileRouting.source = "taskKind";
232
- profileRouting.routingReasons = [
233
- 'taskKind "backend-test" selects the dedicated backend test DAG template',
234
- ];
235
- if (parsed.profile === "auto") {
236
- profileRouting.selectedByProfile = "backend-test";
237
- }
238
- else if (parsed.profileExplicit) {
239
- profileRouting.selectedByProfile = parsed.profile;
240
- }
241
- }
242
233
  const initResult = profileRouting.selectedTemplate === "standard-dag"
243
234
  ? candidateResult
244
235
  : await initHybridDagFromTask(repoRoot, parsed.taskId, {
@@ -10,6 +10,7 @@ import { runInstructions } from "../commands/instructions.js";
10
10
  import { runNewTask } from "../commands/new-task.js";
11
11
  import { runImportPrd } from "../commands/import-prd.js";
12
12
  import { runPlanList } from "../commands/plan-list.js";
13
+ import { runPlanCheck, runPlanComplete, runPlanCreate } from "../commands/plan.js";
13
14
  import { runPromoteRun } from "../commands/promote-run.js";
14
15
  import { runSpine } from "../commands/spine.js";
15
16
  import { runStats } from "../commands/stats.js";
@@ -50,6 +51,23 @@ export function formatSubcommandUsage(name, subcommands) {
50
51
  export function formatSubcommandUsageError(name, subcommands) {
51
52
  return `usage: ${formatSubcommandUsage(name, subcommands)}`;
52
53
  }
54
+ /**
55
+ * Parse a `--summary "..."` flag from a token list. Supports both
56
+ * `--summary <value>` and `--summary=<value>` forms. Returns undefined when
57
+ * the flag is absent so callers can emit their own usage error.
58
+ */
59
+ export function parseSummaryFlag(tokens) {
60
+ for (let i = 0; i < tokens.length; i += 1) {
61
+ const token = tokens[i];
62
+ if (token === "--summary") {
63
+ return tokens[i + 1];
64
+ }
65
+ if (token?.startsWith("--summary=")) {
66
+ return token.slice("--summary=".length);
67
+ }
68
+ }
69
+ return undefined;
70
+ }
53
71
  const DOCS_SUBCOMMANDS = ["audit", "archive"];
54
72
  const INIT_SUBCOMMANDS = [
55
73
  "instructions",
@@ -59,7 +77,7 @@ const INIT_SUBCOMMANDS = [
59
77
  ];
60
78
  const EXAMPLES_SUBCOMMANDS = ["list", "show", "copy"];
61
79
  const CLOSEOUT_SUBCOMMANDS = ["task"];
62
- const PLAN_SUBCOMMANDS = ["list"];
80
+ const PLAN_SUBCOMMANDS = ["list", "create", "complete", "check"];
63
81
  const SPINE_SUBCOMMANDS = ["audit"];
64
82
  const HANDOFF_SUBCOMMANDS = ["check", "coverage"];
65
83
  const HANDOFF_USAGE = "handoff <check|coverage> [taskId]";
@@ -250,14 +268,35 @@ export const COMMAND_DEFINITIONS = [
250
268
  name: "plan",
251
269
  adapter: "required",
252
270
  tier: "operator",
253
- intent: "List repo execution plans.",
254
- usage: formatSubcommandUsage("plan", PLAN_SUBCOMMANDS),
271
+ intent: "Manage repo execution plans: list, create, complete, and index check.",
272
+ usage: "plan <list|create|complete|check> [plan-id] [title|--summary <summary>]",
255
273
  subcommands: [...PLAN_SUBCOMMANDS],
256
- handler: async ({ repoRoot, subcommand }) => {
257
- if (subcommand === "list") {
274
+ handler: async ({ repoRoot, subcommand, rest }) => {
275
+ if (subcommand === "list" || subcommand === undefined) {
258
276
  await runPlanList(repoRoot);
259
277
  return;
260
278
  }
279
+ if (subcommand === "create") {
280
+ const [planId, ...titleParts] = rest;
281
+ if (!planId)
282
+ throw new Error("usage: plan create <plan-id> \"<title>\"");
283
+ await runPlanCreate(repoRoot, planId, titleParts.join(" "));
284
+ return;
285
+ }
286
+ if (subcommand === "complete") {
287
+ const [planId, ...summaryParts] = rest;
288
+ if (!planId)
289
+ throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
290
+ const summary = parseSummaryFlag(summaryParts);
291
+ if (!summary)
292
+ throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
293
+ await runPlanComplete(repoRoot, planId, { summary });
294
+ return;
295
+ }
296
+ if (subcommand === "check") {
297
+ await runPlanCheck(repoRoot);
298
+ return;
299
+ }
261
300
  throw new Error(formatSubcommandUsageError("plan", PLAN_SUBCOMMANDS));
262
301
  },
263
302
  },
@@ -38,6 +38,7 @@ import { runImportPrd } from "../commands/import-prd.js";
38
38
  import { runPiReuseBenchmark } from "../commands/pi-reuse-benchmark.js";
39
39
  import { parsePiPromptArgs, printPiPromptUsage, runPiPrompt, } from "../commands/pi-prompt.js";
40
40
  import { runPlanList } from "../commands/plan-list.js";
41
+ import { runPlanCheck, runPlanComplete, runPlanCreate } from "../commands/plan.js";
41
42
  import { runPromoteRun } from "../commands/promote-run.js";
42
43
  import { runReferenceIndex } from "../commands/reference-index.js";
43
44
  import { runRunDag } from "../commands/run-dag.js";
@@ -155,11 +156,32 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
155
156
  await runStats(ctx.repoRoot);
156
157
  return;
157
158
  case "plan":
158
- if (subcommand === "list") {
159
+ if (subcommand === "list" || subcommand === undefined) {
159
160
  await runPlanList(ctx.repoRoot);
160
161
  return;
161
162
  }
162
- throw new Error("usage: plan <list>");
163
+ if (subcommand === "create") {
164
+ const [planId, ...titleParts] = rest;
165
+ if (!planId)
166
+ throw new Error("usage: plan create <plan-id> \"<title>\"");
167
+ await runPlanCreate(ctx.repoRoot, planId, titleParts.join(" "));
168
+ return;
169
+ }
170
+ if (subcommand === "complete") {
171
+ const [planId, ...summaryParts] = rest;
172
+ if (!planId)
173
+ throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
174
+ const summary = parsePlanSummaryFlag(summaryParts);
175
+ if (!summary)
176
+ throw new Error("usage: plan complete <plan-id> --summary \"<summary>\"");
177
+ await runPlanComplete(ctx.repoRoot, planId, { summary });
178
+ return;
179
+ }
180
+ if (subcommand === "check") {
181
+ await runPlanCheck(ctx.repoRoot);
182
+ return;
183
+ }
184
+ throw new Error("usage: plan <list|create|complete|check> ...");
163
185
  case "spine":
164
186
  await runSpine(ctx.repoRoot, compactArgs([subcommand, ...rest]));
165
187
  return;
@@ -314,6 +336,18 @@ async function runDagAction(repoRoot, subcommand, rest) {
314
336
  function compactArgs(args) {
315
337
  return args.filter((arg) => Boolean(arg));
316
338
  }
339
+ function parsePlanSummaryFlag(tokens) {
340
+ for (let i = 0; i < tokens.length; i += 1) {
341
+ const token = tokens[i];
342
+ if (token === "--summary") {
343
+ return tokens[i + 1];
344
+ }
345
+ if (token?.startsWith("--summary=")) {
346
+ return token.slice("--summary=".length);
347
+ }
348
+ }
349
+ return undefined;
350
+ }
317
351
  function createRawArgsCommand(name, description, defaultRepoRoot) {
318
352
  return new Command(name)
319
353
  .description(description)
@@ -527,7 +561,7 @@ export function buildLoopAgentProgram(options) {
527
561
  .allowExcessArguments(true)
528
562
  .argument("[args...]", "init arguments")
529
563
  .option("--project-name <name>", "target project name")
530
- .option("--governance-root <path>", "governance root", "docs")
564
+ .option("--governance-root <path>", "governance root")
531
565
  .option("--profile <profile>", "full|minimal", "full")
532
566
  .option("--merge", "merge existing files")
533
567
  .option("--no-merge", "skip existing files")
@@ -0,0 +1,117 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { evaluateLatestVersion, shouldCheckForUpdates } from "./policy.js";
3
+ import { acquireUpdateLock, readUpdateState, updateLockPath, updateStatePath, writeUpdateState, } from "./state.js";
4
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
5
+ const FAILURE_COOLDOWN_MS = 10 * 60 * 1000;
6
+ export async function runSelfUpdateNotifier(options) {
7
+ if (!shouldCheckForUpdates(options))
8
+ return;
9
+ const now = options.now ?? (() => new Date());
10
+ const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
11
+ void options.writeStdout;
12
+ try {
13
+ const statePath = updateStatePath(options.homeDir);
14
+ let state = await readUpdateState(statePath);
15
+ if (state.retryAfter && Date.parse(state.retryAfter) > now().getTime())
16
+ return;
17
+ let latestVersion = cachedLatest(state, now());
18
+ if (!latestVersion) {
19
+ latestVersion = await options.client.latestVersion();
20
+ state = {
21
+ ...state,
22
+ lastCheckedAt: now().toISOString(),
23
+ latestVersion,
24
+ };
25
+ await safeWriteState(statePath, state);
26
+ }
27
+ const decision = evaluateLatestVersion({
28
+ currentVersion: options.currentVersion,
29
+ latestVersion,
30
+ dismissedVersion: state.dismissedVersion,
31
+ now: now(),
32
+ retryAfter: state.retryAfter,
33
+ });
34
+ if (!decision.shouldPrompt)
35
+ return;
36
+ const release = await acquireUpdateLock(updateLockPath(options.homeDir), { now });
37
+ if (!release)
38
+ return;
39
+ try {
40
+ state = await readUpdateState(statePath);
41
+ if (state.dismissedVersion === decision.targetVersion)
42
+ return;
43
+ const answer = await ask(options, `loop-agent 有新版本:${options.currentVersion} → ${decision.targetVersion}\n现在更新到 ${decision.targetVersion}?[y/N] `);
44
+ if (!isYes(answer)) {
45
+ await safeWriteState(statePath, {
46
+ ...state,
47
+ latestVersion: decision.targetVersion,
48
+ dismissedVersion: decision.targetVersion,
49
+ });
50
+ return;
51
+ }
52
+ const proof = await options.client.canAutoInstall();
53
+ if (!proof.ok) {
54
+ writeStderr(`当前 loop-agent 不是由此 npm 的正式全局安装管理,无法自动升级。\n请运行:npm install -g @tea-agent/loop-agent@${decision.targetVersion}\n`);
55
+ await safeWriteState(statePath, cooldownState(state, now()));
56
+ return;
57
+ }
58
+ const install = await options.client.installExact(decision.targetVersion);
59
+ if (!install.ok) {
60
+ writeStderr(`loop-agent 更新失败:${install.reason}\n`);
61
+ await safeWriteState(statePath, cooldownState(state, now()));
62
+ return;
63
+ }
64
+ const verified = await options.client.verifyInstalled(decision.targetVersion);
65
+ if (!verified.ok) {
66
+ writeStderr(`loop-agent 已尝试安装,但验证失败:${verified.reason}\n`);
67
+ await safeWriteState(statePath, cooldownState(state, now()));
68
+ return;
69
+ }
70
+ writeStderr(`loop-agent 已更新到 ${decision.targetVersion};新版本将在下一次调用时生效。\n`);
71
+ await safeWriteState(statePath, {
72
+ schemaVersion: 1,
73
+ lastCheckedAt: now().toISOString(),
74
+ latestVersion: decision.targetVersion,
75
+ });
76
+ }
77
+ finally {
78
+ await release().catch(() => undefined);
79
+ }
80
+ }
81
+ catch (error) {
82
+ writeStderr(`loop-agent 更新检查已跳过:${error instanceof Error ? error.message : String(error)}\n`);
83
+ }
84
+ }
85
+ function cachedLatest(state, now) {
86
+ if (!state.lastCheckedAt || !state.latestVersion)
87
+ return undefined;
88
+ const checkedAt = Date.parse(state.lastCheckedAt);
89
+ if (!Number.isFinite(checkedAt))
90
+ return undefined;
91
+ return now.getTime() - checkedAt <= CHECK_INTERVAL_MS ? state.latestVersion : undefined;
92
+ }
93
+ async function ask(options, prompt) {
94
+ if (options.readLine) {
95
+ options.writeStderr?.(prompt);
96
+ return options.readLine(prompt);
97
+ }
98
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
99
+ try {
100
+ return await rl.question(prompt);
101
+ }
102
+ finally {
103
+ rl.close();
104
+ }
105
+ }
106
+ function isYes(answer) {
107
+ return /^(y|yes)$/i.test(answer.trim());
108
+ }
109
+ function cooldownState(state, now) {
110
+ return {
111
+ ...state,
112
+ retryAfter: new Date(now.getTime() + FAILURE_COOLDOWN_MS).toISOString(),
113
+ };
114
+ }
115
+ async function safeWriteState(path, state) {
116
+ await writeUpdateState(path, state).catch(() => undefined);
117
+ }