@tea-agent/loop-agent 0.14.0 → 0.16.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 (117) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +98 -11
  3. package/README.md +24 -5
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/eval.js +1176 -13
  24. package/dist/commands/init.js +4 -1
  25. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  26. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  27. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  28. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  29. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  30. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  31. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  32. package/dist/worker/cli.js +6 -3
  33. package/dist/worker/delivery/final-verification.js +96 -8
  34. package/dist/worker/delivery/package.js +23 -4
  35. package/dist/worker/delivery/verification-bundle.js +521 -0
  36. package/dist/worker/feature/fullstack-validate.js +337 -0
  37. package/dist/worker/feature/profile-schema.js +44 -0
  38. package/dist/worker/feature/ready-plan-projection.js +1 -0
  39. package/dist/worker/feature/reducer.js +2 -0
  40. package/dist/worker/feature/review.js +106 -11
  41. package/dist/worker/materialize/harness-task-materializer.js +5 -0
  42. package/dist/worker/observability/read-model.js +7 -0
  43. package/dist/worker/observe/static/views/task.js +1 -0
  44. package/dist/worker/outcomes/adapters.js +144 -0
  45. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  46. package/dist/worker/outcomes/gate.js +40 -0
  47. package/dist/worker/outcomes/projector.js +185 -0
  48. package/dist/worker/outcomes/registry.js +1 -0
  49. package/dist/worker/outcomes/store.js +131 -0
  50. package/dist/worker/outcomes/types.js +79 -0
  51. package/dist/worker/report/morning-report.js +4 -3
  52. package/dist/worker/run-task/run-task.js +85 -2
  53. package/dist/worker/runner/run-ready.js +32 -1
  54. package/dist/worker/task-graph/acceptance-schema.js +12 -0
  55. package/dist/worker/task-graph/ready-planner.js +131 -0
  56. package/dist/worker/task-graph/task-graph-schema.js +31 -0
  57. package/dist/worker/task-graph/validate.js +44 -4
  58. package/dist/worker/task-spec/schema.js +9 -0
  59. package/dist/worker/task-spec/validate.js +39 -0
  60. package/dist/worker/task-spec/workflow-routing.js +149 -0
  61. package/dist/workflows/dag/budget-enforcement.js +67 -0
  62. package/dist/workflows/dag/context-policy.js +137 -0
  63. package/dist/workflows/dag/init-hybrid.js +27 -11
  64. package/dist/workflows/dag/knowledge-curator.js +3 -0
  65. package/dist/workflows/dag/node-execution.js +11 -4
  66. package/dist/workflows/dag/prompt.js +1 -1
  67. package/dist/workflows/dag/runner.js +43 -16
  68. package/dist/workflows/dag/skill-snapshot.js +11 -7
  69. package/dist/workflows/dag/types.js +18 -0
  70. package/docs/README.md +1 -0
  71. package/docs/init-surface.manifest.json +7 -7
  72. package/docs/templates/branch-merge-report.md +0 -1
  73. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  74. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  75. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  76. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  77. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  78. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  79. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  80. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  81. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  82. package/docs/templates/product-line/AGENTS.md +1 -0
  83. package/docs/templates/product-line/README.md +17 -0
  84. package/docs/templates/product-line/acceptance.yaml +9 -0
  85. package/docs/templates/product-line/feature.yaml +11 -0
  86. package/docs/templates/product-line/task-graph.yaml +8 -0
  87. package/docs/templates/product-line/task.yaml +4 -0
  88. package/package.json +6 -16
  89. package/skills/browser-tools/SKILL.md +2 -2
  90. package/skills/frontend-design-review/references/review-checklist.md +27 -45
  91. package/skills/frontend-implementation/references/node-contracts.md +4 -4
  92. package/skills/frontend-review/SKILL.md +3 -1
  93. package/skills/frontend-review/references/review-findings.md +2 -1
  94. package/skills/frontend-verification/SKILL.md +3 -1
  95. package/skills/frontend-verification/references/verification-checklist.md +13 -22
  96. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  97. package/docs/agent-dag-recovery-playbook.md +0 -195
  98. package/docs/agent-dag-runner.md +0 -67
  99. package/docs/cursor-prompt-sidecar.md +0 -36
  100. package/docs/decisions/README.md +0 -18
  101. package/docs/design/README.md +0 -167
  102. package/docs/development-principles.md +0 -73
  103. package/docs/exec-plans/README.md +0 -6
  104. package/docs/exec-plans/active/README.md +0 -13
  105. package/docs/exec-plans/completed/README.md +0 -108
  106. package/docs/feature-workflow.md +0 -414
  107. package/docs/loop-agent-harness.md +0 -142
  108. package/docs/production-readiness.md +0 -96
  109. package/docs/progress/README.md +0 -81
  110. package/docs/reports/README.md +0 -163
  111. package/docs/verification-matrix.md +0 -70
  112. package/scripts/check-product-line-docs.sh +0 -29
  113. package/scripts/check-task-pool-root.sh +0 -32
  114. package/scripts/kb-graph-incremental-prepare.sh +0 -5
  115. package/scripts/kb-graph-materialize.sh +0 -4
  116. package/scripts/kb-graph-promote.sh +0 -4
  117. package/scripts/kb-query.sh +0 -5
package/AGENTS.md CHANGED
@@ -73,7 +73,7 @@
73
73
  - `src/`:loop-agent 运行时代码
74
74
  - `test/`:Vitest 测试套件
75
75
  - `bin/loop-agent.js`:CLI 可执行入口
76
- - `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/`。
76
+ - `skills/`:loop-agent 源仓库和 npm 包内置 skill 指令与参考资料;目标项目初始化后只生成 `.agents/skills/`,不再生成根 `skills/`。初始化还会向目标项目 `.gitignore` 合并 loop-agent managed block,忽略 `.harness/tasks/*`、`.harness/dag-runs/*`、`.harness/runs/*`、`.harness/evaluation/`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.worktrees/` 等运行态事实,但保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`。
77
77
  - `.harness/`:任务、DAG、run、cache 和 live state 等运行态目录
78
78
  - `docs/`:治理文档、计划、报告和模板
79
79
  - `website/`:Docusaurus 用户文档站
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # 更新日志
2
2
 
