@hunter-harness/workflow-harness 0.2.54 → 0.2.55

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 (186) 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-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -2
  7. package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
  10. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +5 -3
  14. package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +46 -7
  16. package/harness/bundles/general/claude-code/scripts/harness_context.py +71 -22
  17. package/harness/bundles/general/claude-code/scripts/harness_events.py +40 -1
  18. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +93 -14
  19. package/harness/bundles/general/claude-code/scripts/harness_gate.py +165 -1
  20. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +2 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +2 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
  23. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  24. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -2
  29. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
  30. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
  31. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
  32. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
  33. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +5 -3
  36. package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
  37. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +46 -7
  38. package/harness/bundles/general/codebuddy/scripts/harness_context.py +71 -22
  39. package/harness/bundles/general/codebuddy/scripts/harness_events.py +40 -1
  40. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +93 -14
  41. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +165 -1
  42. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +2 -0
  43. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +2 -0
  44. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
  45. package/harness/bundles/general/codex/.harness-build.json +1 -1
  46. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -2
  51. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
  52. package/harness/bundles/general/codex/harness-review/SKILL.md +3 -3
  53. package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
  54. package/harness/bundles/general/codex/harness-run/reference.md +2 -2
  55. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-test/SKILL.md +5 -3
  58. package/harness/bundles/general/codex/harness-test/reference.md +1 -1
  59. package/harness/bundles/general/codex/scripts/harness_archive.py +46 -7
  60. package/harness/bundles/general/codex/scripts/harness_context.py +71 -22
  61. package/harness/bundles/general/codex/scripts/harness_events.py +40 -1
  62. package/harness/bundles/general/codex/scripts/harness_events_sync.py +93 -14
  63. package/harness/bundles/general/codex/scripts/harness_gate.py +165 -1
  64. package/harness/bundles/general/codex/scripts/harness_ledger.py +2 -0
  65. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +2 -0
  66. package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
  67. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  68. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -2
  73. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
  74. package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -3
  75. package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
  76. package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
  77. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-test/SKILL.md +5 -3
  80. package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
  81. package/harness/bundles/general/cursor/scripts/harness_archive.py +46 -7
  82. package/harness/bundles/general/cursor/scripts/harness_context.py +71 -22
  83. package/harness/bundles/general/cursor/scripts/harness_events.py +40 -1
  84. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +93 -14
  85. package/harness/bundles/general/cursor/scripts/harness_gate.py +165 -1
  86. package/harness/bundles/general/cursor/scripts/harness_ledger.py +2 -0
  87. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +2 -0
  88. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
  89. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  90. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  96. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -2
  97. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
  98. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
  99. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  100. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  101. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -3
  103. package/harness/bundles/java/claude-code/scripts/harness_archive.py +46 -7
  104. package/harness/bundles/java/claude-code/scripts/harness_context.py +71 -22
  105. package/harness/bundles/java/claude-code/scripts/harness_events.py +40 -1
  106. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +93 -14
  107. package/harness/bundles/java/claude-code/scripts/harness_gate.py +165 -1
  108. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +2 -0
  109. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +2 -0
  110. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
  111. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  112. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -2
  119. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
  120. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
  121. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  122. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  124. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -3
  125. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +46 -7
  126. package/harness/bundles/java/codebuddy/scripts/harness_context.py +71 -22
  127. package/harness/bundles/java/codebuddy/scripts/harness_events.py +40 -1
  128. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +93 -14
  129. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +165 -1
  130. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +2 -0
  131. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +2 -0
  132. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
  133. package/harness/bundles/java/codex/.harness-build.json +1 -1
  134. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -2
  141. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
  142. package/harness/bundles/java/codex/harness-review/SKILL.md +3 -3
  143. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  144. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  146. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -3
  147. package/harness/bundles/java/codex/scripts/harness_archive.py +46 -7
  148. package/harness/bundles/java/codex/scripts/harness_context.py +71 -22
  149. package/harness/bundles/java/codex/scripts/harness_events.py +40 -1
  150. package/harness/bundles/java/codex/scripts/harness_events_sync.py +93 -14
  151. package/harness/bundles/java/codex/scripts/harness_gate.py +165 -1
  152. package/harness/bundles/java/codex/scripts/harness_ledger.py +2 -0
  153. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +2 -0
  154. package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
  155. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  156. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -2
  163. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
  164. package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -3
  165. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  166. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  168. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -3
  169. package/harness/bundles/java/cursor/scripts/harness_archive.py +46 -7
  170. package/harness/bundles/java/cursor/scripts/harness_context.py +71 -22
  171. package/harness/bundles/java/cursor/scripts/harness_events.py +40 -1
  172. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +93 -14
  173. package/harness/bundles/java/cursor/scripts/harness_gate.py +165 -1
  174. package/harness/bundles/java/cursor/scripts/harness_ledger.py +2 -0
  175. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +2 -0
  176. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
  177. package/harness/manifests/general/claude-code.json +24 -24
  178. package/harness/manifests/general/codebuddy.json +24 -24
  179. package/harness/manifests/general/codex.json +24 -24
  180. package/harness/manifests/general/cursor.json +24 -24
  181. package/harness/manifests/java/claude-code.json +24 -24
  182. package/harness/manifests/java/codebuddy.json +24 -24
  183. package/harness/manifests/java/codex.json +24 -24
  184. package/harness/manifests/java/cursor.json +24 -24
  185. package/hunter-workflow-family.json +5 -4
  186. package/package.json +1 -1
