@tea-agent/loop-agent 0.15.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 (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +59 -11
  3. package/README.md +1 -1
  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/knowledge-curator.js +3 -0
  64. package/dist/workflows/dag/node-execution.js +11 -4
  65. package/dist/workflows/dag/prompt.js +1 -1
  66. package/dist/workflows/dag/runner.js +43 -16
  67. package/dist/workflows/dag/skill-snapshot.js +11 -7
  68. package/dist/workflows/dag/types.js +18 -0
  69. package/docs/init-surface.manifest.json +3 -0
  70. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  71. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  72. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  73. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  74. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  75. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  76. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  77. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  78. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  79. package/docs/templates/product-line/AGENTS.md +1 -0
  80. package/docs/templates/product-line/README.md +17 -0
  81. package/docs/templates/product-line/acceptance.yaml +9 -0
  82. package/docs/templates/product-line/feature.yaml +11 -0
  83. package/docs/templates/product-line/task-graph.yaml +8 -0
  84. package/docs/templates/product-line/task.yaml +4 -0
  85. package/harness.json +1 -1
  86. package/package.json +1 -1
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,44 @@
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
+
3
42
  ## [0.15.0] - 2026-07-18
4
43
 
5
44
  ### 重点更新
@@ -59,17 +98,26 @@
59
98
 
60
99
  ## [Unreleased]
61
100
 
62
- - 新增 Pull Request 的只读 AI 自动审查:在创建、重新打开、转为 ready 或更新提交时检查 bug、测试缺口、runtime boundary、command registry、初始化投影与文档收敛,并更新单条 PR 评论;Fork PR 无模型密钥时安全跳过。
63
- - 修复私有仓库 AI PR 审查因通过网页 `*.diff` 地址拉取差异导致 404 的问题,改为经 GitHub API 获取 diff。
64
- - 新增 `.editorconfig` `.gitattributes`,统一常见文本文件的 UTF-8、LF、缩进和文件末尾换行约定,减少不同 IDE 与操作系统产生的无意义差异;本轮不引入 Git hooks 或格式化依赖。
65
- - 新增面向内部研发人员的轻量 GitHub 协作指南和简短 Pull Request 模板,统一采用短分支、CI Squash Merge 的最小协作闭环,不改变现有 Agent DAG 和发布流程。
66
- - 收紧 npm 发布包中的 `docs/` 范围:只保留 init/runtime 所需的治理文档、architecture、skills templates;源仓库专属说明与 progress/report/exec-plan 等目录契约不再打进包内。
67
- - 收紧 npm 发布包中的 `scripts/`:只保留 `loop-agent knowledge` 实际调用的知识库脚本;源仓治理检查与 `.sh` 薄包装不再打进包内。
68
- - 新增内部测试版本夜间自动发布:达到提交门槛并通过发布检查后,自动更新版本、生成中文说明并发布 GitHub Releasenpm;手动运行默认只做 dry-run
69
- - 发布说明支持 OpenAI-compatible 模型,模型不可用时自动使用保守摘要。
70
- - 修复 PR #34 发布风险:合并到 main 后包版本被错误提升为 `0.14.1-beta.0`,现已恢复为 `0.14.0`;发布门禁由仅按 frontend 分支判断改为按版本号的 prerelease 状态限制 dist-tag(beta prerelease 只允许 `beta` tag,使用 `latest`/空/默认 tag 时 fail-closed;非 beta prerelease 如 alpha/rc 全局拒绝),避免在 main 上执行默认 `npm publish` 时把 prerelease 误发布到 `latest`。
71
- - `frontend` 分支继续保留本地 npm beta 发布门禁,并在合入 main 0.14.0 后把测试版本线调整为 `0.14.1-beta.0`,避免误推正式版本或 `latest`。
72
- - 前端实现 DAG `frontendMock.policy=auto` 不再因为目标项目缺少 Mock 能力而阻塞;无确认 Mock 能力时跳过 Mock、保留真实请求默认路径,并在未真实联调时记录 `Frontend status: locally-validated` `Real integration: pending`。
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 的迁移提示。
73
121
 
74
122
  ## [0.13.0] - 2026-07-18
75
123
 
package/README.md CHANGED
@@ -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
 
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ import { formatContentSha, sha256Hex, } from "./candidate-hash.js";
2
+ /** Frozen placeholder for M4; M5 may replace with real scoring rules bytes. */
3
+ export const SCORING_RULES_V1 = "promotion-policy-v1";
4
+ export function computeScoringRulesHash(policyVersion = SCORING_RULES_V1) {
5
+ return formatContentSha(sha256Hex(policyVersion));
6
+ }
7
+ export function computeBudgetHash(budget) {
8
+ const canonical = {
9
+ schemaVersion: budget.schemaVersion,
10
+ mode: budget.mode,
11
+ limits: Object.fromEntries(Object.entries(budget.limits).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
12
+ };
13
+ return formatContentSha(sha256Hex(JSON.stringify(canonical)));
14
+ }
15
+ export function computePrivateVerifierManifestHash(manifest) {
16
+ const checks = [...manifest.checks]
17
+ .map((check) => ({
18
+ checkId: check.checkId,
19
+ kind: check.kind,
20
+ command: [...check.command],
21
+ cwd: check.cwd ?? ".",
22
+ timeoutMs: check.timeoutMs ?? 60_000,
23
+ expectedExitCode: check.expectedExitCode ?? 0,
24
+ }))
25
+ .sort((a, b) => a.checkId < b.checkId ? -1 : a.checkId > b.checkId ? 1 : 0);
26
+ const canonical = {
27
+ schemaVersion: 1,
28
+ privateVerifierId: manifest.privateVerifierId,
29
+ checks,
30
+ };
31
+ return formatContentSha(sha256Hex(JSON.stringify(canonical)));
32
+ }
33
+ export function computeCampaignPlanHash(campaignId, cells) {
34
+ const sorted = [...cells].sort((a, b) => a.cellId < b.cellId ? -1 : a.cellId > b.cellId ? 1 : 0);
35
+ const canonical = {
36
+ schemaVersion: 1,
37
+ campaignId,
38
+ cells: sorted,
39
+ };
40
+ return formatContentSha(sha256Hex(JSON.stringify(canonical)));
41
+ }
42
+ export function assertPrivateVerifierHash(manifest) {
43
+ const recomputed = computePrivateVerifierManifestHash(manifest);
44
+ if (recomputed !== manifest.manifestHash) {
45
+ throw new Error(`privateVerifier manifestHash mismatch for ${manifest.privateVerifierId}: expected ${recomputed}, got ${manifest.manifestHash}`);
46
+ }
47
+ }