3
+ ## [0.16.0] - 2026-07-19
4
+
5
+ ### 重点更新
6
+
7
+ - Eval Lab 落地受控评测外环,支持 candidate 矩阵编排、Context Policy A/B dogfood 与 Ignition 研究门禁,全程 autoPromote=false,晋升须人工审核
8
+ - Worker 新增 TaskSpec 显式工作流路由,为四类任务生成带哈希锚定的 Task Outcome,并在晋升前校验 run_record、dag_json 等必需产物
9
+ - 新增 fullstack-v1 Feature Profile,支持确定性结构门禁与 Feature Verification Bundle v1,交付与收尾阶段任一身份漂移或产物篡改均阻断发布
10
+ - Eval Lab 落地固定预算运行时,DagSpec v3 可声明硬预算门禁,超限 fail closed 并输出预算账本,缺失 token 不被当作 0
11
+
12
+ ### 新增
13
+
14
+ - Eval Lab 新增 Context Policy A/B dogfood 脚本,串联 corpus/candidate/campaign/scorecard 与人工 promote(默认 dry-run),不改 DAG 默认策略
15
+ - Eval Lab 新增 Ignition 研究门禁命令,记录多代 proposer 并给出趋势 verdict,恒为研究用途,不自动晋升
16
+ - Eval Lab 新增 Corpus 契约命令与 dogfood 样板清单(15 个异质任务,public/private/held_out 齐全)
17
+ - Eval Lab 新增 public/private campaign 与 private-verifier,支持多 seed 可重复计划与 candidate 内容泄漏门禁
18
+ - Eval Lab 新增 anti-hack 审计与 Promotion Policy v1,检测验证弱化与可疑 outlier,promote 须通过多项门禁且默认 dry-run
19
+ - Eval Lab 新增固定预算运行时,支持按节点累计资源消耗,硬门禁超限 fail closed 并写出 budget-ledger
20
+ - Eval Lab 新增可切换 Context Policy(baseline-v1 默认与原先一致,role-specialized-v1 按角色调整预算)
21
+ - 新增 fullstack-v1 dogfood Feature F-2026-005,提供零依赖可运行目标仓与双覆盖验收样板
22
+ - 新增 fullstack-v1 Feature Profile,对 packet 执行确定性结构门禁,未声明 profile 的 legacy packet 行为不变
23
+ - AcceptanceSpec verification 新增可选的实现/验证任务引用、required_evidence 与 integration 声明,保留 legacy 字段兼容
24
+ - TaskGraph 节点新增可选 consumes/produces 声明与 Ready Planner artifact gate,上游产物不可验证时下游阻塞
25
+
26
+ ### 改进
27
+
28
+ - TaskSpec 支持可选 execution.workflow,可确定性物化为任务 DAG 类型并展示在 Worker、Task Pool、晨报与 Observe
29
+ - Ready Planner 明确仅做 envelope 级绑定,不重算产物文件 sha256,文件级 hash 在 Outcome projection 与 Feature Verification Bundle 执行
30
+ - DAG 节点 authoring guidance 明确 Pi 是唯一受治理 writer,cursor-prompt 仅作手工 one-shot sidecar
31
+ - Eval Lab propose 与 experiment 命令支持有界提出 experimenting candidate,accept/reject 须通过 scorecard 门禁,不自动改写已接受 skill
32
+ - 文档基线全面校准至 0.15.0,同步半年规划、仓库分析与路线图,harness Pi MED 路由至 grok-4.5
33
+ - Eval Lab dogfood 脚本改为可安全重复运行,并记录 promote --apply 序列供后续分析
34
+
35
+ ### 修复
36
+
37
+ - 对齐 Task Outcome 与 Ready Planner 契约:Ready 仅做 envelope 级绑定,Outcome 产物补齐独立 schemaId,失败时写入 outcomeFailure
38
+ - 规范化 shell_verification 为标准 evidence token(兼容旧 shell-verification),未知 outputs.required token 保持兼容不阻塞
39
+ - 清理 0.15.0 版本仍残留在 Unreleased 区块的更新日志条目
40
+ - Eval Lab 运行态(candidate/campaign/scorecard)现被 .gitignore 忽略,旧项目可用 init check-update 刷新 ignore 区块
41
+
42
+ ## [0.15.0] - 2026-07-18
43
+
44
+ ### 重点更新
45
+
46
+ - 新增 Pull Request 的只读 AI 自动审查,自动检查代码缺陷与测试缺口并更新评论
47
+ - 引入内部测试版本夜间自动发布工作流,主干达标后自动更新版本并发布
48
+ - 收紧 npm 发布包范围,仅保留 init 与 runtime 必需的文档和脚本
49
+ - 修复预发布版本被误发布至 latest 的风险,发布门禁改为按版本号状态严格限制
50
+
51
+ ### 新增
52
+
53
+ - 新增 Pull Request 的只读 AI 自动审查:在创建、重新打开、转为 ready 或更新提交时检查 bug、测试缺口、runtime boundary 等,并更新单条 PR 评论
54
+ - 新增内部测试版本夜间自动发布:达到提交门槛并通过发布检查后,自动更新版本、生成中文说明并发布 GitHub Release 与 npm
55
+ - 新增面向内部研发人员的轻量 GitHub 协作指南和简短 Pull Request 模板,统一采用短分支与 Squash Merge 的最小协作闭环
56
+ - 新增 `.editorconfig` 与 `.gitattributes`,统一常见文本文件的 UTF-8、LF、缩进和文件末尾换行约定
57
+
58
+ ### 改进
59
+
60
+ - 收紧 npm 发布包中的 `docs/` 范围:只保留 init/runtime 所需的治理文档、architecture、skills 与 templates
61
+ - 收紧 npm 发布包中的 `scripts/`:只保留知识库实际调用的脚本,不再打包源仓治理检查与 `.sh` 包装
62
+ - 前端实现 DAG 的 `frontendMock.policy=auto` 不再因为目标项目缺少 Mock 能力而阻塞,无确认能力时自动跳过并保留真实请求默认路径
63
+ - 文档基线全面校准至 0.14.0,修复了多处的文档索引遗漏与旧路径引用
64
+
65
+ ### 修复
66
+
67
+ - 修复私有仓库 AI PR 审查因通过网页 `*.diff` 地址拉取差异导致 404 的问题,改为经 GitHub API 获取 diff
68
+ - 修复预发布版本误发布到 `latest` 的问题:发布门禁改为按版本号的 prerelease 状态限制 dist-tag,beta prerelease 仅允许 `beta` tag
69
+ - 修复网站更新日志(Changelog)的版本插入位置错误:新版本段落不再错误地插入到读者摘要之前,保持页面结构正常
70
+ - 修复网站更新日志中残留的 `[Unreleased]` 块未随版本发布移除的问题
71
+
3
72
  ## [0.14.0] - 2026-07-18
4
73
 
5
74
  ### 重点更新
@@ -29,8 +98,26 @@
29
98
 
30
99
  ## [Unreleased]
31
100
 
