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,126 @@
1
+ """Shared workflow gates for implementation audit reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.contracts import LifecycleError, read_json_object
9
+ from agent_lifecycle.contracts.paths import normalize_repo_path
10
+ from agent_lifecycle.workflow.artifacts import artifact_identity, package_root
11
+
12
+
13
+ def task_implementation_audit_required(
14
+ state_path: Path,
15
+ state: dict[str, Any],
16
+ task: dict[str, Any],
17
+ ) -> bool:
18
+ if state.get("implementationAuditRequired") is True or _required(state.get("implementationAudit")):
19
+ return True
20
+ if task.get("implementationAuditRequired") is True or _required(task.get("implementationAudit")):
21
+ return True
22
+ manifest = load_state_manifest(state_path, state)
23
+ if manifest is None:
24
+ return False
25
+ if _required(manifest.get("implementationAudit")):
26
+ return True
27
+ for workstream in manifest.get("workstreams", []):
28
+ if isinstance(workstream, dict) and workstream.get("id") == task.get("id"):
29
+ return workstream.get("implementationAuditRequired") is True or _required(workstream.get("implementationAudit"))
30
+ return False
31
+
32
+
33
+ def final_implementation_audit_required(state_path: Path, state: dict[str, Any]) -> bool:
34
+ if state.get("finalImplementationAuditRequired") is True:
35
+ return True
36
+ implementation_audit = state.get("implementationAudit")
37
+ if isinstance(implementation_audit, dict) and implementation_audit.get("finalRequired") is True:
38
+ return True
39
+ manifest = load_state_manifest(state_path, state)
40
+ if manifest is None:
41
+ return False
42
+ manifest_audit = manifest.get("implementationAudit")
43
+ return isinstance(manifest_audit, dict) and manifest_audit.get("finalRequired") is True
44
+
45
+
46
+ def validate_task_implementation_audit_artifact(
47
+ state_path: Path,
48
+ state: dict[str, Any],
49
+ task: dict[str, Any],
50
+ report_path: str,
51
+ ) -> dict[str, Any]:
52
+ root = package_root(state_path, state)
53
+ rel = normalize_repo_path(report_path, label="implementation audit report")
54
+ report = read_json_object(root / rel, label="implementation audit report")
55
+ from agent_lifecycle.audit.implementation import (
56
+ require_implementation_audit_accepted,
57
+ validate_implementation_audit_report,
58
+ )
59
+
60
+ validation = validate_implementation_audit_report(report, state=state, task=task)
61
+ require_implementation_audit_accepted(validation, report)
62
+ identity = artifact_identity(root, rel, report)
63
+ return {
64
+ **identity,
65
+ "taskId": report.get("taskId"),
66
+ "attempt": report.get("attempt"),
67
+ "verdict": report.get("verdict"),
68
+ "reportDigest": report.get("reportDigest"),
69
+ "validation": {
70
+ "status": validation["status"],
71
+ "validationDigest": validation["validationDigest"],
72
+ },
73
+ }
74
+
75
+
76
+ def task_has_accepted_implementation_audit(task: dict[str, Any]) -> bool:
77
+ audit = task.get("implementationAuditReport")
78
+ return isinstance(audit, dict) and audit.get("verdict") == "ACCEPTED"
79
+
80
+
81
+ def missing_required_implementation_audits(state_path: Path, state: dict[str, Any]) -> list[str]:
82
+ missing = []
83
+ for task in state.get("tasks", []):
84
+ if not isinstance(task, dict) or task.get("status") != "ACCEPTED":
85
+ continue
86
+ if task_implementation_audit_required(state_path, state, task) and not task_has_accepted_implementation_audit(task):
87
+ missing.append(str(task.get("id")))
88
+ return missing
89
+
90
+
91
+ def load_state_manifest(state_path: Path, state: dict[str, Any]) -> dict[str, Any] | None:
92
+ manifest_path = state.get("manifestPath")
93
+ if not isinstance(manifest_path, str) or not manifest_path:
94
+ return None
95
+ root = package_root(state_path, state)
96
+ rel = normalize_repo_path(manifest_path, label="manifest path")
97
+ path = root / rel
98
+ if not path.is_file():
99
+ return None
100
+ return read_json_object(path, label="plan manifest")
101
+
102
+
103
+ def implementation_audit_blockers(state_path: Path, state: dict[str, Any]) -> list[dict[str, Any]]:
104
+ blockers = []
105
+ for task in state.get("tasks", []):
106
+ if not isinstance(task, dict):
107
+ continue
108
+ if task.get("status") not in {"VERIFYING", "ACCEPTED"}:
109
+ continue
110
+ if task_implementation_audit_required(state_path, state, task) and not task_has_accepted_implementation_audit(task):
111
+ blockers.append({"code": "implementation-audit-required", "taskId": task.get("id")})
112
+ return blockers
113
+
114
+
115
+ def require_final_implementation_audit_pass(audit: dict[str, Any]) -> dict[str, Any]:
116
+ if audit.get("status") != "PASS":
117
+ raise LifecycleError(
118
+ "final-implementation-audit-not-pass",
119
+ "final implementation audit did not pass",
120
+ {"blockers": audit.get("blockers", [])},
121
+ )
122
+ return audit
123
+
124
+
125
+ def _required(value: Any) -> bool:
126
+ return isinstance(value, dict) and value.get("required") is True
@@ -0,0 +1,220 @@
1
+ """Worker lease and heartbeat receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
9
+
10
+ WORKER_LEASE_RECEIPT_SCHEMA = "agent-worker-lease-receipt.v1"
11
+ WORKER_LEASE_RECEIPT_VALIDATION_SCHEMA = "agent-worker-lease-receipt-validation.v1"
12
+
13
+ LEASE_STATUSES = {"active", "expired", "completed"}
14
+ RECEIPT_STATUSES = {"PASS", "FAIL"}
15
+ LINEAGE_KEYS = ("runId", "packageId", "planRevision", "planDigest", "sourceRevision")
16
+
17
+
18
+ def build_worker_lease_receipt(
19
+ *,
20
+ lineage: dict[str, Any],
21
+ worker_id: str,
22
+ lease_id: str,
23
+ task_id: str,
24
+ acquired_at: str,
25
+ expires_at: str,
26
+ observed_at: str | None = None,
27
+ heartbeat_at: str | None = None,
28
+ completed_at: str | None = None,
29
+ evidence_ids: list[str] | None = None,
30
+ metadata: dict[str, Any] | None = None,
31
+ blockers: list[dict[str, Any]] | None = None,
32
+ ) -> dict[str, Any]:
33
+ """Build a lease receipt with active/expired/completed classification."""
34
+
35
+ observed = observed_at or _now_iso()
36
+ lease_status = classify_lease_status(expires_at=expires_at, observed_at=observed, completed_at=completed_at)
37
+ body = {
38
+ "schemaVersion": WORKER_LEASE_RECEIPT_SCHEMA,
39
+ "status": "FAIL" if blockers else "PASS",
40
+ "lineage": _lineage(lineage, code="invalid-worker-lease-receipt"),
41
+ "workerId": _required_string(worker_id, label="workerId", code="invalid-worker-lease-receipt"),
42
+ "leaseId": _required_string(lease_id, label="leaseId", code="invalid-worker-lease-receipt"),
43
+ "taskId": _required_string(task_id, label="taskId", code="invalid-worker-lease-receipt"),
44
+ "leaseStatus": lease_status,
45
+ "acquiredAt": _iso(acquired_at, label="acquiredAt", code="invalid-worker-lease-receipt"),
46
+ "expiresAt": _iso(expires_at, label="expiresAt", code="invalid-worker-lease-receipt"),
47
+ "observedAt": _iso(observed, label="observedAt", code="invalid-worker-lease-receipt"),
48
+ "heartbeatAt": _optional_iso(heartbeat_at, label="heartbeatAt", code="invalid-worker-lease-receipt"),
49
+ "completedAt": _optional_iso(completed_at, label="completedAt", code="invalid-worker-lease-receipt"),
50
+ "evidenceIds": _string_list(evidence_ids or [], label="evidenceIds", code="invalid-worker-lease-receipt", allow_empty=True),
51
+ "metadata": dict(metadata or {}),
52
+ "blockers": list(blockers or []),
53
+ "productionPromotionClaimed": False,
54
+ }
55
+ return {**body, "receiptDigest": canonical_digest(body)}
56
+
57
+
58
+ def classify_lease_status(*, expires_at: str, observed_at: str, completed_at: str | None = None) -> str:
59
+ """Classify a lease from deterministic timestamps."""
60
+
61
+ if completed_at:
62
+ return "completed"
63
+ expires = _parse_iso(expires_at, label="expiresAt", code="invalid-worker-lease-receipt")
64
+ observed = _parse_iso(observed_at, label="observedAt", code="invalid-worker-lease-receipt")
65
+ return "expired" if observed > expires else "active"
66
+
67
+
68
+ def validate_worker_lease_receipt(receipt: dict[str, Any], *, expected_lineage: dict[str, Any] | None = None) -> dict[str, Any]:
69
+ """Validate a worker lease receipt and recompute lease state."""
70
+
71
+ blockers: list[dict[str, Any]] = []
72
+ if not isinstance(receipt, dict):
73
+ raise LifecycleError("invalid-worker-lease-receipt", "worker lease receipt must be an object")
74
+ if receipt.get("schemaVersion") != WORKER_LEASE_RECEIPT_SCHEMA:
75
+ blockers.append({"code": "worker-lease-schema-invalid"})
76
+ status = receipt.get("status")
77
+ if status not in RECEIPT_STATUSES:
78
+ blockers.append({"code": "worker-lease-status-invalid", "status": status})
79
+ lineage = _checked_lineage(receipt.get("lineage"), blockers)
80
+ if expected_lineage is not None and lineage is not None:
81
+ _compare_lineage(lineage, expected_lineage, blockers)
82
+ for key in ("workerId", "leaseId", "taskId"):
83
+ if not isinstance(receipt.get(key), str) or not receipt[key]:
84
+ blockers.append({"code": "worker-lease-field-missing", "field": key})
85
+ lease_status = receipt.get("leaseStatus")
86
+ if lease_status not in LEASE_STATUSES:
87
+ blockers.append({"code": "worker-lease-status-class-invalid", "leaseStatus": lease_status})
88
+ acquired = _checked_iso(receipt.get("acquiredAt"), "worker-lease-acquired-invalid", blockers)
89
+ expires = _checked_iso(receipt.get("expiresAt"), "worker-lease-expires-invalid", blockers)
90
+ observed = _checked_iso(receipt.get("observedAt"), "worker-lease-observed-invalid", blockers)
91
+ heartbeat = _checked_optional_iso(receipt.get("heartbeatAt"), "worker-lease-heartbeat-invalid", blockers)
92
+ completed = _checked_optional_iso(receipt.get("completedAt"), "worker-lease-completed-invalid", blockers)
93
+ if acquired and expires and acquired > expires:
94
+ blockers.append({"code": "worker-lease-window-invalid"})
95
+ if heartbeat and acquired and heartbeat < acquired:
96
+ blockers.append({"code": "worker-lease-heartbeat-before-acquire"})
97
+ if completed and acquired and completed < acquired:
98
+ blockers.append({"code": "worker-lease-completed-before-acquire"})
99
+ if expires and observed:
100
+ expected_status = "completed" if completed else "expired" if observed > expires else "active"
101
+ if lease_status != expected_status:
102
+ blockers.append({"code": "worker-lease-status-mismatch", "expected": expected_status, "actual": lease_status})
103
+ _check_string_list(receipt.get("evidenceIds", []), "worker-lease-evidence-ids", blockers, allow_empty=True)
104
+ if not isinstance(receipt.get("metadata", {}), dict):
105
+ blockers.append({"code": "worker-lease-metadata-invalid"})
106
+ _check_object_list(receipt.get("blockers", []), "worker-lease-blockers-invalid", blockers)
107
+ if receipt.get("productionPromotionClaimed") is not False:
108
+ blockers.append({"code": "worker-lease-production-claim"})
109
+ expected_digest = canonical_digest({key: value for key, value in receipt.items() if key != "receiptDigest"})
110
+ if receipt.get("receiptDigest") != expected_digest:
111
+ blockers.append({"code": "worker-lease-receipt-digest-mismatch"})
112
+ body = {
113
+ "schemaVersion": WORKER_LEASE_RECEIPT_VALIDATION_SCHEMA,
114
+ "status": "PASS" if not blockers else "FAIL",
115
+ "receiptStatus": status if isinstance(status, str) else None,
116
+ "workerId": receipt.get("workerId") if isinstance(receipt.get("workerId"), str) else None,
117
+ "leaseId": receipt.get("leaseId") if isinstance(receipt.get("leaseId"), str) else None,
118
+ "taskId": receipt.get("taskId") if isinstance(receipt.get("taskId"), str) else None,
119
+ "leaseStatus": lease_status if isinstance(lease_status, str) else None,
120
+ "blockers": blockers,
121
+ "receiptDigest": receipt.get("receiptDigest"),
122
+ }
123
+ return {**body, "validationDigest": canonical_digest(body)}
124
+
125
+
126
+ def require_worker_lease_receipt_pass(validation: dict[str, Any]) -> dict[str, Any]:
127
+ if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
128
+ raise LifecycleError("worker-lease-validation-failed", "worker lease receipt did not pass", {"validation": validation})
129
+ return validation
130
+
131
+
132
+ def _now_iso() -> str:
133
+ return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
134
+
135
+
136
+ def _parse_iso(value: str, *, label: str, code: str) -> datetime:
137
+ if not isinstance(value, str) or not value:
138
+ raise LifecycleError(code, f"{label} must be an ISO timestamp")
139
+ normalized = value.replace("Z", "+00:00")
140
+ try:
141
+ parsed = datetime.fromisoformat(normalized)
142
+ except ValueError as exc:
143
+ raise LifecycleError(code, f"{label} must be an ISO timestamp") from exc
144
+ if parsed.tzinfo is None:
145
+ parsed = parsed.replace(tzinfo=UTC)
146
+ return parsed.astimezone(UTC)
147
+
148
+
149
+ def _iso(value: str, *, label: str, code: str) -> str:
150
+ _parse_iso(value, label=label, code=code)
151
+ return value
152
+
153
+
154
+ def _optional_iso(value: str | None, *, label: str, code: str) -> str | None:
155
+ if value is None:
156
+ return None
157
+ return _iso(value, label=label, code=code)
158
+
159
+
160
+ def _checked_iso(value: Any, code: str, blockers: list[dict[str, Any]]) -> datetime | None:
161
+ if not isinstance(value, str) or not value:
162
+ blockers.append({"code": code})
163
+ return None
164
+ try:
165
+ return _parse_iso(value, label=code, code=code)
166
+ except LifecycleError:
167
+ blockers.append({"code": code})
168
+ return None
169
+
170
+
171
+ def _checked_optional_iso(value: Any, code: str, blockers: list[dict[str, Any]]) -> datetime | None:
172
+ if value is None:
173
+ return None
174
+ return _checked_iso(value, code, blockers)
175
+
176
+
177
+ def _lineage(value: dict[str, Any], *, code: str) -> dict[str, Any]:
178
+ if not isinstance(value, dict):
179
+ raise LifecycleError(code, "lineage must be an object")
180
+ return {key: value[key] for key in LINEAGE_KEYS if key in value}
181
+
182
+
183
+ def _checked_lineage(value: Any, blockers: list[dict[str, Any]]) -> dict[str, Any] | None:
184
+ if not isinstance(value, dict):
185
+ blockers.append({"code": "worker-lease-lineage-missing"})
186
+ return None
187
+ for key in ("runId", "packageId", "planDigest", "sourceRevision"):
188
+ if not isinstance(value.get(key), str) or not value[key]:
189
+ blockers.append({"code": "worker-lease-lineage-field-missing", "field": key})
190
+ if not isinstance(value.get("planRevision"), int) or isinstance(value.get("planRevision"), bool) or value.get("planRevision") < 1:
191
+ blockers.append({"code": "worker-lease-lineage-plan-revision-invalid"})
192
+ return value
193
+
194
+
195
+ def _compare_lineage(actual: dict[str, Any], expected: dict[str, Any], blockers: list[dict[str, Any]]) -> None:
196
+ for key in LINEAGE_KEYS:
197
+ if key in expected and actual.get(key) != expected.get(key):
198
+ blockers.append({"code": "worker-lease-lineage-mismatch", "field": key})
199
+
200
+
201
+ def _required_string(value: Any, *, label: str, code: str) -> str:
202
+ if not isinstance(value, str) or not value:
203
+ raise LifecycleError(code, f"{label} is required")
204
+ return value
205
+
206
+
207
+ def _string_list(value: Any, *, label: str, code: str, allow_empty: bool = False) -> list[str]:
208
+ if not isinstance(value, list) or (not allow_empty and not value) or not all(isinstance(item, str) and item for item in value):
209
+ raise LifecycleError(code, f"{label} must be a list of strings")
210
+ return list(value)
211
+
212
+
213
+ def _check_string_list(value: Any, code: str, blockers: list[dict[str, Any]], *, allow_empty: bool = False) -> None:
214
+ if not isinstance(value, list) or (not allow_empty and not value) or not all(isinstance(item, str) and item for item in value):
215
+ blockers.append({"code": code})
216
+
217
+
218
+ def _check_object_list(value: Any, code: str, blockers: list[dict[str, Any]]) -> None:
219
+ if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
220
+ blockers.append({"code": code})
@@ -0,0 +1,108 @@
1
+ """Shared workflow and release lineage checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import canonical_digest
8
+
9
+
10
+ def check_lineage(
11
+ manifest: dict[str, Any],
12
+ *,
13
+ state: dict[str, Any] | None = None,
14
+ task_packet_index: dict[str, Any] | None = None,
15
+ final_audit: dict[str, Any] | None = None,
16
+ final_proof: dict[str, Any] | None = None,
17
+ release_inventory: dict[str, Any] | None = None,
18
+ lock: dict[str, Any] | None = None,
19
+ ) -> dict[str, Any]:
20
+ """Compare frozen-plan identity across workflow and release artifacts."""
21
+
22
+ package = manifest.get("package", {})
23
+ expected = {
24
+ "packageId": package.get("id"),
25
+ "planRevision": manifest.get("planRevision"),
26
+ "planDigest": canonical_digest(manifest),
27
+ }
28
+ checks: list[dict[str, Any]] = []
29
+ _compare(checks, "manifest.packageId", expected["packageId"], package.get("id"))
30
+ _compare(checks, "manifest.planRevision", expected["planRevision"], manifest.get("planRevision"))
31
+ _compare(checks, "manifest.planDigest", expected["planDigest"], expected["planDigest"])
32
+ if lock is not None:
33
+ _compare(checks, "lock.packageId", expected["packageId"], lock.get("packageId"), optional_actual=True)
34
+ _compare(checks, "lock.planRevision", expected["planRevision"], lock.get("planRevision"))
35
+ _compare(checks, "lock.manifestHash", expected["planDigest"], lock.get("manifestHash"))
36
+ if state is not None:
37
+ _compare_artifact(checks, "state", expected, state)
38
+ _compare_required_tasks(checks, manifest, state)
39
+ if task_packet_index is not None:
40
+ _compare(checks, "taskPacketIndex.packageId", expected["packageId"], task_packet_index.get("packageId"), optional_actual=True)
41
+ _compare(checks, "taskPacketIndex.manifestDigest", expected["planDigest"], task_packet_index.get("manifestDigest"))
42
+ if final_audit is not None:
43
+ _compare_artifact(checks, "finalAudit", expected, final_audit, package_optional=True)
44
+ if final_proof is not None:
45
+ _compare_artifact(checks, "finalProof", expected, final_proof, package_optional=True)
46
+ if release_inventory is not None:
47
+ _compare_artifact(checks, "releaseInventory", expected, release_inventory, package_optional=True)
48
+ status = "PASS" if all(item["status"] == "PASS" for item in checks) else "FAIL"
49
+ return {
50
+ "schemaVersion": "agent-workflow-lineage-check.v1",
51
+ "status": status,
52
+ "packageId": expected["packageId"],
53
+ "planRevision": expected["planRevision"],
54
+ "planDigest": expected["planDigest"],
55
+ "lineageChecks": checks,
56
+ }
57
+
58
+
59
+ def _compare_artifact(
60
+ checks: list[dict[str, Any]],
61
+ prefix: str,
62
+ expected: dict[str, Any],
63
+ artifact: dict[str, Any],
64
+ *,
65
+ package_optional: bool = False,
66
+ ) -> None:
67
+ _compare(
68
+ checks,
69
+ f"{prefix}.packageId",
70
+ expected["packageId"],
71
+ artifact.get("packageId"),
72
+ optional_actual=package_optional,
73
+ )
74
+ _compare(checks, f"{prefix}.planRevision", expected["planRevision"], artifact.get("planRevision"))
75
+ _compare(checks, f"{prefix}.planDigest", expected["planDigest"], artifact.get("planDigest"))
76
+
77
+
78
+ def _compare_required_tasks(
79
+ checks: list[dict[str, Any]],
80
+ manifest: dict[str, Any],
81
+ state: dict[str, Any],
82
+ ) -> None:
83
+ expected = sorted(
84
+ str(item.get("id"))
85
+ for item in manifest.get("workstreams", [])
86
+ if isinstance(item, dict) and item.get("required", True)
87
+ )
88
+ actual = sorted(
89
+ str(item.get("id"))
90
+ for item in state.get("tasks", [])
91
+ if isinstance(item, dict) and item.get("required", True)
92
+ )
93
+ _compare(checks, "requiredTaskSet", expected, actual)
94
+
95
+
96
+ def _compare(
97
+ checks: list[dict[str, Any]],
98
+ check_id: str,
99
+ expected: Any,
100
+ actual: Any,
101
+ *,
102
+ optional_actual: bool = False,
103
+ ) -> None:
104
+ if optional_actual and actual is None:
105
+ checks.append({"id": check_id, "status": "PASS", "expected": expected, "actual": actual, "skipped": True})
106
+ return
107
+ status = "PASS" if actual == expected else "FAIL"
108
+ checks.append({"id": check_id, "status": status, "expected": expected, "actual": actual})
@@ -0,0 +1,167 @@
1
+ """Managed workflow step-function.
2
+
3
+ This module is intentionally read-only. It checks the frozen plan and durable
4
+ workflow state, then returns the next host action as a typed receipt. Host
5
+ adapters remain responsible for launching model/tool work.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest, read_json_object
14
+ from agent_lifecycle.freeze import verify_plan_lock
15
+ from agent_lifecycle.workflow.next_action import MODEL_CALLS_STARTED, build_managed_next_action
16
+ from agent_lifecycle.workflow.implementation_audit_gate import implementation_audit_blockers
17
+ from agent_lifecycle.workflow.state import load_state, state_identity
18
+
19
+
20
+ def run_managed_lifecycle_step(
21
+ *,
22
+ state_path: Path,
23
+ manifest_path: Path,
24
+ operation_id: str,
25
+ expected_revision: int,
26
+ source_revision: str,
27
+ lock_path: Path | None = None,
28
+ reason: str | None = None,
29
+ ) -> dict[str, Any]:
30
+ blockers: list[dict[str, Any]] = []
31
+ manifest: dict[str, Any] | None = None
32
+ lock: dict[str, Any] | None = None
33
+ state: dict[str, Any] | None = None
34
+ next_action: dict[str, Any] | None = None
35
+
36
+ try:
37
+ manifest = read_json_object(manifest_path, label="frozen plan manifest")
38
+ _require_frozen_manifest(manifest)
39
+ lock = _load_lock(manifest_path, lock_path)
40
+ verify_plan_lock(manifest, lock)
41
+ except LifecycleError as exc:
42
+ blockers.append(_blocker(exc.code, exc.message, exc.details))
43
+
44
+ try:
45
+ state = load_state(state_path)
46
+ _require_state_lineage(
47
+ state,
48
+ manifest=manifest,
49
+ expected_revision=expected_revision,
50
+ source_revision=source_revision,
51
+ )
52
+ if not blockers:
53
+ next_action = build_managed_next_action(state)
54
+ audit_blockers = implementation_audit_blockers(state_path, state)
55
+ if audit_blockers:
56
+ blockers.extend(audit_blockers)
57
+ next_action = None
58
+ except LifecycleError as exc:
59
+ blockers.append(_blocker(exc.code, exc.message, exc.details))
60
+
61
+ status = "FAIL" if blockers else "PASS"
62
+ body = {
63
+ "schemaVersion": "agent-managed-lifecycle-runner-receipt.v1",
64
+ "status": status,
65
+ "operationId": operation_id,
66
+ "reason": reason,
67
+ "expectedRevision": expected_revision,
68
+ "sourceRevision": source_revision,
69
+ "state": _state_summary(state_path, state),
70
+ "plan": _plan_summary(manifest, manifest_path),
71
+ "nextAction": next_action or _blocked_next_action(blockers),
72
+ "blockers": blockers,
73
+ "modelCallsStarted": MODEL_CALLS_STARTED,
74
+ "stateWritten": False,
75
+ "hostLaunchStarted": False,
76
+ "productionPromotionClaimed": False,
77
+ }
78
+ return {**body, "receiptDigest": canonical_digest(body)}
79
+
80
+
81
+ def _load_lock(manifest_path: Path, lock_path: Path | None) -> dict[str, Any]:
82
+ path = lock_path or manifest_path.with_name("plan.lock.json")
83
+ return read_json_object(path, label="plan lock")
84
+
85
+
86
+ def _require_frozen_manifest(manifest: dict[str, Any]) -> None:
87
+ if manifest.get("status") != "FROZEN":
88
+ raise LifecycleError("plan-not-frozen", "managed lifecycle run requires a FROZEN plan")
89
+ revision = manifest.get("planRevision")
90
+ if not isinstance(revision, int) or isinstance(revision, bool) or revision < 1:
91
+ raise LifecycleError("invalid-plan-manifest", "planRevision must be a positive integer")
92
+
93
+
94
+ def _require_state_lineage(
95
+ state: dict[str, Any],
96
+ *,
97
+ manifest: dict[str, Any] | None,
98
+ expected_revision: int,
99
+ source_revision: str,
100
+ ) -> None:
101
+ if state["stateRevision"] != expected_revision:
102
+ raise LifecycleError(
103
+ "state-revision-mismatch",
104
+ "workflow state revision mismatch",
105
+ {"expected": expected_revision, "actual": state["stateRevision"]},
106
+ )
107
+ if state.get("sourceRevision") != source_revision:
108
+ raise LifecycleError("source-revision-mismatch", "source revision mismatch")
109
+ if manifest is None:
110
+ return
111
+ package = manifest.get("package", {}) if isinstance(manifest.get("package"), dict) else {}
112
+ package_id = package.get("id")
113
+ if isinstance(package_id, str) and state.get("packageId") != package_id:
114
+ raise LifecycleError("package-id-mismatch", "workflow state packageId does not match manifest")
115
+ if state.get("planRevision") != manifest.get("planRevision"):
116
+ raise LifecycleError("plan-revision-mismatch", "workflow state planRevision does not match manifest")
117
+ digest = canonical_digest(manifest)
118
+ if state.get("planDigest") != digest:
119
+ raise LifecycleError("plan-digest-mismatch", "workflow state planDigest does not match manifest")
120
+
121
+
122
+ def _state_summary(state_path: Path, state: dict[str, Any] | None) -> dict[str, Any] | None:
123
+ if state is None:
124
+ return None
125
+ return {
126
+ **state_identity(state_path, state),
127
+ "runId": state.get("runId"),
128
+ "packageId": state.get("packageId"),
129
+ "planRevision": state.get("planRevision"),
130
+ "planDigest": state.get("planDigest"),
131
+ "phase": state.get("phase"),
132
+ }
133
+
134
+
135
+ def _plan_summary(manifest: dict[str, Any] | None, manifest_path: Path) -> dict[str, Any] | None:
136
+ if manifest is None:
137
+ return None
138
+ package = manifest.get("package", {}) if isinstance(manifest.get("package"), dict) else {}
139
+ return {
140
+ "path": manifest_path.as_posix(),
141
+ "packageId": package.get("id"),
142
+ "planRevision": manifest.get("planRevision"),
143
+ "planDigest": canonical_digest(manifest),
144
+ "status": manifest.get("status"),
145
+ }
146
+
147
+
148
+ def _blocked_next_action(blockers: list[dict[str, Any]]) -> dict[str, Any]:
149
+ action = {
150
+ "schemaVersion": "agent-managed-lifecycle-next-action.v1",
151
+ "type": "blocked",
152
+ "status": "BLOCKED",
153
+ "hostActionRequired": False,
154
+ "modelCallsStarted": MODEL_CALLS_STARTED,
155
+ "stateMutationRequired": False,
156
+ "projectedAction": None,
157
+ "taskIds": [],
158
+ "blockers": blockers,
159
+ }
160
+ return {**action, "actionDigest": canonical_digest(action)}
161
+
162
+
163
+ def _blocker(code: str, message: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
164
+ blocker: dict[str, Any] = {"code": code, "message": message}
165
+ if context:
166
+ blocker["context"] = context
167
+ return blocker