@hunter-harness/workflow-harness 0.2.7 → 0.2.9

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 (230) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +3 -3
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +3 -3
  4. package/harness/bundles/general/claude-code/harness-archive/reference.md +18 -10
  5. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +5 -1
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +5 -0
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +31 -1
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +9 -0
  19. package/harness/bundles/general/claude-code/harness-test/pitfalls-java.md +14 -0
  20. package/harness/bundles/general/claude-code/harness-test/reference.md +36 -0
  21. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +3 -1
  22. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -0
  23. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +16 -0
  24. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +17 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_archive.py +568 -58
  26. package/harness/bundles/general/claude-code/scripts/harness_change.py +20 -2
  27. package/harness/bundles/general/claude-code/scripts/harness_events.py +96 -19
  28. package/harness/bundles/general/claude-code/scripts/harness_gate.py +178 -25
  29. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +17 -0
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +3 -3
  32. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -3
  33. package/harness/bundles/general/codebuddy/harness-archive/reference.md +18 -10
  34. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/checklist.md +5 -0
  43. package/harness/bundles/general/codebuddy/harness-run/reference.md +31 -1
  44. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-test/checklist.md +9 -0
  48. package/harness/bundles/general/codebuddy/harness-test/pitfalls-java.md +14 -0
  49. package/harness/bundles/general/codebuddy/harness-test/reference.md +36 -0
  50. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +3 -1
  51. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -0
  52. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +16 -0
  53. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +17 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +568 -58
  55. package/harness/bundles/general/codebuddy/scripts/harness_change.py +20 -2
  56. package/harness/bundles/general/codebuddy/scripts/harness_events.py +96 -19
  57. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +178 -25
  58. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +17 -0
  59. package/harness/bundles/general/codex/.harness-build.json +1 -1
  60. package/harness/bundles/general/codex/contracts/workflow-policy.json +3 -3
  61. package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -3
  62. package/harness/bundles/general/codex/harness-archive/reference.md +18 -10
  63. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +5 -1
  64. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  69. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-run/checklist.md +5 -0
  72. package/harness/bundles/general/codex/harness-run/reference.md +31 -1
  73. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-test/checklist.md +9 -0
  77. package/harness/bundles/general/codex/harness-test/pitfalls-java.md +14 -0
  78. package/harness/bundles/general/codex/harness-test/reference.md +36 -0
  79. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +3 -1
  80. package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -0
  81. package/harness/bundles/general/codex/protocols/powershell-protocol.md +16 -0
  82. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +17 -0
  83. package/harness/bundles/general/codex/scripts/harness_archive.py +568 -58
  84. package/harness/bundles/general/codex/scripts/harness_change.py +20 -2
  85. package/harness/bundles/general/codex/scripts/harness_events.py +96 -19
  86. package/harness/bundles/general/codex/scripts/harness_gate.py +178 -25
  87. package/harness/bundles/general/codex/scripts/harness_ledger.py +17 -0
  88. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  89. package/harness/bundles/general/cursor/contracts/workflow-policy.json +3 -3
  90. package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -3
  91. package/harness/bundles/general/cursor/harness-archive/reference.md +18 -10
  92. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +5 -1
  93. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  98. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-run/checklist.md +5 -0
  101. package/harness/bundles/general/cursor/harness-run/reference.md +31 -1
  102. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-test/checklist.md +9 -0
  106. package/harness/bundles/general/cursor/harness-test/pitfalls-java.md +14 -0
  107. package/harness/bundles/general/cursor/harness-test/reference.md +36 -0
  108. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +3 -1
  109. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -0
  110. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +16 -0
  111. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +17 -0
  112. package/harness/bundles/general/cursor/scripts/harness_archive.py +568 -58
  113. package/harness/bundles/general/cursor/scripts/harness_change.py +20 -2
  114. package/harness/bundles/general/cursor/scripts/harness_events.py +96 -19
  115. package/harness/bundles/general/cursor/scripts/harness_gate.py +178 -25
  116. package/harness/bundles/general/cursor/scripts/harness_ledger.py +17 -0
  117. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  118. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +3 -3
  119. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -3
  121. package/harness/bundles/java/claude-code/harness-archive/reference.md +18 -10
  122. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +5 -1
  123. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  129. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +3 -1
  135. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -0
  136. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +16 -0
  137. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +17 -0
  138. package/harness/bundles/java/claude-code/scripts/harness_archive.py +568 -58
  139. package/harness/bundles/java/claude-code/scripts/harness_change.py +20 -2
  140. package/harness/bundles/java/claude-code/scripts/harness_events.py +96 -19
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +178 -25
  142. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +17 -0
  143. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  144. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +3 -3
  145. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -3
  147. package/harness/bundles/java/codebuddy/harness-archive/reference.md +18 -10
  148. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
  149. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  155. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  156. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +3 -1
  161. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -0
  162. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +16 -0
  163. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +17 -0
  164. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +568 -58
  165. package/harness/bundles/java/codebuddy/scripts/harness_change.py +20 -2
  166. package/harness/bundles/java/codebuddy/scripts/harness_events.py +96 -19
  167. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +178 -25
  168. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +17 -0
  169. package/harness/bundles/java/codex/.harness-build.json +1 -1
  170. package/harness/bundles/java/codex/contracts/workflow-policy.json +3 -3
  171. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -3
  173. package/harness/bundles/java/codex/harness-archive/reference.md +18 -10
  174. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +5 -1
  175. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  176. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  181. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  186. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +3 -1
  187. package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -0
  188. package/harness/bundles/java/codex/protocols/powershell-protocol.md +16 -0
  189. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +17 -0
  190. package/harness/bundles/java/codex/scripts/harness_archive.py +568 -58
  191. package/harness/bundles/java/codex/scripts/harness_change.py +20 -2
  192. package/harness/bundles/java/codex/scripts/harness_events.py +96 -19
  193. package/harness/bundles/java/codex/scripts/harness_gate.py +178 -25
  194. package/harness/bundles/java/codex/scripts/harness_ledger.py +17 -0
  195. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  196. package/harness/bundles/java/cursor/contracts/workflow-policy.json +3 -3
  197. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  198. package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -3
  199. package/harness/bundles/java/cursor/harness-archive/reference.md +18 -10
  200. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +5 -1
  201. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  204. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  206. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  207. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  209. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  211. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  212. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +3 -1
  213. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -0
  214. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +16 -0
  215. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +17 -0
  216. package/harness/bundles/java/cursor/scripts/harness_archive.py +568 -58
  217. package/harness/bundles/java/cursor/scripts/harness_change.py +20 -2
  218. package/harness/bundles/java/cursor/scripts/harness_events.py +96 -19
  219. package/harness/bundles/java/cursor/scripts/harness_gate.py +178 -25
  220. package/harness/bundles/java/cursor/scripts/harness_ledger.py +17 -0
  221. package/harness/manifests/general/claude-code.json +33 -29
  222. package/harness/manifests/general/codebuddy.json +33 -29
  223. package/harness/manifests/general/codex.json +33 -29
  224. package/harness/manifests/general/cursor.json +33 -29
  225. package/harness/manifests/java/claude-code.json +27 -27
  226. package/harness/manifests/java/codebuddy.json +27 -27
  227. package/harness/manifests/java/codex.json +27 -27
  228. package/harness/manifests/java/cursor.json +27 -27
  229. package/hunter-workflow-family.json +2 -2
  230. package/package.json +1 -1
