@hunter-harness/workflow-harness 0.2.24 → 0.2.26

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 (138) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +6 -4
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +4 -3
  12. package/harness/bundles/general/claude-code/harness-sync/reference.md +12 -3
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/scripts/harness_archive.py +124 -17
  15. package/harness/bundles/general/claude-code/scripts/harness_integration.py +77 -2
  16. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  17. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  18. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +6 -4
  19. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  20. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  21. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +4 -3
  27. package/harness/bundles/general/codebuddy/harness-sync/reference.md +12 -3
  28. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +124 -17
  30. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +77 -2
  31. package/harness/bundles/general/codex/.harness-build.json +1 -1
  32. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  33. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +6 -4
  34. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  35. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  36. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  37. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  38. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  39. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  40. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-sync/SKILL.md +4 -3
  42. package/harness/bundles/general/codex/harness-sync/reference.md +12 -3
  43. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/scripts/harness_archive.py +124 -17
  45. package/harness/bundles/general/codex/scripts/harness_integration.py +77 -2
  46. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  47. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  48. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +6 -4
  49. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  50. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  51. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  52. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  53. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  54. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  55. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  56. package/harness/bundles/general/cursor/harness-sync/SKILL.md +4 -3
  57. package/harness/bundles/general/cursor/harness-sync/reference.md +12 -3
  58. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/scripts/harness_archive.py +124 -17
  60. package/harness/bundles/general/cursor/scripts/harness_integration.py +77 -2
  61. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  62. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  63. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  64. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +6 -4
  65. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  66. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  67. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  68. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  69. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  70. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  71. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  72. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  73. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +4 -3
  74. package/harness/bundles/java/claude-code/harness-sync/reference.md +12 -3
  75. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  76. package/harness/bundles/java/claude-code/scripts/harness_archive.py +124 -17
  77. package/harness/bundles/java/claude-code/scripts/harness_integration.py +77 -2
  78. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  79. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  80. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  81. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +6 -4
  82. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  83. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  84. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  85. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  86. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  87. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  88. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  89. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  90. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +4 -3
  91. package/harness/bundles/java/codebuddy/harness-sync/reference.md +12 -3
  92. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  93. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +124 -17
  94. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +77 -2
  95. package/harness/bundles/java/codex/.harness-build.json +1 -1
  96. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  97. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  98. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +6 -4
  99. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  100. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  101. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  102. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  103. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  104. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  105. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  106. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  107. package/harness/bundles/java/codex/harness-sync/SKILL.md +4 -3
  108. package/harness/bundles/java/codex/harness-sync/reference.md +12 -3
  109. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  110. package/harness/bundles/java/codex/scripts/harness_archive.py +124 -17
  111. package/harness/bundles/java/codex/scripts/harness_integration.py +77 -2
  112. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  113. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  114. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  115. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +6 -4
  116. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  117. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  118. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  119. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  120. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  121. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  122. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  123. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  124. package/harness/bundles/java/cursor/harness-sync/SKILL.md +4 -3
  125. package/harness/bundles/java/cursor/harness-sync/reference.md +12 -3
  126. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  127. package/harness/bundles/java/cursor/scripts/harness_archive.py +124 -17
  128. package/harness/bundles/java/cursor/scripts/harness_integration.py +77 -2
  129. package/harness/manifests/general/claude-code.json +16 -16
  130. package/harness/manifests/general/codebuddy.json +16 -16
  131. package/harness/manifests/general/codex.json +16 -16
  132. package/harness/manifests/general/cursor.json +16 -16
  133. package/harness/manifests/java/claude-code.json +18 -18
  134. package/harness/manifests/java/codebuddy.json +18 -18
  135. package/harness/manifests/java/codex.json +18 -18
  136. package/harness/manifests/java/cursor.json +18 -18
  137. package/hunter-workflow-family.json +2 -2
  138. package/package.json +1 -1
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "8fdeda6094f4f0a5"
5
+ "coreHash": "d516445f37019ef0"
6
6
  }
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-archive — 变更归档
25
25
 
26
26
  ## Purpose
@@ -57,6 +57,7 @@ const stages = Object.entries(record(data.stageStatus));
57
57
  const verification = record(data.verification);
58
58
  const unit = record(verification.unitTests);
59
59
  const api = record(verification.apiTests);
60
+ const performance = record(verification.performance);
60
61
  const durations = record(data.durations);
61
62
  const durationStages = list(durations.stages);
62
63
  const timing = record(data.timing);
@@ -84,9 +85,10 @@ const reasonHtml = statusReasons.length
84
85
 
85
86
  const stageHtml = stages.map(([name, status]) => `<div class="row"><span>${esc(name)}</span>${pill(status)}</div>`).join("") || '<p class="empty">没有阶段状态记录</p>';
