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,94 @@
1
+ """Audit helpers for bug-forensics gate evidence."""
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.workflow.bug_forensics_gates import validate_bug_forensics_gate_receipt
9
+
10
+ BUG_FORENSICS_AUDIT_SCHEMA = "agent-bug-forensics-audit.v1"
11
+ BUG_FORENSICS_AUDIT_VALIDATION_SCHEMA = "agent-bug-forensics-audit-validation.v1"
12
+
13
+
14
+ def build_bug_forensics_audit(*, gate_receipt: dict[str, Any], task_id: str | None = None) -> dict[str, Any]:
15
+ """Audit that an accepted bug-forensics gate covers the required chain."""
16
+
17
+ gate_validation = validate_bug_forensics_gate_receipt(gate_receipt)
18
+ blockers: list[dict[str, Any]] = []
19
+ gate_status = gate_validation.get("gateStatus")
20
+ if gate_validation.get("status") != "PASS":
21
+ blockers.append({"code": "bug-forensics-gate-invalid", "validation": gate_validation})
22
+ if gate_receipt.get("activated") is True and gate_status != "PASS":
23
+ blockers.append({"code": "bug-forensics-active-gate-not-pass", "gateStatus": gate_status})
24
+ evidence = gate_receipt.get("evidence") if isinstance(gate_receipt.get("evidence"), dict) else {}
25
+ coverage = {
26
+ "reproductionBeforeModification": evidence.get("reproduction") is not None,
27
+ "failureFingerprint": evidence.get("failureFingerprint") is not None,
28
+ "hypothesisLedger": evidence.get("hypothesisLedger") is not None,
29
+ "regressionProof": evidence.get("regressionProof") is not None,
30
+ "fixImpactReceiptReused": evidence.get("fixImpact") is not None,
31
+ "crossCheckReceiptReused": evidence.get("crossCheck") is not None,
32
+ "phase2Deferred": list(gate_receipt.get("phase2Deferred", [])),
33
+ }
34
+ status = "SKIPPED" if gate_status == "SKIPPED" else ("PASS" if not blockers else "FAIL")
35
+ body = {
36
+ "schemaVersion": BUG_FORENSICS_AUDIT_SCHEMA,
37
+ "status": status,
38
+ "taskId": task_id or gate_receipt.get("taskId"),
39
+ "gateReceiptDigest": gate_receipt.get("receiptDigest"),
40
+ "coverage": coverage,
41
+ "blockers": blockers,
42
+ "productionPromotionClaimed": False,
43
+ }
44
+ return {**body, "auditDigest": canonical_digest(body)}
45
+
46
+
47
+ def validate_bug_forensics_audit(audit: dict[str, Any]) -> dict[str, Any]:
48
+ blockers: list[dict[str, Any]] = []
49
+ if not isinstance(audit, dict):
50
+ raise LifecycleError("invalid-bug-forensics-audit", "bug-forensics audit must be an object")
51
+ if audit.get("schemaVersion") != BUG_FORENSICS_AUDIT_SCHEMA:
52
+ blockers.append({"code": "bug-forensics-audit-schema-invalid"})
53
+ audit_status = audit.get("status")
54
+ if audit_status not in {"PASS", "FAIL", "SKIPPED"}:
55
+ blockers.append({"code": "bug-forensics-audit-status-invalid", "status": audit_status})
56
+ if audit.get("gateReceiptDigest") is not None and not _is_digest(audit.get("gateReceiptDigest")):
57
+ blockers.append({"code": "bug-forensics-audit-gate-digest-invalid"})
58
+ if not isinstance(audit.get("coverage"), dict):
59
+ blockers.append({"code": "bug-forensics-audit-coverage-invalid"})
60
+ if not isinstance(audit.get("blockers"), list) or not all(isinstance(item, dict) for item in audit.get("blockers", [])):
61
+ blockers.append({"code": "bug-forensics-audit-blockers-invalid"})
62
+ if audit.get("productionPromotionClaimed") is not False:
63
+ blockers.append({"code": "bug-forensics-audit-production-claim"})
64
+ expected_digest = canonical_digest({key: value for key, value in audit.items() if key != "auditDigest"})
65
+ if audit.get("auditDigest") != expected_digest:
66
+ blockers.append({"code": "bug-forensics-audit-digest-mismatch"})
67
+ if audit_status == "FAIL" and not audit.get("blockers"):
68
+ blockers.append({"code": "bug-forensics-audit-fail-without-blockers"})
69
+ body = {
70
+ "schemaVersion": BUG_FORENSICS_AUDIT_VALIDATION_SCHEMA,
71
+ "status": "PASS" if not blockers else "FAIL",
72
+ "auditStatus": audit_status if isinstance(audit_status, str) else None,
73
+ "blockers": blockers,
74
+ "auditDigest": audit.get("auditDigest"),
75
+ }
76
+ return {**body, "validationDigest": canonical_digest(body)}
77
+
78
+
79
+ def require_bug_forensics_audit_pass(validation: dict[str, Any]) -> dict[str, Any]:
80
+ if validation.get("status") != "PASS" or validation.get("auditStatus") not in {"PASS", "SKIPPED"}:
81
+ raise LifecycleError("bug-forensics-audit-validation-failed", "bug-forensics audit did not pass", {"validation": validation})
82
+ return validation
83
+
84
+
85
+ def _is_digest(value: Any) -> bool:
86
+ return isinstance(value, str) and len(value) == 64 and all(char in "0123456789abcdef" for char in value)
87
+
88
+
89
+ __all__ = [
90
+ "BUG_FORENSICS_AUDIT_SCHEMA",
91
+ "build_bug_forensics_audit",
92
+ "require_bug_forensics_audit_pass",
93
+ "validate_bug_forensics_audit",
94
+ ]
@@ -0,0 +1,516 @@
1
+ """Deterministic implementation audit facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.audit.ownership import build_ownership_report, report_has_category
9
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest, read_json_object
10
+ from agent_lifecycle.contracts.paths import normalize_repo_path
11
+ from agent_lifecycle.workflow.artifacts import artifact_identity, package_root
12
+ from agent_lifecycle.workflow.reviews import validate_task_result, validate_task_review
13
+ from agent_lifecycle.workflow.sandbox_policy import validate_task_sandbox_evidence
14
+ from agent_lifecycle.workflow.selectors import find_task
15
+ from agent_lifecycle.workflow.review_mesh_gate import validate_review_mesh_quorum_path
16
+ from agent_lifecycle.workflow.state import load_state
17
+
18
+
19
+ SEVERITY_RANK = {"BLOCKER": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
20
+ IMPLEMENTATION_AUDIT_SCHEMA = "agent-implementation-audit-report.v1"
21
+ FINAL_IMPLEMENTATION_AUDIT_SCHEMA = "agent-final-implementation-audit.v1"
22
+
23
+
24
+ def build_implementation_audit_report(
25
+ *,
26
+ manifest_path: Path,
27
+ state_path: Path,
28
+ task_id: str,
29
+ result_path: str,
30
+ review_path: str,
31
+ evidence_paths: list[str] | None = None,
32
+ sandbox_receipt_paths: list[str] | None = None,
33
+ review_mesh_quorum_paths: list[str] | None = None,
34
+ changed_paths: list[str] | None = None,
35
+ expected_revision: int | None = None,
36
+ base: str | None = None,
37
+ auditor_id: str = "implementation-auditor",
38
+ auditor_surface: str = "cli",
39
+ ) -> dict[str, Any]:
40
+ manifest = read_json_object(manifest_path, label="plan manifest")
41
+ state = load_state(state_path)
42
+ root = package_root(state_path, state)
43
+ task = find_task(state, task_id)
44
+ result_rel = normalize_repo_path(result_path, label="task result")
45
+ review_rel = normalize_repo_path(review_path, label="task review")
46
+ result = read_json_object(root / result_rel, label="task result")
47
+ review = read_json_object(root / review_rel, label="task review")
48
+ result_identity = artifact_identity(root, result_rel, result)
49
+ review_identity = artifact_identity(root, review_rel, review)
50
+
51
+ findings: list[dict[str, Any]] = []
52
+ blockers: list[dict[str, Any]] = []
53
+ _capture("state", findings, blockers, lambda: _validate_state(state, manifest, expected_revision=expected_revision))
54
+ _capture("result", findings, blockers, lambda: validate_task_result(state, task, result, result_identity))
55
+ task_for_review = {**task, "result": result_identity}
56
+ _capture("review", findings, blockers, lambda: validate_task_review(state, task_for_review, review))
57
+ _check_self_certification(result, review, findings, blockers)
58
+
59
+ changed = changed_paths if changed_paths is not None else _result_changed_files(result)
60
+ ownership = build_ownership_report(manifest_path, changed, base=base)
61
+ if report_has_category(ownership, {"forbidden", "read-only", "unowned"}):
62
+ _add_finding(
63
+ findings,
64
+ blockers,
65
+ code="implementation-write-scope-violation",
66
+ severity="HIGH",
67
+ category="ownership",
68
+ message="changed files include forbidden, read-only or unowned paths",
69
+ context=ownership["summary"],
70
+ )
71
+
72
+ evidence = _evidence_summary(root, task, evidence_paths or [])
73
+ for missing in evidence["missingEvidenceIds"]:
74
+ _add_finding(
75
+ findings,
76
+ blockers,
77
+ code="implementation-evidence-missing",
78
+ severity="MEDIUM",
79
+ category="evidence",
80
+ message=f"required evidence is missing: {missing}",
81
+ context={"evidenceId": missing},
82
+ )
83
+
84
+ sandbox = _sandbox_summary(root, state, task, sandbox_receipt_paths or [])
85
+ if sandbox["validation"]["status"] != "PASS":
86
+ for blocker in sandbox["validation"]["blockers"]:
87
+ _add_finding(
88
+ findings,
89
+ blockers,
90
+ code=str(blocker.get("code") or "sandbox-evidence-failed"),
91
+ severity="HIGH",
92
+ category="sandbox",
93
+ message="required sandbox evidence did not pass",
94
+ context=blocker,
95
+ )
96
+
97
+ review_mesh = _review_mesh_summary(root, state, task, review_mesh_quorum_paths or [])
98
+ if review_mesh["validation"]["status"] != "PASS":
99
+ for blocker in review_mesh["validation"]["blockers"]:
100
+ _add_finding(
101
+ findings,
102
+ blockers,
103
+ code=str(blocker.get("code") or "review-mesh-quorum-failed"),
104
+ severity="HIGH",
105
+ category="review-mesh",
106
+ message="required Review Mesh quorum evidence did not pass",
107
+ context=blocker,
108
+ )
109
+
110
+ coverage = _coverage_summary(task, result, review)
111
+ for missing in coverage["missingAcceptanceIds"]:
112
+ _add_finding(
113
+ findings,
114
+ blockers,
115
+ code="implementation-acceptance-missing",
116
+ severity="MEDIUM",
117
+ category="coverage",
118
+ message=f"acceptance criterion is not covered by PASS review: {missing}",
119
+ context={"acceptanceId": missing},
120
+ )
121
+
122
+ findings = _sorted_findings(findings)
123
+ verdict = _verdict(findings)
124
+ status = "PASS" if verdict == "ACCEPTED" else "FAIL"
125
+ body = {
126
+ "schemaVersion": IMPLEMENTATION_AUDIT_SCHEMA,
127
+ "status": status,
128
+ "verdict": verdict,
129
+ "runId": state.get("runId"),
130
+ "packageId": state.get("packageId"),
131
+ "taskId": task.get("id"),
132
+ "attempt": task.get("attempt"),
133
+ "planRevision": state.get("planRevision"),
134
+ "planDigest": state.get("planDigest"),
135
+ "sourceRevision": state.get("sourceRevision"),
136
+ "auditor": {"id": auditor_id, "surface": auditor_surface, "independent": True},
137
+ "plan": {
138
+ "path": manifest_path.as_posix(),
139
+ "status": manifest.get("status"),
140
+ "planDigest": canonical_digest(manifest),
141
+ },
142
+ "result": {**result_identity, "changedFiles": changed, "commands": result.get("commands", [])},
143
+ "review": {
144
+ **review_identity,
145
+ "reviewer": review.get("reviewer"),
146
+ "verdict": review.get("verdict"),
147
+ },
148
+ "ownership": {"status": "PASS" if not blockers_for_categories(ownership, {"forbidden", "read-only", "unowned"}) else "FAIL", "report": ownership},
149
+ "coverage": coverage,
150
+ "evidence": evidence,
151
+ "sandbox": sandbox,
152
+ "reviewMesh": review_mesh,
153
+ "findings": findings,
154
+ "blockers": blockers,
155
+ "productionPromotionClaimed": False,
156
+ }
157
+ return {**body, "reportDigest": canonical_digest(body)}
158
+
159
+
160
+ def validate_implementation_audit_report(
161
+ report: dict[str, Any],
162
+ *,
163
+ state: dict[str, Any] | None = None,
164
+ task: dict[str, Any] | None = None,
165
+ ) -> dict[str, Any]:
166
+ blockers: list[dict[str, Any]] = []
167
+ if report.get("schemaVersion") != IMPLEMENTATION_AUDIT_SCHEMA:
168
+ blockers.append({"code": "implementation-audit-schema", "message": "unsupported implementation audit schemaVersion"})
169
+ body = {key: value for key, value in report.items() if key != "reportDigest"}
170
+ if report.get("reportDigest") != canonical_digest(body):
171
+ blockers.append({"code": "implementation-audit-digest", "message": "reportDigest does not match report body"})
172
+ if report.get("productionPromotionClaimed") is not False:
173
+ blockers.append({"code": "implementation-audit-production-claim", "message": "implementation audit must not claim production promotion"})
174
+ if report.get("status") not in {"PASS", "FAIL"}:
175
+ blockers.append({"code": "implementation-audit-status", "message": "status must be PASS or FAIL"})
176
+ verdict = report.get("verdict")
177
+ if verdict not in {"ACCEPTED", "REWORK", "CONTRACT_CHANGE", "BLOCKED"}:
178
+ blockers.append({"code": "implementation-audit-verdict", "message": "verdict is unsupported"})
179
+ if report.get("status") == "PASS" and verdict != "ACCEPTED":
180
+ blockers.append({"code": "implementation-audit-status-verdict", "message": "PASS status requires ACCEPTED verdict"})
181
+ if report.get("status") == "FAIL" and verdict == "ACCEPTED":
182
+ blockers.append({"code": "implementation-audit-status-verdict", "message": "ACCEPTED verdict requires PASS status"})
183
+ if report.get("status") == "PASS" and _has_blockers(report):
184
+ blockers.append({"code": "implementation-audit-open-blockers", "message": "PASS report must not contain blockers"})
185
+ if report.get("status") == "PASS" and _has_open_blocking_findings(report):
186
+ blockers.append({"code": "implementation-audit-open-findings", "message": "PASS report must not contain open Medium or higher findings"})
187
+ auditor = report.get("auditor")
188
+ if not isinstance(auditor, dict) or auditor.get("independent") is not True:
189
+ blockers.append({"code": "implementation-audit-auditor", "message": "auditor must be independent"})
190
+ if state is not None:
191
+ expected = {
192
+ "runId": state.get("runId"),
193
+ "packageId": state.get("packageId"),
194
+ "planRevision": state.get("planRevision"),
195
+ "planDigest": state.get("planDigest"),
196
+ "sourceRevision": state.get("sourceRevision"),
197
+ }
198
+ _check_expected(report, expected, blockers, prefix="implementation-audit")
199
+ if task is not None:
200
+ _check_expected(
201
+ report,
202
+ {"taskId": task.get("id"), "attempt": task.get("attempt")},
203
+ blockers,
204
+ prefix="implementation-audit",
205
+ )
206
+ body = {
207
+ "schemaVersion": "agent-implementation-audit-report-validation.v1",
208
+ "status": "PASS" if not blockers else "FAIL",
209
+ "verdict": verdict,
210
+ "blockers": blockers,
211
+ "reportDigest": report.get("reportDigest"),
212
+ }
213
+ return {**body, "validationDigest": canonical_digest(body)}
214
+
215
+
216
+ def require_implementation_audit_accepted(validation: dict[str, Any], report: dict[str, Any]) -> dict[str, Any]:
217
+ if validation.get("status") != "PASS" or report.get("verdict") != "ACCEPTED" or report.get("status") != "PASS":
218
+ raise LifecycleError(
219
+ "implementation-audit-not-accepted",
220
+ "implementation audit report is not accepted",
221
+ {"validation": validation, "verdict": report.get("verdict")},
222
+ )
223
+ return validation
224
+
225
+
226
+ def build_final_implementation_audit(
227
+ *,
228
+ manifest_path: Path,
229
+ state_path: Path,
230
+ report_paths: list[str],
231
+ auditor_id: str = "final-implementation-auditor",
232
+ auditor_surface: str = "cli",
233
+ ) -> dict[str, Any]:
234
+ manifest = read_json_object(manifest_path, label="plan manifest")
235
+ state = load_state(state_path)
236
+ root = package_root(state_path, state)
237
+ reports = []
238
+ findings: list[dict[str, Any]] = []
239
+ blockers: list[dict[str, Any]] = []
240
+ for raw_path in report_paths:
241
+ rel = normalize_repo_path(raw_path, label="implementation audit report")
242
+ report = read_json_object(root / rel, label="implementation audit report")
243
+ task = find_task(state, str(report.get("taskId"))) if isinstance(report.get("taskId"), str) else None
244
+ validation = validate_implementation_audit_report(report, state=state, task=task)
245
+ identity = artifact_identity(root, rel, report)
246
+ accepted = validation["status"] == "PASS" and report.get("status") == "PASS" and report.get("verdict") == "ACCEPTED"
247
+ if not accepted:
248
+ blockers.append({"code": "implementation-audit-report-not-accepted", "path": rel, "validation": validation})
249
+ reports.append({
250
+ **identity,
251
+ "taskId": report.get("taskId"),
252
+ "attempt": report.get("attempt"),
253
+ "verdict": report.get("verdict"),
254
+ "validation": validation,
255
+ })
256
+ findings.extend(report.get("findings", []) if isinstance(report.get("findings"), list) else [])
257
+ accepted_report_task_ids = {item.get("taskId") for item in reports if item.get("verdict") == "ACCEPTED"}
258
+ missing_task_ids = [
259
+ str(task.get("id"))
260
+ for task in state.get("tasks", [])
261
+ if task.get("required", True) and task.get("status") == "ACCEPTED" and task.get("id") not in accepted_report_task_ids
262
+ ]
263
+ for task_id in missing_task_ids:
264
+ blockers.append({"code": "implementation-audit-report-missing", "taskId": task_id})
265
+ if manifest.get("status") != "FROZEN":
266
+ blockers.append({"code": "plan-not-frozen", "message": "final implementation audit requires a FROZEN plan"})
267
+ body = {
268
+ "schemaVersion": FINAL_IMPLEMENTATION_AUDIT_SCHEMA,
269
+ "status": "PASS" if not blockers else "FAIL",
270
+ "runId": state.get("runId"),
271
+ "packageId": state.get("packageId"),
272
+ "planRevision": state.get("planRevision"),
273
+ "planDigest": state.get("planDigest"),
274
+ "sourceRevision": state.get("sourceRevision"),
275
+ "auditor": {"id": auditor_id, "surface": auditor_surface, "independent": True},
276
+ "plan": {
277
+ "path": manifest_path.as_posix(),
278
+ "status": manifest.get("status"),
279
+ "planDigest": canonical_digest(manifest),
280
+ },
281
+ "reports": reports,
282
+ "missingTaskIds": missing_task_ids,
283
+ "findings": _sorted_findings(findings),
284
+ "blockers": blockers,
285
+ "productionPromotionClaimed": False,
286
+ }
287
+ return {**body, "auditDigest": canonical_digest(body)}
288
+
289
+
290
+ def validate_final_implementation_audit(audit: dict[str, Any], *, state: dict[str, Any] | None = None) -> dict[str, Any]:
291
+ blockers: list[dict[str, Any]] = []
292
+ if audit.get("schemaVersion") != FINAL_IMPLEMENTATION_AUDIT_SCHEMA:
293
+ blockers.append({"code": "final-implementation-audit-schema", "message": "unsupported final implementation audit schemaVersion"})
294
+ body = {key: value for key, value in audit.items() if key != "auditDigest"}
295
+ if audit.get("auditDigest") != canonical_digest(body):
296
+ blockers.append({"code": "final-implementation-audit-digest", "message": "auditDigest does not match audit body"})
297
+ if audit.get("status") != "PASS":
298
+ blockers.append({"code": "final-implementation-audit-status", "message": "final implementation audit status must be PASS"})
299
+ if audit.get("status") == "PASS" and _has_blockers(audit):
300
+ blockers.append({"code": "final-implementation-audit-open-blockers", "message": "PASS final implementation audit must not contain blockers"})
301
+ if audit.get("status") == "PASS" and _has_open_blocking_findings(audit):
302
+ blockers.append({"code": "final-implementation-audit-open-findings", "message": "PASS final implementation audit must not contain open Medium or higher findings"})
303
+ if audit.get("productionPromotionClaimed") is not False:
304
+ blockers.append({"code": "final-implementation-audit-production-claim", "message": "audit must not claim production promotion"})
305
+ if state is not None:
306
+ _check_expected(
307
+ audit,
308
+ {
309
+ "runId": state.get("runId"),
310
+ "packageId": state.get("packageId"),
311
+ "planRevision": state.get("planRevision"),
312
+ "planDigest": state.get("planDigest"),
313
+ "sourceRevision": state.get("sourceRevision"),
314
+ },
315
+ blockers,
316
+ prefix="final-implementation-audit",
317
+ )
318
+ body = {
319
+ "schemaVersion": "agent-final-implementation-audit-validation.v1",
320
+ "status": "PASS" if not blockers else "FAIL",
321
+ "blockers": blockers,
322
+ "auditDigest": audit.get("auditDigest"),
323
+ }
324
+ return {**body, "validationDigest": canonical_digest(body)}
325
+
326
+
327
+ def _validate_state(state: dict[str, Any], manifest: dict[str, Any], *, expected_revision: int | None) -> None:
328
+ if expected_revision is not None and state.get("stateRevision") != expected_revision:
329
+ raise LifecycleError(
330
+ "state-revision-mismatch",
331
+ "workflow state revision mismatch",
332
+ {"expected": expected_revision, "actual": state.get("stateRevision")},
333
+ )
334
+ if manifest.get("status") != "FROZEN":
335
+ raise LifecycleError("plan-not-frozen", "implementation audit requires a FROZEN plan")
336
+ if canonical_digest(manifest) != state.get("planDigest"):
337
+ raise LifecycleError("plan-digest-mismatch", "workflow state planDigest does not match manifest")
338
+
339
+
340
+ def _capture(
341
+ category: str,
342
+ findings: list[dict[str, Any]],
343
+ blockers: list[dict[str, Any]],
344
+ callback: Any,
345
+ ) -> None:
346
+ try:
347
+ callback()
348
+ except LifecycleError as exc:
349
+ severity = "BLOCKER" if exc.code in {"state-revision-mismatch", "plan-digest-mismatch"} else "HIGH"
350
+ _add_finding(
351
+ findings,
352
+ blockers,
353
+ code=exc.code,
354
+ severity=severity,
355
+ category=category,
356
+ message=exc.message,
357
+ context=exc.details,
358
+ )
359
+
360
+
361
+ def _check_self_certification(
362
+ result: dict[str, Any],
363
+ review: dict[str, Any],
364
+ findings: list[dict[str, Any]],
365
+ blockers: list[dict[str, Any]],
366
+ ) -> None:
367
+ reviewer = review.get("reviewer")
368
+ if not isinstance(reviewer, dict):
369
+ return
370
+ same_id = reviewer.get("id") == result.get("actor")
371
+ same_run = reviewer.get("runId") == result.get("actorRunId")
372
+ if same_id or same_run:
373
+ _add_finding(
374
+ findings,
375
+ blockers,
376
+ code="worker-self-certification",
377
+ severity="HIGH",
378
+ category="review",
379
+ message="worker and reviewer identity overlap",
380
+ context={"actor": result.get("actor"), "reviewer": reviewer.get("id")},
381
+ )
382
+
383
+
384
+ def _result_changed_files(result: dict[str, Any]) -> list[str]:
385
+ changed = result.get("changedFiles")
386
+ return [item for item in changed if isinstance(item, str)] if isinstance(changed, list) else []
387
+
388
+
389
+ def _evidence_summary(root: Path, task: dict[str, Any], evidence_paths: list[str]) -> dict[str, Any]:
390
+ identities = []
391
+ for raw_path in evidence_paths:
392
+ rel = normalize_repo_path(raw_path, label="evidence")
393
+ payload = read_json_object(root / rel, label="evidence")
394
+ identities.append(artifact_identity(root, rel, payload))
395
+ required_ids = [item for item in task.get("evidenceIds", []) if isinstance(item, str)]
396
+ # The facade is path-based; supplied evidence paths satisfy the task evidence set.
397
+ missing = required_ids if required_ids and not identities else []
398
+ return {
399
+ "requiredEvidenceIds": required_ids,
400
+ "suppliedEvidence": identities,
401
+ "missingEvidenceIds": missing,
402
+ }
403
+
404
+
405
+ def _sandbox_summary(root: Path, state: dict[str, Any], task: dict[str, Any], sandbox_receipt_paths: list[str]) -> dict[str, Any]:
406
+ receipt = None
407
+ receipt_identity = None
408
+ if sandbox_receipt_paths:
409
+ rel = normalize_repo_path(sandbox_receipt_paths[0], label="sandbox receipt")
410
+ receipt = read_json_object(root / rel, label="sandbox receipt")
411
+ receipt_identity = artifact_identity(root, rel, receipt)
412
+ lineage = {
413
+ "runId": state.get("runId"),
414
+ "packageId": state.get("packageId"),
415
+ "planRevision": state.get("planRevision"),
416
+ "planDigest": state.get("planDigest"),
417
+ "sourceRevision": state.get("sourceRevision"),
418
+ }
419
+ validation = validate_task_sandbox_evidence(task, receipt=receipt, expected_lineage=lineage, attempt=task.get("attempt"))
420
+ return {"receipt": receipt_identity, "validation": validation}
421
+
422
+
423
+ def _review_mesh_summary(root: Path, state: dict[str, Any], task: dict[str, Any], quorum_receipt_paths: list[str]) -> dict[str, Any]:
424
+ config = task.get("reviewMesh") if isinstance(task.get("reviewMesh"), dict) else state.get("reviewMesh")
425
+ receipt_path = quorum_receipt_paths[0] if quorum_receipt_paths else None
426
+ validation = validate_review_mesh_quorum_path(
427
+ root=root,
428
+ phase="implementation-audit",
429
+ config=config if isinstance(config, dict) else None,
430
+ receipt_path=receipt_path,
431
+ )
432
+ return {"receiptPath": receipt_path, "validation": validation}
433
+
434
+
435
+ def _coverage_summary(task: dict[str, Any], result: dict[str, Any], review: dict[str, Any]) -> dict[str, Any]:
436
+ item_outcomes = [item for item in result.get("itemOutcomes", []) if isinstance(item, dict)]
437
+ acceptance_checks = [item for item in review.get("acceptanceChecks", []) if isinstance(item, dict)]
438
+ required_acceptance = [item for item in task.get("acceptanceIds", []) if isinstance(item, str)]
439
+ passing_acceptance = {
440
+ item.get("acceptanceId")
441
+ for item in acceptance_checks
442
+ if item.get("status") == "PASS"
443
+ }
444
+ return {
445
+ "plannedItemCount": len(item_outcomes),
446
+ "completedItemCount": sum(1 for item in item_outcomes if item.get("status") == "COMPLETE"),
447
+ "requiredAcceptanceIds": required_acceptance,
448
+ "passingAcceptanceIds": sorted(item for item in passing_acceptance if isinstance(item, str)),
449
+ "missingAcceptanceIds": sorted(set(required_acceptance).difference(passing_acceptance)),
450
+ "validationCommands": result.get("commands", []),
451
+ }
452
+
453
+
454
+ def blockers_for_categories(report: dict[str, Any], categories: set[str]) -> list[dict[str, Any]]:
455
+ return [entry for entry in report.get("entries", []) if entry.get("category") in categories]
456
+
457
+
458
+ def _has_blockers(payload: dict[str, Any]) -> bool:
459
+ blockers = payload.get("blockers")
460
+ return isinstance(blockers, list) and len(blockers) > 0
461
+
462
+
463
+ def _has_open_blocking_findings(payload: dict[str, Any]) -> bool:
464
+ findings = payload.get("findings")
465
+ if not isinstance(findings, list):
466
+ return False
467
+ return any(
468
+ isinstance(item, dict)
469
+ and item.get("status") == "open"
470
+ and item.get("severity") in {"BLOCKER", "HIGH", "MEDIUM"}
471
+ for item in findings
472
+ )
473
+
474
+
475
+ def _add_finding(
476
+ findings: list[dict[str, Any]],
477
+ blockers: list[dict[str, Any]],
478
+ *,
479
+ code: str,
480
+ severity: str,
481
+ category: str,
482
+ message: str,
483
+ context: dict[str, Any] | None = None,
484
+ ) -> None:
485
+ finding = {
486
+ "id": f"finding-{canonical_digest({'code': code, 'category': category, 'message': message})[:16]}",
487
+ "code": code,
488
+ "severity": severity,
489
+ "category": category,
490
+ "status": "open",
491
+ "message": message,
492
+ "context": context or {},
493
+ }
494
+ findings.append(finding)
495
+ if severity in {"BLOCKER", "HIGH", "MEDIUM"}:
496
+ blockers.append({"code": code, "severity": severity, "message": message, "context": context or {}})
497
+
498
+
499
+ def _sorted_findings(findings: list[dict[str, Any]]) -> list[dict[str, Any]]:
500
+ return sorted(findings, key=lambda item: (SEVERITY_RANK.get(str(item.get("severity")), 99), str(item.get("code"))))
501
+
502
+
503
+ def _verdict(findings: list[dict[str, Any]]) -> str:
504
+ if any(item.get("severity") == "BLOCKER" for item in findings):
505
+ return "BLOCKED"
506
+ if any(item.get("category") == "ownership" for item in findings):
507
+ return "CONTRACT_CHANGE"
508
+ if any(item.get("severity") in {"HIGH", "MEDIUM"} for item in findings):
509
+ return "REWORK"
510
+ return "ACCEPTED"
511
+
512
+
513
+ def _check_expected(payload: dict[str, Any], expected: dict[str, Any], blockers: list[dict[str, Any]], *, prefix: str) -> None:
514
+ for key, value in expected.items():
515
+ if payload.get(key) != value:
516
+ blockers.append({"code": f"{prefix}-lineage-mismatch", "field": key, "expected": value, "actual": payload.get(key)})