32
- - 新增内部测试版本夜间自动发布:达到提交门槛并通过发布检查后,自动更新版本、生成中文说明并发布 GitHub Release npm;手动运行默认只做 dry-run。
33
- - 发布说明支持 OpenAI-compatible 模型,模型不可用时自动使用保守摘要。
101
+ - `.gitignore` `loop-agent init` managed block 现忽略 `.harness/evaluation/`,避免把 Eval Lab 运行态(candidate/campaign/scorecard)误提交;旧目标项目可用 `init check-update` / `update --apply-safe` 刷新 ignore 区块。
102
+ - Eval Lab 增加可复现 Context Policy A/B dogfood:`bash scripts/eval-dogfood-context-policy-ab.sh` 串联 corpus/candidate/campaign/scorecard 与人工 promote(默认 dry-run);证据见 `docs/reports/2026-07-19-eval-dogfood-context-policy-ab.md`。只演评测协议,不改 DAG 默认 context policy,也不声称自进化成功。
103
+ - Eval Lab 增加 Ignition 研究门禁:`eval ignition record|evaluate|show|list` 记录多代 proposer 并给出趋势 verdict(`insufficient_evidence` / `trend_*` / `inconclusive`)。恒为研究用途:`rsiLevel1ClaimAllowed=false`,从不 auto-promote,也不构成 RSI Level 1 产品承诺。
104
+ - Eval Lab 落地受控外环与 candidate 矩阵:`eval propose from-curate` 有界提出 experimenting candidate;`eval campaign matrix` 支持 dry-run / stub / pi-plan,以及 `--mode pi --dag-template` 的有界 DAG dry-run;`eval outer-loop run` 可串联 propose→campaign→matrix。全程 `autoPromote=false`,晋升仍须人工门禁。
105
+ - Eval Lab 打通 learned guidance 实验路径:`eval experiment from-curate` 将 `knowledge curate` 提案登记为 `experimenting` candidate(写入 `ai_workspace/.../learned-proposal.md`,不改已接受 skill);`accept` 须 campaign scorecard 的 held-out 不回退与 policy 通过,`reject` 保留 pattern 适用范围证据。仍无 auto-promote。
106
+ - Eval Lab 补齐 anti-hack 与 Promotion Policy v1:`eval campaign scorecard|audit` 检测 verification weakening / command 缩减 / 可疑 outlier(只标记人工复核、不自动剔分);`eval promote --campaign-id` 须通过 private 改进、held-out 不回退、预算与安全门禁,并仍默认 dry-run / `--apply` + `--reason`。不自动晋升。
107
+ - Eval Lab 新增 public/private campaign:`eval private-verifier *` 与 `eval campaign create|plan|run`;多 seed 计划可重复(`planHash`),private verifier 仅控制器侧可见并对 candidate 内容做泄漏门禁;held_out 仅作 promotion gate 占位。仍不自动晋升,也不自动跑完整 Pi candidate 矩阵。
108
+ - Eval Lab 落地固定预算运行时:DagSpec v3 可声明 `budget`(hard / record-only),runner 按节点累计 calls/wall/passes/tokens/context,硬门禁超限 fail closed 并写出 `budget-ledger.json` 与 budget report;`eval budget show --run-id` 可只读查看。缺失 token 不会被当成 0。
109
+ - DAG 节点 authoring guidance 改为明确 Pi 是唯一受治理 writer,`cursor-prompt` 仅作手工 one-shot sidecar,不再把 Cursor 写成 first-class executor。
110
+ - Eval Lab 抽出可切换的 Context Policy:`baseline-v1`(默认,行为与原先一致)与 `role-specialized-v1`(按角色调整 upstream/skill 预算);DagSpec 可选 `defaults.contextPolicyId`,`eval context-policy list|show` 可查看策略清单。不含 live campaign 编排。
111
+ - Eval Lab 新增初始 Corpus 契约:`eval corpus register|show|list|validate`,附带可提交样板 `docs/templates/evaluation/corpus-dogfood-v0.manifest.json`(15 个异质任务、public/private/held_out 齐全、每任务 2–3 seed);只冻结评测样本合同与 `corpusHash`,不跑 live campaign。
112
+ - Eval Lab 补齐 incumbent alias 与人工门禁的 promote/rollback:`eval alias show|list`、`eval promote|rollback`(默认 dry-run,`--apply` 才移动别名);只改 alias/decision 事实,不改写 candidate bundle 或已完成 DAG facts;仍无 live campaign、硬预算或自动晋升。
113
+ - 对齐 Task Outcome / Ready Planner 契约:Ready 仅做 envelope 级绑定;Outcome 产物补齐独立 `schemaId`;失败 envelope 写入 `outcomeFailure`;`shell_verification` 为规范 evidence token(兼容旧 `shell-verification`);未知 `outputs.required` token 保持兼容并固化测试。
114
+ - 新增 fullstack-v1 dogfood Feature `F-2026-005`(本地欢迎语端到端样板):显式 `execution.workflow`、验收双覆盖、TaskGraph `produces`/`consumes` 与 Final Verification 入口;并提供可运行目标仓 `dogfood/fullstack-welcome/`(零依赖 Node HTTP + WelcomeBanner + API/e2e/smoke)。`docs/templates/product-line/` 同步补充 `feature.yaml` 与 workflow/双覆盖指引。`F-2026-001`~`004` 仍作 generic/legacy 对照。
115
+ - `agent-worker feature verify-final` 现可聚合 hash-bound `backend-test` / `frontend-test` Outcome,生成 Feature Verification Bundle v1;Delivery 与 Closeout 会重新验证 bundle、typed outcome、Feature/Task/workflow/controller identity 和 Delivery HEAD,任一漂移或篡改都会阻止交付。既有 `qa-execute` QA aggregate 继续兼容。
116
+ - 新增 `fullstack-v1` Feature Profile:Feature Packet 可选 `feature.yaml` 声明 profile 与 scope,`agent-worker task validate-feature` 对 `fullstack-v1` packet 执行确定性结构门禁(required backend/frontend/backend-test/frontend-test workflow、frontend-test 依赖实现与后端验证、final-verify 覆盖、测试 workflow 不含产品写路径、writer writeSet 重叠须串行)。fullstack profile 必须有 scope;required AC 必须声明实现、验证与 evidence refs,任一门禁失败 fail closed 并输出阶段缺口与修复建议;未声明 profile 的 generic/legacy packet 行为零变化。
117
+ - AcceptanceSpec `verification` 新增全部 optional 的 `implementation_task_refs`、`verification_task_refs`、`required_evidence`、`integration`(`not-applicable`/`mock-allowed`/`real-required`),保留 legacy `expected_task_refs`,schema 仍为 `.strict()`;Feature review 的 coverage 投影在 dual-mode 下复用 Task Outcome envelope 的 `integrationStatus`:实现 Done 但缺验证 evidence、或 `real-required` 仅由 mock/local 满足时,required AC 派生为 `awaiting-verification`(只读派生,非新状态机),Feature 不得被报告为 deliverable。
118
+ - 为 TaskGraph 节点新增可选 `consumes`/`produces` 声明与 Ready Planner artifact gate:上游 Done 但 Outcome envelope 级绑定(Feature/producer/kind/可选 schemaId/source binding)不可验证时,下游以 `required-artifact-missing` 阻塞;Ready Planner 只做 envelope 绑定,不重算产物文件 sha256(文件级 hash 在 Outcome projection 与 Feature Verification Bundle / verify-final / Delivery / Closeout 执行)。artifact 资格是额外门禁,priority 不得越过 gate。gate 原因经 `ReadyExecutionPlan.blocked[*].artifactGate` 投影,供 Feature review、晨报与 Observe 共用。
119
+ - Worker 现为四类 TaskSpec workflow 生成 hash-anchored Task Outcome(含独立 `schemaId` 与失败时的 `outcomeFailure`),并在 promotion 前校验 `run_record`、`dag_json`、`shell_verification` 等已知 required output(未知 legacy token 保持兼容、不阻塞);Feature review、晨报与 Observe 可引用 outcome 证据,缺失或不一致会以 ContractMismatch/EnvFailure 失败收口。
120
+ - TaskSpec 现支持可选 `execution.workflow`,可确定性物化为任务 DAG 类型并展示在 Worker、Task Pool、晨报与 Observe;旧 QA 任务保持原有执行行为,同时给出需人工选择测试 workflow 的迁移提示。
34
121
 
