@hunter-harness/workflow-harness 0.2.26 → 0.2.28

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 (210) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +25 -6
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +44 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  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-review/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +8 -4
  14. package/harness/bundles/general/claude-code/harness-sync/reference.md +28 -0
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/scripts/harness_archive.py +2147 -189
  17. package/harness/bundles/general/claude-code/scripts/harness_change.py +8 -3
  18. package/harness/bundles/general/claude-code/scripts/harness_events.py +57 -9
  19. package/harness/bundles/general/claude-code/scripts/harness_gate.py +530 -8
  20. package/harness/bundles/general/claude-code/scripts/harness_integration.py +263 -6
  21. package/harness/bundles/general/claude-code/scripts/harness_phase.py +758 -1
  22. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +74 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_retry.py +175 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +702 -22
  25. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  26. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
  28. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +44 -0
  29. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  32. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +8 -4
  38. package/harness/bundles/general/codebuddy/harness-sync/reference.md +28 -0
  39. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +2147 -189
  41. package/harness/bundles/general/codebuddy/scripts/harness_change.py +8 -3
  42. package/harness/bundles/general/codebuddy/scripts/harness_events.py +57 -9
  43. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +530 -8
  44. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +263 -6
  45. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +758 -1
  46. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +74 -0
  47. package/harness/bundles/general/codebuddy/scripts/harness_retry.py +175 -0
  48. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +702 -22
  49. package/harness/bundles/general/codex/.harness-build.json +1 -1
  50. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +25 -6
  52. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +44 -0
  53. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  56. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-sync/SKILL.md +8 -4
  62. package/harness/bundles/general/codex/harness-sync/reference.md +28 -0
  63. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/scripts/harness_archive.py +2147 -189
  65. package/harness/bundles/general/codex/scripts/harness_change.py +8 -3
  66. package/harness/bundles/general/codex/scripts/harness_events.py +57 -9
  67. package/harness/bundles/general/codex/scripts/harness_gate.py +530 -8
  68. package/harness/bundles/general/codex/scripts/harness_integration.py +263 -6
  69. package/harness/bundles/general/codex/scripts/harness_phase.py +758 -1
  70. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +74 -0
  71. package/harness/bundles/general/codex/scripts/harness_retry.py +175 -0
  72. package/harness/bundles/general/codex/scripts/harness_test_guard.py +702 -22
  73. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  74. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +25 -6
  76. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +44 -0
  77. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  80. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  81. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  83. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  85. package/harness/bundles/general/cursor/harness-sync/SKILL.md +8 -4
  86. package/harness/bundles/general/cursor/harness-sync/reference.md +28 -0
  87. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/scripts/harness_archive.py +2147 -189
  89. package/harness/bundles/general/cursor/scripts/harness_change.py +8 -3
  90. package/harness/bundles/general/cursor/scripts/harness_events.py +57 -9
  91. package/harness/bundles/general/cursor/scripts/harness_gate.py +530 -8
  92. package/harness/bundles/general/cursor/scripts/harness_integration.py +263 -6
  93. package/harness/bundles/general/cursor/scripts/harness_phase.py +758 -1
  94. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +74 -0
  95. package/harness/bundles/general/cursor/scripts/harness_retry.py +175 -0
  96. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +702 -22
  97. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  98. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  100. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +25 -6
  101. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +44 -0
  102. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  104. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  105. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  106. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  108. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  109. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  110. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +8 -4
  112. package/harness/bundles/java/claude-code/harness-sync/reference.md +28 -0
  113. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  114. package/harness/bundles/java/claude-code/scripts/harness_archive.py +2147 -189
  115. package/harness/bundles/java/claude-code/scripts/harness_change.py +8 -3
  116. package/harness/bundles/java/claude-code/scripts/harness_events.py +57 -9
  117. package/harness/bundles/java/claude-code/scripts/harness_gate.py +530 -8
  118. package/harness/bundles/java/claude-code/scripts/harness_integration.py +263 -6
  119. package/harness/bundles/java/claude-code/scripts/harness_phase.py +758 -1
  120. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +74 -0
  121. package/harness/bundles/java/claude-code/scripts/harness_retry.py +175 -0
  122. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +702 -22
  123. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  124. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  126. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +25 -6
  127. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +44 -0
  128. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  129. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  130. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  131. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  132. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  133. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  134. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  135. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  136. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  137. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +8 -4
  138. package/harness/bundles/java/codebuddy/harness-sync/reference.md +28 -0
  139. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  140. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +2147 -189
  141. package/harness/bundles/java/codebuddy/scripts/harness_change.py +8 -3
  142. package/harness/bundles/java/codebuddy/scripts/harness_events.py +57 -9
  143. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +530 -8
  144. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +263 -6
  145. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +758 -1
  146. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +74 -0
  147. package/harness/bundles/java/codebuddy/scripts/harness_retry.py +175 -0
  148. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +702 -22
  149. package/harness/bundles/java/codex/.harness-build.json +1 -1
  150. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  151. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  152. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +25 -6
  153. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +44 -0
  154. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  155. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  156. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  157. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  158. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  159. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  160. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  161. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  162. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  163. package/harness/bundles/java/codex/harness-sync/SKILL.md +8 -4
  164. package/harness/bundles/java/codex/harness-sync/reference.md +28 -0
  165. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  166. package/harness/bundles/java/codex/scripts/harness_archive.py +2147 -189
  167. package/harness/bundles/java/codex/scripts/harness_change.py +8 -3
  168. package/harness/bundles/java/codex/scripts/harness_events.py +57 -9
  169. package/harness/bundles/java/codex/scripts/harness_gate.py +530 -8
  170. package/harness/bundles/java/codex/scripts/harness_integration.py +263 -6
  171. package/harness/bundles/java/codex/scripts/harness_phase.py +758 -1
  172. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +74 -0
  173. package/harness/bundles/java/codex/scripts/harness_retry.py +175 -0
  174. package/harness/bundles/java/codex/scripts/harness_test_guard.py +702 -22
  175. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  176. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  177. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  178. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +25 -6
  179. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +44 -0
  180. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  181. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  182. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +493 -74
  183. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  184. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  185. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  186. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  187. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  188. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  189. package/harness/bundles/java/cursor/harness-sync/SKILL.md +8 -4
  190. package/harness/bundles/java/cursor/harness-sync/reference.md +28 -0
  191. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  192. package/harness/bundles/java/cursor/scripts/harness_archive.py +2147 -189
  193. package/harness/bundles/java/cursor/scripts/harness_change.py +8 -3
  194. package/harness/bundles/java/cursor/scripts/harness_events.py +57 -9
  195. package/harness/bundles/java/cursor/scripts/harness_gate.py +530 -8
  196. package/harness/bundles/java/cursor/scripts/harness_integration.py +263 -6
  197. package/harness/bundles/java/cursor/scripts/harness_phase.py +758 -1
  198. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +74 -0
  199. package/harness/bundles/java/cursor/scripts/harness_retry.py +175 -0
  200. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +702 -22
  201. package/harness/manifests/general/claude-code.json +28 -24
  202. package/harness/manifests/general/codebuddy.json +28 -24
  203. package/harness/manifests/general/codex.json +28 -24
  204. package/harness/manifests/general/cursor.json +28 -24
  205. package/harness/manifests/java/claude-code.json +30 -26
  206. package/harness/manifests/java/codebuddy.json +30 -26
  207. package/harness/manifests/java/codex.json +30 -26
  208. package/harness/manifests/java/cursor.json +30 -26
  209. package/hunter-workflow-family.json +2 -2
  210. package/package.json +1 -1
@@ -42,6 +42,12 @@ VALIDATION_PHASES = {
42
42
  "dbCompatibility": "test",
43
43
  "package": "package",
44
44
  }
45
+ FINAL_SEQUENCE_RECEIPTS_REL = (
46
+ Path("evidence") / "final-sequence-receipts.json"
47
+ )
48
+ ENVIRONMENT_EXECUTIONS_REL = (
49
+ Path("evidence") / "environment-executions.json"
50
+ )
45
51
 
46
52
 
47
53
  def _read_json(path: Path) -> dict[str, Any]:
@@ -66,6 +72,621 @@ def _write_json(path: Path, value: Any) -> None:
66
72
  raise
67
73
 
68
74
 
75
+ def _final_sequence_contract(change_dir: Path) -> dict[str, Any] | None:
76
+ policy_path = change_dir / "meta" / "gate-policy.json"
77
+ if not policy_path.is_file():
78
+ return None
79
+ policy = _read_json(policy_path)
80
+ declared = policy.get("finalSequence")
81
+ if isinstance(declared, dict) and isinstance(declared.get("nodeIds"), list):
82
+ return declared
83
+ dag = policy.get("requiredGateDag")
84
+ if not isinstance(dag, dict):
85
+ return None
86
+ sequence_nodes = [
87
+ node
88
+ for node in (dag.get("nodes") or [])
89
+ if isinstance(node, dict) and node.get("kind") == "sequence"
90
+ ]
91
+ if not sequence_nodes:
92
+ return None
93
+ sequence_nodes.sort(key=lambda node: int(node.get("sequenceIndex") or 0))
94
+ node_ids = [str(node.get("id") or "") for node in sequence_nodes]
95
+ return {
96
+ "schemaVersion": 1,
97
+ "nodeIds": node_ids,
98
+ "freezeNodeId": (
99
+ "sequence:code-freeze"
100
+ if "sequence:code-freeze" in node_ids
101
+ else None
102
+ ),
103
+ "terminalNodeId": node_ids[-1],
104
+ }
105
+
106
+
107
+ def _load_sequence_receipts(change_dir: Path) -> dict[str, Any]:
108
+ path = change_dir / FINAL_SEQUENCE_RECEIPTS_REL
109
+ if not path.is_file():
110
+ return {
111
+ "schemaVersion": 1,
112
+ "changeId": change_dir.name,
113
+ "receipts": {},
114
+ "attempts": [],
115
+ }
116
+ value = _read_json(path)
117
+ if value.get("schemaVersion") != 1:
118
+ raise ValueError("unsupported final-sequence receipt schema")
119
+ receipts = value.get("receipts")
120
+ if not isinstance(receipts, dict):
121
+ raise ValueError("final-sequence receipts must be an object")
122
+ attempts = value.get("attempts")
123
+ if attempts is not None and not isinstance(attempts, list):
124
+ raise ValueError("final-sequence attempts must be an array")
125
+ value.setdefault("attempts", [])
126
+ return value
127
+
128
+
129
+ def _validated_sequence_subject(subject: dict[str, Any]) -> dict[str, str]:
130
+ required = ("productCommit", "productTreeHash", "environmentHash")
131
+ missing = [
132
+ field
133
+ for field in required
134
+ if not isinstance(subject.get(field), str)
135
+ or not str(subject[field]).strip()
136
+ ]
137
+ if missing:
138
+ raise ValueError(
139
+ "final-sequence subject missing: " + ", ".join(missing)
140
+ )
141
+ return {field: str(subject[field]).strip() for field in required}
142
+
143
+
144
+ def _record_sequence_attempt(
145
+ change_dir: Path,
146
+ journal: dict[str, Any],
147
+ *,
148
+ node_id: str,
149
+ ok: bool,
150
+ code: str,
151
+ subject: dict[str, str],
152
+ ) -> None:
153
+ attempts = journal.setdefault("attempts", [])
154
+ attempts.append(
155
+ {
156
+ "nodeId": node_id,
157
+ "ok": ok,
158
+ "code": code,
159
+ "subject": subject,
160
+ "recordedAt": he.now_iso(),
161
+ }
162
+ )
163
+ _write_json(change_dir / FINAL_SEQUENCE_RECEIPTS_REL, journal)
164
+
165
+
166
+ def record_sequence_receipt(
167
+ change_dir: Path,
168
+ node_id: str,
169
+ subject: dict[str, Any],
170
+ *,
171
+ status: str = "OK",
172
+ evidence: dict[str, Any] | None = None,
173
+ ) -> dict[str, Any]:
174
+ """Record one final-sequence transition after validating its predecessor."""
175
+ change_dir = change_dir.resolve()
176
+ contract = _final_sequence_contract(change_dir)
177
+ if contract is None:
178
+ return {
179
+ "ok": False,
180
+ "code": "FINAL_SEQUENCE_NOT_CONFIGURED",
181
+ "message": "gate policy does not declare finalSequence",
182
+ }
183
+ node_ids = [
184
+ str(item)
185
+ for item in contract.get("nodeIds") or []
186
+ if isinstance(item, str)
187
+ ]
188
+ if node_id not in node_ids:
189
+ return {
190
+ "ok": False,
191
+ "code": "SEQUENCE_NODE_UNKNOWN",
192
+ "message": f"node is not in finalSequence: {node_id}",
193
+ }
194
+ try:
195
+ normalized_subject = _validated_sequence_subject(subject)
196
+ journal = _load_sequence_receipts(change_dir)
197
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
198
+ return {
199
+ "ok": False,
200
+ "code": "SEQUENCE_RECEIPT_INVALID",
201
+ "message": str(exc),
202
+ }
203
+ receipts = journal["receipts"]
204
+ index = node_ids.index(node_id)
205
+ predecessor_id = node_ids[index - 1] if index else None
206
+ if predecessor_id is not None:
207
+ predecessor = receipts.get(predecessor_id)
208
+ if (
209
+ not isinstance(predecessor, dict)
210
+ or str(predecessor.get("status") or "").upper() != "OK"
211
+ ):
212
+ code = "SEQUENCE_PREDECESSOR_MISSING"
213
+ _record_sequence_attempt(
214
+ change_dir,
215
+ journal,
216
+ node_id=node_id,
217
+ ok=False,
218
+ code=code,
219
+ subject=normalized_subject,
220
+ )
221
+ return {
222
+ "ok": False,
223
+ "code": code,
224
+ "message": f"required predecessor is not complete: {predecessor_id}",
225
+ "predecessor": predecessor_id,
226
+ }
227
+ if predecessor.get("subject") != normalized_subject:
228
+ code = "SEQUENCE_SUBJECT_CHANGED"
229
+ _record_sequence_attempt(
230
+ change_dir,
231
+ journal,
232
+ node_id=node_id,
233
+ ok=False,
234
+ code=code,
235
+ subject=normalized_subject,
236
+ )
237
+ return {
238
+ "ok": False,
239
+ "code": code,
240
+ "message": "candidate subject differs from predecessor receipt",
241
+ "expected": predecessor.get("subject"),
242
+ "actual": normalized_subject,
243
+ }
244
+
245
+ freeze_node_id = str(contract.get("freezeNodeId") or "")
246
+ freeze_receipt = receipts.get(freeze_node_id)
247
+ if (
248
+ freeze_node_id
249
+ and isinstance(freeze_receipt, dict)
250
+ and freeze_receipt.get("subject") != normalized_subject
251
+ ):
252
+ code = "SEQUENCE_SUBJECT_CHANGED"
253
+ _record_sequence_attempt(
254
+ change_dir,
255
+ journal,
256
+ node_id=node_id,
257
+ ok=False,
258
+ code=code,
259
+ subject=normalized_subject,
260
+ )
261
+ return {
262
+ "ok": False,
263
+ "code": code,
264
+ "message": "candidate subject changed after code-freeze",
265
+ "expected": freeze_receipt.get("subject"),
266
+ "actual": normalized_subject,
267
+ }
268
+
269
+ normalized_status = status.strip().upper()
270
+ if normalized_status not in {"OK", "WARN", "FAIL"}:
271
+ return {
272
+ "ok": False,
273
+ "code": "SEQUENCE_STATUS_INVALID",
274
+ "message": f"unsupported final-sequence status: {status}",
275
+ }
276
+ normalized_evidence = evidence if isinstance(evidence, dict) else {}
277
+ if node_id == "sequence:unit-test-full":
278
+ full_execution_id = str(
279
+ normalized_evidence.get("fullExecutionId") or ""
280
+ ).strip()
281
+ try:
282
+ environment_journal = load_environment_execution_journal(
283
+ change_dir
284
+ )
285
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
286
+ environment_journal = {"fullExecutions": []}
287
+ environment_error = str(exc)
288
+ else:
289
+ environment_error = ""
290
+ full_execution = next(
291
+ (
292
+ item
293
+ for item in environment_journal.get("fullExecutions") or []
294
+ if isinstance(item, dict)
295
+ and item.get("fullExecutionId") == full_execution_id
296
+ ),
297
+ None,
298
+ )
299
+ if (
300
+ not full_execution_id
301
+ or not isinstance(full_execution, dict)
302
+ or full_execution.get("subject") != normalized_subject
303
+ or full_execution.get("status") != "OK"
304
+ or full_execution.get("assertionBearing") is not True
305
+ ):
306
+ code = "FULL_EXECUTION_EVIDENCE_REQUIRED"
307
+ _record_sequence_attempt(
308
+ change_dir,
309
+ journal,
310
+ node_id=node_id,
311
+ ok=False,
312
+ code=code,
313
+ subject=normalized_subject,
314
+ )
315
+ return {
316
+ "ok": False,
317
+ "code": code,
318
+ "message": (
319
+ "unit-test-full requires a successful assertion-bearing "
320
+ "Full execution bound to the frozen subject"
321
+ ),
322
+ "fullExecutionId": full_execution_id or None,
323
+ "journalError": environment_error or None,
324
+ }
325
+ receipt = {
326
+ "schemaVersion": 1,
327
+ "nodeId": node_id,
328
+ "status": normalized_status,
329
+ "subject": normalized_subject,
330
+ "evidence": normalized_evidence,
331
+ "recordedAt": he.now_iso(),
332
+ }
333
+ existing = receipts.get(node_id)
334
+ reused = (
335
+ isinstance(existing, dict)
336
+ and existing.get("status") == normalized_status
337
+ and existing.get("subject") == normalized_subject
338
+ and existing.get("evidence") == receipt["evidence"]
339
+ )
340
+ if not reused:
341
+ receipts[node_id] = receipt
342
+ _record_sequence_attempt(
343
+ change_dir,
344
+ journal,
345
+ node_id=node_id,
346
+ ok=normalized_status == "OK",
347
+ code=(
348
+ "SEQUENCE_RECEIPT_REUSED"
349
+ if reused
350
+ else (
351
+ "SEQUENCE_STEP_RECORDED"
352
+ if normalized_status == "OK"
353
+ else "SEQUENCE_STEP_FAILED"
354
+ )
355
+ ),
356
+ subject=normalized_subject,
357
+ )
358
+ ok = normalized_status == "OK"
359
+ return {
360
+ "ok": ok,
361
+ "code": (
362
+ "SEQUENCE_RECEIPT_REUSED"
363
+ if reused
364
+ else ("SEQUENCE_STEP_RECORDED" if ok else "SEQUENCE_STEP_FAILED")
365
+ ),
366
+ "nodeId": node_id,
367
+ "receipt": receipts.get(node_id),
368
+ "reused": reused,
369
+ }
370
+
371
+
372
+ def evaluate_final_sequence(
373
+ change_dir: Path,
374
+ subject: dict[str, Any],
375
+ *,
376
+ exclude_nodes: set[str] | None = None,
377
+ ) -> dict[str, Any]:
378
+ """Evaluate all required sequence receipts against one candidate subject."""
379
+ change_dir = change_dir.resolve()
380
+ try:
381
+ contract = _final_sequence_contract(change_dir)
382
+ if contract is None:
383
+ return {
384
+ "ok": True,
385
+ "code": "FINAL_SEQUENCE_NOT_CONFIGURED",
386
+ "skipped": True,
387
+ "missing": [],
388
+ "invalid": [],
389
+ }
390
+ normalized_subject = _validated_sequence_subject(subject)
391
+ journal = _load_sequence_receipts(change_dir)
392
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
393
+ return {
394
+ "ok": False,
395
+ "code": "FINAL_SEQUENCE_INVALID",
396
+ "message": str(exc),
397
+ }
398
+ excluded = exclude_nodes or set()
399
+ required = [
400
+ str(item)
401
+ for item in contract.get("nodeIds") or []
402
+ if isinstance(item, str) and item not in excluded
403
+ ]
404
+ receipts = journal["receipts"]
405
+ missing = [node_id for node_id in required if node_id not in receipts]
406
+ invalid = []
407
+ for node_id in required:
408
+ receipt = receipts.get(node_id)
409
+ if not isinstance(receipt, dict):
410
+ continue
411
+ if (
412
+ str(receipt.get("status") or "").upper() != "OK"
413
+ or receipt.get("subject") != normalized_subject
414
+ ):
415
+ invalid.append(
416
+ {
417
+ "nodeId": node_id,
418
+ "status": receipt.get("status"),
419
+ "subject": receipt.get("subject"),
420
+ }
421
+ )
422
+ ok = not missing and not invalid
423
+ return {
424
+ "ok": ok,
425
+ "code": (
426
+ "FINAL_SEQUENCE_COMPLETE"
427
+ if ok
428
+ else "FINAL_SEQUENCE_INCOMPLETE"
429
+ ),
430
+ "message": (
431
+ "all required final-sequence receipts match the candidate"
432
+ if ok
433
+ else f"missing={len(missing)} invalid={len(invalid)}"
434
+ ),
435
+ "subject": normalized_subject,
436
+ "required": required,
437
+ "missing": missing,
438
+ "invalid": invalid,
439
+ "receiptsPath": str(change_dir / FINAL_SEQUENCE_RECEIPTS_REL),
440
+ }
441
+
442
+
443
+ def load_environment_execution_journal(
444
+ change_dir: Path,
445
+ ) -> dict[str, Any]:
446
+ path = change_dir.resolve() / ENVIRONMENT_EXECUTIONS_REL
447
+ if not path.is_file():
448
+ return {
449
+ "schemaVersion": 1,
450
+ "changeId": change_dir.name,
451
+ "environmentAttempts": [],
452
+ "fullExecutions": [],
453
+ }
454
+ journal = _read_json(path)
455
+ if journal.get("schemaVersion") != 1:
456
+ raise ValueError("unsupported environment execution journal schema")
457
+ for field in ("environmentAttempts", "fullExecutions"):
458
+ if not isinstance(journal.get(field), list):
459
+ raise ValueError(f"environment journal {field} must be an array")
460
+ return journal
461
+
462
+
463
+ def record_environment_attempt(
464
+ change_dir: Path,
465
+ subject: dict[str, Any],
466
+ *,
467
+ stage: str,
468
+ status: str,
469
+ duration_ms: int,
470
+ failure_signature: str | None = None,
471
+ ) -> dict[str, Any]:
472
+ """Record environment lifecycle work without manufacturing Full results."""
473
+ change_dir = change_dir.resolve()
474
+ if stage not in {"prepare", "verify", "release"}:
475
+ return {
476
+ "ok": False,
477
+ "code": "ENVIRONMENT_STAGE_INVALID",
478
+ "message": f"unsupported environment stage: {stage}",
479
+ }
480
+ normalized_status = status.strip().upper()
481
+ if normalized_status not in {
482
+ "OK",
483
+ "ENVIRONMENT_ERROR",
484
+ "INTERRUPTED",
485
+ "FAIL",
486
+ }:
487
+ return {
488
+ "ok": False,
489
+ "code": "ENVIRONMENT_STATUS_INVALID",
490
+ "message": f"unsupported environment status: {status}",
491
+ }
492
+ if not isinstance(duration_ms, int) or duration_ms < 0:
493
+ return {
494
+ "ok": False,
495
+ "code": "ENVIRONMENT_DURATION_INVALID",
496
+ "message": "duration_ms must be a non-negative integer",
497
+ }
498
+ try:
499
+ normalized_subject = _validated_sequence_subject(subject)
500
+ journal = load_environment_execution_journal(change_dir)
501
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
502
+ return {
503
+ "ok": False,
504
+ "code": "ENVIRONMENT_JOURNAL_INVALID",
505
+ "message": str(exc),
506
+ }
507
+ attempt = {
508
+ "attemptId": "env-" + uuid.uuid4().hex,
509
+ "stage": stage,
510
+ "status": normalized_status,
511
+ "subject": normalized_subject,
512
+ "durationMs": duration_ms,
513
+ "failureSignature": failure_signature,
514
+ "recordedAt": he.now_iso(),
515
+ }
516
+ journal["environmentAttempts"].append(attempt)
517
+ _write_json(change_dir / ENVIRONMENT_EXECUTIONS_REL, journal)
518
+ ok = normalized_status == "OK"
519
+ return {
520
+ "ok": ok,
521
+ "code": (
522
+ "ENVIRONMENT_STAGE_RECORDED"
523
+ if ok
524
+ else "ENVIRONMENT_ERROR_RECORDED"
525
+ ),
526
+ "attempt": attempt,
527
+ "fullExecutionCreated": False,
528
+ }
529
+
530
+
531
+ def record_full_test_execution(
532
+ change_dir: Path,
533
+ subject: dict[str, Any],
534
+ *,
535
+ status: str,
536
+ prepare_ms: int,
537
+ test_ms: int,
538
+ cleanup_ms: int,
539
+ result_digest: str,
540
+ supersedes: str | None = None,
541
+ ) -> dict[str, Any]:
542
+ """Record one assertion-bearing Full execution after environment success."""
543
+ change_dir = change_dir.resolve()
544
+ normalized_status = status.strip().upper()
545
+ if normalized_status not in {"OK", "FAIL"}:
546
+ return {
547
+ "ok": False,
548
+ "code": "FULL_STATUS_INVALID",
549
+ "message": "Full status must be OK or FAIL",
550
+ }
551
+ durations = (prepare_ms, test_ms, cleanup_ms)
552
+ if any(not isinstance(value, int) or value < 0 for value in durations):
553
+ return {
554
+ "ok": False,
555
+ "code": "FULL_DURATION_INVALID",
556
+ "message": "prepare/test/cleanup durations must be non-negative",
557
+ }
558
+ if not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", result_digest):
559
+ return {
560
+ "ok": False,
561
+ "code": "FULL_RESULT_DIGEST_INVALID",
562
+ "message": "result_digest must be a sha256 digest",
563
+ }
564
+ try:
565
+ normalized_subject = _validated_sequence_subject(subject)
566
+ journal = load_environment_execution_journal(change_dir)
567
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
568
+ return {
569
+ "ok": False,
570
+ "code": "ENVIRONMENT_JOURNAL_INVALID",
571
+ "message": str(exc),
572
+ }
573
+ successful_environment = {
574
+ str(item.get("stage") or ""): item
575
+ for item in journal["environmentAttempts"]
576
+ if isinstance(item, dict)
577
+ and item.get("subject") == normalized_subject
578
+ and str(item.get("status") or "").upper() == "OK"
579
+ }
580
+ missing = [
581
+ stage
582
+ for stage in ("prepare", "verify")
583
+ if stage not in successful_environment
584
+ ]
585
+ if missing:
586
+ return {
587
+ "ok": False,
588
+ "code": "ENVIRONMENT_NOT_READY",
589
+ "message": (
590
+ "Full execution requires successful environment stages: "
591
+ + ", ".join(missing)
592
+ ),
593
+ "missing": missing,
594
+ }
595
+ prior_success = next(
596
+ (
597
+ item
598
+ for item in reversed(journal["fullExecutions"])
599
+ if isinstance(item, dict)
600
+ and item.get("subject") == normalized_subject
601
+ and item.get("status") == "OK"
602
+ ),
603
+ None,
604
+ )
605
+ if prior_success is not None:
606
+ if prior_success.get("resultDigest") == result_digest:
607
+ sequence_receipt = record_sequence_receipt(
608
+ change_dir,
609
+ "sequence:unit-test-full",
610
+ normalized_subject,
611
+ evidence={
612
+ "fullExecutionId": prior_success.get("fullExecutionId"),
613
+ "resultDigest": result_digest,
614
+ "assertionBearing": True,
615
+ },
616
+ )
617
+ return {
618
+ "ok": True,
619
+ "code": "FULL_EXECUTION_REUSED",
620
+ "execution": prior_success,
621
+ "reused": True,
622
+ "sequenceReceipt": sequence_receipt,
623
+ }
624
+ return {
625
+ "ok": False,
626
+ "code": "FULL_ALREADY_COMPLETED",
627
+ "message": (
628
+ "a successful assertion-bearing Full already exists for this identity"
629
+ ),
630
+ "existingExecutionId": prior_success.get("fullExecutionId"),
631
+ }
632
+ prior_failed_ids = {
633
+ str(item.get("fullExecutionId") or "")
634
+ for item in journal["fullExecutions"]
635
+ if isinstance(item, dict)
636
+ and item.get("subject") == normalized_subject
637
+ and item.get("status") == "FAIL"
638
+ }
639
+ if prior_failed_ids and supersedes not in prior_failed_ids:
640
+ return {
641
+ "ok": False,
642
+ "code": "FULL_SUPERSESSION_REQUIRED",
643
+ "message": "Full rerun after assertion failure must name supersedes",
644
+ "priorFailedExecutionIds": sorted(prior_failed_ids),
645
+ }
646
+ execution = {
647
+ "fullExecutionId": "full-" + uuid.uuid4().hex,
648
+ "status": normalized_status,
649
+ "subject": normalized_subject,
650
+ "environmentAttemptIds": [
651
+ successful_environment[stage]["attemptId"]
652
+ for stage in ("prepare", "verify")
653
+ ],
654
+ "prepareMs": prepare_ms,
655
+ "testExecutionMs": test_ms,
656
+ "cleanupMs": cleanup_ms,
657
+ "totalMs": prepare_ms + test_ms + cleanup_ms,
658
+ "resultDigest": result_digest,
659
+ "supersedes": supersedes,
660
+ "assertionBearing": True,
661
+ "recordedAt": he.now_iso(),
662
+ }
663
+ journal["fullExecutions"].append(execution)
664
+ _write_json(change_dir / ENVIRONMENT_EXECUTIONS_REL, journal)
665
+ sequence_receipt = None
666
+ if normalized_status == "OK":
667
+ sequence_receipt = record_sequence_receipt(
668
+ change_dir,
669
+ "sequence:unit-test-full",
670
+ normalized_subject,
671
+ evidence={
672
+ "fullExecutionId": execution["fullExecutionId"],
673
+ "resultDigest": result_digest,
674
+ "assertionBearing": True,
675
+ },
676
+ )
677
+ return {
678
+ "ok": normalized_status == "OK",
679
+ "code": (
680
+ "FULL_EXECUTION_RECORDED"
681
+ if normalized_status == "OK"
682
+ else "FULL_ASSERTIONS_FAILED"
683
+ ),
684
+ "execution": execution,
685
+ "reused": False,
686
+ "sequenceReceipt": sequence_receipt,
687
+ }
688
+
689
+
69
690
  def _stable_hex(*parts: Any, length: int) -> str:
70
691
  raw = "\x1f".join(str(part) for part in parts).encode("utf-8")
71
692
  return hashlib.sha256(raw).hexdigest()[:length]
@@ -667,9 +1288,26 @@ def collect_node_evidence(
667
1288
  ) -> dict[str, dict[str, Any]]:
668
1289
  result: dict[str, dict[str, Any]] = {}
669
1290
  validations = (ledger or {}).get("validations") or {}
1291
+ sequence_receipts = _load_sequence_receipts(change_dir).get("receipts") or {}
670
1292
  for node in dag.get("nodes") or []:
671
1293
  node_id = str(node.get("id") or "")
672
- if node.get("kind") == "validation" or node_id.startswith("validation:"):
1294
+ if node.get("kind") == "sequence" or node_id.startswith("sequence:"):
1295
+ receipt = (
1296
+ sequence_receipts.get(node_id)
1297
+ if isinstance(sequence_receipts, dict)
1298
+ else None
1299
+ )
1300
+ status = (
1301
+ str(receipt.get("status") or "").upper()
1302
+ if isinstance(receipt, dict)
1303
+ else ""
1304
+ )
1305
+ result[node_id] = {
1306
+ "reusable": status == "OK",
1307
+ "reason": f"sequence-status:{status or 'missing'}",
1308
+ "receipt": receipt,
1309
+ }
1310
+ elif node.get("kind") == "validation" or node_id.startswith("validation:"):
673
1311
  verification = node_id.split(":", 1)[1]
674
1312
  entry = validations.get(verification) if isinstance(validations, dict) else None
675
1313
  if not isinstance(entry, dict):
@@ -895,6 +1533,78 @@ def _cmd_metrics(args: argparse.Namespace) -> int:
895
1533
  return 0
896
1534
 
897
1535
 
1536
+ def _cmd_record_sequence(args: argparse.Namespace) -> int:
1537
+ result = record_sequence_receipt(
1538
+ Path(args.change_dir),
1539
+ args.node,
1540
+ {
1541
+ "productCommit": args.product_commit,
1542
+ "productTreeHash": args.product_tree_hash,
1543
+ "environmentHash": args.environment_hash,
1544
+ },
1545
+ status=args.status,
1546
+ )
1547
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1548
+ return 0 if result.get("ok") else 2
1549
+
1550
+
1551
+ def _cmd_sequence_status(args: argparse.Namespace) -> int:
1552
+ result = evaluate_final_sequence(
1553
+ Path(args.change_dir),
1554
+ {
1555
+ "productCommit": args.product_commit,
1556
+ "productTreeHash": args.product_tree_hash,
1557
+ "environmentHash": args.environment_hash,
1558
+ },
1559
+ exclude_nodes={"sequence:archive"} if args.pre_archive else set(),
1560
+ )
1561
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1562
+ return 0 if result.get("ok") else 2
1563
+
1564
+
1565
+ def _subject_from_args(args: argparse.Namespace) -> dict[str, str]:
1566
+ return {
1567
+ "productCommit": args.product_commit,
1568
+ "productTreeHash": args.product_tree_hash,
1569
+ "environmentHash": args.environment_hash,
1570
+ }
1571
+
1572
+
1573
+ def _cmd_record_environment(args: argparse.Namespace) -> int:
1574
+ result = record_environment_attempt(
1575
+ Path(args.change_dir),
1576
+ _subject_from_args(args),
1577
+ stage=args.stage,
1578
+ status=args.status,
1579
+ duration_ms=args.duration_ms,
1580
+ failure_signature=args.failure_signature,
1581
+ )
1582
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1583
+ return 0 if result.get("ok") else 2
1584
+
1585
+
1586
+ def _cmd_record_full(args: argparse.Namespace) -> int:
1587
+ result = record_full_test_execution(
1588
+ Path(args.change_dir),
1589
+ _subject_from_args(args),
1590
+ status=args.status,
1591
+ prepare_ms=args.prepare_ms,
1592
+ test_ms=args.test_ms,
1593
+ cleanup_ms=args.cleanup_ms,
1594
+ result_digest=args.result_digest,
1595
+ supersedes=args.supersedes,
1596
+ )
1597
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1598
+ return 0 if result.get("ok") else 2
1599
+
1600
+
1601
+ def _add_subject_arguments(parser: argparse.ArgumentParser) -> None:
1602
+ parser.add_argument("--change-dir", required=True)
1603
+ parser.add_argument("--product-commit", required=True)
1604
+ parser.add_argument("--product-tree-hash", required=True)
1605
+ parser.add_argument("--environment-hash", required=True)
1606
+
1607
+
898
1608
  def build_parser() -> argparse.ArgumentParser:
899
1609
  parser = argparse.ArgumentParser(prog="harness_phase.py")
900
1610
  sub = parser.add_subparsers(dest="command", required=True)
@@ -914,6 +1624,53 @@ def build_parser() -> argparse.ArgumentParser:
914
1624
  metrics_parser.add_argument("--head-sha")
915
1625
  metrics_parser.add_argument("--output")
916
1626
  metrics_parser.set_defaults(func=_cmd_metrics)
1627
+
1628
+ record_parser = sub.add_parser("record-sequence")
1629
+ record_parser.add_argument("--change-dir", required=True)
1630
+ record_parser.add_argument("--node", required=True)
1631
+ record_parser.add_argument("--product-commit", required=True)
1632
+ record_parser.add_argument("--product-tree-hash", required=True)
1633
+ record_parser.add_argument("--environment-hash", required=True)
1634
+ record_parser.add_argument(
1635
+ "--status",
1636
+ choices=("OK", "WARN", "FAIL"),
1637
+ default="OK",
1638
+ )
1639
+ record_parser.set_defaults(func=_cmd_record_sequence)
1640
+
1641
+ sequence_parser = sub.add_parser("sequence-status")
1642
+ sequence_parser.add_argument("--change-dir", required=True)
1643
+ sequence_parser.add_argument("--product-commit", required=True)
1644
+ sequence_parser.add_argument("--product-tree-hash", required=True)
1645
+ sequence_parser.add_argument("--environment-hash", required=True)
1646
+ sequence_parser.add_argument("--pre-archive", action="store_true")
1647
+ sequence_parser.set_defaults(func=_cmd_sequence_status)
1648
+
1649
+ environment_parser = sub.add_parser("record-environment")
1650
+ _add_subject_arguments(environment_parser)
1651
+ environment_parser.add_argument(
1652
+ "--stage",
1653
+ choices=("prepare", "verify", "release"),
1654
+ required=True,
1655
+ )
1656
+ environment_parser.add_argument(
1657
+ "--status",
1658
+ choices=("OK", "ENVIRONMENT_ERROR", "INTERRUPTED", "FAIL"),
1659
+ required=True,
1660
+ )
1661
+ environment_parser.add_argument("--duration-ms", type=int, required=True)
1662
+ environment_parser.add_argument("--failure-signature")
1663
+ environment_parser.set_defaults(func=_cmd_record_environment)
1664
+
1665
+ full_parser = sub.add_parser("record-full")
1666
+ _add_subject_arguments(full_parser)
1667
+ full_parser.add_argument("--status", choices=("OK", "FAIL"), required=True)
1668
+ full_parser.add_argument("--prepare-ms", type=int, required=True)
1669
+ full_parser.add_argument("--test-ms", type=int, required=True)
1670
+ full_parser.add_argument("--cleanup-ms", type=int, required=True)
1671
+ full_parser.add_argument("--result-digest", required=True)
1672
+ full_parser.add_argument("--supersedes")
1673
+ full_parser.set_defaults(func=_cmd_record_full)
917
1674
  return parser
918
1675
 
919
1676