86
87
  const verificationHtml = [
87
- ["单元测试", unit.status || ((number(unit.failures) + number(unit.errors)) > 0 ? "FAIL" : (number(unit.run) > 0 ? "OK" : "NOT_RUN")), `${number(unit.run)} 个 · ${number(unit.failures)} 失败 · ${number(unit.errors)} 错误`],
88
- ["API 测试", api.status || "NOT_RUN", `${number(api.passed)}/${number(api.total)} 通过 · ${number(api.blocked)} 阻塞`],
89
- ["数据库兼容", verification.dbCompatibility || "NOT_RUN", verification.coverageDisplay || "未记录覆盖率"]
88
+ ["单元测试", unit.status || ((number(unit.failures) + number(unit.errors)) > 0 ? "FAIL" : (number(unit.run) > 0 ? "OK" : "NOT_RUN")), `${number(unit.run)} 个 · ${number(unit.failures)} 失败 · ${number(unit.errors)} 错误 · ${number(unit.skipped)} 跳过 · ${number(unit.deselected)} 未选择`],
89
+ ["API 测试", api.status || "NOT_RUN", `${number(api.executed)}/${number(api.total)} 已执行 · ${number(api.passed)} 通过 · ${number(api.failed)} 失败 · ${number(api.blocked)} 阻塞 · 通过率 ${api.passRate || "N/A"} · 执行率 ${api.executionRate || "N/A"}`],
90
+ ["数据库兼容", verification.dbCompatibility || "NOT_RUN", verification.coverageDisplay || "未记录覆盖率"],
91
+ ["性能验证", performance.status || "NOT_RUN", Object.keys(record(performance.metrics)).length ? JSON.stringify(performance.metrics) : "未记录性能指标"]
90
92
  ].map(([name, status, note]) => `<div class="row"><div><strong>${esc(name)}</strong><small>${esc(note)}</small></div>${pill(status)}</div>`).join("");