35
122
  ## [0.13.0] - 2026-07-18
36
123
 
@@ -47,21 +134,21 @@
47
134
 
48
135
  ## [0.12.0] - 2026-07-16
49
136
 
50
- - **只读 Pi 节点安全重试**:opt-in `retryPolicy`(network / rate-limit / unavailable / timeout);writer / shell 等声明策略会校验失败。见 [Agent DAG](./guides/agent-dag.md)。
137
+ - **只读 Pi 节点安全重试**:opt-in `retryPolicy`(network / rate-limit / unavailable / timeout);writer / shell 等声明策略会校验失败。见 [Agent DAG](./website/docs/guides/agent-dag.md)。
51
138
  - **`taskKind: backend-test`**:需求分析 → 功能用例 → pytest → 复盘。
52
139
  - **知识同步与业务图谱 MVP**:`taskKind: knowledge-sync`(须 `featureId`)/ `knowledge-graph-bootstrap`;`loop-agent knowledge query|graph-init|graph-materialize|graph-promote|graph-incremental-prepare`(`knowledge curate` 仍只负责 repair guidance 提案)。
53
140
  - **init surface 全量跟踪**:`check-update` / `update --apply-safe` 覆盖包内全部 `docs/templates/**` 与 `skills/**` 到目标目录的投影。
54
- - 详见根 `CHANGELOG.md [0.12.0]`;站上 [当前规划](./overview/roadmap.md) 与 [功能导览](./overview/feature-map.md)。
141
+ - 详见根 `CHANGELOG.md [0.12.0]`;站上 [当前规划](./website/docs/overview/roadmap.md) 与 [功能导览](./website/docs/overview/feature-map.md)。
55
142
 
56
143
  ## [0.11.0] - 2026-07-15
57
144
 
58
- - **Observe 本地运营面 R1–R5**:资源池 / Task 下钻、Batch·Run 三层事实、Feature 决策详情与对象关系条、有界事件历史、ES modules 前端模块化;`observe serve` keep-alive / SSE keepalive,可选非 loopback 绑定(默认仍 `127.0.0.1`)。见 [Observe](./guides/observe-ui.md)。
145
+ - **Observe 本地运营面 R1–R5**:资源池 / Task 下钻、Batch·Run 三层事实、Feature 决策详情与对象关系条、有界事件历史、ES modules 前端模块化;`observe serve` keep-alive / SSE keepalive,可选非 loopback 绑定(默认仍 `127.0.0.1`)。见 [Observe](./website/docs/guides/observe-ui.md)。
59
146
  - **版本化自举**:controller identity / package fingerprint / expected gate、run-owned skill snapshot、deterministic `self-host:canary`、`skills/agent-worker` 及目标镜像;dogfood Feature `F-2026-004`。
60
147
  - **DagSpec v3 + repair 契约**:新 DAG 携带 `runtimeContract`;supervised `repairNodeId`;run-owned controller identity pin。
61
- - **`plan create` / `plan complete` / `plan check`**:exec-plan 生命周期与 DAG 前置索引校验。见 [CLI 参考](./reference/cli.md)。
62
- - **CLI 自更新提醒**:交互式成功命令后检查 npm 新版本;`LOOP_AGENT_DISABLE_UPDATE_CHECK=1` 可关。见 [安装](./quick-start/installation.md)。
148
+ - **`plan create` / `plan complete` / `plan check`**:exec-plan 生命周期与 DAG 前置索引校验。见 [CLI 参考](./website/docs/reference/cli.md)。
149
+ - **CLI 自更新提醒**:交互式成功命令后检查 npm 新版本;`LOOP_AGENT_DISABLE_UPDATE_CHECK=1` 可关。见 [安装](./website/docs/quick-start/installation.md)。
63
150
  - **init 布局收敛**:目标项目默认 `ai_workspace/loop-agent/` + `.agents/skills/`;`init update --apply-safe` 可安全迁移旧根 `docs/` / `skills/`。
64
- - **文档站 IA**:intro 概念地图与 30 分钟路径;[功能导览](./overview/feature-map.md)、[架构导读](./overview/architecture.md)、[当前规划](./overview/roadmap.md);Markdown 断链构建失败;`skills/loop-agent/references/docs-converge.md` 固化文档收敛检查表。
151
+ - **文档站 IA**:intro 概念地图与 30 分钟路径;[功能导览](./website/docs/overview/feature-map.md)、[架构导读](./website/docs/overview/architecture.md)、[当前规划](./website/docs/overview/roadmap.md);Markdown 断链构建失败;`skills/loop-agent/references/docs-converge.md` 固化文档收敛检查表。
65
152
  - 可选 repo-local SDD skill 嵌入;前端 DAG skill 契约与验证命令安全解析。
66
153
 
67
154
  ## [0.10.0] - 2026-07-12
@@ -73,17 +160,17 @@
73
160
 
74
161
  ## [0.9.0] - 2026-07-12
75
162
 
76
- - Observe 看板重设计为暖白运行控制台:统一信息层级、依赖图 + 节点表、右侧节点检查器、安全 Markdown;本机资源 guardrail。见 [Observe](./guides/observe-ui.md)。
163
+ - Observe 看板重设计为暖白运行控制台:统一信息层级、依赖图 + 节点表、右侧节点检查器、安全 Markdown;本机资源 guardrail。见 [Observe](./website/docs/guides/observe-ui.md)。
77
164
 
78
165
  ## [0.8.0] 及更早
79
166
 
80
- - 0.8.0:[CLI 参考](./reference/cli.md) 补充 `task validate-feature`、`task retry`、`scripts/worker-nightly.sh` + GitHub Actions、`capabilities: [interactive-ui]`,Task Pool 唯一根 `.harness/task-pool/`。
167
+ - 0.8.0:[CLI 参考](./website/docs/reference/cli.md) 补充 `task validate-feature`、`task retry`、`scripts/worker-nightly.sh` + GitHub Actions、`capabilities: [interactive-ui]`,Task Pool 唯一根 `.harness/task-pool/`。
81
168
  - 0.7.5:Observe 新增 DAG 依赖图可视化。
82
169
  - 0.7.0:interactive-ui writer-only HIGH 路由与 UI 交付契约。
83
170
  - 更早条目见根目录 `CHANGELOG.md`。
84
171
 
85
172
  ## 文档站点同步说明
86
173
 
