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,439 @@
1
+ """Frozen-plan adoption and execution-start transitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest, read_json_object
11
+ from agent_lifecycle.specification import validate_completion_check
12
+ from agent_lifecycle.workflow.artifacts import artifact_identity, package_root
13
+ from agent_lifecycle.workflow.operation_kernel import commit_state, load_for_update
14
+ from agent_lifecycle.workflow.query import status
15
+ from agent_lifecycle.workflow.review_mesh_gate import require_review_mesh_quorum_gate_pass, validate_review_mesh_quorum_path
16
+ from agent_lifecycle.workflow.selectors import unlock_ready_tasks
17
+ from agent_lifecycle.workflow.state import (
18
+ TERMINAL_PHASES,
19
+ deadline_after,
20
+ now_iso,
21
+ )
22
+
23
+
24
+ def adopt_plan(
25
+ state_path: Path,
26
+ *,
27
+ manifest_path: Path,
28
+ operation_id: str,
29
+ expected_revision: int,
30
+ source_revision: str,
31
+ reset_tasks: bool,
32
+ preserve_accepted_compatible: bool = False,
33
+ start_mode: str,
34
+ authorized_by: str | None = None,
35
+ ) -> dict[str, Any]:
36
+ state = load_for_update(state_path, operation_id=operation_id, expected_revision=expected_revision)
37
+ _require_adoptable(state, reset_tasks=reset_tasks)
38
+ root = package_root(state_path, state)
39
+ manifest = read_json_object(manifest_path, label="frozen plan manifest")
40
+ digest = _verify_frozen_manifest(root, manifest)
41
+ revision = _plan_revision(manifest)
42
+ _require_revision_upgrade(state, digest, revision, reset_tasks=reset_tasks)
43
+ packet_set, packets = _packet_set(root, manifest, digest)
44
+ tasks = _build_tasks(manifest, packets)
45
+ if preserve_accepted_compatible:
46
+ if not reset_tasks:
47
+ raise LifecycleError(
48
+ "reset-required",
49
+ "preserving accepted tasks requires resetTasks",
50
+ )
51
+ tasks = _preserve_accepted_tasks(state, tasks)
52
+ previous_phase = state["phase"]
53
+ _archive_prior_snapshot(state)
54
+ _replace_plan_state(
55
+ state,
56
+ state_path=state_path,
57
+ manifest_path=manifest_path,
58
+ manifest=manifest,
59
+ digest=digest,
60
+ revision=revision,
61
+ root=root,
62
+ source_revision=source_revision,
63
+ start_mode=start_mode,
64
+ authorized_by=authorized_by,
65
+ packet_set=packet_set,
66
+ tasks=tasks,
67
+ )
68
+ commit_state(
69
+ state_path,
70
+ state,
71
+ operation_id=operation_id,
72
+ event_type="plan-adopted",
73
+ payload={
74
+ "previousPhase": previous_phase,
75
+ "planRevision": revision,
76
+ "resetTasks": reset_tasks,
77
+ "preserveAcceptedCompatible": preserve_accepted_compatible,
78
+ },
79
+ )
80
+ return status(state_path)
81
+
82
+
83
+ def start_execution(
84
+ state_path: Path,
85
+ *,
86
+ operation_id: str,
87
+ expected_revision: int,
88
+ source_revision: str,
89
+ reason: str,
90
+ ) -> dict[str, Any]:
91
+ state = load_for_update(state_path, operation_id=operation_id, expected_revision=expected_revision)
92
+ if state["phase"] != "READY":
93
+ raise LifecycleError("invalid-phase", "execution can only start from READY")
94
+ if state.get("sourceRevision") != source_revision:
95
+ raise LifecycleError("source-revision-mismatch", "source revision mismatch")
96
+ authorization = state.get("authorization")
97
+ if not isinstance(authorization, dict) or authorization.get("granted") is not True:
98
+ raise LifecycleError("authorization-required", "execution authorization is required")
99
+ previous = state["phase"]
100
+ state["phase"] = "RUNNING"
101
+ commit_state(
102
+ state_path,
103
+ state,
104
+ operation_id=operation_id,
105
+ event_type="execution-started",
106
+ payload={"previousPhase": previous, "reason": reason},
107
+ )
108
+ return status(state_path)
109
+
110
+
111
+ def _require_adoptable(state: dict[str, Any], *, reset_tasks: bool) -> None:
112
+ if state["phase"] in TERMINAL_PHASES:
113
+ raise LifecycleError("terminal-run", "terminal workflow state cannot adopt a plan")
114
+ task_started = any(task.get("attempt") or task.get("status") not in {"PENDING", "READY"} for task in state["tasks"])
115
+ if task_started and not reset_tasks:
116
+ raise LifecycleError("reset-required", "adopting a changed plan after task start requires resetTasks")
117
+
118
+
119
+ def _verify_frozen_manifest(root: Path, manifest: dict[str, Any]) -> str:
120
+ if manifest.get("status") != "FROZEN":
121
+ raise LifecycleError("plan-not-frozen", "only FROZEN plans can be adopted")
122
+ digest = canonical_digest(manifest)
123
+ plan_root = manifest.get("package", {}).get("planArtifactRoot")
124
+ if not isinstance(plan_root, str) or not plan_root:
125
+ raise LifecycleError("invalid-plan-manifest", "package.planArtifactRoot is required")
126
+ lock = read_json_object(root / plan_root / "plan.lock.json", label="plan lock")
127
+ if lock.get("manifestHash") != digest:
128
+ raise LifecycleError("plan-lock-mismatch", "plan lock manifestHash does not match manifest")
129
+ if lock.get("planRevision") != manifest.get("planRevision"):
130
+ raise LifecycleError("plan-lock-mismatch", "plan lock revision does not match manifest")
131
+ review_mesh = manifest.get("reviewMesh") if isinstance(manifest.get("reviewMesh"), dict) else None
132
+ require_review_mesh_quorum_gate_pass(
133
+ validate_review_mesh_quorum_path(
134
+ root=root,
135
+ phase="freeze",
136
+ config=review_mesh,
137
+ receipt_path=None,
138
+ )
139
+ )
140
+ return digest
141
+
142
+
143
+ def _plan_revision(manifest: dict[str, Any]) -> int:
144
+ revision = manifest.get("planRevision")
145
+ if not isinstance(revision, int) or isinstance(revision, bool) or revision < 1:
146
+ raise LifecycleError("invalid-plan-manifest", "planRevision must be a positive integer")
147
+ return revision
148
+
149
+
150
+ def _require_revision_upgrade(
151
+ state: dict[str, Any],
152
+ digest: str,
153
+ revision: int,
154
+ *,
155
+ reset_tasks: bool,
156
+ ) -> None:
157
+ if revision < int(state.get("planRevision", 0)):
158
+ raise LifecycleError("older-plan", "cannot adopt an older plan revision")
159
+ if reset_tasks and digest == state.get("planDigest"):
160
+ raise LifecycleError("same-plan-reset", "resetTasks requires a changed plan digest")
161
+
162
+
163
+ def _packet_set(root: Path, manifest: dict[str, Any], digest: str) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
164
+ artifact_root = manifest.get("package", {}).get("artifactRoot")
165
+ if not isinstance(artifact_root, str) or not artifact_root:
166
+ raise LifecycleError("invalid-plan-manifest", "package.artifactRoot is required")
167
+ index_rel = f"{artifact_root.rstrip('/')}/workflow/task-packets/index.json"
168
+ index = read_json_object(root / index_rel, label="task packet index")
169
+ if index.get("manifestDigest") != digest:
170
+ raise LifecycleError("packet-set-mismatch", "task packet index is not bound to the adopted plan")
171
+ packets = {
172
+ packet["taskId"]: packet
173
+ for packet in index.get("packets", [])
174
+ if isinstance(packet, dict) and isinstance(packet.get("taskId"), str)
175
+ }
176
+ identity = artifact_identity(root, index_rel, index)
177
+ lock_rel = f"{manifest['package']['planArtifactRoot'].rstrip('/')}/plan.lock.json"
178
+ return {
179
+ **identity,
180
+ "manifestDigest": digest,
181
+ "packetSetHash": index.get("packetSetHash"),
182
+ "controllerValidation": index.get("controllerValidation"),
183
+ "planLockSha256": _raw_file_identity(root / lock_rel)["sha256"],
184
+ }, packets
185
+
186
+
187
+ def _build_tasks(
188
+ manifest: dict[str, Any],
189
+ packets: dict[str, dict[str, Any]],
190
+ ) -> list[dict[str, Any]]:
191
+ gates = manifest.get("controllerGates", {}).get("gates", [])
192
+ tasks: list[dict[str, Any]] = []
193
+ for workstream in manifest.get("workstreams", []):
194
+ task_id = workstream["id"]
195
+ depends_on = list(workstream.get("dependsOn", []))
196
+ tasks.append({
197
+ "id": task_id,
198
+ "title": workstream.get("title"),
199
+ "owner": workstream.get("owner"),
200
+ "dependsOn": depends_on,
201
+ "writes": list(workstream.get("writes", [])),
202
+ "reviewer": workstream.get("reviewer"),
203
+ "launchGate": workstream.get("launchGate"),
204
+ "capabilityHints": list(workstream.get("capabilityHints", [])),
205
+ "requiredTools": list(workstream.get("requiredTools", [])),
206
+ "contextRefs": list(workstream.get("contextRefs", [])),
207
+ "evidenceIds": list(workstream.get("evidenceIds", [])),
208
+ "executionPolicy": workstream.get("executionPolicy", {}),
209
+ "modelRoute": dict(workstream.get("modelRoute", {})) if isinstance(workstream.get("modelRoute"), dict) else None,
210
+ "reviewMesh": dict(workstream.get("reviewMesh", {})) if isinstance(workstream.get("reviewMesh"), dict) else None,
211
+ "artifactPaths": workstream.get("artifactPaths", _default_artifacts(manifest, task_id)),
212
+ "controllerGates": _task_gates(gates, task_id),
213
+ "packet": packets.get(task_id),
214
+ "required": workstream.get("required", True),
215
+ "status": "READY" if not depends_on else "PENDING",
216
+ "attempt": 0,
217
+ "attemptHistory": [],
218
+ "usageIterations": [],
219
+ "usageTotals": {"iterations": 0, "reportedTokens": 0, "toolCalls": 0},
220
+ "validationRuns": 0,
221
+ "controllerGateReceipts": [],
222
+ "remediationFindingIds": [],
223
+ })
224
+ return tasks
225
+
226
+
227
+ def _preserve_accepted_tasks(
228
+ previous_state: dict[str, Any],
229
+ new_tasks: list[dict[str, Any]],
230
+ ) -> list[dict[str, Any]]:
231
+ previous = {
232
+ task.get("id"): task
233
+ for task in previous_state.get("tasks", [])
234
+ if isinstance(task, dict) and task.get("status") == "ACCEPTED"
235
+ }
236
+ prior_digest = previous_state.get("planDigest")
237
+ prior_revision = previous_state.get("planRevision")
238
+ for task in new_tasks:
239
+ accepted = previous.get(task.get("id"))
240
+ if not accepted or not _task_contract_compatible(accepted, task):
241
+ continue
242
+ _copy_accepted_runtime(task, accepted)
243
+ task["adoptedFromPlanDigest"] = prior_digest
244
+ task["adoptedFromPlanRevision"] = prior_revision
245
+ return new_tasks
246
+
247
+
248
+ def _task_contract_compatible(
249
+ previous: dict[str, Any],
250
+ current: dict[str, Any],
251
+ ) -> bool:
252
+ keys = (
253
+ "id",
254
+ "title",
255
+ "owner",
256
+ "dependsOn",
257
+ "writes",
258
+ "reviewer",
259
+ "launchGate",
260
+ "capabilityHints",
261
+ "requiredTools",
262
+ "contextRefs",
263
+ "evidenceIds",
264
+ "executionPolicy",
265
+ "modelRoute",
266
+ "reviewMesh",
267
+ "artifactPaths",
268
+ "required",
269
+ )
270
+ return all(previous.get(key) == current.get(key) for key in keys)
271
+
272
+
273
+ def _copy_accepted_runtime(
274
+ target: dict[str, Any],
275
+ source: dict[str, Any],
276
+ ) -> None:
277
+ preserved_keys = (
278
+ "attempt",
279
+ "attemptHistory",
280
+ "result",
281
+ "review",
282
+ "status",
283
+ "usageIterations",
284
+ "usageTotals",
285
+ "validationRuns",
286
+ "controllerGateReceipts",
287
+ "remediationFindingIds",
288
+ "lastReason",
289
+ )
290
+ for key in preserved_keys:
291
+ if key in source:
292
+ target[key] = source[key]
293
+
294
+
295
+ def _task_gates(gates: list[Any], task_id: str) -> list[dict[str, Any]]:
296
+ return [
297
+ {
298
+ "id": gate.get("id"),
299
+ "phases": gate.get("phases", []),
300
+ "receiptPath": gate.get("receiptPath"),
301
+ "maxAgeSeconds": gate.get("maxAgeSeconds"),
302
+ "attestationRequired": gate.get("attestationRequired", False),
303
+ "dependsOnGateIds": gate.get("dependsOnGateIds", []),
304
+ }
305
+ for gate in gates
306
+ if isinstance(gate, dict) and task_id in gate.get("appliesTo", [])
307
+ ]
308
+
309
+
310
+ def _default_artifacts(manifest: dict[str, Any], task_id: str) -> dict[str, str]:
311
+ artifact_root = manifest["package"]["artifactRoot"].rstrip("/")
312
+ base = f"{artifact_root}/workflow/work/{task_id}/attempt-{{attempt}}"
313
+ return {"result": f"{base}/task-result.json", "review": f"{base}/task-review.json"}
314
+
315
+
316
+ def _archive_prior_snapshot(state: dict[str, Any]) -> None:
317
+ state.setdefault("priorSnapshots", []).append({
318
+ "planRevision": state.get("planRevision"),
319
+ "planDigest": state.get("planDigest"),
320
+ "phase": state.get("phase"),
321
+ "taskSummary": {task.get("id"): task.get("status") for task in state.get("tasks", [])},
322
+ "archivedAt": now_iso(),
323
+ })
324
+
325
+
326
+ def _replace_plan_state(
327
+ state: dict[str, Any],
328
+ *,
329
+ state_path: Path,
330
+ manifest_path: Path,
331
+ manifest: dict[str, Any],
332
+ digest: str,
333
+ revision: int,
334
+ root: Path,
335
+ source_revision: str,
336
+ start_mode: str,
337
+ authorized_by: str | None,
338
+ packet_set: dict[str, Any],
339
+ tasks: list[dict[str, Any]],
340
+ ) -> None:
341
+ state["manifestPath"] = os.path.relpath(manifest_path, state_path.parent)
342
+ state["planRevision"] = revision
343
+ state["planDigest"] = digest
344
+ state["sourceRevision"] = source_revision
345
+ state["startMode"] = start_mode
346
+ state["budgets"] = dict(manifest.get("orchestration", {}))
347
+ state["writePolicy"] = {
348
+ "readOnly": list(manifest.get("readOnly", [])),
349
+ "forbiddenWrites": list(manifest.get("forbiddenWrites", [])),
350
+ "leadOwned": list(manifest.get("leadOwned", [])),
351
+ }
352
+ if isinstance(manifest.get("reviewMesh"), dict):
353
+ state["reviewMesh"] = dict(manifest["reviewMesh"])
354
+ else:
355
+ state.pop("reviewMesh", None)
356
+ _replace_completion_check_state(state, manifest)
357
+ state["runDeadlineAt"] = deadline_after(state["runStartedAt"], int(state["budgets"].get("maxRunWallSeconds", 86400)))
358
+ state["packetSet"] = packet_set
359
+ state["tasks"] = tasks
360
+ unlock_ready_tasks(state)
361
+ state["taskAcceptanceActions"] = []
362
+ state["blocker"] = None
363
+ state["reconciliation"] = None
364
+ state["authorization"] = _authorization(start_mode, authorized_by)
365
+ state["phase"] = "READY" if state["authorization"].get("granted") else "AWAITING_AUTHORIZATION"
366
+ state["lastPlanReview"] = _last_plan_review(root, manifest)
367
+
368
+
369
+ def _replace_completion_check_state(state: dict[str, Any], manifest: dict[str, Any]) -> None:
370
+ specification = manifest.get("specification", {})
371
+ check = specification.get("completionCheck") if isinstance(specification, dict) else None
372
+ if check is None:
373
+ state.pop("completionCheck", None)
374
+ state.pop("completionCheckValidation", None)
375
+ state.pop("completionCheckReceipt", None)
376
+ return
377
+ validation = validate_completion_check(check)
378
+ state["completionCheck"] = {
379
+ **check,
380
+ "receiptPath": validation["receiptPath"],
381
+ }
382
+ state["completionCheckValidation"] = validation
383
+ state.pop("completionCheckReceipt", None)
384
+
385
+
386
+ def _authorization(start_mode: str, authorized_by: str | None) -> dict[str, Any]:
387
+ if start_mode == "auto-after-freeze":
388
+ if not authorized_by:
389
+ raise LifecycleError("authorization-required", "auto-after-freeze requires authorizedBy")
390
+ return {"required": False, "granted": True, "grantedBy": authorized_by, "grantedAt": now_iso()}
391
+ if start_mode == "approval-required":
392
+ return {"required": True, "granted": False}
393
+ if start_mode == "plan-only":
394
+ return {"required": False, "granted": False}
395
+ raise LifecycleError("invalid-start-mode", "unsupported start mode")
396
+
397
+
398
+ def _last_plan_review(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
399
+ review = manifest.get("planReview", {})
400
+ report = review.get("report") if isinstance(review, dict) else None
401
+ if not isinstance(report, str):
402
+ return _last_plan_lock_review(root, manifest)
403
+ review = read_json_object(root / report, label="plan review")
404
+ identity = _raw_file_identity(root / report)
405
+ return {
406
+ **identity,
407
+ "path": report,
408
+ "reviewId": review.get("reviewId"),
409
+ "reviewer": review.get("reviewer", {}).get("id"),
410
+ "reviewerRunId": review.get("reviewer", {}).get("runId"),
411
+ "surface": review.get("reviewer", {}).get("surface"),
412
+ "verdict": review.get("verdict"),
413
+ }
414
+
415
+
416
+ def _last_plan_lock_review(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
417
+ plan_root = manifest.get("package", {}).get("planArtifactRoot")
418
+ if not isinstance(plan_root, str) or not plan_root:
419
+ raise LifecycleError("invalid-plan-manifest", "package.planArtifactRoot is required")
420
+ lock = read_json_object(root / plan_root / "plan.lock.json", label="plan lock")
421
+ review_path = lock.get("reviewPath")
422
+ if not isinstance(review_path, str) or not review_path:
423
+ raise LifecycleError("invalid-plan-manifest", "planReview.report or plan lock reviewPath is required")
424
+ identity = _raw_file_identity(root / review_path)
425
+ return {
426
+ **identity,
427
+ "path": review_path,
428
+ "reviewId": lock.get("reviewId"),
429
+ "reviewer": lock.get("frozenBy"),
430
+ "reviewerRunId": None,
431
+ "surface": "plan-lock",
432
+ "verdict": manifest.get("planReview", {}).get("requiredVerdict"),
433
+ "reviewedPlanHash": lock.get("reviewedPlanHash"),
434
+ }
435
+
436
+
437
+ def _raw_file_identity(path: Path) -> dict[str, Any]:
438
+ data = path.read_bytes()
439
+ return {"sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data)}
@@ -0,0 +1,52 @@
1
+ """Read-only workflow projections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.workflow.selectors import active_tasks, ready_tasks
9
+ from agent_lifecycle.workflow.state import (
10
+ EXECUTION_PHASES,
11
+ TERMINAL_PHASES,
12
+ load_state,
13
+ summarize_state,
14
+ )
15
+
16
+
17
+ def status(state_path: Path, *, full: bool = False) -> dict[str, Any]:
18
+ state = load_state(state_path)
19
+ if full:
20
+ return {
21
+ "schemaVersion": "agent-workflow-status-full.v1",
22
+ "state": state,
23
+ "nextAction": next_action(state),
24
+ }
25
+ return {**summarize_state(state_path, state), "nextAction": next_action(state)}
26
+
27
+
28
+ def next_action(state: dict[str, Any]) -> dict[str, Any]:
29
+ phase = state["phase"]
30
+ if phase in TERMINAL_PHASES:
31
+ return {"type": "none", "reason": f"run is {phase}"}
32
+ if phase == "BLOCKED":
33
+ return {"type": "request-human-decision", "blocker": state.get("blocker")}
34
+ if phase == "WAITING_FOR_BUDGET_DECISION":
35
+ return {"type": "record-budget-decision", "blocker": state.get("blocker")}
36
+ if phase == "WAITING_FOR_EXTERNAL_ACTION":
37
+ return {"type": "record-external-action-receipt", "externalAction": state.get("externalAction")}
38
+ if phase == "AWAITING_AUTHORIZATION":
39
+ return {"type": "request-execution-authorization"}
40
+ if phase == "READY":
41
+ return {"type": "start-execution"}
42
+ if phase in EXECUTION_PHASES:
43
+ ready = ready_tasks(state)
44
+ if ready:
45
+ return {"type": "launch-tasks", "taskIds": ready}
46
+ active = active_tasks(state)
47
+ if active:
48
+ return {"type": "wait-for-active-tasks", "taskIds": active}
49
+ return {"type": "run-final-audit"}
50
+ if phase == "FINAL_AUDIT":
51
+ return {"type": "finalize-run"}
52
+ return {"type": "continue-phase", "phase": phase}
@@ -0,0 +1,93 @@
1
+ """Workflow gate helpers for optional Review Mesh quorum evidence."""
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, canonical_digest, read_json_object
9
+ from agent_lifecycle.contracts.paths import normalize_repo_path
10
+ from agent_lifecycle.workflow.artifacts import artifact_identity
11
+
12
+
13
+ def review_mesh_required(config: dict[str, Any] | None, *, phase: str) -> bool:
14
+ if not isinstance(config, dict) or config.get("required") is not True:
15
+ return False
16
+ phases = config.get("phases", [])
17
+ return not isinstance(phases, list) or not phases or phase in phases
18
+
19
+
20
+ def validate_review_mesh_quorum_gate(
21
+ *,
22
+ phase: str,
23
+ config: dict[str, Any] | None,
24
+ receipt: dict[str, Any] | None = None,
25
+ receipt_identity: dict[str, Any] | None = None,
26
+ ) -> dict[str, Any]:
27
+ required = review_mesh_required(config, phase=phase)
28
+ blockers: list[dict[str, Any]] = []
29
+ if receipt is None:
30
+ if required:
31
+ blockers.append({"code": "review-mesh-quorum-receipt-missing", "phase": phase})
32
+ return _gate_receipt(phase=phase, required=required, receipt_identity=receipt_identity, blockers=blockers)
33
+ from agent_lifecycle.review_mesh.contracts import validate_review_mesh_quorum_receipt
34
+
35
+ validation = validate_review_mesh_quorum_receipt(receipt)
36
+ if validation["status"] != "PASS":
37
+ blockers.append({"code": "review-mesh-quorum-validation-failed", "validation": validation})
38
+ if required:
39
+ expected_profile_digest = config.get("profileDigest") if isinstance(config, dict) else None
40
+ if expected_profile_digest and receipt.get("profileDigest") != expected_profile_digest:
41
+ blockers.append({"code": "review-mesh-quorum-profile-digest-mismatch"})
42
+ if receipt.get("status") != "PASS" or receipt.get("quorumSatisfied") is not True:
43
+ blockers.append({"code": "review-mesh-quorum-not-satisfied", "receiptStatus": receipt.get("status")})
44
+ return _gate_receipt(phase=phase, required=required, receipt_identity=receipt_identity, blockers=blockers, validation=validation)
45
+
46
+
47
+ def require_review_mesh_quorum_gate_pass(gate: dict[str, Any]) -> dict[str, Any]:
48
+ if gate.get("status") == "FAIL":
49
+ raise LifecycleError("review-mesh-quorum-required", "required Review Mesh quorum evidence is missing or failed", {"gate": gate})
50
+ return gate
51
+
52
+
53
+ def validate_review_mesh_quorum_path(
54
+ *,
55
+ root: Path,
56
+ phase: str,
57
+ config: dict[str, Any] | None,
58
+ receipt_path: str | None,
59
+ ) -> dict[str, Any]:
60
+ if receipt_path is None:
61
+ configured = config.get("quorumReceiptPath") if isinstance(config, dict) else None
62
+ receipt_path = configured if isinstance(configured, str) and configured else None
63
+ if receipt_path is None:
64
+ return validate_review_mesh_quorum_gate(phase=phase, config=config)
65
+ receipt_rel = normalize_repo_path(receipt_path, label="review mesh quorum receipt")
66
+ receipt = read_json_object(root / receipt_rel, label="review mesh quorum receipt")
67
+ return validate_review_mesh_quorum_gate(
68
+ phase=phase,
69
+ config=config,
70
+ receipt=receipt,
71
+ receipt_identity=artifact_identity(root, receipt_rel, receipt),
72
+ )
73
+
74
+
75
+ def _gate_receipt(
76
+ *,
77
+ phase: str,
78
+ required: bool,
79
+ receipt_identity: dict[str, Any] | None,
80
+ blockers: list[dict[str, Any]],
81
+ validation: dict[str, Any] | None = None,
82
+ ) -> dict[str, Any]:
83
+ body = {
84
+ "schemaVersion": "agent-review-mesh-gate-validation.v1",
85
+ "status": "FAIL" if blockers else "PASS",
86
+ "phase": phase,
87
+ "required": required,
88
+ "receipt": receipt_identity,
89
+ "validation": validation,
90
+ "blockers": blockers,
91
+ "productionPromotionClaimed": False,
92
+ }
93
+ return {**body, "validationDigest": canonical_digest(body)}