@@ -172,12 +172,30 @@ def list_active_changes(project_root: Path) -> list[dict[str, Any]]:
172
172
  archived = {}
173
173
  if isinstance(archived, dict) and archived.get("status") == "archived":
174
174
  continue
175
+ has_plan = any((entry / "plans").glob("*-plan.md"))
176
+ has_context = (entry / CHANGE_CONTEXT_REL).is_file()
177
+ has_checkpoints = (entry / CHECKPOINTS_REL).is_file()
178
+ worktree_path = entry / WORKTREE_META_REL
179
+ has_active_worktree = False
180
+ if worktree_path.is_file():
181
+ try:
182
+ worktree = _read_json(worktree_path)
183
+ has_active_worktree = isinstance(worktree, dict) and (
184
+ worktree.get("created") is True or
185
+ worktree.get("requested") is True
186
+ )
187
+ except (OSError, json.JSONDecodeError):
188
+ has_active_worktree = False
189
+ # Runtime notes, event logs and requested=false worktree metadata can
190
+ # survive archive/submit cleanup. They are residues, not active changes.
191
+ if not (has_plan or has_context or has_checkpoints or has_active_worktree):
192
+ continue
175
193
  active.append(
176
194
  {
177
195
  "changeId": entry.name,
178
196
  "path": str(entry.resolve()),
179
- "hasPlan": (entry / "plans").is_dir(),
180
- "hasWorktreeMeta": (entry / WORKTREE_META_REL).is_file(),
197
+ "hasPlan": has_plan,
198
+ "hasWorktreeMeta": worktree_path.is_file(),
181
199
  }
182
200
  )
183
201
  return active
@@ -43,6 +43,7 @@ EVENT_TYPES = frozenset(
43
43
  )
44
44
 
45
45
  SCHEMA_VERSION = 3
46
+ _NOTE_FALLBACK_MAXLEN = 60
46
47
  HEADER_LINE = (
47
48
  "本文件由 harness_events.py 自动渲染,请勿手工编辑;事实源为 events.ndjson"
48
49
  )
@@ -106,6 +107,25 @@ def execution_log_path(change_dir: Path) -> Path:
106
107
  return change_dir / "logs" / "execution-log.md"