87
- - 站上 guide / reference 与根 `docs/` 双树边界见 [架构导读 · 双树边界](./overview/architecture.md#双树边界)。
174
+ - 站上 guide / reference 与根 `docs/` 双树边界见 [架构导读 · 双树边界](./website/docs/overview/architecture.md#双树边界)。
88
175
  - 活能力短摘要:[`docs/reports/current-capability-summary.md`](https://github.com/tea-agent/loop-agent/blob/main/docs/reports/current-capability-summary.md)。
89
176
  - 冻结基线长文:[`docs/reports/2026-07-02-repository-analysis.md`](https://github.com/tea-agent/loop-agent/blob/main/docs/reports/2026-07-02-repository-analysis.md)(2026-07-16 再采样对齐 0.12.0 + 主干 Unreleased;勿当第二 CHANGELOG)。
package/README.md CHANGED
@@ -45,7 +45,7 @@ loop-agent inspect
45
45
 
46
46
  ## 自动发布
47
47
 
48
- 仓库通过 GitHub Actions 定时检查 `main`,达到提交门槛并通过发布检查后,自动更新版本、生成 Release 说明并发布 GitHub Release 与 npm。手动触发默认只执行 dry-run,不产生远端发布副作用;具体配置、版本规则和恢复步骤见 [`docs/exec-plans/active/2026-07-18-nightly-auto-release.md`](docs/exec-plans/active/2026-07-18-nightly-auto-release.md)。
48
+ 仓库通过 GitHub Actions 定时检查 `main`,达到提交门槛并通过发布检查后,自动更新版本、生成 Release 说明并发布 GitHub Release 与 npm。手动触发默认只执行 dry-run,不产生远端发布副作用;具体配置、版本规则和恢复步骤见 [`docs/exec-plans/completed/2026-07-18-nightly-auto-release.md`](docs/exec-plans/completed/2026-07-18-nightly-auto-release.md)。
49
49
 
50
50
  ## 初始化目标项目
51
51
 
@@ -77,7 +77,7 @@ loop-agent init --repo-root <target-repo> --profile full --merge
77
77
  loop-agent init doctor --repo-root <target-repo>
78
78
  ```
79
79
 
80
- `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/`、`.agents/skills/*/node_modules/`、`.worktrees/` 等个人/会话运行态事实和 repo-local skill 本地依赖忽略掉,同时保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`,也不会覆盖用户已有的 ignore 规则。
80
+ `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/evaluation/`、`.harness/live/`、`.harness/cache/`、`.harness/init-surface.json`、`.harness/task-pool/*`、`.task-pool/`、`.agents/skills/*/node_modules/`、`.worktrees/` 等个人/会话运行态事实和 repo-local skill 本地依赖忽略掉,同时保留 `.harness/prompts/` 和目录占位可共享,不会整目录忽略 `.harness/`,也不会覆盖用户已有的 ignore 规则。
81
81
 
82
82
  新初始化会写入 `.harness/init-surface.json`,记录当前 controller 版本、初始化投影文件 hash 和 manifest hash。已用旧版本初始化的目标项目,可以用下面的维护入口对齐新版本初始化能力:
83
83
 
@@ -105,7 +105,7 @@ loop-agent dag validate --dag <temp-dir>/<task-id>-dag.json --strict-models --st
105
105
  loop-agent run-dag --dag <temp-dir>/<task-id>-dag.json --cwd .
106
106
  ```
107
107
 
108
- `dag run-task` 会先尊重显式声明的专用 `taskKind`。对于默认 `standard` 任务,它会根据任务标题、`source/需求.md` 和结构化 `allowedPaths` 做保守、确定性的需求分类;只有高置信的前端实现需求才会自动进入前端专用节点链。只读 `frontend-mock-assess-pi` 在计划前读取 Mock/API/schema 证据,并通过确定性 contract gate 选择原生 Mock、浏览器拦截、请求适配层、`not-needed` 或明确阻塞;它只能使用 DAG 生成时已固化的验证入口。可选 `task.json.frontendMock` 可设置 `auto|required|disabled`、既有服务目录和专项验证命令;不安全或不完整的显式 required 合同不会生成 writer。真实请求始终是默认路径,唯一写节点仍是 `frontend-implement-pi`。自动分类不会覆盖显式 profile、`workflowPolicy` 或 supervised quality gate。后端、混合或证据不足的需求继续使用通用模板,也不会自动进入 `backend-test`。Mock 验证只证明前端状态与交互;未实际请求后端时,收尾保留 `Frontend status: mock-validated`、`Real integration: pending`,并给出 `<task-id>-real-api-integration-verify`。该复验任务不会自动创建或执行,需要在后端就绪后显式运行。
108
+ `dag run-task` 会先尊重显式声明的专用 `taskKind`。对于默认 `standard` 任务,它会根据任务标题、`source/需求.md` 和结构化 `allowedPaths` 做保守、确定性的需求分类;只有高置信的前端实现需求才会自动进入前端专用节点链。只读 `frontend-mock-assess-pi` 在计划前读取 Mock/API/schema 证据,并通过确定性 contract gate 选择原生 Mock、浏览器拦截、请求适配层、`not-needed` 或明确阻塞;它只能使用 DAG 生成时已固化的验证入口。可选 `task.json.frontendMock` 可设置 `auto|required|disabled`、既有服务目录和专项验证命令;默认 `auto` 下没有已确认 Mock 能力时会跳过 Mock 继续前端实现,并保留真实联调缺口,不会因为缺 Mock 本身阻塞。不安全或不完整的显式 required 合同仍不会生成 writer。真实请求始终是默认路径,唯一写节点仍是 `frontend-implement-pi`。自动分类不会覆盖显式 profile、`workflowPolicy` 或 supervised quality gate。后端、混合或证据不足的需求继续使用通用模板,也不会自动进入 `backend-test`。Mock 验证只证明前端状态与交互;未实际请求后端时,收尾保留 `Real integration: pending`,并给出 `<task-id>-real-api-integration-verify`。该复验任务不会自动创建或执行,需要在后端就绪后显式运行。
109
109
 
110
110
  测试结论写回与业务知识图谱(结构化 Git,非 RAG):
111
111
 
@@ -281,6 +281,7 @@ npm run self-host:canary -- --deterministic --tarball <candidate.tgz> --output <
281
281
  | 路径 | 用途 |
282
282
  |---|---|
283
283
  | `AGENTS.md` | 本仓库的 agent 开工协议、会话协议和长期工作规则 |
284
+ | `docs/github-collaboration.md` | 内部贡献者的轻量 GitHub 协作指南:短分支、PR、CI 与 Squash Merge |
284
285
  | `harness.json` | loop-agent 在本仓库的模型、executor、治理根目录和脚本配置 |
285
286
  | `docs/README.md` | 治理文档索引 |
286
287
  | `docs/verification-matrix.md` | 不同变更类型对应的验证命令 |
@@ -293,6 +294,8 @@ npm run self-host:canary -- --deterministic --tarball <candidate.tgz> --output <
293
294
 
294
295
  ## 本仓库开发
295
296
 
297
+ 内部贡献代码时,先阅读 [`docs/github-collaboration.md`](docs/github-collaboration.md)。当前采用轻量协作方式:一个小工作块使用一个短分支,通过简短 Pull Request、CI 和同事确认后,默认 Squash Merge 到 `main`。
298
+
296
299
  本地源码开发:
297
300
 
298
301
  ```bash
@@ -318,9 +321,9 @@ Windows 上运行 `scripts/*.sh` 时使用 Git Bash 或已配置的兼容 Bash
318
321
 
319
322
  ## 发布包内容
320
323
 
321
- 发布包包含静态运行和指导资料:`bin/`、`dist/`、`skills/`(包括 `loop-agent` 与可选的 `agent-worker` operator skill)、`docs/*.md`、`docs/architecture/runtime-boundaries.md`、`docs/skills/`、`docs/templates/`、`docs/init-surface.manifest.json`、`examples/`、`harness.json`、`AGENTS.md`、`README.md` 和 `CHANGELOG.md`。
324
+ 发布包包含静态运行和指导资料:`bin/`、`dist/`、`skills/`(包括 `loop-agent` 与可选的 `agent-worker` operator skill)、必要的治理文档(`docs/README.md`、三份 methodology、显式列出的 `docs/architecture/*.md`)、`docs/skills/`、`docs/templates/`、`docs/init-surface.manifest.json`、知识库运行脚本(`scripts/kb-*.mjs` 与 `kb-bootstrap-init-skeleton.sh`)、`examples/`、`harness.json`、`AGENTS.md`、`README.md` 和 `CHANGELOG.md`。
322
325
 
323
- `docs/progress/`、`docs/reports/`、`docs/exec-plans/`、`docs/decisions/` 等目录下的任务正文是目标仓库实时生成或历史事实;npm 包只携带这些目录的 README,不携带本仓库已有历史记录。
326
+ `docs/progress/`、`docs/reports/`、`docs/exec-plans/`、`docs/decisions/`、`docs/design/` 的任务正文与目录 README 不随包发布;`loop-agent init` 会在目标项目生成空目录契约,具体 plan/progress/report/decision 由后续真实任务写入。源仓库专属说明(如 agent-dag playbook、sidecar 说明)也不再打进 npm 包。
324
327
 
325
328
  DAG skill 指令优先从用户配置目录和目标项目 `.agents/skills/` 解析;目标项目未提供本地 skill 时,CLI 会回退到 npm 包内置的 `skills/`。因此普通项目不需要复制 loop-agent 仓库历史文档或根 `skills/` 目录才可获得默认 DAG 能力。
326
329
 
@@ -336,4 +339,20 @@ node bin/loop-agent.js --help
336
339
  npm pack --dry-run
337
340
  ```
338
341
 
342
+ `npm publish` 时会通过 `prepublishOnly` 依次执行:
343
+
344
+ 1. `node scripts/check-npm-publish-policy.mjs` — 发布策略门禁
345
+ 2. `npm run typecheck`
346
+ 3. `npm test`
347
+ 4. `npm run build`
348
+
349
+ ### frontend 分支发布限制
350
+
351
+ 在 `frontend` 分支执行 `npm publish` 时,必须同时满足:
352
+
353
+ - `package.json` 版本为合法的 SemVer **beta** 预发布版本(例如 `0.13.0-beta.1`)
354
+ - 必须显式指定 `--tag beta`(即 `npm publish --tag beta`)
355
+
356
+ 任一条件不满足时,`prepublishOnly` 会在上传 npm registry 前失败并给出中文错误提示。其他分支不受此限制。
357
+
339
358
  发布入口 `bin/loop-agent.js` 只加载 `dist/cli.js`;`npm run dev -- <args>` 只用于源码开发和定位问题。
@@ -0,0 +1,184 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { aliasHistoryPath, commitAliasMove, listAliasNames, listIncumbentCandidateIds, makeDecisionId, readIncumbentAlias, readPromotionDecision, } from "../../infrastructure/evaluation/alias-store.js";
3
+ import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
4
+ async function requireAcceptedCandidate(input) {
5
+ const record = await readCandidateRecord(input.repoRoot, input.candidateId);
6
+ if (record.status !== "accepted") {
7
+ throw new Error(`alias move requires candidate lifecycle accepted; ${input.candidateId} is ${record.status}`);
8
+ }
9
+ return {
10
+ candidateId: record.manifest.candidateId,
11
+ bundleHash: record.manifest.bundleHash,
12
+ };
13
+ }
14
+ function buildDecision(input) {
15
+ const decisionId = makeDecisionId({
16
+ alias: input.alias,
17
+ action: input.action,
18
+ toCandidateId: input.toCandidateId,
19
+ createdAt: input.createdAt,
20
+ });
21
+ return {
22
+ schemaVersion: 1,
23
+ decisionId,
24
+ alias: input.alias,
25
+ action: input.action,
26
+ fromCandidateId: input.fromCandidateId,
27
+ toCandidateId: input.toCandidateId,
28
+ toBundleHash: input.toBundleHash,
29
+ reason: input.reason,
30
+ actor: "human",
31
+ humanRequired: true,
32
+ applied: input.applied,
33
+ createdAt: input.createdAt,
34
+ campaignId: input.campaignId ?? null,
35
+ };
36
+ }
37
+ async function planAliasMove(input) {
38
+ const reason = input.reason.trim();
39
+ if (!reason) {
40
+ throw new Error("alias move requires non-empty --reason");
41
+ }
42
+ const createdAt = input.now ?? new Date().toISOString();
43
+ const previous = await readIncumbentAlias(input.repoRoot, input.alias);
44
+ if (input.action === "rollback" && !previous) {
45
+ throw new Error(`cannot rollback alias ${input.alias}: alias does not exist yet`);
46
+ }
47
+ const target = await requireAcceptedCandidate({
48
+ repoRoot: input.repoRoot,
49
+ candidateId: input.toCandidateId,
50
+ });
51
+ if (previous && previous.candidateId === target.candidateId) {
52
+ throw new Error(`alias ${input.alias} already points at candidate ${target.candidateId}`);
53
+ }
54
+ const decision = buildDecision({
55
+ alias: input.alias,
56
+ action: input.action,
57
+ fromCandidateId: previous?.candidateId ?? null,
58
+ toCandidateId: target.candidateId,
59
+ toBundleHash: target.bundleHash,
60
+ reason,
61
+ applied: !input.dryRun,
62
+ createdAt,
63
+ campaignId: input.campaignId,
64
+ });
65
+ const nextAlias = {
66
+ schemaVersion: 1,
67
+ alias: input.alias,
68
+ candidateId: target.candidateId,
69
+ bundleHash: target.bundleHash,
70
+ updatedAt: createdAt,
71
+ updatedByDecisionId: decision.decisionId,
72
+ previousCandidateId: previous?.candidateId ?? null,
73
+ };
74
+ if (input.dryRun) {
75
+ return {
76
+ dryRun: true,
77
+ decision: { ...decision, applied: false },
78
+ alias: nextAlias,
79
+ previous,
80
+ };
81
+ }
82
+ const historySeq = previous ? await nextHistorySeq(input.repoRoot, input.alias) : 1;
83
+ const historyEvent = {
84
+ schemaVersion: 1,
85
+ seq: historySeq,
86
+ decisionId: decision.decisionId,
87
+ action: input.action,
88
+ fromCandidateId: previous?.candidateId ?? null,
89
+ toCandidateId: target.candidateId,
90
+ toBundleHash: target.bundleHash,
91
+ at: createdAt,
92
+ };
93
+ await commitAliasMove({
94
+ repoRoot: input.repoRoot,
95
+ decision,
96
+ nextAlias,
97
+ historyEvent,
98
+ });
99
+ // W2.4 does not auto-retire the previous incumbent: lifecycle `accepted` ≠
100
+ // alias pointer, and rollback drills must be able to point back without
101
+ // inventing a new candidate id. Operators may `eval candidate transition
102
+ // --to retired` separately.
103
+ return {
104
+ dryRun: false,
105
+ decision,
106
+ alias: (await readIncumbentAlias(input.repoRoot, input.alias)),
107
+ previous,
108
+ };
109
+ }
110
+ async function nextHistorySeq(repoRoot, alias) {
111
+ const text = await readFile(aliasHistoryPath(repoRoot, alias), "utf-8").catch(() => "");
112
+ const lines = text
113
+ .split("\n")
114
+ .map((line) => line.trim())
115
+ .filter(Boolean);
116
+ return lines.length + 1;
117
+ }
118
+ export async function promoteAlias(input) {
119
+ return planAliasMove({
120
+ ...input,
121
+ action: "promote",
122
+ dryRun: input.dryRun !== false,
123
+ });
124
+ }
125
+ export async function rollbackAlias(input) {
126
+ return planAliasMove({
127
+ ...input,
128
+ action: "rollback",
129
+ dryRun: input.dryRun !== false,
130
+ });
131
+ }
132
+ export async function showAlias(input) {
133
+ const current = await readIncumbentAlias(input.repoRoot, input.alias);
134
+ if (!current) {
135
+ throw new Error(`alias not found: ${input.alias}`);
136
+ }
137
+ return current;
138
+ }
139
+ export async function listAliases(input) {
140
+ const names = await listAliasNames(input.repoRoot);
141
+ const rows = [];
142
+ for (const name of names) {
143
+ const current = await readIncumbentAlias(input.repoRoot, name);
144
+ if (current)
145
+ rows.push(current);
146
+ }
147
+ return rows;
148
+ }
149
+ export async function isCandidatePromotionApplied(input) {
150
+ const incumbents = await listIncumbentCandidateIds(input.repoRoot);
151
+ return incumbents.has(input.candidateId);
152
+ }
153
+ export function formatAliasMarkdown(alias) {
154
+ return [
155
+ `# Alias: ${alias.alias}`,
156
+ "",
157
+ `- candidateId: \`${alias.candidateId}\``,
158
+ `- bundleHash: \`${alias.bundleHash}\``,
159
+ `- previousCandidateId: \`${alias.previousCandidateId ?? "null"}\``,
160
+ `- updatedByDecisionId: \`${alias.updatedByDecisionId}\``,
161
+ `- updatedAt: \`${alias.updatedAt}\``,
162
+ "",
163
+ ].join("\n");
164
+ }
165
+ export function formatAliasMoveMarkdown(result) {
166
+ const mode = result.dryRun ? "dry-run" : "applied";
167
+ return [
168
+ `# Alias ${result.decision.action} (${mode})`,
169
+ "",
170
+ `- alias: \`${result.decision.alias}\``,
171
+ `- from: \`${result.decision.fromCandidateId ?? "null"}\``,
172
+ `- to: \`${result.decision.toCandidateId}\``,
173
+ `- bundleHash: \`${result.decision.toBundleHash}\``,
174
+ `- decisionId: \`${result.decision.decisionId}\``,
175
+ `- applied: \`${result.decision.applied}\``,
176
+ `- reason: ${result.decision.reason}`,
177
+ "",
178
+ result.dryRun
179
+ ? "_No alias files were written (dry-run). Pass `--apply` to commit._"
180
+ : "_Alias pointer updated; candidate bundles and completed DAG facts were not rewritten._",
181
+ "",
182
+ ].join("\n");
183
+ }
184
+ export { readPromotionDecision };
@@ -0,0 +1,192 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Campaign / DAG hard-budget contract (Eval Lab W3.4–W3.5 / M3).
4
+ *
5
+ * Token incompleteness: missing `tokensUsed` must never be treated as 0.
6
+ * When `maxTokens` is set, only known token samples accumulate; breach fires
7
+ * only when the known sum exceeds the limit. Calls + wall time + repair passes
8
+ * are the primary hard dimensions for v1.
9
+ */
10
+ export const campaignBudgetModeSchema = z.enum(["hard", "record-only"]);
11
+ export const campaignBudgetLimitsSchema = z
12
+ .object({
13
+ maxTokens: z.number().int().positive().optional(),
14
+ maxWallTimeMs: z.number().int().positive().optional(),
15
+ maxExecutorCalls: z.number().int().positive().optional(),
16
+ maxRepairPasses: z.number().int().nonnegative().optional(),
17
+ maxConcurrency: z.number().int().positive().optional(),
18
+ maxContextChars: z.number().int().positive().optional(),
19
+ })
20
+ .strict()
21
+ .superRefine((limits, ctx) => {
22
+ const hasHardDim = limits.maxTokens !== undefined ||
23
+ limits.maxWallTimeMs !== undefined ||
24
+ limits.maxExecutorCalls !== undefined ||
25
+ limits.maxRepairPasses !== undefined ||
26
+ limits.maxContextChars !== undefined;
27
+ if (!hasHardDim) {
28
+ ctx.addIssue({
29
+ code: z.ZodIssueCode.custom,
30
+ message: "budget.limits requires at least one of maxTokens, maxWallTimeMs, maxExecutorCalls, maxRepairPasses, maxContextChars",
31
+ });
32
+ }
33
+ });
34
+ export const campaignBudgetSchema = z
35
+ .object({
36
+ schemaVersion: z.literal(1),
37
+ mode: campaignBudgetModeSchema.default("hard"),
38
+ limits: campaignBudgetLimitsSchema,
39
+ })
40
+ .strict();
41
+ export function createBudgetLedger(budget) {
42
+ return {
43
+ schemaVersion: 1,
44
+ mode: budget.mode,
45
+ limits: { ...budget.limits },
46
+ consumed: {
47
+ tokens: null,
48
+ wallTimeMs: 0,
49
+ executorCalls: 0,
50
+ repairPasses: 0,
51
+ peakContextChars: 0,
52
+ missingTokenNodeIds: [],
53
+ },
54
+ breaches: [],
55
+ status: budget.mode === "record-only" ? "record-only" : "ok",
56
+ };
57
+ }
58
+ export function resolveEffectiveMaxConcurrent(requested, budget) {
59
+ const limit = budget?.limits.maxConcurrency;
60
+ if (limit === undefined) {
61
+ return { maxConcurrent: Math.max(1, requested), clamped: false };
62
+ }
63
+ const maxConcurrent = Math.max(1, Math.min(requested, limit));
64
+ return { maxConcurrent, clamped: maxConcurrent < requested };
65
+ }
66
+ function pushBreach(ledger, breach) {
67
+ const full = {
68
+ ...breach,
69
+ at: breach.at ?? new Date().toISOString(),
70
+ };
71
+ const duplicate = ledger.breaches.some((existing) => existing.dimension === full.dimension &&
72
+ existing.nodeId === full.nodeId &&
73
+ existing.limit === full.limit);
74
+ if (!duplicate) {
75
+ ledger.breaches.push(full);
76
+ }
77
+ if (ledger.mode === "hard") {
78
+ ledger.status = "breached";
79
+ }
80
+ }
81
+ function checkLimit(ledger, dimension, consumed, limit, opts) {
82
+ if (limit === undefined)
83
+ return undefined;
84
+ const exceeded = opts?.inclusive ? consumed >= limit : consumed > limit;
85
+ if (!exceeded)
86
+ return undefined;
87
+ const breach = {
88
+ dimension,
89
+ limit,
90
+ consumed,
91
+ at: new Date().toISOString(),
92
+ ...(opts?.nodeId ? { nodeId: opts.nodeId } : {}),
93
+ };
94
+ pushBreach(ledger, breach);
95
+ return breach;
96
+ }
97
+ /**
98
+ * Pre-node check (no call increment yet).
99
+ * Discrete call budget uses inclusive compare so maxExecutorCalls=1 allows the
100
+ * first node then blocks starting a second.
101
+ */
102
+ export function checkBudgetPreNode(ledger, sample) {
103
+ ledger.consumed.wallTimeMs = sample.wallTimeMs;
104
+ ledger.consumed.repairPasses = sample.repairPasses;
105
+ return (checkLimit(ledger, "executorCalls", ledger.consumed.executorCalls, ledger.limits.maxExecutorCalls, { inclusive: true }) ??
106
+ checkLimit(ledger, "wallTimeMs", sample.wallTimeMs, ledger.limits.maxWallTimeMs) ??
107
+ checkLimit(ledger, "repairPasses", sample.repairPasses, ledger.limits.maxRepairPasses));
108
+ }
109
+ /**
110
+ * Record one finished node execution attempt and evaluate hard limits.
111
+ * Increments executorCalls by 1. Does not invent tokens when missing.
112
+ */
113
+ export function recordNodeBudgetSample(ledger, sample) {
114
+ ledger.consumed.executorCalls += 1;
115
+ ledger.consumed.wallTimeMs = sample.wallTimeMs;
116
+ ledger.consumed.repairPasses = sample.repairPasses;
117
+ if (sample.tokensUsed === undefined) {
118
+ if (!ledger.consumed.missingTokenNodeIds.includes(sample.nodeId)) {
119
+ ledger.consumed.missingTokenNodeIds.push(sample.nodeId);
120
+ }
121
+ }
122
+ else {
123
+ ledger.consumed.tokens = (ledger.consumed.tokens ?? 0) + sample.tokensUsed;
124
+ }
125
+ if (sample.contextChars !== undefined) {
126
+ ledger.consumed.peakContextChars = Math.max(ledger.consumed.peakContextChars, sample.contextChars);
127
+ }
128
+ return (checkLimit(ledger, "executorCalls", ledger.consumed.executorCalls, ledger.limits.maxExecutorCalls, { nodeId: sample.nodeId, inclusive: true }) ??
129
+ checkLimit(ledger, "wallTimeMs", ledger.consumed.wallTimeMs, ledger.limits.maxWallTimeMs, { nodeId: sample.nodeId }) ??
130
+ checkLimit(ledger, "repairPasses", ledger.consumed.repairPasses, ledger.limits.maxRepairPasses, { nodeId: sample.nodeId }) ??
131
+ (ledger.consumed.tokens !== null
132
+ ? checkLimit(ledger, "tokens", ledger.consumed.tokens, ledger.limits.maxTokens, { nodeId: sample.nodeId })
133
+ : undefined) ??
134
+ checkLimit(ledger, "contextChars", ledger.consumed.peakContextChars, ledger.limits.maxContextChars, { nodeId: sample.nodeId }));
135
+ }
136
+ export function isHardBudgetBreached(ledger) {
137
+ return Boolean(ledger && ledger.mode === "hard" && ledger.status === "breached");
138
+ }
139
+ /** Mark all PENDING nodes as SKIPPED due to budget breach (fail closed). */
140
+ export function skipPendingNodesForBudgetBreach(nodes, breach) {
141
+ const skipped = [];
142
+ const reason = `budget_breach:${breach.dimension}`;
143
+ for (const [id, node] of Object.entries(nodes)) {
144
+ if (node.status !== "PENDING")
145
+ continue;
146
+ node.status = "SKIPPED";
147
+ node.skippedReason = reason;
148
+ skipped.push(id);
149
+ }
150
+ return skipped;
151
+ }
152
+ export function formatBudgetReportMarkdown(ledger) {
153
+ const limits = ledger.limits;
154
+ const c = ledger.consumed;
155
+ const lines = [
156
+ `# Budget Ledger`,
157
+ ``,
158
+ `- **mode**: ${ledger.mode}`,
159
+ `- **status**: ${ledger.status}`,
160
+ ``,
161
+ `## Limits`,
162
+ ``,
163
+ `| dimension | limit |`,
164
+ `|---|---|`,
165
+ `| maxTokens | ${limits.maxTokens ?? "n/a"} |`,
166
+ `| maxWallTimeMs | ${limits.maxWallTimeMs ?? "n/a"} |`,
167
+ `| maxExecutorCalls | ${limits.maxExecutorCalls ?? "n/a"} |`,
168
+ `| maxRepairPasses | ${limits.maxRepairPasses ?? "n/a"} |`,
169
+ `| maxConcurrency | ${limits.maxConcurrency ?? "n/a"} |`,
170
+ `| maxContextChars | ${limits.maxContextChars ?? "n/a"} |`,
171
+ ``,
172
+ `## Consumed`,
173
+ ``,
174
+ `| dimension | value |`,
175
+ `|---|---|`,
176
+ `| tokens | ${c.tokens ?? "n/a (missing)"} |`,
177
+ `| wallTimeMs | ${c.wallTimeMs} |`,
178
+ `| executorCalls | ${c.executorCalls} |`,
179
+ `| repairPasses | ${c.repairPasses} |`,
180
+ `| peakContextChars | ${c.peakContextChars} |`,
181
+ `| missingTokenNodes | ${c.missingTokenNodeIds.join(", ") || "none"} |`,
182
+ ];
183
+ if (ledger.breaches.length > 0) {
184
+ lines.push(``, `## Breaches`, ``);
185
+ for (const breach of ledger.breaches) {
186
+ lines.push(`- **${breach.dimension}**: consumed ${breach.consumed} > limit ${breach.limit}` +
187
+ (breach.nodeId ? ` (node ${breach.nodeId})` : "") +
188
+ ` at ${breach.at}`);
189
+ }
190
+ }
191
+ return `${lines.join("\n")}\n`;
192
+ }