@hunter-harness/workflow-harness 0.2.27 → 0.2.29

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 (226) 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 +34 -2
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +25 -6
  5. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +44 -0
  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-ingest/scripts/harness_knowledge.py +493 -74
  9. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  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-submit/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +26 -2
  16. package/harness/bundles/general/claude-code/harness-test/checklist.md +15 -0
  17. package/harness/bundles/general/claude-code/harness-test/reference.md +28 -4
  18. package/harness/bundles/general/claude-code/scripts/harness_archive.py +2061 -187
  19. package/harness/bundles/general/claude-code/scripts/harness_events.py +43 -9
  20. package/harness/bundles/general/claude-code/scripts/harness_gate.py +491 -7
  21. package/harness/bundles/general/claude-code/scripts/harness_integration.py +232 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_phase.py +758 -1
  23. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +74 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_retry.py +175 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +702 -22
  26. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +974 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +87 -0
  28. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  29. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +34 -2
  30. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
  32. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +44 -0
  33. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  36. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +26 -2
  43. package/harness/bundles/general/codebuddy/harness-test/checklist.md +15 -0
  44. package/harness/bundles/general/codebuddy/harness-test/reference.md +28 -4
  45. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +2061 -187
  46. package/harness/bundles/general/codebuddy/scripts/harness_events.py +43 -9
  47. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +491 -7
  48. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +232 -0
  49. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +758 -1
  50. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +74 -0
  51. package/harness/bundles/general/codebuddy/scripts/harness_retry.py +175 -0
  52. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +702 -22
  53. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +974 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +87 -0
  55. package/harness/bundles/general/codex/.harness-build.json +1 -1
  56. package/harness/bundles/general/codex/contracts/workflow-policy.json +34 -2
  57. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +25 -6
  59. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +44 -0
  60. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  62. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  63. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-test/SKILL.md +26 -2
  70. package/harness/bundles/general/codex/harness-test/checklist.md +15 -0
  71. package/harness/bundles/general/codex/harness-test/reference.md +28 -4
  72. package/harness/bundles/general/codex/scripts/harness_archive.py +2061 -187
  73. package/harness/bundles/general/codex/scripts/harness_events.py +43 -9
  74. package/harness/bundles/general/codex/scripts/harness_gate.py +491 -7
  75. package/harness/bundles/general/codex/scripts/harness_integration.py +232 -0
  76. package/harness/bundles/general/codex/scripts/harness_phase.py +758 -1
  77. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +74 -0
  78. package/harness/bundles/general/codex/scripts/harness_retry.py +175 -0
  79. package/harness/bundles/general/codex/scripts/harness_test_guard.py +702 -22
  80. package/harness/bundles/general/codex/scripts/harness_test_runner.py +974 -0
  81. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +87 -0
  82. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  83. package/harness/bundles/general/cursor/contracts/workflow-policy.json +34 -2
  84. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  85. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +25 -6
  86. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +44 -0
  87. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  90. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  91. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-test/SKILL.md +26 -2
  97. package/harness/bundles/general/cursor/harness-test/checklist.md +15 -0
  98. package/harness/bundles/general/cursor/harness-test/reference.md +28 -4
  99. package/harness/bundles/general/cursor/scripts/harness_archive.py +2061 -187
  100. package/harness/bundles/general/cursor/scripts/harness_events.py +43 -9
  101. package/harness/bundles/general/cursor/scripts/harness_gate.py +491 -7
  102. package/harness/bundles/general/cursor/scripts/harness_integration.py +232 -0
  103. package/harness/bundles/general/cursor/scripts/harness_phase.py +758 -1
  104. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +74 -0
  105. package/harness/bundles/general/cursor/scripts/harness_retry.py +175 -0
  106. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +702 -22
  107. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +974 -0
  108. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +87 -0
  109. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  110. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +34 -2
  111. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  113. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +25 -6
  114. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +44 -0
  115. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  118. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-test/SKILL.md +26 -2
  126. package/harness/bundles/java/claude-code/scripts/harness_archive.py +2061 -187
  127. package/harness/bundles/java/claude-code/scripts/harness_events.py +43 -9
  128. package/harness/bundles/java/claude-code/scripts/harness_gate.py +491 -7
  129. package/harness/bundles/java/claude-code/scripts/harness_integration.py +232 -0
  130. package/harness/bundles/java/claude-code/scripts/harness_phase.py +758 -1
  131. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +74 -0
  132. package/harness/bundles/java/claude-code/scripts/harness_retry.py +175 -0
  133. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +702 -22
  134. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +974 -0
  135. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +87 -0
  136. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  137. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +34 -2
  138. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
  141. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +44 -0
  142. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  144. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  145. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  148. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +26 -2
  153. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +2061 -187
  154. package/harness/bundles/java/codebuddy/scripts/harness_events.py +43 -9
  155. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +491 -7
  156. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +232 -0
  157. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +758 -1
  158. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +74 -0
  159. package/harness/bundles/java/codebuddy/scripts/harness_retry.py +175 -0
  160. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +702 -22
  161. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +974 -0
  162. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +87 -0
  163. package/harness/bundles/java/codex/.harness-build.json +1 -1
  164. package/harness/bundles/java/codex/contracts/workflow-policy.json +34 -2
  165. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  166. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +25 -6
  168. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +44 -0
  169. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  170. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  172. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  175. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  176. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-test/SKILL.md +26 -2
  180. package/harness/bundles/java/codex/scripts/harness_archive.py +2061 -187
  181. package/harness/bundles/java/codex/scripts/harness_events.py +43 -9
  182. package/harness/bundles/java/codex/scripts/harness_gate.py +491 -7
  183. package/harness/bundles/java/codex/scripts/harness_integration.py +232 -0
  184. package/harness/bundles/java/codex/scripts/harness_phase.py +758 -1
  185. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +74 -0
  186. package/harness/bundles/java/codex/scripts/harness_retry.py +175 -0
  187. package/harness/bundles/java/codex/scripts/harness_test_guard.py +702 -22
  188. package/harness/bundles/java/codex/scripts/harness_test_runner.py +974 -0
  189. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +87 -0
  190. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  191. package/harness/bundles/java/cursor/contracts/workflow-policy.json +34 -2
  192. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  193. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  194. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +25 -6
  195. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +44 -0
  196. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  197. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  198. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  199. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  200. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  201. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  204. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  206. package/harness/bundles/java/cursor/harness-test/SKILL.md +26 -2
  207. package/harness/bundles/java/cursor/scripts/harness_archive.py +2061 -187
  208. package/harness/bundles/java/cursor/scripts/harness_events.py +43 -9
  209. package/harness/bundles/java/cursor/scripts/harness_gate.py +491 -7
  210. package/harness/bundles/java/cursor/scripts/harness_integration.py +232 -0
  211. package/harness/bundles/java/cursor/scripts/harness_phase.py +758 -1
  212. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +74 -0
  213. package/harness/bundles/java/cursor/scripts/harness_retry.py +175 -0
  214. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +702 -22
  215. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +974 -0
  216. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +87 -0
  217. package/harness/manifests/general/claude-code.json +34 -26
  218. package/harness/manifests/general/codebuddy.json +34 -26
  219. package/harness/manifests/general/codex.json +34 -26
  220. package/harness/manifests/general/cursor.json +34 -26
  221. package/harness/manifests/java/claude-code.json +34 -26
  222. package/harness/manifests/java/codebuddy.json +34 -26
  223. package/harness/manifests/java/codex.json +34 -26
  224. package/harness/manifests/java/cursor.json +34 -26
  225. package/hunter-workflow-family.json +2 -2
  226. package/package.json +1 -1
@@ -32,6 +32,12 @@ _LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
32
32
 
33
33
  # C8: valid ownerPhase values (lifecycle phases that can own tasks).
34
34
  VALID_OWNER_PHASES = {"plan", "run", "test", "review", "submit"}
35
+ SLICE_CANDIDATE_TYPES = {
36
+ "independent-release-candidate",
37
+ "child-of-aggregate",
38
+ "fixback-of",
39
+ "evidence-only",
40
+ }
35
41
 
36
42
  # C9: map scenario priority to required evidence kind.
37
43
  PRIORITY_EVIDENCE_KIND = {
@@ -173,6 +179,70 @@ def _frontmatter(text: str) -> dict[str, str] | None:
173
179
  return None
174
180
 
175
181
 
182
+ def lint_slice_plan(plan_path: Path) -> dict[str, Any]:
183
+ """Validate the identity, reuse, and storage contract for a slice plan."""
184
+ text = Path(plan_path).read_text(encoding="utf-8-sig")
185
+ frontmatter = _frontmatter(text)
186
+ if frontmatter is None:
187
+ return _result_error(
188
+ "SLICE_PLAN_CONTRACT_INVALID",
189
+ f"{plan_path.name}: frontmatter is required",
190
+ )
191
+ enabled = (
192
+ str(frontmatter.get("slice-plan") or "").strip().lower()
193
+ in {"true", "yes", "1"}
194
+ or "candidate-type" in frontmatter
195
+ )
196
+ if not enabled:
197
+ return {
198
+ "ok": True,
199
+ "configured": False,
200
+ "code": "SLICE_PLAN_NOT_CONFIGURED",
201
+ }
202
+
203
+ issues: list[str] = []
204
+ candidate_type = str(frontmatter.get("candidate-type") or "").strip()
205
+ aggregate_parent = str(frontmatter.get("aggregate-parent") or "").strip()
206
+ evidence_reuse = str(frontmatter.get("evidence-reuse") or "").strip()
207
+ raw_budget = str(frontmatter.get("artifact-budget-bytes") or "").strip()
208
+ if candidate_type not in SLICE_CANDIDATE_TYPES:
209
+ issues.append(
210
+ "candidate-type must be one of "
211
+ + ", ".join(sorted(SLICE_CANDIDATE_TYPES))
212
+ )
213
+ if not aggregate_parent:
214
+ issues.append(
215
+ "aggregate-parent is required (use 'none' for an independent candidate)"
216
+ )
217
+ if not evidence_reuse:
218
+ issues.append(
219
+ "evidence-reuse is required (use 'none' when no evidence is reused)"
220
+ )
221
+ try:
222
+ artifact_budget = int(raw_budget)
223
+ if artifact_budget <= 0:
224
+ raise ValueError
225
+ except ValueError:
226
+ artifact_budget = 0
227
+ issues.append("artifact-budget-bytes must be a positive integer")
228
+ if issues:
229
+ return {
230
+ "ok": False,
231
+ "code": "SLICE_PLAN_CONTRACT_INVALID",
232
+ "error": f"{plan_path.name}: " + "; ".join(issues),
233
+ "issues": issues,
234
+ }
235
+ return {
236
+ "ok": True,
237
+ "configured": True,
238
+ "code": "SLICE_PLAN_CONTRACT_OK",
239
+ "candidateType": candidate_type,
240
+ "aggregateParent": aggregate_parent,
241
+ "evidenceReuse": evidence_reuse,
242
+ "artifactBudgetBytes": artifact_budget,
243
+ }
244
+
245
+
176
246
  def _artifact_files(staging: Path) -> list[Path]:
177
247
  files: list[Path] = []
178
248
  for path in staging.rglob("*"):
@@ -262,6 +332,9 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
262
332
  "PLAN_OWNER_PHASE_INVALID",
263
333
  f"task {task.get('num', '?')}: ownerPhase '{owner}' not in {sorted(VALID_OWNER_PHASES)}",
264
334
  )
335
+ slice_plan = lint_slice_plan(plan_path)
336
+ if not slice_plan.get("ok"):
337
+ return slice_plan
265
338
 
266
339
  # C9: parse test-scenarios.md for scenario manifest.
267
340
  scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
@@ -282,6 +355,7 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
282
355
  "artifactsHash": "sha256:" + digest.hexdigest(),
283
356
  "tasks": tasks,
284
357
  "scenarios": scenarios,
358
+ "slicePlan": slice_plan,
285
359
  }
286
360
 
287
361
 
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic retry classifier for remote and deployment side effects."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ DECISIONS = {
13
+ "RETRY_ALLOWED_INPUT_CHANGED",
14
+ "WAIT_EXISTING_RUN",
15
+ "BLOCKED_EXTERNAL_PREREQUISITE",
16
+ "NO_NEW_INFORMATION",
17
+ "REOPEN_CANDIDATE",
18
+ }
19
+
20
+
21
+ def _same_identity(left: Any, right: Any) -> bool:
22
+ return (
23
+ isinstance(left, dict)
24
+ and isinstance(right, dict)
25
+ and all(
26
+ str(left.get(field) or "") == str(right.get(field) or "")
27
+ for field in (
28
+ "productCommit",
29
+ "productTreeHash",
30
+ "environmentHash",
31
+ )
32
+ )
33
+ )
34
+
35
+
36
+ def classify_retry(
37
+ history: list[dict[str, Any]],
38
+ *,
39
+ candidate_identity: dict[str, Any],
40
+ failure_signature: str,
41
+ input_hash: str,
42
+ external_prerequisite_hash: str | None = None,
43
+ projection_status: str = "verified",
44
+ ) -> dict[str, Any]:
45
+ """Return a side-effect-safe retry decision from durable facts."""
46
+ matching = [
47
+ item
48
+ for item in history
49
+ if isinstance(item, dict)
50
+ and _same_identity(item.get("candidateIdentity"), candidate_identity)
51
+ ]
52
+ active = next(
53
+ (
54
+ item
55
+ for item in reversed(matching)
56
+ if str(item.get("status") or "").upper()
57
+ in {"QUEUED", "RUNNING", "IN_PROGRESS", "PENDING"}
58
+ ),
59
+ None,
60
+ )
61
+ if active is not None:
62
+ return {
63
+ "ok": True,
64
+ "allowed": False,
65
+ "decision": "WAIT_EXISTING_RUN",
66
+ "message": "an equivalent candidate run is still active",
67
+ "existingRun": active,
68
+ }
69
+ if projection_status.strip().lower() == "degraded":
70
+ return {
71
+ "ok": True,
72
+ "allowed": False,
73
+ "decision": "BLOCKED_EXTERNAL_PREREQUISITE",
74
+ "message": (
75
+ "adapter/config projection is degraded; protocol identity is unknown"
76
+ ),
77
+ }
78
+ latest = matching[-1] if matching else None
79
+ if latest is None:
80
+ return {
81
+ "ok": True,
82
+ "allowed": True,
83
+ "decision": "REOPEN_CANDIDATE",
84
+ "message": "no prior equivalent candidate attempt exists",
85
+ }
86
+ prior_input = str(latest.get("inputHash") or "")
87
+ if prior_input != input_hash:
88
+ return {
89
+ "ok": True,
90
+ "allowed": True,
91
+ "decision": "RETRY_ALLOWED_INPUT_CHANGED",
92
+ "message": "candidate inputs changed since the prior attempt",
93
+ "priorInputHash": prior_input,
94
+ "inputHash": input_hash,
95
+ }
96
+ prior_external = str(latest.get("externalPrerequisiteHash") or "")
97
+ current_external = str(external_prerequisite_hash or "")
98
+ same_failure = (
99
+ str(latest.get("failureSignature") or "") == failure_signature
100
+ )
101
+ if same_failure and prior_external == current_external:
102
+ return {
103
+ "ok": True,
104
+ "allowed": False,
105
+ "decision": "NO_NEW_INFORMATION",
106
+ "message": (
107
+ "same candidate, failure signature, inputs, and external "
108
+ "prerequisites; retry would repeat the same side effect"
109
+ ),
110
+ "priorAttempt": latest,
111
+ }
112
+ if same_failure and prior_external != current_external:
113
+ return {
114
+ "ok": True,
115
+ "allowed": True,
116
+ "decision": "REOPEN_CANDIDATE",
117
+ "message": "external prerequisite identity changed",
118
+ }
119
+ if re_external_failure(failure_signature) and prior_external == current_external:
120
+ return {
121
+ "ok": True,
122
+ "allowed": False,
123
+ "decision": "BLOCKED_EXTERNAL_PREREQUISITE",
124
+ "message": "external prerequisite has not changed",
125
+ }
126
+ return {
127
+ "ok": True,
128
+ "allowed": True,
129
+ "decision": "REOPEN_CANDIDATE",
130
+ "message": "failure signature changed; a new attempt may add information",
131
+ }
132
+
133
+
134
+ def re_external_failure(signature: str) -> bool:
135
+ lowered = signature.lower()
136
+ return any(
137
+ marker in lowered
138
+ for marker in (
139
+ "permission",
140
+ "credential",
141
+ "secret",
142
+ "registry",
143
+ "allowlist",
144
+ "quota",
145
+ )
146
+ )
147
+
148
+
149
+ def main(argv: list[str] | None = None) -> int:
150
+ parser = argparse.ArgumentParser(prog="harness_retry.py")
151
+ parser.add_argument("--history", required=True)
152
+ parser.add_argument("--candidate", required=True)
153
+ parser.add_argument("--failure-signature", required=True)
154
+ parser.add_argument("--input-hash", required=True)
155
+ parser.add_argument("--external-prerequisite-hash")
156
+ parser.add_argument("--projection-status", default="verified")
157
+ args = parser.parse_args(argv)
158
+ history = json.loads(Path(args.history).read_text(encoding="utf-8-sig"))
159
+ candidate = json.loads(Path(args.candidate).read_text(encoding="utf-8-sig"))
160
+ if not isinstance(history, list) or not isinstance(candidate, dict):
161
+ raise ValueError("history must be an array and candidate an object")
162
+ result = classify_retry(
163
+ history,
164
+ candidate_identity=candidate,
165
+ failure_signature=args.failure_signature,
166
+ input_hash=args.input_hash,
167
+ external_prerequisite_hash=args.external_prerequisite_hash,
168
+ projection_status=args.projection_status,
169
+ )
170
+ print(json.dumps(result, ensure_ascii=False, indent=2))
171
+ return 0 if result["allowed"] else 2
172
+
173
+
174
+ if __name__ == "__main__":
175
+ raise SystemExit(main())