107
108
 
108
109
 
110
+ def archived_change_dir(change_dir: Path) -> Path | None:
111
+ """Return the matching archive when change_dir follows the project layout."""
112
+ changes_root = change_dir.parent
113
+ harness_root = changes_root.parent
114
+ if changes_root.name != "changes" or harness_root.name != ".harness":
115
+ return None
116
+ archive_root = harness_root / "archive"
117
+ if not archive_root.is_dir():
118
+ return None
119
+ suffix = "-" + change_dir.name
120
+ matches = sorted(
121
+ candidate for candidate in archive_root.iterdir()
122
+ if candidate.is_dir() and (
123
+ candidate.name == change_dir.name or candidate.name.endswith(suffix)
124
+ )
125
+ )
126
+ return matches[-1] if matches else None
127
+
128
+
109
129
  def parse_timestamp(value: Any) -> dt.datetime | None:
110
130
  if not isinstance(value, str) or not value.strip():
111
131
  return None
@@ -297,6 +317,8 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
297
317
  def status_symbol(status: Any, reason: Any = None) -> str:
298
318
  text = str(status or "").strip().lower()
299
319
  reason_text = str(reason or "").strip()
320
+ if not text:
321
+ return "—"
300
322
  ok_set = {"ok", "passed", "pass", "success", "green", "✅", "✅ok"}
301
323
  warn_set = {"warn", "warning", "yellow", "skipped", "skip", "🟡", "🟡warn"}
302
324
  fail_set = {"fail", "failed", "error", "red", "blocked", "❌", "❌fail"}
@@ -308,7 +330,7 @@ def status_symbol(status: Any, reason: Any = None) -> str:
308
330
  return f"❌FAIL({reason_text})" if reason_text else "❌FAIL"
309
331
  if reason_text:
310
332
  return f"{status}({reason_text})"
311
- return str(status or "unknown")
333
+ return str(status)
312
334
 
313
335
 
314
336
  def severity_symbol(severity: Any, message: Any = None) -> str:
@@ -320,7 +342,10 @@ def severity_symbol(severity: Any, message: Any = None) -> str:
320
342
  return f"🟡WARN({msg})" if msg else "🟡WARN"
321
343
  if text in {"info", "ok", "note"}:
322
344
  return f"✅OK({msg})" if msg else "✅OK"
323
- return f"{severity}: {msg}" if msg else str(severity or "issue")
345
+ # Empty/unknown severity: never emit literal "issue"/"None".
346
+ if not text:
347
+ return ""
348
+ return f"{severity}: {msg}" if msg else str(severity)
324
349
 
325
350
 
326
351
  def format_duration(ms: int | None) -> str:
@@ -421,35 +446,54 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
421
446
  def render_command_block(commands: list[dict[str, Any]]) -> list[str]:
422
447
  if not commands:
423
448
  return []
449
+
450
+ def command_display(event: dict[str, Any]) -> str:
451
+ cmd = str(event.get("command") or "").strip()
452
+ if cmd:
453
+ return cmd
454
+ return str(event.get("note") or "").strip()
455
+
424
456
  if len(commands) == 1:
425
457
  event = commands[0]
426
- cmd = event.get("command") or ""
458
+ display = command_display(event)
459
+ if not display:
460
+ return []
427
461
  exit_code = event.get("exit_code")
428
462
  exit_text = "?" if exit_code is None else str(exit_code)
429
463
  duration = format_duration(
430
464
  int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
431
465
  )
432
466
  note = str(event.get("note") or "").strip()
433
- parts = [f"- command: `{cmd}`", f"exit={exit_text}", f"duration={duration}"]
434
- if note:
467
+ parts = [f"- command: `{display}`", f"exit={exit_text}", f"duration={duration}"]
468
+ # Avoid duplicating note when it was already used as the display text.
469
+ if note and note != display:
435
470
  parts.append(f"note={note}")
436
471
  return [" · ".join(parts)]
437
472
 
438
- lines = [
439
- "",
440
- "| 命令 | exit | duration | note |",
441
- "| --- | ---: | ---: | --- |",
442
- ]
473
+ rows: list[str] = []
443
474
  for event in commands:
444
- cmd = str(event.get("command") or "").replace("|", "\\|")
475
+ display = command_display(event)
476
+ if not display:
477
+ continue
478
+ cmd = display.replace("|", "\\|")
445
479
  exit_code = event.get("exit_code")
446
480
  exit_text = "?" if exit_code is None else str(exit_code)
447
481
  duration = format_duration(
448
482
  int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
449
483
  )
450
484
  note = str(event.get("note") or "").replace("|", "\\|")
451
- lines.append(f"| `{cmd}` | {exit_text} | {duration} | {note} |")
452
- lines.append("")
485
+ # If note was promoted to the command cell, leave the note column empty.
486
+ note_cell = "" if note == display else note
487
+ rows.append(f"| `{cmd}` | {exit_text} | {duration} | {note_cell} |")
488
+ if not rows:
489
+ return []
490
+ lines = [
491
+ "",
492
+ "| 命令 | exit | duration | note |",
493
+ "| --- | ---: | ---: | --- |",
494
+ *rows,
495
+ "",
496
+ ]
453
497
  return lines
454
498
 
455
499
 
@@ -472,23 +516,37 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
472
516
  suffix = f" — {note}" if note else ""
473
517
  return [f"- phase.end @ {event.get('timestamp', '')}{suffix}"]
474
518
  if etype == "verification":
475
- name = event.get("name") or "(unnamed)"
476
- symbol = status_symbol(event.get("status"), event.get("reason") or event.get("note"))
519
+ note = str(event.get("note") or "").strip()
520
+ name = str(event.get("name") or "").strip() or (
521
+ note[:_NOTE_FALLBACK_MAXLEN] if note else ""
522
+ )
523
+ if not name:
524
+ return []
525
+ symbol = status_symbol(event.get("status"), event.get("reason"))
477
526
  return [f"- verification: {name} → {symbol}"]
478
527
  if etype == "decision":
479
- decision = event.get("decision") or ""
480
- reason = event.get("reason") or event.get("note") or ""
528
+ decision = str(event.get("decision") or "").strip()
529
+ reason = str(event.get("reason") or event.get("note") or "").strip()
530
+ if not decision:
531
+ return [f"- decision: {reason}"] if reason else []
481
532
  if reason:
482
533
  return [f"- decision: {decision} — {reason}"]
483
534
  return [f"- decision: {decision}"]
484
535
  if etype == "issue":
485
- symbol = severity_symbol(event.get("severity"), event.get("message"))
536
+ note = str(event.get("note") or "").strip()
537
+ message = str(event.get("message") or "").strip() or note
538
+ symbol = severity_symbol(event.get("severity"), message)
539
+ if not symbol:
540
+ return [f"- issue: {note}"] if note else []
486
541
  code = event.get("code")
487
542
  prefix = f"[{code}] " if code else ""
488
543
  return [f"- issue: {prefix}{symbol}"]
489
544
  if etype == "artifact":
490
- path = event.get("path") or ""
545
+ path = str(event.get("path") or "").strip()
546
+ note = str(event.get("note") or "").strip()
491
547
  kind = event.get("kind")
548
+ if not path:
549
+ return [f"- artifact: {note}"] if note else []
492
550
  if kind:
493
551
  return [f"- artifact: `{path}` ({kind})"]
494
552
  return [f"- artifact: `{path}`"]
@@ -620,6 +678,13 @@ def cmd_append(args: argparse.Namespace) -> int:
620
678
  as_json=as_json,
621
679
  )
622
680
  change_dir = resolve_change_dir(args.change_dir)
681
+ archived = archived_change_dir(change_dir)
682
+ if archived is not None:
683
+ return emit_error(
684
+ "ARCHIVED_CHANGE_IMMUTABLE: refusing to append to archived change "
685
+ f"{change_dir.name} ({archived})",
686
+ as_json=as_json,
687
+ )
623
688
  path = events_path(change_dir)
624
689
  lock_path = change_dir / "events.ndjson.lock"
625
690
 
@@ -627,6 +692,18 @@ def cmd_append(args: argparse.Namespace) -> int:
627
692
  # new_event_id 用完整 uuid,无需扫描去重。锁覆盖 atomic_append_line 的
628
693
  # open/write/flush/fsync 全过程。
629
694
  event = build_event(args, [])
695
+ # Semantic hints (non-fatal): guide agents toward required fields.
696
+ if args.type == "issue" and not args.severity:
697
+ event["severity"] = "info"
698
+ print(
699
+ "warning: issue without --severity, defaulted to info",
700
+ file=sys.stderr,
701
+ )
702
+ if args.type == "verification" and (not args.name or not args.status):
703
+ print(
704
+ "warning: verification missing --name or --status",
705
+ file=sys.stderr,
706
+ )
630
707
  line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
631
708
  try:
632
709
  with event_file_lock(lock_path):
@@ -149,7 +149,8 @@ def checkpoint_status(checkpoints: dict[str, Any] | None, checkpoint_id: str) ->
149
149
  def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[str, Any] | None:
150
150
  checkpoints = load_checkpoints(change_dir)
151
151
  status = checkpoint_status(checkpoints, "foundation-gate")
152
- if status == "approved":
152
+ # missing = checkpoint not enabled for this change (no file / no entry).
153
+ if status in {"approved", "missing"}:
153
154
  return None
154
155
  if task_number is None:
155
156
  return {
@@ -174,8 +175,25 @@ def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[st
174
175
  }
175
176
 
176
177
 
177
- def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[str]:
178
+ def is_degraded_ledger_entry(entry: dict[str, Any]) -> bool:
179
+ """NOT_RUN + evidence starting with 'DEGRADED: <non-empty reason>'."""
180
+ if entry.get("status") != "NOT_RUN":
181
+ return False
182
+ evidence = str(entry.get("evidence") or "").strip()
183
+ if not evidence.startswith("DEGRADED:"):
184
+ return False
185
+ reason = evidence.split(":", 1)[1].strip()
186
+ return bool(reason)
187
+
188
+
189
+ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> tuple[list[str], bool]:
190
+ """Internal helper for ledger close validation (not a public API).
191
+
192
+ Returns ``(missing_fields, degraded_ok)`` where ``degraded_ok`` is True only when
193
+ the entry is a valid DEGRADED NOT_RUN record with no other missing fields.
194
+ """
178
195
  missing: list[str] = []
196
+ degraded = is_degraded_ledger_entry(entry)
179
197
  for field in LEDGER_V2_REQUIRED_ENTRY_FIELDS:
180
198
  value = entry.get(field)
181
199
  if field == "inputsFiles":
@@ -184,7 +202,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
184
202
  elif verification == "unitTestFull" and not value:
185
203
  missing.append("inputsFiles(non-empty)")
186
204
  elif field == "status":
187
- if value != "OK":
205
+ if value != "OK" and not degraded:
188
206
  missing.append("status=OK")
189
207
  elif not (isinstance(value, str) and value.strip()):
190
208
  missing.append(field)
@@ -192,7 +210,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
192
210
  missing.append("coverage(valid)")
193
211
  elif field == "algorithmVersion" and str(value).strip() != hl.LEDGER_VERSION:
194
212
  missing.append("algorithmVersion(harness-ledger-2)")
195
- return missing
213
+ return missing, degraded and not missing
196
214
 
197
215
 
198
216
  def validate_ledger_for_phase_close(
@@ -225,6 +243,7 @@ def validate_ledger_for_phase_close(
225
243
  }
226
244
 
227
245
  problems: list[dict[str, Any]] = []
246
+ degraded: list[str] = []
228
247
  for verification in required:
229
248
  entry = validations.get(verification)
230
249
  if not isinstance(entry, dict):
@@ -236,7 +255,7 @@ def validate_ledger_for_phase_close(
236
255
  }
237
256
  )
238
257
  continue
239
- missing = validate_ledger_entry_v2(entry, verification)
258
+ missing, is_degraded = validate_ledger_entry_v2(entry, verification)
240
259
  if missing:
241
260
  code = (
242
261
  "MISSING_V2_FIELDS"
@@ -258,6 +277,8 @@ def validate_ledger_for_phase_close(
258
277
  "code": code,
259
278
  }
260
279
  )
280
+ elif is_degraded:
281
+ degraded.append(verification)
261
282
 
262
283
  if problems:
263
284
  return {
@@ -269,6 +290,15 @@ def validate_ledger_for_phase_close(
269
290
  "ledgerPath": str(ledger_path) if ledger_path else None,
270
291
  "detail": "natural-language override is not permitted",
271
292
  }
293
+ if degraded:
294
+ return {
295
+ "ok": True,
296
+ "code": "LEDGER_OK_DEGRADED",
297
+ "phase": phase,
298
+ "validated": required,
299
+ "degraded": degraded,
300
+ "ledgerPath": str(ledger_path) if ledger_path else None,
301
+ }
272
302
  return {
273
303
  "ok": True,
274
304
  "code": "LEDGER_OK",
@@ -417,7 +447,11 @@ def change_code_root(change_dir: Path) -> Path:
417
447
  return project
418
448
 
419
449
 
420
- def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
450
+ def classify_risk(
451
+ change_dir: Path,
452
+ stage: str,
453
+ workflow: dict[str, Any] | None = None,
454
+ ) -> dict[str, Any]:
421
455
  tier = "full"
422
456
  source = "default-full"
423
457
  plan_path = change_dir / "plans"
@@ -484,7 +518,31 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
484
518
  tier = observed
485
519
  source = f"{source}+post-run"
486
520
  main_project = change_dir.parents[2]
487
- workflow = _load_workflow_policy(project=main_project)
521
+ if workflow is None:
522
+ workflow = _load_workflow_policy(project=main_project)
523
+ tier_policy = workflow["riskTiers"][tier]
524
+ unique_signals = sorted(set(signals))
525
+ payload = {
526
+ "ok": True,
527
+ "code": "CLASSIFIED",
528
+ "stage": stage,
529
+ "tier": tier,
530
+ "source": source,
531
+ "changeId": change_dir.name,
532
+ "signals": unique_signals,
533
+ "defaultPhases": list(tier_policy["defaultPhases"]),
534
+ "requiredValidations": list(tier_policy["requiredValidations"]),
535
+ "conditionalStages": list(tier_policy["conditionalStages"]),
536
+ "stageDecisions": _stage_decisions_for_tier(workflow, tier, unique_signals),
537
+ }
538
+ if stage == "post-run":
539
+ _write_json(change_dir / "meta" / "risk-classification.json", payload)
540
+ return payload
541
+
542
+
543
+ def _stage_decisions_for_tier(
544
+ workflow: dict[str, Any], tier: str, signals: list[str]
545
+ ) -> dict[str, dict[str, Any]]:
488
546
  tier_policy = workflow["riskTiers"][tier]
489
547
  unique_signals = sorted(set(signals))
490
548
  stage_decisions: dict[str, dict[str, Any]] = {}
@@ -501,22 +559,66 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
501
559
  ),
502
560
  "matchedSignals": matched,
503
561
  }
504
- payload = {
562
+ return stage_decisions
563
+
564
+
565
+ def apply_tier_override(
566
+ payload: dict[str, Any],
567
+ workflow: dict[str, Any],
568
+ *,
569
+ tier: str,
570
+ override_by: str,
571
+ ) -> dict[str, Any]:
572
+ """Rebind payload to an explicit tier override (source=override)."""
573
+ tier_policy = workflow["riskTiers"][tier]
574
+ now = hc.now_iso()
575
+ signals = list(payload.get("signals") or [])
576
+ payload = dict(payload)
577
+ payload["tier"] = tier
578
+ payload["source"] = "override"
579
+ payload["defaultPhases"] = list(tier_policy["defaultPhases"])
580
+ payload["requiredValidations"] = list(tier_policy["requiredValidations"])
581
+ payload["conditionalStages"] = list(tier_policy["conditionalStages"])
582
+ payload["stageDecisions"] = _stage_decisions_for_tier(workflow, tier, signals)
583
+ payload["tierOverride"] = {"tier": tier, "by": override_by, "at": now}
584
+ return payload
585
+
586
+
587
+ def gate_policy_document(payload: dict[str, Any]) -> dict[str, Any]:
588
+ """Cross-change contract: meta/gate-policy.json (schemaVersion=1)."""
589
+ return {
590
+ "schemaVersion": 1,
591
+ "tier": payload["tier"],
592
+ "source": payload["source"],
593
+ "defaultPhases": list(payload.get("defaultPhases") or []),
594
+ "requiredValidations": list(payload.get("requiredValidations") or []),
595
+ "classifiedAt": payload.get("classifiedAt") or hc.now_iso(),
596
+ "tierOverride": payload.get("tierOverride"),
597
+ }
598
+
599
+
600
+ def classify_defaults(
601
+ workflow: dict[str, Any],
602
+ *,
603
+ change_id: str,
604
+ stage: str,
605
+ ) -> dict[str, Any]:
606
+ tier = "full"
607
+ source = "default-full"
608
+ tier_policy = workflow["riskTiers"][tier]
609
+ return {
505
610
  "ok": True,
506
611
  "code": "CLASSIFIED",
507
612
  "stage": stage,
508
613
  "tier": tier,
509
614
  "source": source,
510
- "changeId": change_dir.name,
511
- "signals": unique_signals,
615
+ "changeId": change_id,
616
+ "signals": [],
512
617
  "defaultPhases": list(tier_policy["defaultPhases"]),
513
618
  "requiredValidations": list(tier_policy["requiredValidations"]),
514
619
  "conditionalStages": list(tier_policy["conditionalStages"]),
515
- "stageDecisions": stage_decisions,
620
+ "stageDecisions": _stage_decisions_for_tier(workflow, tier, []),
516
621
  }
517
- if stage == "post-run":
518
- _write_json(change_dir / "meta" / "risk-classification.json", payload)
519
- return payload
520
622
 
521
623
 
522
624
  def lint_skill_tree(skills_root: Path) -> dict[str, Any]:
@@ -823,6 +925,14 @@ def cmd_close(args: argparse.Namespace) -> int:
823
925
  extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
824
926
  )
825
927
 
928
+ close_status = args.status
929
+ close_code = "PHASE_CLOSED"
930
+ if ledger_result.get("code") == "LEDGER_OK_DEGRADED":
931
+ close_code = "CLOSED_DEGRADED"
932
+ # Degraded close: phase.end status must not exceed WARN (OK → WARN).
933
+ if close_status == "OK":
934
+ close_status = "WARN"
935
+
826
936
  guard_result = None
827
937
  if args.phase in {"run", "test"}:
828
938
  project_root = args.project or str(project)
@@ -839,7 +949,7 @@ def cmd_close(args: argparse.Namespace) -> int:
839
949
  change_dir,
840
950
  phase=args.phase,
841
951
  type_="phase.end",
842
- status=args.status,
952
+ status=close_status,
843
953
  note=args.note or "",
844
954
  run_id=run_id,
845
955
  ) if not _phase_event_exists(change_dir, args.phase, "phase.end", run_id) else {
@@ -862,9 +972,9 @@ def cmd_close(args: argparse.Namespace) -> int:
862
972
 
863
973
  payload = {
864
974
  "ok": True,
865
- "code": "PHASE_CLOSED",
975
+ "code": close_code,
866
976
  "phase": args.phase,
867
- "status": args.status,
977
+ "status": close_status,
868
978
  "changeId": resolved["changeId"],
869
979
  "ledger": ledger_result,
870
980
  "testGuard": guard_result,
@@ -878,19 +988,56 @@ def cmd_close(args: argparse.Namespace) -> int:
878
988
  def cmd_classify(args: argparse.Namespace) -> int:
879
989
  as_json = bool(args.json)
880
990
  project = hc.resolve_main_project_root()
991
+ try:
992
+ workflow = _load_workflow_policy(project=project)
993
+ except (OSError, ValueError, hwp.PolicyValidationError) as exc:
994
+ return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
995
+
881
996
  resolved = hc.resolve_change(project, args.change)
882
- if not resolved.get("ok"):
883
- return emit_error(
884
- str(resolved.get("code", "RESOLVE_FAILED")),
885
- str(resolved.get("message", "change resolve failed")),
886
- as_json=as_json,
887
- extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
997
+ change_dir: Path | None = None
998
+ change_id = str(args.change or "")
999
+ if resolved.get("ok"):
1000
+ change_dir = Path(resolved["changeDir"])
1001
+ change_id = str(resolved.get("changeId") or change_dir.name)
1002
+ if not change_dir.is_dir():
1003
+ change_dir = None
1004
+
1005
+ if change_dir is not None:
1006
+ payload = classify_risk(change_dir, args.stage, workflow=workflow)
1007
+ else:
1008
+ payload = classify_defaults(workflow, change_id=change_id or "unknown", stage=args.stage)
1009
+ if not resolved.get("ok"):
1010
+ payload["resolveCode"] = resolved.get("code")
1011
+
1012
+ tier_override = getattr(args, "tier_override", None)
1013
+ if tier_override:
1014
+ payload = apply_tier_override(
1015
+ payload,
1016
+ workflow,
1017
+ tier=str(tier_override),
1018
+ override_by=str(getattr(args, "override_by", None) or "user"),
888
1019
  )
889
- payload = classify_risk(Path(resolved["changeDir"]), args.stage)
1020
+ else:
1021
+ payload.setdefault("tierOverride", None)
1022
+
1023
+ classified_at = hc.now_iso()
1024
+ payload["classifiedAt"] = classified_at
1025
+
1026
+ if change_dir is not None and change_dir.is_dir():
1027
+ policy_doc = gate_policy_document(payload)
1028
+ policy_path = change_dir / "meta" / "gate-policy.json"
1029
+ _write_json(policy_path, policy_doc)
1030
+ payload["policyPersisted"] = True
1031
+ payload["policyPath"] = str(policy_path)
1032
+ else:
1033
+ payload["policyPersisted"] = False
1034
+ payload["warning"] = (
1035
+ "change directory does not exist; gate-policy.json not written"
1036
+ )
1037
+
890
1038
  emit(payload, as_json=as_json)
891
1039
  return 0
892
1040
 
893
-
894
1041
  def cmd_checkpoint(args: argparse.Namespace) -> int:
895
1042
  as_json = bool(args.json)
896
1043
  project = hc.resolve_main_project_root()
@@ -1012,6 +1159,12 @@ def build_parser() -> argparse.ArgumentParser:
1012
1159
  p_classify = sub.add_parser("classify", parents=[shared])
1013
1160
  p_classify.add_argument("--change", default=None)
1014
1161
  p_classify.add_argument("--stage", required=True, choices=["plan", "post-run"])
1162
+ p_classify.add_argument(
1163
+ "--tier-override",
1164
+ default=None,
1165
+ choices=["fast", "standard", "full"],
1166
+ )
1167
+ p_classify.add_argument("--override-by", default="user")
1015
1168
  p_classify.set_defaults(func=cmd_classify)
1016
1169
 
1017
1170
  p_checkpoint = sub.add_parser("checkpoint", parents=[shared])
@@ -816,6 +816,18 @@ def cmd_record(args: argparse.Namespace) -> int:
816
816
  "finishedAt": now_iso(),
817
817
  }
818
818
  )
819
+ metrics_raw = getattr(args, "metrics_json", None)
820
+ if metrics_raw is not None and str(metrics_raw).strip() != "":
821
+ try:
822
+ metrics_obj = json.loads(str(metrics_raw))
823
+ except json.JSONDecodeError:
824
+ return emit_error("invalid --metrics-json", as_json=as_json)
825
+ if not isinstance(metrics_obj, dict):
826
+ return emit_error("invalid --metrics-json", as_json=as_json)
827
+ entry["metrics"] = metrics_obj
828
+ elif "metrics" in entry:
829
+ # Fresh record without metrics must not keep stale metrics from prev.
830
+ entry.pop("metrics", None)
819
831
  if args.scope is not None and str(args.scope).strip():
820
832
  entry["scope"] = str(args.scope).strip()
821
833
  # No default scope: recording an incremental run as broad "module" scope
@@ -1014,6 +1026,11 @@ def build_parser() -> argparse.ArgumentParser:
1014
1026
  default=None,
1015
1027
  help="package: prior verifications reused when testsExecuted=false",
1016
1028
  )
1029
+ p_record.add_argument(
1030
+ "--metrics-json",
1031
+ default=None,
1032
+ help='structured counts, e.g. \'{"run":155,"failures":0}\' or \'{"total":3,"passed":3}\'',
1033
+ )
1017
1034
  p_record.set_defaults(func=cmd_record)
1018
1035
 
1019
1036
  p_diff = sub.add_parser(
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "codebuddy",
5
- "coreHash": "89e664933ed69572"
5
+ "coreHash": "76b89dae06dadb8a"
6
6
  }
@@ -2,21 +2,21 @@
2
2
  "schemaVersion": 1,
3
3
  "riskTiers": {
4
4
  "fast": {
5
- "description": "Low-risk docs/comments/non-behavioral config; no API/DB/security/concurrency.",
5
+ "description": "Low-risk docs/comments/non-behavioral config; no API/DB/security/concurrency. Policy notes: checkpointRules apply only when meta/implementation-checkpoints.json defines the entry for this change; missing file/entry = disabled (gate must not require --task).",
6
6
  "defaultPhases": ["plan", "run", "submit", "archive"],
7
7
  "requiredValidations": ["unitTest"],
8
8
  "conditionalStages": ["review", "package", "apidoc"],
9
9
  "upgradeTriggers": ["api-change", "security", "migration", "concurrency", "breaking-contract"]
10
10
  },
11
11
  "standard": {
12
- "description": "Ordinary production code and non-breaking API/features.",
12
+ "description": "Ordinary production code and non-breaking API/features. Policy notes: requiredValidations may close DEGRADED when ledger status=NOT_RUN and evidence starts with 'DEGRADED: <reason>' (close code CLOSED_DEGRADED, phase status <= WARN); do not fake OK with unrelated tests.",
13
13
  "defaultPhases": ["plan", "run", "test", "submit", "archive"],
14
14
  "requiredValidations": ["compile", "unitTest", "unitTestFull"],
15
15
  "conditionalStages": ["review", "package", "apidoc"],
16
16
  "upgradeTriggers": ["auth", "security", "delete", "migration", "concurrency", "artifact-protocol", "shared-state", "breaking-api"]
17
17
  },
18
18
  "full": {
19
- "description": "Auth/security/delete/migration/concurrency/service protocol/shared state/breaking contracts.",
19
+ "description": "Auth/security/delete/migration/concurrency/service protocol/shared state/breaking contracts. Policy notes: (1) DEGRADED close — ledger status=NOT_RUN and evidence 'DEGRADED: <reason>' → CLOSED_DEGRADED with phase <= WARN; never fake OK with unrelated tests. (2) checkpointRules — only when plan created meta/implementation-checkpoints.json entry; missing file/entry = not enabled, gate must not require --task.",
20
20
  "defaultPhases": ["plan", "run", "test", "review", "submit", "archive"],
21
21
  "requiredValidations": ["compile", "unitTest", "unitTestFull", "apiTest"],
22
22
  "conditionalStages": ["package", "apidoc"],
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
4
  .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=89e664933ed69572; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=76b89dae06dadb8a; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -86,8 +86,8 @@ AskUserQuestion 让用户确认归档操作。**用户拒绝 → 终止流程,
86
86
  ### Phase 3:执行归档
87
87
 
88
88
  1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --json` 前置检查。
89
- 2. 如需维护者结论,在移动前补全 `meta/archive-meta.md`;不得在 finalize 后修改已校验的归档文件。
90
- 3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(事件、移动、collect、render、validate、manifest 比对)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
89
+ 2. `meta/archive-meta.md` **由 `harness_archive.py finalize` 自动生成**(与 summary-data `finalStatus` 同源);**禁止 agent 手写**该文件,手写视为数据丢失。维护者结论写入 events(decision/issue)即可,finalize 会汇总到 summary / archive-meta。
90
+ 3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
91
91
 
92
92
  - **Read `reference.md`** — finalize 输出字段、archive-meta 格式、CONDITIONAL_OK 规则
93
93
  - **Read `templates/summary-data-template.json`** — summary-data 数据结构