@@ -3,7 +3,7 @@ name: harness-plan
3
3
  description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入
4
4
  Harness 规划阶段时使用;不得因用户描述需求就自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
7
7
  # harness-plan — 需求规划
8
8
 
9
9
  ## Purpose
@@ -73,7 +73,7 @@ description: 将需求转化为设计文档+实施计划+完整测试场景表
73
73
  | 4 | **设计审批包** blocking user confirmation;确认事件早于 approved 设计文档和 `meta/worktree.json` |
74
74
  | 5–6 | plan + implementation-detail + test-scenarios → `plans/` |
75
75
  | 7.5 | 仅 `--adversarial` 对抗评审 |
76
- | 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后执行 `harness_context.py close --from-phase plan --to-phase run --executor <tool> --artifact <plan-finalization> --json` append-only handoff receipt;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
76
+ | 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后把 finalizer 返回的绝对 `receiptPath` 原样传给 `harness_context.py close --from-phase plan --to-phase run --executor <tool> --artifact "<receiptPath>" --json`(也可使用返回的稳定 `artifactRef=meta/plan-finalization.json`),写 append-only handoff receipt;不得手写占位路径;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
77
77
 
78
78
  change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`)
79
79
 
@@ -211,13 +211,14 @@ source: harness-plan
211
211
 
212
212
  - [ ] 所有待发布产物先写入 staging,不直接覆盖正式 change 目录
213
213
  - [ ] 执行 `harness_plan_finalize.py finalize --change-dir ... --staging-dir ... --change ... --run-id <plan-run-id> --attempt <attempt> --json`;身份必须与本次 `phase.start` 完全相同
214
- - [ ] finalizer 返回 `ok=true` `artifactsHash`;重复执行返回 `idempotent=true`
214
+ - [ ] finalizer 返回 `ok=true`、`artifactsHash`、绝对 `receiptPath` 与稳定 `artifactRef=meta/plan-finalization.json`;重复执行返回 `idempotent=true`
215
215
  - [ ] 紧接着执行 `harness_plan_finalize.py verify --change-dir ... --json`
216
216
  - [ ] verify 返回 `phaseStartCount=1`、`phaseEndCount=1`、`phaseEndStatus=OK`、`receiptConsistent=true`
217
217
  - [ ] 收据 `files` 完整包含 design / plan / implementation-detail / test-scenarios / gate-policy / worktree 六项标准产物;不得省略、重复或经 symlink/junction/reparse point 引用
218
218
  - [ ] verify 的 `taskCount` 等于全部任务表行数,`scenarioCount` 等于 Markdown 中全部唯一场景 ID 数;任一为 0 或不一致即 ❌FAIL
219
219
  - [ ] finalizer 失败时正式目录无半发布产物、无成功 `phase.end`、无伪造 execution log
220
220
  - [ ] 禁止在 finalizer 之前手工追加成功 `phase.end`
221
+ - [ ] context close 的 `--artifact` 只传 finalizer 原样返回的 `receiptPath` 或 `artifactRef`,不得猜测、拼接或使用 `<plan-finalization>` 占位文本
221
222
 
222
223
  | 文件 | 必须存在 | 检查结果 |
223
224
  |------|:---:|:---:|
@@ -4,7 +4,7 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
4
4
  .harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
5
5
  test 结束后自动接续执行。
6
6
  ---
7
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
7
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
8
8
  # harness-review — 代码审查
9
9
 
10
10
  ## Purpose
@@ -45,14 +45,14 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
45
45
 
46
46
  ## Workflow
47
47
 
48
- 先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json` `harness_gate.py close --phase review --change <id> --status <OK|WARN|FAIL> --run-id <begin-run-id> --json`;之后通过 `harness_context.py close --from-phase review --to-phase submit|run --executor <tool> --json` 交接或 fixback。不得手工追加 `phase.start/phase.end`;任一 close 失败不得宣称审查完成。
48
+ 先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json`;结束时只执行一次 `harness_gate.py close --phase review --change <id> --status <OK|WARN|FAIL> --run-id <begin-run-id> --to-phase submit|run --executor <tool> --json`,由 gate 完成阶段事件、租约、上下文交接与远端补传。不得手工追加 `phase.start/phase.end` 或再次调用 context close;任一 close 失败不得宣称审查完成。
49
49
 
50
50
  0. **启动准备** — 确定变更名(Glob `.harness/changes/*/plans/*-plan.md`,排除 `.harness/archive/*/`,读 frontmatter 提取 change-name);**append `phase.start` 事件**(不得等审查完成才补)
51
51
  1. **读取 worktree 状态(门禁检查)** — 读 `.harness/changes/<change-name>/meta/worktree.json`:`requested=true` 但 worktree 不存在 → 停止并提示先修复 `harness-run`,不得静默回主目录(否则 git diff 为空);`requested=true` 且 worktree 已创建 → spawned agent 用该 worktree 路径执行 `git diff`(确保审查 worktree 变更而非主目录);`requested=false` → 审查主目录变更
52
52
  <!-- @section-id review.delegate -->
53
53
  ### 2. 审查执行(主会话优先)
54
54
 
55
- 默认在主会话按同一 6 维度检查清单执行,**不运行固定 `harness-reviewer` 预检**。仅发布候选或命中权限/安全、支付、迁移、并发幂等、公共契约、跨模块核心路径,且当前宿主显式提供通用隔离任务工具时,才可临时委派一次只读审查。spawn 失败、空返回或无报告正文 → 不 retry,继续主会话审查并记一次 `issue`;正常 inline 只记 `decision`,不显示不可用告警。
55
+ Cursor 提供通用隔离任务能力时,默认优先临时委派一次只读审查,**不要求安装固定 `harness-reviewer`**。子任务只读取 diff、规则和测试证据;主会话核验并写报告。无法委派、spawn 失败、空返回或无报告正文 → 不 retry,继续主会话审查。每次都记一条 `decision`,用 `REVIEW_DELEGATED` 或相应 `REVIEW_INLINE_*` 原因码说明是否委派及原因;正常 inline 不显示为故障。
56
56
  ## Review 定位(重要)
57
57
 
58
58
  **harness-review 是参考性代码审查阶段,不是硬门禁。**
@@ -3,7 +3,7 @@ name: harness-run
3
3
  description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。仅当用户显式调用
4
4
  /harness-run 时使用;不得因用户提到编码/实现就自动触发,也不得被其他阶段 skill 自动接续。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
7
7
  # harness-run — 需求编码
8
8
 
9
9
  ## Purpose
@@ -60,11 +60,11 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
60
60
  ## Workflow 概要
61
61
 
62
62
  0. 加载上下文:先 `harness_context.py prepare --phase run --executor <tool> [--change <id>] --json`,以返回的唯一 change/executionRoot/transition receipt 为准(多 active 缺参 → `ACTIVE_CHANGE_AMBIGUOUS`,禁止按 mtime 猜测);再 **`harness_context.py begin --phase run --change <id> --executor <tool> --json`** 校验 Plan artifact/hash/HEAD,读 spec/plan/detail/scenarios/ledger/run-task-status/worktree;`--fixback` 读 fixback → **`harness_gate.py begin --phase run --change <id>`**(claim + phase.start + identity;禁止手工 Write `events.ndjson` / 手工 `phase.end`)。**C3 钩子**:`harness_gate.py begin` 在已连接平台时会 best-effort 调用 events-sync(注册 run / `running`);失败只告警,不阻断 begin。
63
- 0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5;进入 TDD 前执行 `harness_test_guard.py begin --project . --change-dir ".harness/changes/<cn>" --json`
63
+ 0.5. **测试基础设施探测**(先写 `CHECKING`,四项证据齐备后再结论)→ `reference.md` Step 0.5;测试基线已由上一步 gate begin 内部建立,不得再次执行 guard begin
64
64
  1. **变更簇 TDD** — `protocols.md` `run-tdd-protocol`;批量 RED/GREEN;按需 `change-cluster-review-protocol`(高风险 + reviewer 预检可用)
65
65
  2. 构建验证 + **仅**通过 `harness_ledger.py record` 写 ledger(禁止 Write/Edit `verification-ledger.json`);`diff-hash --change-dir` 纳入 ignored tests → `reference.md` Step 2c
66
66
  3. **场景覆盖检查**(场景表映射,禁止用用例数冒充场景数)
67
- 4. **关门检查**(10 项)→ `harness_test_guard.py close` → **`harness_gate.py close --phase run --change <id> --status <OK|WARN|FAIL>`** → `harness_context.py close --from-phase run --to-phase test --executor <tool> --json`(内部 phase.end + append-only handoff + 释放租约;任一 close 失败不得用自然语言宣称成功)
67
+ 4. **关门检查**(10 项)→ 只执行一次 **`harness_gate.py close --phase run --change <id> --status <OK|WARN|FAIL> --to-phase test --executor <tool> --json`**。该命令内部依次关闭 test guard、写 `phase.end`、释放阶段租约、写 append-only handoff,并立即补传远端事件;不得再单独调用 test-guard/context close。若返回 `PHASE_HANDOFF_PENDING`,修正明确指出的输入后原样重试同一命令,已完成步骤会幂等复用。
68
68
 
69
69
  **Fixback**:`--fixback` 或用户要求时读 `reports/review/fixback-*.md`;RED 优先;未选用则记 `fixback: advisory-not-applied`。
70
70
 
@@ -101,7 +101,7 @@ description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编
101
101
  | **RED/GREEN** | RED 须有效;静态验证 ≠ 测试通过;greenfield 大重写豁免见 reference |
102
102
  | **Mapper/DB** | 纯 Mock 不得宣称 DB 验证通过;迁移脚本**永不自动执行** |
103
103
  | **探测/ledger** | 基础设施先探测;每次构建/测试经 `harness_ledger.py record`;禁止手写 ledger JSON;用 canonical `diff-hash --change-dir` |
104
- | **Gate/Guard** | 跨 Agent/阶段先用 `harness_context.py prepare/begin/close`;阶段门禁用 `harness_gate.py begin/close`;测试跟踪用 `harness_test_guard.py begin/close/record/stage` |
104
+ | **Gate/Guard** | 跨 Agent/阶段先用 `harness_context.py prepare/begin`;阶段门禁与上下文交接统一用 `harness_gate.py begin/close`;gate 内部负责 guard begin/close,模型只在需要记录测试来源或修复时调用 guard 的 `record/stage/mark` |
105
105
  | **预存变更** | 保留 → baseline 隔离;存在则最终 ≥ 🟡WARN |
106
106
  | **关门/状态** | 10 项关门检查;持久化 run-task-status;P0 静态-only 不得建议 submit |
107
107
  | **Worktree** | `requested=true` 时代码只写 worktree |
@@ -3,7 +3,7 @@ name: harness-submit
3
3
  description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff 合并回主分支。仅当用户显式调用
4
4
  /harness-submit(或 /harness-merge 重入合并段)时使用;用户口头说'提交/commit/push'时必须先确认,不得自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
7
7
  # harness-submit — 最终提交(含 worktree 合并)
8
8
 
9
9
  ## Purpose
@@ -4,7 +4,7 @@ description: Use when the user asks to synchronize, refresh, or validate Harness
4
4
  metadata, adapters, remote knowledge ownership, instruction entrypoints,
5
5
  config origins, or CodeGraph status.
6
6
  ---
7
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
7
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
8
8
  # harness-sync
9
9
 
10
10
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-test
3
3
  description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
4
4
  时使用;不得在 run 结束后自动接续执行。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5b2201cfc2c196c1; overlay=java; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=44594ca0f316c6fa; overlay=java; agent=cursor; do not edit -->
7
7
  # harness-test — 测试执行
8
8
 
9
9
  ## Purpose
@@ -46,7 +46,9 @@ description: 测试执行:读取场景表,执行单元测试+API接口测试
46
46
 
47
47
  ## Workflow
48
48
 
49
- 测试跟踪的确定性边界为 `harness_test_guard.py begin` `harness_test_guard.py close`;任一失败都必须传递给 `harness_gate.py close`,不得以自然语言覆盖失败状态。
49
+ 测试跟踪由 `harness_gate.py begin/close` 统一协调 test guard;不得额外手工执行 guard close。任一失败都必须使用 gate 返回的结构化状态,不得以自然语言覆盖失败状态。
50
+
51
+ 兼容实现中的底层命令名是 `harness_test_guard.py begin` 与 `harness_test_guard.py close`,两者只允许由 gate 内部调用;写在这里用于能力审计,不构成模型执行步骤。
50
52
 
51
53
  并行服务测试先运行 `harness_change.py lease-port --change <id> --run-id <run-id> --range <start-end> --json`,再把返回端口传给 `harness_service.py ensure --leased-port <port> --lease-owner <run-id>`。`serviceStart` 的 command/health/overlay 可用 `{leasedPort}` 占位符;用户自启进程仍只进入 Service Gate,禁止 kill。测试清理的 `finally` 中运行 `harness_change.py release-port --change <id> --run-id <run-id> --json`,避免租约池耗尽。
52
54
 
@@ -85,7 +87,7 @@ Runner 强制同项目单实例、低调度优先级、逐命令超时、正常
85
87
 
86
88
  先 `harness_context.py prepare --phase test --executor <tool> [--change <id>] --json`,再 `harness_context.py begin --phase test --change <id> --executor <tool> --json` 校验最新 run→test receipt 的 artifact/hash/HEAD;然后 **`harness_gate.py begin --phase test --change <id>`**(禁止手工 phase.start / 手写 ledger)。执行各项强制环境检查 + **命令执行模式 preflight (0.1)**;只有首选执行器不可用时,才执行 fallback 执行器探测。
87
89
 
88
- 验证写入**仅**允许 `harness_ledger.py record` / `can-reuse`;禁止 Write/Edit `verification-ledger.json`。测试跟踪:`harness_test_guard.py begin`执行 `close`(可选 `mark` stale-test-repair)。阶段结束必须 `harness_gate.py close --phase test --status ...`,随后 `harness_context.py close --from-phase test --to-phase review --executor <tool> --json`;fixback 时由 `harness_fixback.py` 合批记录 RED/GREEN 和 affected verification,再 `--to-phase run` 并由 context 失效受影响 target。
90
+ 验证写入**仅**允许 `harness_ledger.py record` / `can-reuse`;禁止 Write/Edit `verification-ledger.json`。测试跟踪:gate begin → 执行(可选 `harness_test_guard.py mark stale-test-repair`)→ 单次 `harness_gate.py close --phase test --status ... --to-phase review --executor <tool> --json`。fixback 时由 `harness_fixback.py` 合批记录 RED/GREEN 和 affected verification,再把同一 close 命令的 `--to-phase` 改为 `run`,由 context 失效受影响 target。
89
91
 
90
92
  - **Read `checklist.md`** — 各项检查详情 + 0.1 preflight + Playwright 探测 + 避坑规则指引
91
93
  - **失败处理**:任一项检查失败 → 终止流程并报告原因,用户确认修复后才能继续
@@ -17,6 +17,7 @@ import argparse
17
17
  import datetime as dt
18
18
  import hashlib
19
19
  import io
20
+ import ipaddress
20
21
  import json
21
22
  import os
22
23
  import re
@@ -31,6 +32,7 @@ import zipfile
31
32
  from collections.abc import Mapping
32
33
  from pathlib import Path
33
34
  from typing import Any
35
+ from urllib.parse import urlsplit
34
36
 
35
37
 
36
38
  if hasattr(sys.stdout, "reconfigure"):
@@ -192,9 +194,9 @@ def append_event(
192
194
  for key, value in fields.items():
193
195
  if value is not None:
194
196
  event[key] = value
195
- line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
196
- he.atomic_append_line(path, line)
197
- all_events = existing + [he.normalize_event(event)]
197
+ result = he.append_with_auto_seal(path, event, existing_events=existing)
198
+ event = result["event"]
199
+ all_events = he.load_events(path)
198
200
  he.write_execution_log(change_dir, he.render_execution_log(all_events))
199
201
  return event
200
202
 
@@ -3053,6 +3055,25 @@ def build_verification_projection(
3053
3055
  "apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
3054
3056
  }
3055
3057
  db_compatibility = _ledger_db_compat(effective_ledger)
3058
+ if db_compatibility["status"] == "NOT_RUN" and change_dir is not None:
3059
+ gate_policy_path = change_dir / "meta" / "gate-policy.json"
3060
+ try:
3061
+ gate_policy = read_json(gate_policy_path) if gate_policy_path.is_file() else None
3062
+ except (OSError, json.JSONDecodeError):
3063
+ gate_policy = None
3064
+ if isinstance(gate_policy, dict):
3065
+ capabilities = gate_policy.get("capabilities")
3066
+ required = gate_policy.get("requiredValidations")
3067
+ if (
3068
+ isinstance(capabilities, list)
3069
+ and "database" not in capabilities
3070
+ and (not isinstance(required, list) or "dbCompatibility" not in required)
3071
+ ):
3072
+ db_compatibility = {
3073
+ "status": "NOT_APPLICABLE",
3074
+ "reason": "项目能力画像未声明数据库能力",
3075
+ "source": "capability-profile",
3076
+ }
3056
3077
  projection["dbCompatibility"] = db_compatibility["status"]
3057
3078
  projection["dbCompatibilityEvidence"] = db_compatibility
3058
3079
  api_contract = validations.get("apiContract")
@@ -8377,6 +8398,27 @@ def _read_optional_text(path: Path) -> str:
8377
8398
  return ""
8378
8399
 
8379
8400
 
8401
+ def _archive_server_url_allowed(value: str | None) -> bool:
8402
+ """Require TLS remotely while allowing explicit loopback HTTP for local testing."""
8403
+ if value is None or not value.strip() or re.search(r"\s", value):
8404
+ return False
8405
+ try:
8406
+ parsed = urlsplit(value)
8407
+ host = (parsed.hostname or "").lower()
8408
+ except ValueError:
8409
+ return False
8410
+ if parsed.scheme.lower() == "https":
8411
+ return bool(host)
8412
+ if parsed.scheme.lower() != "http":
8413
+ return False
8414
+ if host == "localhost":
8415
+ return True
8416
+ try:
8417
+ return ipaddress.ip_address(host).is_loopback
8418
+ except ValueError:
8419
+ return False
8420
+
8421
+
8380
8422
  def _resolve_archive_remote_credentials(
8381
8423
  project_root: Path,
8382
8424
  environment: Mapping[str, str],
@@ -8403,10 +8445,7 @@ def _resolve_archive_remote_credentials(
8403
8445
  else ""
8404
8446
  )
8405
8447
  token_available = bool(env_token or local_token)
8406
- url_available = bool(
8407
- server_url
8408
- and re.fullmatch(r"https://[^\s]+", server_url, flags=re.IGNORECASE)
8409
- )
8448
+ url_available = _archive_server_url_allowed(server_url)
8410
8449
  return {
8411
8450
  "configured": bool(url_available and token_available),
8412
8451
  "serverUrl": server_url if url_available else None,
@@ -474,7 +474,7 @@ def close_transition(
474
474
  *,
475
475
  from_phase: str,
476
476
  to_phase: str,
477
- executor: str,
477
+ executor: str | None,
478
478
  artifacts: list[str] | None = None,
479
479
  status: str = "OK",
480
480
  ) -> dict[str, Any]:
@@ -491,13 +491,81 @@ def close_transition(
491
491
  except (OSError, ValueError, json.JSONDecodeError) as exc:
492
492
  return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
493
493
  paths = _paths(state_root)
494
+
495
+ artifact_entries: list[dict[str, Any]] = []
496
+ for raw in artifacts or []:
497
+ requested = Path(raw)
498
+ candidates = (
499
+ [requested]
500
+ if requested.is_absolute()
501
+ else [project / requested, contract_root / requested]
502
+ )
503
+ valid: list[Path] = []
504
+ for candidate in candidates:
505
+ try:
506
+ resolved_candidate = candidate.resolve()
507
+ except OSError:
508
+ continue
509
+ if resolved_candidate in valid:
510
+ continue
511
+ if resolved_candidate.is_relative_to(project) and resolved_candidate.is_file():
512
+ valid.append(resolved_candidate)
513
+ if len(valid) > 1:
514
+ return {
515
+ "ok": False,
516
+ "code": "TRANSITION_ARTIFACT_AMBIGUOUS",
517
+ "path": raw,
518
+ "candidates": [item.relative_to(project).as_posix() for item in valid],
519
+ "message": "relative artifact exists under both project and change roots",
520
+ }
521
+ if not valid:
522
+ return {
523
+ "ok": False,
524
+ "code": "TRANSITION_ARTIFACT_INVALID",
525
+ "path": raw,
526
+ "acceptedBases": [str(project), str(contract_root)],
527
+ "examples": [
528
+ str(contract_root / "meta" / "plan-finalization.json"),
529
+ f".harness/changes/{change}/meta/plan-finalization.json",
530
+ "meta/plan-finalization.json",
531
+ ],
532
+ }
533
+ resolved_artifact = valid[0]
534
+ artifact_entries.append(
535
+ {
536
+ "path": resolved_artifact.relative_to(project).as_posix(),
537
+ "sha256": _sha256(resolved_artifact),
538
+ }
539
+ )
540
+
541
+ transitions = _read_ndjson(paths["transitions"])
494
542
  if not paths["lease"].is_file():
543
+ latest = transitions[-1] if transitions else None
544
+ if (
545
+ isinstance(latest, dict)
546
+ and latest.get("fromPhase") == from_phase
547
+ and latest.get("toPhase") == to_phase
548
+ and latest.get("status") == status
549
+ and latest.get("artifacts") == artifact_entries
550
+ and (executor is None or latest.get("executor") == executor)
551
+ ):
552
+ return {
553
+ "ok": True,
554
+ "code": "TRANSITION_ALREADY_CLOSED",
555
+ "idempotent": True,
556
+ "receipt": latest,
557
+ "path": str(paths["transitions"]),
558
+ "invalidation": None,
559
+ }
495
560
  return {"ok": False, "code": "CONTEXT_LEASE_REQUIRED"}
496
561
  try:
497
562
  lease = _read_json(paths["lease"])
498
563
  except (OSError, ValueError, json.JSONDecodeError):
499
564
  return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
500
- if lease.get("owner") != executor or lease.get("phase") != from_phase:
565
+ effective_executor = executor or str(lease.get("owner") or "")
566
+ if not effective_executor:
567
+ return {"ok": False, "code": "CONTEXT_EXECUTOR_REQUIRED"}
568
+ if lease.get("owner") != effective_executor or lease.get("phase") != from_phase:
501
569
  return {
502
570
  "ok": False,
503
571
  "code": "CONTEXT_LEASE_MISMATCH",
@@ -508,25 +576,6 @@ def close_transition(
508
576
  if expiry is None or _now() >= expiry:
509
577
  return {"ok": False, "code": "CONTEXT_LEASE_EXPIRED"}
510
578
 
511
- artifact_entries: list[dict[str, Any]] = []
512
- for raw in artifacts or []:
513
- path = Path(raw)
514
- if not path.is_absolute():
515
- path = contract_root / path
516
- resolved = path.resolve()
517
- if not resolved.is_relative_to(project) or not resolved.is_file():
518
- return {
519
- "ok": False,
520
- "code": "TRANSITION_ARTIFACT_INVALID",
521
- "path": str(path),
522
- }
523
- artifact_entries.append(
524
- {
525
- "path": resolved.relative_to(project).as_posix(),
526
- "sha256": _sha256(resolved),
527
- }
528
- )
529
- transitions = _read_ndjson(paths["transitions"])
530
579
  previous_hash = transitions[-1].get("receiptHash") if transitions else None
531
580
  attempt = (
532
581
  1
@@ -543,7 +592,7 @@ def close_transition(
543
592
  "fromPhase": from_phase,
544
593
  "toPhase": to_phase,
545
594
  "status": status,
546
- "executor": executor,
595
+ "executor": effective_executor,
547
596
  "productCommit": _head(project),
548
597
  "artifacts": artifact_entries,
549
598
  "attempt": attempt,
@@ -41,7 +41,7 @@ def _project_root_for_change(change_dir: Path) -> Path | None:
41
41
  return None
42
42
 
43
43
 
44
- def _nudge_remote_sync(change_dir: Path) -> None:
44
+ def nudge_remote_sync(change_dir: Path) -> None:
45
45
  """Best-effort wake-up after the event append has been flushed to disk."""
46
46
  project_root = _project_root_for_change(change_dir)
47
47
  if project_root is None:
@@ -55,6 +55,11 @@ def _nudge_remote_sync(change_dir: Path) -> None:
55
55
  return
56
56
 
57
57
 
58
+ def _nudge_remote_sync(change_dir: Path) -> None:
59
+ """Compatibility wrapper for older callers and focused tests."""
60
+ nudge_remote_sync(change_dir)
61
+
62
+
58
63
  if hasattr(sys.stdout, "reconfigure"):
59
64
  sys.stdout.reconfigure(encoding="utf-8")
60
65
  if hasattr(sys.stderr, "reconfigure"):
@@ -121,6 +126,12 @@ OPTIONAL_FIELDS = (
121
126
  "renamed_from",
122
127
  "renamed_to",
123
128
  "change_uuid",
129
+ "execution_mode",
130
+ "decision_reason_code",
131
+ "fallback_reason_code",
132
+ "trigger",
133
+ "from_phase",
134
+ "result_status",
124
135
  )
125
136
 
126
137
  _PROVENANCE_FIELDS = frozenset(
@@ -139,6 +150,12 @@ _PROVENANCE_FIELDS = frozenset(
139
150
  "orchestration_active_ms",
140
151
  "wall_clock_ms",
141
152
  "user_wait_ms",
153
+ "execution_mode",
154
+ "decision_reason_code",
155
+ "fallback_reason_code",
156
+ "trigger",
157
+ "from_phase",
158
+ "result_status",
142
159
  }
143
160
  )
144
161
  _EVENT_ALLOWED_FIELDS = {
@@ -1698,6 +1715,22 @@ def append_with_auto_seal(
1698
1715
  else []
1699
1716
  )
1700
1717
  )
1718
+ if event_type in {"phase.start", "phase.end", "phase.auto_sealed"} and phase:
1719
+ phase_attempts = split_phase_attempts(
1720
+ [item for item in loaded if item.get("phase") == phase]
1721
+ )
1722
+ latest_attempt = max(
1723
+ (
1724
+ int(item.get("attempt"))
1725
+ for item in phase_attempts
1726
+ if isinstance(item.get("attempt"), int)
1727
+ ),
1728
+ default=0,
1729
+ )
1730
+ if not isinstance(event.get("attempt"), int) or int(event["attempt"]) <= 0:
1731
+ event["attempt"] = (
1732
+ latest_attempt + 1 if event_type == "phase.start" else latest_attempt or 1
1733
+ )
1701
1734
  if event_type == "phase.start" and phase:
1702
1735
  auto_sealed = seal_open_phase_attempts(loaded, phase=phase)
1703
1736
  for seal_event in auto_sealed:
@@ -1967,6 +2000,12 @@ def build_parser() -> argparse.ArgumentParser:
1967
2000
  p_append.add_argument("--orchestration-active-ms", type=int, default=None)
1968
2001
  p_append.add_argument("--wall-clock-ms", type=int, default=None)
1969
2002
  p_append.add_argument("--user-wait-ms", type=int, default=None)
2003
+ p_append.add_argument("--execution-mode", default=None)
2004
+ p_append.add_argument("--decision-reason-code", default=None)
2005
+ p_append.add_argument("--fallback-reason-code", default=None)
2006
+ p_append.add_argument("--trigger", default=None)
2007
+ p_append.add_argument("--from-phase", default=None)
2008
+ p_append.add_argument("--result-status", default=None)
1970
2009
  p_append.add_argument(
1971
2010
  "--legacy-lenient",
1972
2011
  action="store_true",
@@ -51,6 +51,15 @@ WHITELIST = {
51
51
  "duration_ms",
52
52
  "schemaVersion",
53
53
  "summary",
54
+ "executor_tool",
55
+ "executor_agent",
56
+ "executor_model",
57
+ "execution_mode",
58
+ "decision_reason_code",
59
+ "fallback_reason_code",
60
+ "trigger",
61
+ "from_phase",
62
+ "result_status",
54
63
  }
55
64
 
56
65
  MAX_BATCH_SIZE = 100
@@ -110,20 +119,43 @@ def _summary_text(value: Any) -> str:
110
119
  return text[:EVENT_SUMMARY_MAX_LENGTH].rstrip()
111
120
 
112
121
 
122
+ _MACHINE_DETAIL_RE = re.compile(
123
+ r"(?:\s*[;;,,]?\s*)"
124
+ r"(?:artifactsHash|artifactHash|receiptHash|content_sha256|contentSha256)"
125
+ r"\s*[:=]\s*(?:sha256:)?[0-9a-f]{32,64}",
126
+ re.IGNORECASE,
127
+ )
128
+
129
+
130
+ def _readable_summary_text(value: Any) -> str:
131
+ text = _summary_text(value)
132
+ if not text:
133
+ return ""
134
+ text = _MACHINE_DETAIL_RE.sub("", text)
135
+ text = re.sub(
136
+ r"\bfinalize\s+ok\b",
137
+ "规划产物已校验并发布",
138
+ text,
139
+ flags=re.IGNORECASE,
140
+ )
141
+ text = re.sub(r"\s*([;;,,])\s*([;;,,])", r"\1", text)
142
+ return _summary_text(text.strip(" ;;,,"))
143
+
144
+
113
145
  def build_event_summary(event: dict[str, Any]) -> str:
114
- explicit = _summary_text(event.get("summary"))
146
+ explicit = _readable_summary_text(event.get("summary"))
115
147
  if explicit:
116
148
  return explicit
117
149
 
118
150
  event_type = _summary_text(event.get("type"))
119
151
  phase = _summary_text(event.get("phase"))
120
152
  phase_label = PHASE_LABELS.get(phase, phase or "当前")
121
- note = _summary_text(event.get("note"))
122
- message = _summary_text(event.get("message"))
123
- decision = _summary_text(event.get("decision"))
124
- reason = _summary_text(event.get("reason"))
125
- name = _summary_text(event.get("name"))
126
- code = _summary_text(event.get("code"))
153
+ note = _readable_summary_text(event.get("note"))
154
+ message = _readable_summary_text(event.get("message"))
155
+ decision = _readable_summary_text(event.get("decision"))
156
+ reason = _readable_summary_text(event.get("reason"))
157
+ name = _readable_summary_text(event.get("name"))
158
+ code = _readable_summary_text(event.get("code"))
127
159
  status = _summary_text(event.get("status"))
128
160
 
129
161
  if event_type == "decision" and decision:
@@ -754,6 +786,39 @@ def _discover_changes(project_root: Path) -> list[Path]:
754
786
  return sorted(path for path in root.glob("*") if path.is_dir()) if root.is_dir() else []
755
787
 
756
788
 
789
+ def _change_has_open_phase(change_dir: Path) -> bool:
790
+ """Project the append-only stream into open/closed phase state."""
791
+ try:
792
+ path = Path(harness_paths.resolve_state_dir_for_contract(change_dir)) / "events.ndjson"
793
+ except (OSError, ValueError):
794
+ return False
795
+ if not path.is_file():
796
+ return False
797
+ open_phases: dict[str, bool] = {}
798
+ try:
799
+ for line in path.read_text(encoding="utf-8-sig").splitlines():
800
+ if not line.strip():
801
+ continue
802
+ event = json.loads(line)
803
+ if not isinstance(event, dict):
804
+ continue
805
+ phase = event.get("phase")
806
+ event_type = event.get("type")
807
+ if not isinstance(phase, str) or not phase:
808
+ continue
809
+ if event_type == "phase.start":
810
+ open_phases[phase] = True
811
+ elif event_type in {"phase.end", "phase.auto_sealed", "workflow.end", "run.end"}:
812
+ open_phases[phase] = False
813
+ except (OSError, json.JSONDecodeError):
814
+ return False
815
+ return any(open_phases.values())
816
+
817
+
818
+ def _project_has_open_phase(project_root: Path) -> bool:
819
+ return any(_change_has_open_phase(path) for path in _discover_changes(project_root))
820
+
821
+
757
822
  def _run_agent_session(project_root: Path) -> dict[str, Any]:
758
823
  attempts = 0
759
824
  uploaded = 0
@@ -766,14 +831,28 @@ def _run_agent_session(project_root: Path) -> dict[str, Any]:
766
831
  if all(item.get("ok") for item in results):
767
832
  time.sleep(0.6)
768
833
  if generation == _read_nudge_generation(project_root):
769
- return {
770
- "ok": True,
771
- "uploaded": uploaded,
772
- "quarantined": quarantined,
773
- "attempts": attempts + 1,
774
- }
834
+ if not _project_has_open_phase(project_root):
835
+ return {
836
+ "ok": True,
837
+ "uploaded": uploaded,
838
+ "quarantined": quarantined,
839
+ "attempts": attempts + 1,
840
+ }
841
+ # Keep a finite workflow lease alive while a phase is open.
842
+ # A phase.end nudge is observed on the next pass and releases
843
+ # the worker naturally; no terminal window is created.
844
+ time.sleep(10.0)
845
+ heartbeat_results = [
846
+ sync_change(project_root, path, heartbeat_only=True)
847
+ for path in _discover_changes(project_root)
848
+ ]
849
+ if all(item.get("ok") for item in heartbeat_results):
850
+ attempts = 0
851
+ continue
852
+ results = heartbeat_results
775
853
  attempts = 0
776
- continue
854
+ if generation != _read_nudge_generation(project_root):
855
+ continue
777
856
  time.sleep(min(16.0, float(2 ** attempts)))
778
857
  attempts += 1
779
858
  return {