91
93
  const durationHtml = durationStages.map((item) => {
92
94
  const width = Math.max(2, Math.round(number(item.minutes) / maxMinutes * 100));
@@ -112,7 +114,7 @@ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><met
112
114
  <details><summary>变更文件(${files.length})</summary><div><table><thead><tr><th>文件</th><th>新增</th><th>删除</th></tr></thead><tbody>${fileRows}</tbody></table></div></details>
113
115
  <details><summary>执行时间线与工具交接(${timeline.length})</summary><div><table><thead><tr><th>阶段</th><th>尝试</th><th>状态</th><th>来源 / 摘要</th></tr></thead><tbody>${timelineRows}</tbody></table></div></details>
114
116
  <details><summary>命令证据(${commands.length})</summary><div><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandRows}</tbody></table></div></details>
115
- <details><summary>技术元数据</summary><div><dl><dt>产品提交</dt><dd><code>${esc(data.productCommit || data.finalCommit || "N/A")}</code></dd><dt>产品树哈希</dt><dd><code>${esc(data.productTreeHash || "N/A")}</code></dd><dt>归档提交</dt><dd><code>${esc(data.archiveCommit || data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
117
+ <details><summary>技术元数据</summary><div><dl><dt>功能合并提交</dt><dd><code>${esc(data.featureMergeHash || data.productCommit || data.finalCommit || "N/A")}</code></dd><dt>发布线尖端</dt><dd><code>${esc(data.releaseTipHash || data.archiveCommit || "N/A")}</code></dd><dt>产品提交</dt><dd><code>${esc(data.productCommit || data.finalCommit || "N/A")}</code></dd><dt>产品树哈希</dt><dd><code>${esc(data.productTreeHash || "N/A")}</code></dd><dt>归档提交</dt><dd><code>${esc(data.archiveCommit || data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
116
118
  </main></body></html>`;
117
119
 
118
120
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-ingest
25
25
 
26
26
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-query
25
25
 
26
26
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-plan — 需求规划
25
25
 
26
26
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(curl *)
22
22
  - Bash(codegraph *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-review — 代码审查
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(curl *)
22
22
  - Bash(codegraph *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-run — 需求编码
26
26
 
27
27
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-submit — 最终提交(含 worktree 合并)
25
25
 
26
26
  ## Purpose
@@ -19,12 +19,12 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
23
23
  # harness-sync — 元数据同步
24
24
 
25
25
  ## Purpose
26
26
 
27
- 检查并更新 AI 元数据(CodeGraph、codebase map、knowledge 索引、CLAUDE.md、AGENTS.md、`.harness/`),使理解与代码一致。
27
+ 检查并更新 AI 元数据(CodeGraph、codebase map、knowledge 索引、公共规则、CLAUDE.md、AGENTS.md、`.harness/`),使理解与代码一致。
28
28
 
29
29
  ## When to Use
30
30
 
@@ -57,6 +57,7 @@ python <skills-root>/scripts/harness_sync.py finalize --project . --run-id <run-
57
57
  | 3.6 | 单次运行 `harness_knowledge.py maintain --project . --drain --json`,有界推进全部 `.harness/knowledge/maintenance-outbox/{pending,failed}`;不得为每个条目重复启动 Python/重建索引(§8.2:archive close 只 enqueue,sync 异步推进 outbox 到 completed/pending-judge) |
58
58
  | 4 | CLAUDE.md 完整性/行数 → 超限 blocking user confirmation 瘦身 |
59
59
  | 5 | AGENTS.md 与各 Agent 指令入口一致;Claude Code 启用时验证 CLAUDE.md 引用 AGENTS.md,禁止反向循环引用 |
60
+ | 5.5 | 运行 `npx hunter-harness rules-sync --json`:扫描各 Agent 用户规则并收敛到 `.harness/rules/`,刷新受管投影;读取结构化 review/test/archive 证据生成 `.harness/knowledge/rule-candidates.json`。分歧只报告不覆盖,候选只生成不激活 |
60
61
  | 6 | `.harness/` 结构(init 规程 → `reference.md` 第 6 步);已装 skill 新鲜度只经 `hunter-harness refresh --dry-run --json` 的 post-adaptation freshness 判断,禁止 raw build 字节比较 |
61
62
  | 7–9 | `项目规则(见 .harness/context-index.json)/`、构建配置、测试目录 — **只提示不自动修复** |
62
63
 
@@ -71,7 +72,7 @@ python <skills-root>/scripts/harness_sync.py finalize --project . --run-id <run-
71
72
 
72
73
  ## 关键规则
73
74
 
74
- 变更量先行 · CodeGraph 依赖编译产物 · map 与 Repomix 互斥 · CLAUDE 瘦身须用户确认 · Phase 7–9 只提示 · knowledge sync 失败记 WARN/FAIL
75
+ 变更量先行 · CodeGraph 依赖编译产物 · map 与 Repomix 互斥 · CLAUDE 瘦身须用户确认 · 公共规则自动收敛但分歧不覆盖 · 历史规则只生成候选 · Phase 7–9 只提示 · knowledge sync 失败记 WARN/FAIL
75
76
 
76
77
  ## Output Format
77
78
 
@@ -64,6 +64,15 @@ powershell.exe -Command "git -C '<项目路径>' diff --stat HEAD~5 2>$null"
64
64
 
65
65
  检查 `AGENTS.md` 是否包含项目概述和规则索引。Claude Code adapter 启用时,检查 `CLAUDE.md` 引用 `AGENTS.md`(canonical managed block 使用 `@AGENTS.md`),让共享约束保持单一来源;不得要求 `AGENTS.md` 反向引用 `CLAUDE.md`,否则会形成循环,且会破坏未启用 Claude Code 的项目。
66
66
 
67
+ ### 5.5 公共规则收敛与历史候选
68
+
69
+ 运行 `npx hunter-harness rules-sync --json`。该命令执行两条相互隔离的链路:
70
+
71
+ - 扫描 Claude、Cursor、CodeBuddy 的用户规则;相同且全局适用的内容归一到 `.harness/rules/*.md`,再按启用 Agent 生成受管投影。receipt 已确认的投影不作为导入源;手工修改或同名异义规则返回冲突且不覆盖;带 glob/path 范围的规则保留为 Agent 专属。
72
+ - 增量比对结构化 review findings、test failure 与 archive summary,只有跨两个独立归档重复出现,或单次高严重度且证据充分的问题才进入 `.harness/knowledge/rule-candidates.json`。候选包含来源和置信度,但不会自动写入 `.harness/rules/`。
73
+
74
+ `exit_code=5` 表示存在需要人工取舍的规则分歧,应在状态表中列出路径;不得用 `--force-managed` 绕过。第二次运行无输入变化时必须无新增迁移、无重复提示、候选文件保持不变。
75
+
67
76
  ### 6. .harness/ 完整性
68
77
 
69
78
  检查 `.harness/` 目录结构和配置文件。结构以产品 file-policy(`requirements/.../22-FILE-POLICY-MATRIX`)为准,init 实际产出的核心路径如下:
@@ -86,11 +95,11 @@ powershell.exe -Command "git -C '<项目路径>' diff --stat HEAD~5 2>$null"
86
95
  > 4. **恢复**:逐个比对 `harness-*/SKILL.md` 与备份;被覆盖的从备份恢复,新增 skill(如 `harness-knowledge-ingest`、`harness-skill-optimizer`)保留。
87
96
  > 5. **验证**:`.harness/project.yaml`、`context-index.json` 就位;`CLAUDE.md`/`AGENTS.md` 的 managed block 为增量插入(原内容保留)。
88
97
  >
89
- > file-policy 备注:`.harness/changes/**`(user_editable,push/update=never,本地工作材料)、`.harness/rules/**`(internal_state,本地默认不创建)都不作为完整性判断标志;旧文本的 `.harness/config/harness-test-config.md` 路径在产品中不存在,勿作检查项。
98
+ > file-policy 备注:`.harness/changes/**`(user_editable,push/update=never,本地工作材料)不作为完整性判断标志;`.harness/rules/**` 是可推送的公共规则唯一真源,由 rules-sync 管理投影。旧文本的 `.harness/config/harness-test-config.md` 路径在产品中不存在,勿作检查项。
90
99
 
91
- ### 7. 项目规则(见 .harness/context-index.json)/ 完整性
100
+ ### 7. `.harness/rules/` 完整性
92
101
 
93
- 用 Glob 搜索 `项目规则(见 .harness/context-index.json)/*.md`,检查是否覆盖 5 个必要主题:
102
+ 用 Glob 搜索 `.harness/rules/*.md`,检查是否覆盖 5 个必要主题:
94
103
 
95
104
  | 必要主题 | 检测方式 | 缺失影响 |
96
105
  |----------|----------|----------|
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(node *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=8fdeda6094f4f0a5; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=d516445f37019ef0; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-test — 测试执行
27
27
 
28
28
  ## Purpose
@@ -900,10 +900,22 @@ def resolve_product_archive_identity(
900
900
  or ledger.get("finalCommit")
901
901
  or ""
902
902
  )
903
+ feature_merge = str(
904
+ ledger.get("featureMergeHash")
905
+ or ledger.get("mergeFinalHash")
906
+ or product
907
+ ).strip()
908
+ code, current_head, _ = git_run(project_root, "rev-parse", "HEAD")
909
+ release_tip = str(
910
+ ledger.get("releaseTipHash")
911
+ or (current_head if code == 0 else "")
912
+ or ledger.get("archiveCommit")
913
+ or product
914
+ ).strip()
903
915
  archive = (
904
916
  archive_commit
905
917
  or ledger.get("archiveCommit")
906
- or ledger.get("finalCommit")
918
+ or release_tip
907
919
  or product
908
920
  )
909
921
  tree = str(ledger.get("productTreeHash") or "")
@@ -930,6 +942,8 @@ def resolve_product_archive_identity(
930
942
  tree = hashlib.sha256(b"fixture-no-product-tree").hexdigest()
931
943
  identity = {
932
944
  "productCommit": str(product),
945
+ "featureMergeHash": feature_merge,
946
+ "releaseTipHash": release_tip,
933
947
  "productTreeHash": str(tree),
934
948
  "archiveCommit": str(archive),
935
949
  "productTreeHashTruncated": bool(tree_meta.get("truncated")),
@@ -1659,6 +1673,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1659
1673
 
1660
1674
  run = failures = errors = skipped = 0
1661
1675
  passed_count: int | None = None
1676
+ deselected = 0
1662
1677
  pass_rate: Any = None
1663
1678
  source = "committed"
1664
1679
  counted = False
@@ -1675,6 +1690,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1675
1690
  "failures",
1676
1691
  "errors",
1677
1692
  "skipped",
1693
+ "deselected",
1678
1694
  )
1679
1695
  ):
1680
1696
  if "total" in metrics:
@@ -1690,6 +1706,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1690
1706
  failures = int(metrics.get("failed", metrics.get("failures", 0)) or 0)
1691
1707
  errors = int(metrics.get("errors", 0) or 0)
1692
1708
  skipped = int(metrics.get("skipped", 0) or 0)
1709
+ deselected = int(metrics.get("deselected", 0) or 0)
1693
1710
  if "passed" in metrics:
1694
1711
  passed_count = int(metrics.get("passed", 0) or 0)
1695
1712
  pass_rate = metrics.get("passRate")
@@ -1708,6 +1725,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1708
1725
  failures = int(evidence.get("failures", unit.get("failures", 0)) or 0)
1709
1726
  errors = int(evidence.get("errors", unit.get("errors", 0)) or 0)
1710
1727
  skipped = int(evidence.get("skipped", unit.get("skipped", 0)) or 0)
1728
+ deselected = int(evidence.get("deselected", unit.get("deselected", 0)) or 0)
1711
1729
  pass_rate = evidence.get("passRate") or unit.get("passRate")
1712
1730
  source = "committed"
1713
1731
  counted = run > 0 or failures > 0 or errors > 0 or skipped > 0
@@ -1729,6 +1747,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1729
1747
  failures = int(unit.get("failures", 0) or 0)
1730
1748
  errors = int(unit.get("errors", 0) or 0)
1731
1749
  skipped = int(unit.get("skipped", 0) or 0)
1750
+ deselected = int(unit.get("deselected", 0) or 0)
1732
1751
  pass_rate = unit.get("passRate")
1733
1752
  source = "committed"
1734
1753
 
@@ -1755,6 +1774,7 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
1755
1774
  "failures": int(failures or 0),
1756
1775
  "errors": int(errors or 0),
1757
1776
  "skipped": int(skipped or 0),
1777
+ "deselected": int(deselected or 0),
1758
1778
  "passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
1759
1779
  "source": source,
1760
1780
  }
@@ -1771,10 +1791,12 @@ def _ledger_api_tests(
1771
1791
  empty = {
1772
1792
  "status": "NOT_RUN",
1773
1793
  "total": 0,
1794
+ "executed": 0,
1774
1795
  "passed": 0,
1775
1796
  "failed": 0,
1776
1797
  "blocked": 0,
1777
1798
  "passRate": NOT_AVAILABLE,
1799
+ "executionRate": NOT_AVAILABLE,
1778
1800
  "source": "not-run",
1779
1801
  }
1780
1802
  if not ledger:
@@ -1853,18 +1875,19 @@ def _ledger_api_tests(
1853
1875
  pass_rate = api.get("passRate")
1854
1876
  source = "committed"
1855
1877
 
1856
- if pass_rate is None and total > 0:
1857
- pass_rate = f"{passed / total:.0%}"
1858
- elif pass_rate is None:
1859
- pass_rate = NOT_AVAILABLE
1878
+ executed = passed + failed
1879
+ pass_rate = f"{passed / executed:.0%}" if executed > 0 else NOT_AVAILABLE
1880
+ execution_rate = f"{executed / total:.0%}" if total > 0 else NOT_AVAILABLE
1860
1881
 
1861
1882
  return {
1862
1883
  "status": status,
1863
1884
  "total": int(total or 0),
1885
+ "executed": int(executed or 0),
1864
1886
  "passed": int(passed or 0),
1865
1887
  "failed": int(failed or 0),
1866
1888
  "blocked": int(blocked or 0),
1867
- "passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
1889
+ "passRate": pass_rate,
1890
+ "executionRate": execution_rate,
1868
1891
  "source": source,
1869
1892
  }
1870
1893
 
@@ -1937,13 +1960,16 @@ def _typed_test_metrics(entry: dict[str, Any], *, total_key: str) -> dict[str, A
1937
1960
  total = int(metrics.get(total_key, 0) or 0)
1938
1961
  passed = int(metrics.get("passed", 0) or 0)
1939
1962
  failed = int(metrics.get("failed", 0) or 0)
1963
+ executed = passed + failed
1940
1964
  status = str(entry.get("status") or "").upper() or "NOT_RUN"
1941
1965
  out: dict[str, Any] = {
1942
1966
  "status": status,
1943
1967
  "total": total,
1968
+ "executed": executed,
1944
1969
  "passed": passed,
1945
1970
  "failed": failed,
1946
- "passRate": f"{passed / total:.0%}" if total > 0 else NOT_AVAILABLE,
1971
+ "passRate": f"{passed / executed:.0%}" if executed > 0 else NOT_AVAILABLE,
1972
+ "executionRate": f"{executed / total:.0%}" if total > 0 else NOT_AVAILABLE,
1947
1973
  "source": "committed" if total > 0 else "not-run",
1948
1974
  }
1949
1975
  if "blocked" in metrics:
@@ -1980,6 +2006,17 @@ def build_verification_projection(
1980
2006
  browser_e2e = validations.get("browserE2E")
1981
2007
  if isinstance(browser_e2e, dict):
1982
2008
  projection["browserE2E"] = _typed_test_metrics(browser_e2e, total_key="total")
2009
+ performance = validations.get("performance")
2010
+ if isinstance(performance, dict):
2011
+ projection["performance"] = {
2012
+ "status": str(performance.get("status") or "NOT_RUN").upper(),
2013
+ "metrics": _deepcopy_json(
2014
+ performance.get("metrics")
2015
+ if isinstance(performance.get("metrics"), dict)
2016
+ else {}
2017
+ ),
2018
+ "source": "committed",
2019
+ }
1983
2020
  return projection
1984
2021
 
1985
2022
 
@@ -2142,14 +2179,25 @@ def _verification_checks_from_events(
2142
2179
  return checks
2143
2180
 
2144
2181
 
2145
- def _artifacts_from_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
2182
+ def _artifacts_from_events(
2183
+ events: list[dict[str, Any]],
2184
+ *,
2185
+ change_dir: Path | None = None,
2186
+ ) -> list[dict[str, Any]]:
2146
2187
  out: list[dict[str, Any]] = []
2147
2188
  for e in events:
2148
2189
  if e.get("type") != "artifact":
2149
2190
  continue
2191
+ raw_path = str(e.get("path") or "")
2192
+ archived_path = raw_path
2193
+ if change_dir is not None and raw_path:
2194
+ product_copy = change_dir / "artifacts" / "product" / raw_path
2195
+ if not (change_dir / raw_path).is_file() and product_copy.is_file():
2196
+ archived_path = product_copy.relative_to(change_dir).as_posix()
2150
2197
  out.append(
2151
2198
  {
2152
- "path": e.get("path") or "",
2199
+ "path": archived_path,
2200
+ **({"sourcePath": raw_path} if archived_path != raw_path else {}),
2153
2201
  "kind": e.get("kind") or "",
2154
2202
  "phase": e.get("phase") or "",
2155
2203
  }
@@ -2882,7 +2930,7 @@ def collect_summary_data(
2882
2930
  "dbCompatibility": db,
2883
2931
  "coverageDisplay": coverage,
2884
2932
  }
2885
- for typed_key in ("apiContract", "browserE2E"):
2933
+ for typed_key in ("apiContract", "browserE2E", "performance"):
2886
2934
  if typed_key in projection:
2887
2935
  data["verification"][typed_key] = projection[typed_key]
2888
2936
  if ci_metrics is not None:
@@ -2982,6 +3030,8 @@ def collect_summary_data(
2982
3030
  if not for_replay:
2983
3031
  identity = resolve_product_archive_identity(change_dir, project=project)
2984
3032
  data["productCommit"] = identity.get("productCommit") or data.get("finalCommit")
3033
+ data["featureMergeHash"] = identity.get("featureMergeHash") or data.get("finalCommit")
3034
+ data["releaseTipHash"] = identity.get("releaseTipHash") or data.get("archiveCommit")
2985
3035
  data["productTreeHash"] = identity.get("productTreeHash")
2986
3036
  data["archiveCommit"] = identity.get("archiveCommit") or data.get("finalCommit")
2987
3037
  data["changeIdentity"] = identity
@@ -3049,6 +3099,8 @@ def collect_summary_data(
3049
3099
  data["gitFacts"] = {
3050
3100
  "baseCommit": data.get("baseCommit") or "",
3051
3101
  "finalCommit": data.get("finalCommit") or "",
3102
+ "featureMergeHash": data.get("featureMergeHash") or "",
3103
+ "releaseTipHash": data.get("releaseTipHash") or "",
3052
3104
  "filesChanged": int(diff_for_facts.get("filesChanged") or 0),
3053
3105
  "insertions": int(diff_for_facts.get("insertions") or 0),
3054
3106
  "deletions": int(diff_for_facts.get("deletions") or 0),
@@ -3069,7 +3121,9 @@ def collect_summary_data(
3069
3121
  if not isinstance(data.get("artifacts"), list):
3070
3122
  data["artifacts"] = []
3071
3123
  if not for_replay:
3072
- data["artifacts"] = _artifacts_from_events(projected_events)
3124
+ data["artifacts"] = _artifacts_from_events(
3125
+ projected_events, change_dir=change_dir
3126
+ )
3073
3127
 
3074
3128
  data["reviewSummary"] = _review_summary(
3075
3129
  change_dir,
@@ -3151,7 +3205,9 @@ def collect_summary_data(
3151
3205
  # Cannot invent commands
3152
3206
  pass
3153
3207
  verification_checks = _verification_checks_from_events(projected_events, ledger)
3154
- pipeline_artifacts = _artifacts_from_events(projected_events)
3208
+ pipeline_artifacts = _artifacts_from_events(
3209
+ projected_events, change_dir=change_dir
3210
+ )
3155
3211
  if not sources:
3156
3212
  sources = [NOT_AVAILABLE]
3157
3213
 
@@ -4012,6 +4068,7 @@ def artifact_preflight(change_dir: Path) -> dict[str, Any]:
4012
4068
  if not events_p.is_file():
4013
4069
  return {"ok": True, "items": [], "blocking": []}
4014
4070
  change_id = change_dir.name
4071
+ project_root = find_project_root(change_dir)
4015
4072
  for line in events_p.read_text(encoding="utf-8").splitlines():
4016
4073
  try:
4017
4074
  event = json.loads(line)
@@ -4058,16 +4115,56 @@ def artifact_preflight(change_dir: Path) -> dict[str, Any]:
4058
4115
  "correction": f"append correction --target-event-id {event_id} --target-field path --new-value-json \"{canonical}\"",
4059
4116
  })
4060
4117
  continue
4061
- # Change-relative path: OK.
4062
- items.append({
4118
+ change_file = change_dir / path
4119
+ if change_file.is_file():
4120
+ items.append({
4121
+ "eventId": event_id,
4122
+ "category": "file-backed",
4123
+ "path": path,
4124
+ "exists": True,
4125
+ })
4126
+ continue
4127
+ repository_file = project_root / path
4128
+ if repository_file.is_file():
4129
+ items.append({
4130
+ "eventId": event_id,
4131
+ "category": "repository-file",
4132
+ "path": path,
4133
+ "archivePath": f"artifacts/product/{path}",
4134
+ "exists": True,
4135
+ })
4136
+ continue
4137
+ item = {
4063
4138
  "eventId": event_id,
4064
- "category": "file-backed",
4139
+ "category": "blocking",
4065
4140
  "path": path,
4066
- "exists": (change_dir / path).is_file(),
4067
- })
4141
+ "kind": kind,
4142
+ "reason": "artifact file not found in change or project root",
4143
+ }
4144
+ items.append(item)
4145
+ blocking.append(item)
4068
4146
  return {"ok": not blocking, "items": items, "blocking": blocking}
4069
4147
 
4070
4148
 
4149
+ def materialize_repository_artifacts(change_dir: Path) -> dict[str, Any]:
4150
+ """Copy project-root business artifacts into the immutable archive namespace."""
4151
+ change_dir = change_dir.resolve()
4152
+ project_root = find_project_root(change_dir)
4153
+ preflight = artifact_preflight(change_dir)
4154
+ copied: list[str] = []
4155
+ for item in preflight.get("items") or []:
4156
+ if item.get("category") != "repository-file":
4157
+ continue
4158
+ source = (project_root / str(item["path"])).resolve()
4159
+ target = (change_dir / str(item["archivePath"])).resolve()
4160
+ if not source.is_relative_to(project_root) or not target.is_relative_to(change_dir):
4161
+ raise ValueError(f"artifact materialization escaped boundary: {item['path']}")
4162
+ target.parent.mkdir(parents=True, exist_ok=True)
4163
+ shutil.copy2(source, target)
4164
+ copied.append(target.relative_to(change_dir).as_posix())
4165
+ return {"ok": True, "copied": sorted(copied)}
4166
+
4167
+
4071
4168
  def run_knowledge_poststeps(project_root: Path) -> dict[str, Any]:
4072
4169
  results: dict[str, Any] = {"ran": False, "steps": [], "warnings": []}
4073
4170
  if not KNOWLEDGE_SCRIPT.is_file():
@@ -4343,6 +4440,16 @@ def cmd_finalize(
4343
4440
  warnings.append(f"cleanup failed: {exc}")
4344
4441
  payload["steps"]["cleanup"] = {"ok": False, "error": str(exc)}
4345
4442
 
4443
+ # Product/business artifacts may live at repository-relative paths. Freeze
4444
+ # byte-for-byte copies inside the staged archive before either manifest.
4445
+ try:
4446
+ materialized = materialize_repository_artifacts(work_dir)
4447
+ payload["steps"]["artifact_materialization"] = materialized
4448
+ except (OSError, ValueError) as exc:
4449
+ payload["error"] = f"artifact materialization failed: {exc}"
4450
+ _restore_finalize_failure()
4451
+ return 1, payload
4452
+
4346
4453
  # --- 1. before-manifest ---
4347
4454
  before_path = work_dir / "evidence" / "archive-manifest-before.json"
4348
4455
  try:
@@ -257,6 +257,42 @@ def journal_path(project_root: Path, transaction_id: str) -> Path:
257
257
  return journal_dir(project_root, transaction_id) / "journal.json"
258
258
 
259
259
 
260
+ def _write_verify_log(
261
+ project_root: Path,
262
+ transaction_id: str,
263
+ index: int,
264
+ *,
265
+ command: Sequence[str],
266
+ resolved_command: Sequence[str],
267
+ started: dt.datetime,
268
+ finished: dt.datetime,
269
+ exit_code: int | None,
270
+ stdout: str | bytes | None = None,
271
+ stderr: str | bytes | None = None,
272
+ error: str | None = None,
273
+ ) -> str:
274
+ def as_text(value: str | bytes | None) -> str:
275
+ if isinstance(value, bytes):
276
+ return value.decode("utf-8", errors="replace")
277
+ return value or ""
278
+
279
+ path = journal_dir(project_root, transaction_id) / "logs" / f"verify-{index + 1}.log"
280
+ path.parent.mkdir(parents=True, exist_ok=True)
281
+ lines = [
282
+ f"command: {json.dumps(list(command), ensure_ascii=False)}",
283
+ f"resolvedCommand: {json.dumps(list(resolved_command), ensure_ascii=False)}",
284
+ f"startedAt: {started.isoformat(timespec='milliseconds')}",
285
+ f"finishedAt: {finished.isoformat(timespec='milliseconds')}",
286
+ f"durationMs: {int((finished - started).total_seconds() * 1000)}",
287
+ f"exitCode: {exit_code if exit_code is not None else 'null'}",
288
+ ]
289
+ if error:
290
+ lines.append(f"error: {error}")
291
+ lines.extend(["--- stdout ---", as_text(stdout), "--- stderr ---", as_text(stderr)])
292
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
293
+ return path.relative_to(project_root).as_posix()
294
+
295
+
260
296
  def load_journal(project_root: Path, transaction_id: str) -> dict[str, Any]:
261
297
  path = journal_path(project_root, transaction_id)
262
298
  if not path.is_file():
@@ -965,7 +1001,7 @@ class IntegrationTransaction:
965
1001
  )
966
1002
  intg_root = Path(journal["integrationRoot"]).resolve()
967
1003
  results: list[dict[str, Any]] = []
968
- for command in commands:
1004
+ for index, command in enumerate(commands):
969
1005
  executable_command = _normalize_verify_command(command)
970
1006
  started = dt.datetime.now().astimezone()
971
1007
  try:
@@ -979,8 +1015,21 @@ class IntegrationTransaction:
979
1015
  check=False,
980
1016
  timeout=timeout_seconds,
981
1017
  )
982
- except subprocess.TimeoutExpired:
1018
+ except subprocess.TimeoutExpired as exc:
983
1019
  finished = dt.datetime.now().astimezone()
1020
+ log_path = _write_verify_log(
1021
+ self.project_root,
1022
+ self.transaction_id,
1023
+ index,
1024
+ command=command,
1025
+ resolved_command=executable_command,
1026
+ started=started,
1027
+ finished=finished,
1028
+ exit_code=None,
1029
+ stdout=exc.stdout,
1030
+ stderr=exc.stderr,
1031
+ error=f"timed out after {timeout_seconds}s",
1032
+ )
984
1033
  results.append(
985
1034
  {
986
1035
  "command": list(command),
@@ -993,6 +1042,7 @@ class IntegrationTransaction:
993
1042
  "durationMs": int(
994
1043
  (finished - started).total_seconds() * 1000
995
1044
  ),
1045
+ "logPath": log_path,
996
1046
  }
997
1047
  )
998
1048
  journal["verifyResults"] = results
@@ -1002,6 +1052,17 @@ class IntegrationTransaction:
1002
1052
  )
1003
1053
  except FileNotFoundError as exc:
1004
1054
  finished = dt.datetime.now().astimezone()
1055
+ log_path = _write_verify_log(
1056
+ self.project_root,
1057
+ self.transaction_id,
1058
+ index,
1059
+ command=command,
1060
+ resolved_command=executable_command,
1061
+ started=started,
1062
+ finished=finished,
1063
+ exit_code=None,
1064
+ error=str(exc),
1065
+ )
1005
1066
  results.append(
1006
1067
  {
1007
1068
  "command": list(command),
@@ -1013,6 +1074,7 @@ class IntegrationTransaction:
1013
1074
  "durationMs": int(
1014
1075
  (finished - started).total_seconds() * 1000
1015
1076
  ),
1077
+ "logPath": log_path,
1016
1078
  }
1017
1079
  )
1018
1080
  journal["verifyResults"] = results
@@ -1021,6 +1083,18 @@ class IntegrationTransaction:
1021
1083
  f"verification executable not found: {executable_command[0]}"
1022
1084
  ) from exc
1023
1085
  finished = dt.datetime.now().astimezone()
1086
+ log_path = _write_verify_log(
1087
+ self.project_root,
1088
+ self.transaction_id,
1089
+ index,
1090
+ command=command,
1091
+ resolved_command=executable_command,
1092
+ started=started,
1093
+ finished=finished,
1094
+ exit_code=proc.returncode,
1095
+ stdout=proc.stdout,
1096
+ stderr=proc.stderr,
1097
+ )
1024
1098
  results.append(
1025
1099
  {
1026
1100
  "command": list(command),
@@ -1032,6 +1106,7 @@ class IntegrationTransaction:
1032
1106
  "durationMs": int(
1033
1107
  (finished - started).total_seconds() * 1000
1034
1108
  ),
1109
+ "logPath": log_path,
1035
1110
  }
1036
1111
  )
1037
1112
  if proc.returncode != 0: