agent-lifecycle-kit 1.50.1__py3-none-any.whl

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 (227) hide show
  1. agent_lifecycle/__init__.py +5 -0
  2. agent_lifecycle/__main__.py +5 -0
  3. agent_lifecycle/_version.py +3 -0
  4. agent_lifecycle/adapter_sessions/__init__.py +41 -0
  5. agent_lifecycle/adapter_sessions/contracts.py +133 -0
  6. agent_lifecycle/adapter_sessions/env.py +44 -0
  7. agent_lifecycle/adapter_sessions/launcher.py +101 -0
  8. agent_lifecycle/adapter_sessions/process.py +39 -0
  9. agent_lifecycle/adapter_sessions/redaction.py +30 -0
  10. agent_lifecycle/adapter_sessions/session_store.py +64 -0
  11. agent_lifecycle/adapter_sessions/task_intake.py +450 -0
  12. agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
  13. agent_lifecycle/audit/__init__.py +67 -0
  14. agent_lifecycle/audit/bug_forensics.py +94 -0
  15. agent_lifecycle/audit/implementation.py +516 -0
  16. agent_lifecycle/audit/ownership.py +119 -0
  17. agent_lifecycle/audit/proof_integrity.py +814 -0
  18. agent_lifecycle/audit/review_verdict.py +116 -0
  19. agent_lifecycle/changesets/__init__.py +5 -0
  20. agent_lifecycle/changesets/git.py +35 -0
  21. agent_lifecycle/cli/__init__.py +5 -0
  22. agent_lifecycle/cli/adapter.py +360 -0
  23. agent_lifecycle/cli/dispatch.py +1220 -0
  24. agent_lifecycle/cli/followup.py +89 -0
  25. agent_lifecycle/cli/main.py +41 -0
  26. agent_lifecycle/cli/metrics_parser.py +49 -0
  27. agent_lifecycle/cli/parsers.py +618 -0
  28. agent_lifecycle/cli/policy.py +101 -0
  29. agent_lifecycle/cli/progress_hooks.py +117 -0
  30. agent_lifecycle/cli/worktree.py +74 -0
  31. agent_lifecycle/compiler/__init__.py +17 -0
  32. agent_lifecycle/compiler/small_model_packets.py +322 -0
  33. agent_lifecycle/compiler/task_packets.py +229 -0
  34. agent_lifecycle/context/__init__.py +20 -0
  35. agent_lifecycle/context/episode_retrieval.py +30 -0
  36. agent_lifecycle/context/external_memory.py +71 -0
  37. agent_lifecycle/context/profiles.py +104 -0
  38. agent_lifecycle/context/rendering.py +226 -0
  39. agent_lifecycle/contracts/__init__.py +25 -0
  40. agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
  41. agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
  42. agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
  43. agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
  44. agent_lifecycle/contracts/audit_schemas.py +133 -0
  45. agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
  46. agent_lifecycle/contracts/canonical.py +55 -0
  47. agent_lifecycle/contracts/compatibility.py +242 -0
  48. agent_lifecycle/contracts/context_model_schemas.py +265 -0
  49. agent_lifecycle/contracts/core_schemas.py +252 -0
  50. agent_lifecycle/contracts/cross_check_schemas.py +121 -0
  51. agent_lifecycle/contracts/errors.py +26 -0
  52. agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
  53. agent_lifecycle/contracts/host_capability_schemas.py +64 -0
  54. agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
  55. agent_lifecycle/contracts/metric_schemas.py +315 -0
  56. agent_lifecycle/contracts/paths.py +31 -0
  57. agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
  58. agent_lifecycle/contracts/policy_schemas.py +350 -0
  59. agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
  60. agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
  61. agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
  62. agent_lifecycle/contracts/release_contract_schemas.py +476 -0
  63. agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
  64. agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
  65. agent_lifecycle/contracts/review_quality_schemas.py +109 -0
  66. agent_lifecycle/contracts/runner_schemas.py +91 -0
  67. agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
  68. agent_lifecycle/contracts/sandbox_schemas.py +159 -0
  69. agent_lifecycle/contracts/schema_builders.py +23 -0
  70. agent_lifecycle/contracts/schemas.py +93 -0
  71. agent_lifecycle/contracts/status_goal_schemas.py +469 -0
  72. agent_lifecycle/contracts/task_template_schemas.py +96 -0
  73. agent_lifecycle/contracts/usage_export_schemas.py +68 -0
  74. agent_lifecycle/diagnostics/__init__.py +17 -0
  75. agent_lifecycle/diagnostics/bundles.py +124 -0
  76. agent_lifecycle/diagnostics/readiness.py +733 -0
  77. agent_lifecycle/evidence_index/__init__.py +39 -0
  78. agent_lifecycle/evidence_index/core.py +272 -0
  79. agent_lifecycle/evidence_index/episode_index.py +329 -0
  80. agent_lifecycle/evidence_index/external_context.py +276 -0
  81. agent_lifecycle/followup/__init__.py +23 -0
  82. agent_lifecycle/followup/records.py +404 -0
  83. agent_lifecycle/freeze/__init__.py +5 -0
  84. agent_lifecycle/freeze/locks.py +23 -0
  85. agent_lifecycle/goal/__init__.py +15 -0
  86. agent_lifecycle/goal/records.py +244 -0
  87. agent_lifecycle/goal/view.py +109 -0
  88. agent_lifecycle/host_protocol/__init__.py +65 -0
  89. agent_lifecycle/host_protocol/acp_capability.py +195 -0
  90. agent_lifecycle/host_protocol/capabilities.py +268 -0
  91. agent_lifecycle/host_protocol/contracts.py +214 -0
  92. agent_lifecycle/host_protocol/event_capture.py +390 -0
  93. agent_lifecycle/host_protocol/events.py +196 -0
  94. agent_lifecycle/host_protocol/inspection.py +194 -0
  95. agent_lifecycle/host_protocol/inspection_common.py +310 -0
  96. agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
  97. agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
  98. agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
  99. agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
  100. agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
  101. agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
  102. agent_lifecycle/host_protocol/receipts.py +58 -0
  103. agent_lifecycle/host_protocol/scaffold.py +329 -0
  104. agent_lifecycle/host_protocol/validation.py +229 -0
  105. agent_lifecycle/imports/__init__.py +76 -0
  106. agent_lifecycle/imports/agentskills_profile.py +54 -0
  107. agent_lifecycle/imports/bmad_profile.py +49 -0
  108. agent_lifecycle/imports/constitution_adr.py +91 -0
  109. agent_lifecycle/imports/dialect_profiles.py +121 -0
  110. agent_lifecycle/imports/external_agent.py +28 -0
  111. agent_lifecycle/imports/external_dialects.py +486 -0
  112. agent_lifecycle/imports/external_workflow.py +28 -0
  113. agent_lifecycle/imports/multi_markdown.py +111 -0
  114. agent_lifecycle/imports/openspec_profile.py +49 -0
  115. agent_lifecycle/imports/planning.py +536 -0
  116. agent_lifecycle/imports/spec_kit_profile.py +49 -0
  117. agent_lifecycle/imports/spec_kitty_profile.py +49 -0
  118. agent_lifecycle/metrics/__init__.py +60 -0
  119. agent_lifecycle/metrics/cost_collection.py +315 -0
  120. agent_lifecycle/metrics/costs.py +203 -0
  121. agent_lifecycle/metrics/outcome_index.py +330 -0
  122. agent_lifecycle/metrics/phase_resources.py +208 -0
  123. agent_lifecycle/metrics/recommendations.py +507 -0
  124. agent_lifecycle/metrics/regression_signals.py +64 -0
  125. agent_lifecycle/metrics/usage_export.py +390 -0
  126. agent_lifecycle/model_routing/__init__.py +19 -0
  127. agent_lifecycle/model_routing/profiles.py +323 -0
  128. agent_lifecycle/model_routing/receipts.py +140 -0
  129. agent_lifecycle/model_routing/resolver.py +566 -0
  130. agent_lifecycle/neutrality/__init__.py +12 -0
  131. agent_lifecycle/neutrality/__main__.py +6 -0
  132. agent_lifecycle/neutrality/authority.py +265 -0
  133. agent_lifecycle/neutrality/canonical.py +28 -0
  134. agent_lifecycle/neutrality/cli.py +294 -0
  135. agent_lifecycle/neutrality/ed25519.py +124 -0
  136. agent_lifecycle/neutrality/errors.py +89 -0
  137. agent_lifecycle/neutrality/gate.py +269 -0
  138. agent_lifecycle/neutrality/paths.py +92 -0
  139. agent_lifecycle/neutrality/policy.py +76 -0
  140. agent_lifecycle/neutrality/receipt.py +135 -0
  141. agent_lifecycle/neutrality/scanner.py +310 -0
  142. agent_lifecycle/neutrality/test_runner.py +116 -0
  143. agent_lifecycle/planning/__init__.py +47 -0
  144. agent_lifecycle/planning/acceptance_markdown.py +159 -0
  145. agent_lifecycle/planning/completeness.py +362 -0
  146. agent_lifecycle/planning/continuity.py +255 -0
  147. agent_lifecycle/planning/templates.py +294 -0
  148. agent_lifecycle/planning/tiering.py +87 -0
  149. agent_lifecycle/planning/validation.py +72 -0
  150. agent_lifecycle/policy/__init__.py +37 -0
  151. agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
  152. agent_lifecycle/policy/apply.py +44 -0
  153. agent_lifecycle/policy/proposals.py +189 -0
  154. agent_lifecycle/policy/quality_floor.py +144 -0
  155. agent_lifecycle/policy/runtime_receipts.py +160 -0
  156. agent_lifecycle/py.typed +1 -0
  157. agent_lifecycle/quality/__init__.py +89 -0
  158. agent_lifecycle/quality/bug_forensics.py +727 -0
  159. agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
  160. agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
  161. agent_lifecycle/quality/cross_check.py +437 -0
  162. agent_lifecycle/quality/failure_classification.py +318 -0
  163. agent_lifecycle/quality/packs.py +265 -0
  164. agent_lifecycle/reporting/__init__.py +37 -0
  165. agent_lifecycle/reporting/change_summary.py +156 -0
  166. agent_lifecycle/reporting/event_feed.py +101 -0
  167. agent_lifecycle/reporting/progress_bridge.py +153 -0
  168. agent_lifecycle/reporting/progress_hooks.py +161 -0
  169. agent_lifecycle/reporting/progress_terminal.py +95 -0
  170. agent_lifecycle/reporting/progress_view.py +284 -0
  171. agent_lifecycle/reporting/status_view.py +125 -0
  172. agent_lifecycle/reporting/usage_export.py +59 -0
  173. agent_lifecycle/review/__init__.py +5 -0
  174. agent_lifecycle/review/validation.py +29 -0
  175. agent_lifecycle/review_mesh/__init__.py +83 -0
  176. agent_lifecycle/review_mesh/assignments.py +160 -0
  177. agent_lifecycle/review_mesh/contracts.py +685 -0
  178. agent_lifecycle/review_mesh/operator_templates.py +320 -0
  179. agent_lifecycle/review_mesh/quorum.py +38 -0
  180. agent_lifecycle/review_mesh/recommendation.py +487 -0
  181. agent_lifecycle/review_mesh/results.py +98 -0
  182. agent_lifecycle/review_mesh/synthesis.py +102 -0
  183. agent_lifecycle/runner/__init__.py +47 -0
  184. agent_lifecycle/runner/attempt_snapshots.py +244 -0
  185. agent_lifecycle/runner/core.py +653 -0
  186. agent_lifecycle/runner/sandbox_receipts.py +628 -0
  187. agent_lifecycle/specification/__init__.py +23 -0
  188. agent_lifecycle/specification/completion_check.py +135 -0
  189. agent_lifecycle/specification/completion_gate.py +501 -0
  190. agent_lifecycle/specification/completion_signal.py +75 -0
  191. agent_lifecycle/specification/validation.py +48 -0
  192. agent_lifecycle/workflow/__init__.py +79 -0
  193. agent_lifecycle/workflow/artifacts.py +74 -0
  194. agent_lifecycle/workflow/budget_decisions.py +370 -0
  195. agent_lifecycle/workflow/budget_policy.py +140 -0
  196. agent_lifecycle/workflow/budget_receipts.py +70 -0
  197. agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
  198. agent_lifecycle/workflow/controller.py +45 -0
  199. agent_lifecycle/workflow/events.py +50 -0
  200. agent_lifecycle/workflow/final_proof_integrity.py +74 -0
  201. agent_lifecycle/workflow/finalization.py +422 -0
  202. agent_lifecycle/workflow/gates.py +185 -0
  203. agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
  204. agent_lifecycle/workflow/leases.py +220 -0
  205. agent_lifecycle/workflow/lineage.py +108 -0
  206. agent_lifecycle/workflow/managed_runner.py +167 -0
  207. agent_lifecycle/workflow/model_usage.py +106 -0
  208. agent_lifecycle/workflow/next_action.py +86 -0
  209. agent_lifecycle/workflow/operation_kernel.py +98 -0
  210. agent_lifecycle/workflow/plan_adoption.py +439 -0
  211. agent_lifecycle/workflow/query.py +52 -0
  212. agent_lifecycle/workflow/review_mesh_gate.py +93 -0
  213. agent_lifecycle/workflow/reviews.py +168 -0
  214. agent_lifecycle/workflow/run_transitions.py +163 -0
  215. agent_lifecycle/workflow/sandbox_policy.py +214 -0
  216. agent_lifecycle/workflow/selectors.py +57 -0
  217. agent_lifecycle/workflow/state.py +130 -0
  218. agent_lifecycle/workflow/task_transitions.py +421 -0
  219. agent_lifecycle/worktree/__init__.py +17 -0
  220. agent_lifecycle/worktree/isolation.py +407 -0
  221. agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
  222. agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
  223. agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
  224. agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
  225. agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
  226. agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
  227. agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,135 @@
1
+ """Observable completion contract validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+ from agent_lifecycle.contracts.paths import normalize_repo_path
9
+
10
+ CHECK_KINDS = {"verification", "external-action"}
11
+
12
+
13
+ def validate_completion_check(check: dict[str, Any]) -> dict[str, Any]:
14
+ """Validate an optional specification-level completion check."""
15
+
16
+ if not isinstance(check, dict):
17
+ raise LifecycleError("invalid-completion-check", "completionCheck must be an object")
18
+ if check.get("schemaVersion") != "agent-completion-check.v1":
19
+ raise LifecycleError("invalid-completion-check", "completionCheck schemaVersion is unsupported")
20
+ check_id = _required_string(check.get("checkId"), label="completionCheck.checkId")
21
+ kind = check.get("kind")
22
+ if kind not in CHECK_KINDS:
23
+ raise LifecycleError("invalid-completion-check", "completionCheck kind is unsupported")
24
+ _required_string(check.get("description"), label="completionCheck.description")
25
+ receipt_path = normalize_repo_path(check.get("receiptPath"), label="completion check receipt")
26
+ evidence_ids = _string_list(
27
+ check.get("requiredEvidenceIds"),
28
+ label="completionCheck.requiredEvidenceIds",
29
+ error_code="invalid-completion-check",
30
+ )
31
+ if not evidence_ids:
32
+ raise LifecycleError("invalid-completion-check", "completionCheck requiredEvidenceIds are required")
33
+ return {
34
+ "schemaVersion": "agent-completion-check-validation.v1",
35
+ "checkId": check_id,
36
+ "kind": kind,
37
+ "receiptPath": receipt_path,
38
+ "requiredEvidenceIds": evidence_ids,
39
+ "checkDigest": canonical_digest(check),
40
+ }
41
+
42
+
43
+ def validate_completion_check_receipt(
44
+ receipt: dict[str, Any],
45
+ *,
46
+ check: dict[str, Any],
47
+ state: dict[str, Any],
48
+ ) -> dict[str, Any]:
49
+ """Validate a finalization receipt against the declared completion check."""
50
+
51
+ check_validation = validate_completion_check(check)
52
+ if not isinstance(receipt, dict):
53
+ raise LifecycleError("completion-check-receipt-required", "completion check receipt must be an object")
54
+ if receipt.get("schemaVersion") != "agent-completion-check-receipt.v1":
55
+ raise LifecycleError("invalid-completion-check-receipt", "completion check receipt schemaVersion is unsupported")
56
+ if receipt.get("checkId") != check_validation["checkId"]:
57
+ raise LifecycleError("completion-check-receipt-mismatch", "completion check receipt checkId mismatch")
58
+ status = receipt.get("status")
59
+ if status == "FAIL":
60
+ raise LifecycleError("completion-check-not-satisfied", "completion check receipt reports FAIL")
61
+ if status != "PASS":
62
+ raise LifecycleError("invalid-completion-check-receipt", "completion check receipt status is unsupported")
63
+ _require_lineage(receipt, state)
64
+ evidence_ids = _string_list(
65
+ receipt.get("evidenceIds"),
66
+ label="completionCheckReceipt.evidenceIds",
67
+ error_code="invalid-completion-check-receipt",
68
+ )
69
+ missing_evidence = sorted(set(check_validation["requiredEvidenceIds"]) - set(evidence_ids))
70
+ if missing_evidence:
71
+ raise LifecycleError(
72
+ "completion-check-evidence-missing",
73
+ "completion check receipt is missing required evidence",
74
+ {"evidenceIds": missing_evidence},
75
+ )
76
+ verifier = receipt.get("verifier")
77
+ if not isinstance(verifier, dict) or not isinstance(verifier.get("id"), str) or not verifier["id"]:
78
+ raise LifecycleError("invalid-completion-check-receipt", "completion check receipt verifier.id is required")
79
+ if check_validation["kind"] == "external-action":
80
+ _validate_external_action_binding(receipt, state)
81
+ return {
82
+ "schemaVersion": "agent-completion-check-receipt-validation.v1",
83
+ "status": "PASS",
84
+ "checkId": check_validation["checkId"],
85
+ "checkKind": check_validation["kind"],
86
+ "evidenceIds": evidence_ids,
87
+ "receiptDigest": canonical_digest(receipt),
88
+ "checkDigest": check_validation["checkDigest"],
89
+ }
90
+
91
+
92
+ def _require_lineage(receipt: dict[str, Any], state: dict[str, Any]) -> None:
93
+ expected = {
94
+ "runId": state.get("runId"),
95
+ "packageId": state.get("packageId"),
96
+ "planRevision": state.get("planRevision"),
97
+ "planDigest": state.get("planDigest"),
98
+ "sourceRevision": state.get("sourceRevision"),
99
+ }
100
+ for key, value in expected.items():
101
+ if receipt.get(key) != value:
102
+ raise LifecycleError("completion-check-lineage-mismatch", f"completion check receipt {key} mismatch")
103
+
104
+
105
+ def _validate_external_action_binding(receipt: dict[str, Any], state: dict[str, Any]) -> None:
106
+ expected = state.get("externalActionReceipt")
107
+ if not isinstance(expected, dict):
108
+ raise LifecycleError(
109
+ "completion-check-external-action-missing",
110
+ "external-action completion check requires an externalActionReceipt in workflow state",
111
+ )
112
+ actual = receipt.get("externalActionReceipt")
113
+ if not isinstance(actual, dict):
114
+ raise LifecycleError(
115
+ "completion-check-external-action-missing",
116
+ "completion check receipt must bind externalActionReceipt",
117
+ )
118
+ for key in ("path", "sha256", "bytes"):
119
+ if actual.get(key) != expected.get(key):
120
+ raise LifecycleError(
121
+ "completion-check-external-action-mismatch",
122
+ f"completion check externalActionReceipt {key} mismatch",
123
+ )
124
+
125
+
126
+ def _required_string(value: Any, *, label: str) -> str:
127
+ if not isinstance(value, str) or not value:
128
+ raise LifecycleError("invalid-completion-check", f"{label} is required")
129
+ return value
130
+
131
+
132
+ def _string_list(value: Any, *, label: str, error_code: str) -> list[str]:
133
+ if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
134
+ raise LifecycleError(error_code, f"{label} must be a list of non-empty strings")
135
+ return value
@@ -0,0 +1,501 @@
1
+ """Deterministic completion gate decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+ from agent_lifecycle.followup import validate_followup_register
9
+
10
+ COMPLETION_GATE_RECEIPT_SCHEMA = "agent-completion-gate-receipt.v1"
11
+ COMPLETION_GATE_VALIDATION_SCHEMA = "agent-completion-gate-validation.v1"
12
+
13
+ COMPLETION_GATE_DECISIONS = {"STOP", "CONTINUE", "ESCALATE", "SPLIT", "FOLLOW_UP"}
14
+ FINALIZATION_DECISIONS = {"STOP", "FOLLOW_UP"}
15
+
16
+
17
+ def build_completion_gate_receipt(
18
+ *,
19
+ state: dict[str, Any],
20
+ final_audit: dict[str, Any] | None = None,
21
+ validation_results: list[dict[str, Any]] | None = None,
22
+ required_validation_ids: list[str] | None = None,
23
+ follow_up_register: dict[str, Any] | None = None,
24
+ follow_up_candidates: list[dict[str, Any]] | None = None,
25
+ regression_signals: list[dict[str, Any]] | None = None,
26
+ risk_flags: list[dict[str, Any]] | None = None,
27
+ split_candidates: list[dict[str, Any]] | None = None,
28
+ verifier_id: str = "completion-gate",
29
+ ) -> dict[str, Any]:
30
+ """Build a receipt deciding whether the current work should stop or continue."""
31
+
32
+ _require_state_lineage(state)
33
+ verifier = _required_string(verifier_id, label="verifierId", error_code="invalid-completion-gate-input")
34
+ validations = _validation_summary(validation_results or [], required_validation_ids or [])
35
+ acceptance = _acceptance_summary(state)
36
+ blocker = _blocker_summary(state)
37
+ final_proof = _final_proof_summary(final_audit)
38
+ follow_up = _follow_up_summary(follow_up_register, follow_up_candidates or [])
39
+ regression = _signal_summary(regression_signals or [], blocker_statuses={"FAIL", "BLOCKING"})
40
+ risk = _signal_summary(risk_flags or [], blocker_statuses={"HIGH", "CRITICAL", "SECURITY", "RELEASE"})
41
+ split = _split_summary(split_candidates or [])
42
+ decision, reason_codes, blockers = _decide(
43
+ acceptance=acceptance,
44
+ blocker=blocker,
45
+ validations=validations,
46
+ final_proof=final_proof,
47
+ follow_up=follow_up,
48
+ regression=regression,
49
+ risk=risk,
50
+ split=split,
51
+ )
52
+ input_digests = {
53
+ "stateDigest": canonical_digest(_state_binding(state)),
54
+ "finalAuditDigest": canonical_digest(final_audit) if final_audit is not None else None,
55
+ "followUpRegisterDigest": canonical_digest(follow_up_register) if follow_up_register is not None else None,
56
+ "validationResultsDigest": canonical_digest(validation_results or []),
57
+ "followUpCandidatesDigest": canonical_digest(follow_up_candidates or []),
58
+ "regressionSignalsDigest": canonical_digest(regression_signals or []),
59
+ "riskFlagsDigest": canonical_digest(risk_flags or []),
60
+ "splitCandidatesDigest": canonical_digest(split_candidates or []),
61
+ }
62
+ body = {
63
+ "schemaVersion": COMPLETION_GATE_RECEIPT_SCHEMA,
64
+ "status": "PASS",
65
+ "runId": state.get("runId"),
66
+ "packageId": state.get("packageId"),
67
+ "planRevision": state.get("planRevision"),
68
+ "planDigest": state.get("planDigest"),
69
+ "sourceRevision": state.get("sourceRevision"),
70
+ "stateRevision": state.get("stateRevision"),
71
+ "decision": decision,
72
+ "reasonCodes": reason_codes,
73
+ "blockers": blockers,
74
+ "acceptance": acceptance,
75
+ "validation": validations,
76
+ "finalProof": final_proof,
77
+ "followUp": follow_up,
78
+ "regression": regression,
79
+ "risk": risk,
80
+ "split": split,
81
+ "inputDigests": input_digests,
82
+ "verifier": {"id": verifier},
83
+ "productionPromotionClaimed": False,
84
+ }
85
+ return {**body, "gateDigest": canonical_digest(body)}
86
+
87
+
88
+ def validate_completion_gate_receipt(
89
+ receipt: dict[str, Any],
90
+ *,
91
+ state: dict[str, Any] | None = None,
92
+ final_audit: dict[str, Any] | None = None,
93
+ follow_up_register: dict[str, Any] | None = None,
94
+ ) -> dict[str, Any]:
95
+ """Validate a completion gate receipt and optional current input bindings."""
96
+
97
+ if not isinstance(receipt, dict):
98
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate receipt must be an object")
99
+ if receipt.get("schemaVersion") != COMPLETION_GATE_RECEIPT_SCHEMA:
100
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate receipt schemaVersion is unsupported")
101
+ if receipt.get("status") != "PASS":
102
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate receipt status must be PASS")
103
+ if receipt.get("productionPromotionClaimed") is not False:
104
+ raise LifecycleError("completion-gate-production-claim", "completion gate must not claim production promotion")
105
+ decision = receipt.get("decision")
106
+ if decision not in COMPLETION_GATE_DECISIONS:
107
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate decision is unsupported")
108
+ reason_codes = _string_list(receipt.get("reasonCodes"), label="reasonCodes", error_code="invalid-completion-gate-receipt")
109
+ if not reason_codes:
110
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate reasonCodes are required")
111
+ _require_verifier(receipt.get("verifier"))
112
+ _validate_gate_digest(receipt)
113
+ input_checks = _validate_input_bindings(receipt, state=state, final_audit=final_audit, follow_up_register=follow_up_register)
114
+ finalization_allowed = decision in FINALIZATION_DECISIONS
115
+ final_proof = receipt.get("finalProof")
116
+ if decision in FINALIZATION_DECISIONS and not _receipt_final_proof_ready(final_proof):
117
+ raise LifecycleError("completion-gate-final-proof-missing", "stop/follow-up decisions require ready final proof evidence")
118
+ if decision in FINALIZATION_DECISIONS:
119
+ _require_no_finalization_blockers(receipt, state=state, final_audit=final_audit, follow_up_register=follow_up_register)
120
+ validation = {
121
+ "schemaVersion": COMPLETION_GATE_VALIDATION_SCHEMA,
122
+ "status": "PASS",
123
+ "decision": decision,
124
+ "reasonCodes": reason_codes,
125
+ "finalizationAllowed": finalization_allowed,
126
+ "inputChecks": input_checks,
127
+ "receiptDigest": canonical_digest(receipt),
128
+ }
129
+ return {**validation, "validationDigest": canonical_digest(validation)}
130
+
131
+
132
+ def require_completion_gate_finalization(
133
+ receipt: dict[str, Any],
134
+ *,
135
+ state: dict[str, Any],
136
+ final_audit: dict[str, Any],
137
+ follow_up_register: dict[str, Any] | None = None,
138
+ ) -> dict[str, Any]:
139
+ """Require a completion gate receipt that allows finalization."""
140
+
141
+ validation = validate_completion_gate_receipt(
142
+ receipt,
143
+ state=state,
144
+ final_audit=final_audit,
145
+ follow_up_register=follow_up_register,
146
+ )
147
+ if validation["decision"] not in FINALIZATION_DECISIONS:
148
+ raise LifecycleError(
149
+ "completion-gate-not-ready",
150
+ "completion gate does not allow finalization",
151
+ {"decision": validation["decision"], "reasonCodes": validation["reasonCodes"]},
152
+ )
153
+ return validation
154
+
155
+
156
+ def _decide(
157
+ *,
158
+ acceptance: dict[str, Any],
159
+ blocker: dict[str, Any],
160
+ validations: dict[str, Any],
161
+ final_proof: dict[str, Any],
162
+ follow_up: dict[str, Any],
163
+ regression: dict[str, Any],
164
+ risk: dict[str, Any],
165
+ split: dict[str, Any],
166
+ ) -> tuple[str, list[str], list[dict[str, Any]]]:
167
+ blockers: list[dict[str, Any]] = []
168
+ reason_codes: list[str] = []
169
+ if acceptance["missingTaskIds"]:
170
+ blockers.append({"code": "required-acceptance-missing", "taskIds": acceptance["missingTaskIds"]})
171
+ reason_codes.append("required-acceptance-missing")
172
+ if blocker["open"]:
173
+ blockers.append({"code": "open-workflow-blocker", "blocker": blocker["blocker"]})
174
+ reason_codes.append("open-workflow-blocker")
175
+ if validations["missingIds"]:
176
+ blockers.append({"code": "validation-missing", "validationIds": validations["missingIds"]})
177
+ reason_codes.append("validation-missing")
178
+ if validations["failedIds"]:
179
+ blockers.append({"code": "validation-failed", "validationIds": validations["failedIds"]})
180
+ reason_codes.append("validation-failed")
181
+ if follow_up["finalizationBlockerIds"]:
182
+ blockers.append({"code": "follow-up-blocks-finalization", "itemIds": follow_up["finalizationBlockerIds"]})
183
+ reason_codes.append("follow-up-blocks-finalization")
184
+ if regression["blockingIds"]:
185
+ blockers.append({"code": "regression-signal-blocking", "signalIds": regression["blockingIds"]})
186
+ reason_codes.append("regression-signal-blocking")
187
+ if risk["blockingIds"]:
188
+ blockers.append({"code": "risk-escalation-required", "flagIds": risk["blockingIds"]})
189
+ reason_codes.append("risk-escalation-required")
190
+ if split["candidateIds"]:
191
+ blockers.append({"code": "split-required", "candidateIds": split["candidateIds"]})
192
+ reason_codes.append("split-required")
193
+ if not final_proof["ready"]:
194
+ blockers.append({"code": "final-proof-not-ready", "status": final_proof["status"]})
195
+ reason_codes.append("final-proof-not-ready")
196
+
197
+ if split["candidateIds"]:
198
+ return "SPLIT", _dedupe(reason_codes), blockers
199
+ if blocker["open"] or risk["blockingIds"] or follow_up["finalizationBlockerIds"]:
200
+ return "ESCALATE", _dedupe(reason_codes), blockers
201
+ if blockers:
202
+ return "CONTINUE", _dedupe(reason_codes), blockers
203
+ if follow_up["nonBlockingItemIds"] or follow_up["candidateIds"]:
204
+ return "FOLLOW_UP", ["non-blocking-follow-up"], []
205
+ return "STOP", ["all-required-evidence-passed"], []
206
+
207
+
208
+ def _acceptance_summary(state: dict[str, Any]) -> dict[str, Any]:
209
+ required = []
210
+ missing = []
211
+ for task in state.get("tasks", []):
212
+ if not isinstance(task, dict) or not task.get("required", True):
213
+ continue
214
+ task_id = str(task.get("id", ""))
215
+ required.append(task_id)
216
+ if task.get("status") != "ACCEPTED":
217
+ missing.append(task_id)
218
+ return {
219
+ "requiredTaskIds": required,
220
+ "missingTaskIds": missing,
221
+ "accepted": not missing,
222
+ }
223
+
224
+
225
+ def _blocker_summary(state: dict[str, Any]) -> dict[str, Any]:
226
+ blocker = state.get("blocker")
227
+ return {"open": isinstance(blocker, dict) and bool(blocker), "blocker": blocker if isinstance(blocker, dict) else None}
228
+
229
+
230
+ def _validation_summary(results: list[dict[str, Any]], required_ids: list[str]) -> dict[str, Any]:
231
+ seen: dict[str, str] = {}
232
+ for index, item in enumerate(results):
233
+ if not isinstance(item, dict):
234
+ raise LifecycleError("invalid-completion-gate-input", "validationResults items must be objects", {"index": index})
235
+ result_id = _required_string(item.get("id"), label="validationResults.id", error_code="invalid-completion-gate-input")
236
+ status = _required_string(item.get("status"), label="validationResults.status", error_code="invalid-completion-gate-input")
237
+ seen[result_id] = status
238
+ required = _string_list(required_ids, label="requiredValidationIds", error_code="invalid-completion-gate-input", allow_empty=True)
239
+ failed = sorted(result_id for result_id, status in seen.items() if status not in {"PASS", "WAIVED"})
240
+ missing = sorted(set(required).difference(seen))
241
+ return {
242
+ "requiredIds": required,
243
+ "providedIds": sorted(seen),
244
+ "missingIds": missing,
245
+ "failedIds": failed,
246
+ }
247
+
248
+
249
+ def _final_proof_summary(final_audit: dict[str, Any] | None) -> dict[str, Any]:
250
+ if final_audit is None:
251
+ return {"required": True, "ready": False, "status": "missing", "finalAuditDigest": None}
252
+ status = final_audit.get("status")
253
+ semantic = final_audit.get("semanticStatus")
254
+ ready = (
255
+ status == "PASS"
256
+ and semantic == "READY_FOR_FINALIZATION"
257
+ and final_audit.get("productionPromotionClaimed") is False
258
+ and not final_audit.get("notAcceptedTasks")
259
+ and not final_audit.get("missingReleaseEvidence")
260
+ and not _open_blocking_findings(final_audit)
261
+ )
262
+ return {
263
+ "required": True,
264
+ "ready": ready,
265
+ "status": status or "unknown",
266
+ "semanticStatus": semantic,
267
+ "finalAuditDigest": canonical_digest(final_audit),
268
+ }
269
+
270
+
271
+ def _follow_up_summary(register: dict[str, Any] | None, candidates: list[dict[str, Any]]) -> dict[str, Any]:
272
+ finalization_blockers: list[str] = []
273
+ non_blocking_items: list[str] = []
274
+ if register is not None:
275
+ validation = validate_followup_register(register)
276
+ finalization_blockers = [str(item["id"]) for item in validation["finalizationBlockers"]]
277
+ blocking = set(finalization_blockers)
278
+ non_blocking_items = [
279
+ str(item.get("id"))
280
+ for item in register.get("items", [])
281
+ if isinstance(item, dict) and item.get("status") in {"OPEN", "BLOCKED", "SCHEDULED"} and item.get("id") not in blocking
282
+ ]
283
+ candidate_ids = []
284
+ for index, item in enumerate(candidates):
285
+ if not isinstance(item, dict):
286
+ raise LifecycleError("invalid-completion-gate-input", "followUpCandidates items must be objects", {"index": index})
287
+ candidate_ids.append(_required_string(item.get("id"), label="followUpCandidates.id", error_code="invalid-completion-gate-input"))
288
+ return {
289
+ "candidateIds": sorted(candidate_ids),
290
+ "nonBlockingItemIds": sorted(non_blocking_items),
291
+ "finalizationBlockerIds": sorted(finalization_blockers),
292
+ }
293
+
294
+
295
+ def _signal_summary(items: list[dict[str, Any]], *, blocker_statuses: set[str]) -> dict[str, Any]:
296
+ blocking = []
297
+ observed = []
298
+ normalized_blockers = {item.lower() for item in blocker_statuses}
299
+ for index, item in enumerate(items):
300
+ if not isinstance(item, dict):
301
+ raise LifecycleError("invalid-completion-gate-input", "signal items must be objects", {"index": index})
302
+ item_id = _required_string(item.get("id"), label="signal.id", error_code="invalid-completion-gate-input")
303
+ observed.append(item_id)
304
+ status = str(item.get("status") or item.get("severity") or "").lower()
305
+ if item.get("blocking") is True or status in normalized_blockers:
306
+ blocking.append(item_id)
307
+ return {"observedIds": sorted(observed), "blockingIds": sorted(blocking)}
308
+
309
+
310
+ def _split_summary(items: list[dict[str, Any]]) -> dict[str, Any]:
311
+ candidate_ids = []
312
+ for index, item in enumerate(items):
313
+ if not isinstance(item, dict):
314
+ raise LifecycleError("invalid-completion-gate-input", "splitCandidates items must be objects", {"index": index})
315
+ item_id = _required_string(item.get("id"), label="splitCandidates.id", error_code="invalid-completion-gate-input")
316
+ if item.get("required", True):
317
+ candidate_ids.append(item_id)
318
+ return {"candidateIds": sorted(candidate_ids)}
319
+
320
+
321
+ def _validate_input_bindings(
322
+ receipt: dict[str, Any],
323
+ *,
324
+ state: dict[str, Any] | None,
325
+ final_audit: dict[str, Any] | None,
326
+ follow_up_register: dict[str, Any] | None,
327
+ ) -> list[dict[str, Any]]:
328
+ checks: list[dict[str, Any]] = []
329
+ digests = receipt.get("inputDigests")
330
+ if not isinstance(digests, dict):
331
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate inputDigests are required")
332
+ for name, source in (
333
+ ("stateDigest", state),
334
+ ("finalAuditDigest", final_audit),
335
+ ("followUpRegisterDigest", follow_up_register),
336
+ ):
337
+ if source is None:
338
+ checks.append({"name": name, "status": "NOT_CHECKED"})
339
+ continue
340
+ expected = canonical_digest(_state_binding(source)) if name == "stateDigest" else canonical_digest(source)
341
+ if digests.get(name) != expected:
342
+ raise LifecycleError("completion-gate-input-mismatch", f"completion gate {name} mismatch")
343
+ checks.append({"name": name, "status": "PASS"})
344
+ return checks
345
+
346
+
347
+ def _validate_gate_digest(receipt: dict[str, Any]) -> None:
348
+ gate_digest = receipt.get("gateDigest")
349
+ if not isinstance(gate_digest, str) or len(gate_digest) != 64:
350
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate gateDigest is required")
351
+ body = {key: value for key, value in receipt.items() if key != "gateDigest"}
352
+ if canonical_digest(body) != gate_digest:
353
+ raise LifecycleError("completion-gate-digest-mismatch", "completion gate digest mismatch")
354
+
355
+
356
+ def _state_binding(state: dict[str, Any]) -> dict[str, Any]:
357
+ return {
358
+ "runId": state.get("runId"),
359
+ "packageId": state.get("packageId"),
360
+ "planRevision": state.get("planRevision"),
361
+ "planDigest": state.get("planDigest"),
362
+ "sourceRevision": state.get("sourceRevision"),
363
+ "stateRevision": state.get("stateRevision"),
364
+ "phase": state.get("phase"),
365
+ "blocker": state.get("blocker"),
366
+ "tasks": [
367
+ {
368
+ "id": task.get("id"),
369
+ "required": task.get("required", True),
370
+ "status": task.get("status"),
371
+ "attempt": task.get("attempt"),
372
+ "review": task.get("review"),
373
+ }
374
+ for task in state.get("tasks", [])
375
+ if isinstance(task, dict)
376
+ ],
377
+ }
378
+
379
+
380
+ def _receipt_final_proof_ready(final_proof: Any) -> bool:
381
+ return isinstance(final_proof, dict) and final_proof.get("required") is True and final_proof.get("ready") is True
382
+
383
+
384
+ def _require_no_finalization_blockers(
385
+ receipt: dict[str, Any],
386
+ *,
387
+ state: dict[str, Any] | None,
388
+ final_audit: dict[str, Any] | None,
389
+ follow_up_register: dict[str, Any] | None,
390
+ ) -> None:
391
+ blockers = _receipt_finalization_blockers(receipt)
392
+ if state is not None:
393
+ acceptance = _acceptance_summary(state)
394
+ if acceptance["missingTaskIds"]:
395
+ blockers.append({"code": "required-acceptance-missing", "taskIds": acceptance["missingTaskIds"]})
396
+ state_blocker = _blocker_summary(state)
397
+ if state_blocker["open"]:
398
+ blockers.append({"code": "open-workflow-blocker", "blocker": state_blocker["blocker"]})
399
+ if final_audit is not None and not _final_proof_summary(final_audit)["ready"]:
400
+ blockers.append({"code": "final-proof-not-ready"})
401
+ if follow_up_register is not None:
402
+ finalization_blockers = validate_followup_register(follow_up_register)["finalizationBlockers"]
403
+ if finalization_blockers:
404
+ blockers.append({"code": "follow-up-blocks-finalization", "items": finalization_blockers})
405
+ if blockers:
406
+ raise LifecycleError(
407
+ "completion-gate-blocked",
408
+ "stop/follow-up decisions cannot carry blocking evidence",
409
+ {"blockers": blockers},
410
+ )
411
+
412
+
413
+ def _receipt_finalization_blockers(receipt: dict[str, Any]) -> list[dict[str, Any]]:
414
+ blockers: list[dict[str, Any]] = []
415
+ receipt_blockers = receipt.get("blockers")
416
+ if isinstance(receipt_blockers, list) and receipt_blockers:
417
+ blockers.append({"code": "receipt-blockers-present", "blockers": receipt_blockers})
418
+ acceptance = receipt.get("acceptance")
419
+ if isinstance(acceptance, dict) and acceptance.get("missingTaskIds"):
420
+ blockers.append({"code": "required-acceptance-missing", "taskIds": acceptance.get("missingTaskIds")})
421
+ validation = receipt.get("validation")
422
+ if isinstance(validation, dict):
423
+ if validation.get("missingIds"):
424
+ blockers.append({"code": "validation-missing", "validationIds": validation.get("missingIds")})
425
+ if validation.get("failedIds"):
426
+ blockers.append({"code": "validation-failed", "validationIds": validation.get("failedIds")})
427
+ follow_up = receipt.get("followUp")
428
+ if isinstance(follow_up, dict) and follow_up.get("finalizationBlockerIds"):
429
+ blockers.append({"code": "follow-up-blocks-finalization", "itemIds": follow_up.get("finalizationBlockerIds")})
430
+ for key, code, id_key in (
431
+ ("regression", "regression-signal-blocking", "signalIds"),
432
+ ("risk", "risk-escalation-required", "flagIds"),
433
+ ("split", "split-required", "candidateIds"),
434
+ ):
435
+ summary = receipt.get(key)
436
+ ids = summary.get("candidateIds" if key == "split" else "blockingIds") if isinstance(summary, dict) else None
437
+ if ids:
438
+ blockers.append({"code": code, id_key: ids})
439
+ return blockers
440
+
441
+
442
+ def _open_blocking_findings(final_audit: dict[str, Any]) -> list[str]:
443
+ findings = final_audit.get("findings", [])
444
+ if not isinstance(findings, list):
445
+ return ["invalid-findings"]
446
+ return [
447
+ str(item.get("id"))
448
+ for item in findings
449
+ if isinstance(item, dict)
450
+ and item.get("status") == "open"
451
+ and item.get("severity") in {"BLOCKER", "HIGH", "MEDIUM"}
452
+ ]
453
+
454
+
455
+ def _require_state_lineage(state: dict[str, Any]) -> None:
456
+ if not isinstance(state, dict):
457
+ raise LifecycleError("invalid-completion-gate-input", "state must be an object")
458
+ for key in ("runId", "packageId", "planDigest", "sourceRevision"):
459
+ _required_string(state.get(key), label=f"state.{key}", error_code="invalid-completion-gate-input")
460
+ revision = state.get("planRevision")
461
+ if not isinstance(revision, int) or isinstance(revision, bool) or revision < 1:
462
+ raise LifecycleError("invalid-completion-gate-input", "state.planRevision must be a positive integer")
463
+ state_revision = state.get("stateRevision")
464
+ if not isinstance(state_revision, int) or isinstance(state_revision, bool) or state_revision < 1:
465
+ raise LifecycleError("invalid-completion-gate-input", "state.stateRevision must be a positive integer")
466
+
467
+
468
+ def _require_verifier(value: Any) -> None:
469
+ if not isinstance(value, dict) or not isinstance(value.get("id"), str) or not value["id"]:
470
+ raise LifecycleError("invalid-completion-gate-receipt", "completion gate verifier.id is required")
471
+
472
+
473
+ def _string_list(
474
+ value: Any,
475
+ *,
476
+ label: str,
477
+ error_code: str,
478
+ allow_empty: bool = False,
479
+ ) -> list[str]:
480
+ if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
481
+ raise LifecycleError(error_code, f"{label} must be a list of non-empty strings")
482
+ if not value and not allow_empty:
483
+ raise LifecycleError(error_code, f"{label} must not be empty")
484
+ return list(value)
485
+
486
+
487
+ def _required_string(value: Any, *, label: str, error_code: str) -> str:
488
+ if not isinstance(value, str) or not value:
489
+ raise LifecycleError(error_code, f"{label} is required")
490
+ return value
491
+
492
+
493
+ def _dedupe(values: list[str]) -> list[str]:
494
+ seen: set[str] = set()
495
+ result: list[str] = []
496
+ for value in values:
497
+ if value in seen:
498
+ continue
499
+ seen.add(value)
500
+ result.append(value)
501
+ return result