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,130 @@
1
+ """Workflow state loading, validation and atomic persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from datetime import UTC, datetime
7
+ from datetime import timedelta
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from agent_lifecycle.contracts import LifecycleError, read_json_object
12
+ from agent_lifecycle.contracts.canonical import canonical_bytes, canonical_digest
13
+
14
+ TERMINAL_PHASES = {"COMPLETE", "FAILED", "CANCELLED"}
15
+ EXECUTION_PHASES = {"RUNNING", "STEP_REVIEW", "REMEDIATING"}
16
+
17
+
18
+ def now_iso() -> str:
19
+ return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
20
+
21
+
22
+ def load_state(path: Path) -> dict[str, Any]:
23
+ state = read_json_object(path, label="workflow state")
24
+ if state.get("schemaVersion") != "agent-workflow-state.v3":
25
+ raise LifecycleError("unsupported-workflow-state", "workflow state schemaVersion is unsupported")
26
+ if not isinstance(state.get("stateRevision"), int) or state["stateRevision"] < 1:
27
+ raise LifecycleError("invalid-workflow-state", "stateRevision must be a positive integer")
28
+ if not isinstance(state.get("phase"), str) or not state["phase"]:
29
+ raise LifecycleError("invalid-workflow-state", "phase is required")
30
+ if not isinstance(state.get("tasks"), list):
31
+ raise LifecycleError("invalid-workflow-state", "tasks must be an array")
32
+ return state
33
+
34
+
35
+ def state_identity(path: Path, state: dict[str, Any]) -> dict[str, Any]:
36
+ data = canonical_bytes(state) + b"\n"
37
+ return {
38
+ "path": path.as_posix(),
39
+ "sha256": canonical_digest(state),
40
+ "bytes": len(data),
41
+ "stateRevision": state["stateRevision"],
42
+ }
43
+
44
+
45
+ def require_expected_revision(state: dict[str, Any], expected_revision: int) -> None:
46
+ if state["stateRevision"] != expected_revision:
47
+ raise LifecycleError(
48
+ "state-revision-mismatch",
49
+ "workflow state revision mismatch",
50
+ {"expected": expected_revision, "actual": state["stateRevision"]},
51
+ )
52
+
53
+
54
+ def require_operation_unused(state: dict[str, Any], operation_id: str) -> None:
55
+ ledger = state.setdefault("operationLedger", {})
56
+ if not isinstance(ledger, dict):
57
+ raise LifecycleError("invalid-workflow-state", "operationLedger must be an object")
58
+ if operation_id in ledger:
59
+ raise LifecycleError("duplicate-operation", f"operation already recorded: {operation_id}")
60
+
61
+
62
+ def record_operation(state: dict[str, Any], *, operation_id: str, event_type: str) -> None:
63
+ ledger = state.setdefault("operationLedger", {})
64
+ if not isinstance(ledger, dict):
65
+ raise LifecycleError("invalid-workflow-state", "operationLedger must be an object")
66
+ ledger[operation_id] = {
67
+ "stateRevision": state["stateRevision"],
68
+ "eventType": event_type,
69
+ "recordedAt": now_iso(),
70
+ }
71
+
72
+
73
+ def deadline_after(started_at: str, seconds: int) -> str:
74
+ try:
75
+ start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
76
+ except ValueError as exc:
77
+ raise LifecycleError("invalid-timestamp", f"invalid timestamp: {started_at}") from exc
78
+ return (start + timedelta(seconds=seconds)).isoformat().replace("+00:00", "Z")
79
+
80
+
81
+ def write_state_replace(path: Path, state: dict[str, Any]) -> None:
82
+ data = canonical_bytes(state) + b"\n"
83
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
84
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
85
+ try:
86
+ with os.fdopen(os.open(tmp, flags, 0o644), "wb") as handle:
87
+ handle.write(data)
88
+ handle.flush()
89
+ os.fsync(handle.fileno())
90
+ os.replace(tmp, path)
91
+ _fsync_dir(path.parent)
92
+ finally:
93
+ if tmp.exists():
94
+ tmp.unlink()
95
+
96
+
97
+ def summarize_state(path: Path, state: dict[str, Any]) -> dict[str, Any]:
98
+ tasks = [
99
+ {
100
+ "id": task.get("id"),
101
+ "status": task.get("status"),
102
+ "attempt": task.get("attempt"),
103
+ "dependsOn": task.get("dependsOn", []),
104
+ "required": task.get("required", True),
105
+ }
106
+ for task in state["tasks"]
107
+ ]
108
+ return {
109
+ "schemaVersion": "agent-workflow-status.v1",
110
+ "identity": state_identity(path, state),
111
+ "runId": state.get("runId"),
112
+ "packageId": state.get("packageId"),
113
+ "planRevision": state.get("planRevision"),
114
+ "planDigest": state.get("planDigest"),
115
+ "sourceRevision": state.get("sourceRevision"),
116
+ "phase": state["phase"],
117
+ "blocker": state.get("blocker"),
118
+ "authorization": state.get("authorization"),
119
+ "tasks": tasks,
120
+ }
121
+
122
+
123
+ def _fsync_dir(path: Path) -> None:
124
+ if os.name == "nt":
125
+ return
126
+ fd = os.open(path, os.O_RDONLY)
127
+ try:
128
+ os.fsync(fd)
129
+ finally:
130
+ os.close(fd)
@@ -0,0 +1,421 @@
1
+ """Task-level workflow transitions."""
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 is_under_repo_path, normalize_repo_path
10
+ from agent_lifecycle.workflow.artifacts import (
11
+ artifact_identity,
12
+ artifact_path,
13
+ next_available_attempt,
14
+ package_root,
15
+ )
16
+ from agent_lifecycle.workflow.gates import record_gate_receipts, validate_controller_gates
17
+ from agent_lifecycle.workflow.implementation_audit_gate import (
18
+ task_implementation_audit_required,
19
+ validate_task_implementation_audit_artifact,
20
+ )
21
+ from agent_lifecycle.workflow.model_usage import (
22
+ model_usage_receipt_required,
23
+ validate_attempt_model_route,
24
+ validate_task_model_usage_receipt,
25
+ )
26
+ from agent_lifecycle.workflow.operation_kernel import commit_state, load_for_update
27
+ from agent_lifecycle.workflow.query import status
28
+ from agent_lifecycle.workflow.reviews import validate_task_result, validate_task_review
29
+ from agent_lifecycle.workflow.selectors import find_task, ready_tasks, unlock_ready_tasks
30
+ from agent_lifecycle.workflow.state import (
31
+ deadline_after,
32
+ now_iso,
33
+ )
34
+
35
+
36
+ def start_task(
37
+ state_path: Path,
38
+ *,
39
+ task_id: str,
40
+ operation_id: str,
41
+ expected_revision: int,
42
+ source_revision: str,
43
+ reason: str,
44
+ ) -> dict[str, Any]:
45
+ state = _mutable_state(state_path, operation_id, expected_revision)
46
+ if state["phase"] not in {"RUNNING", "REMEDIATING"}:
47
+ raise LifecycleError("invalid-phase", f"cannot start task from phase {state['phase']}")
48
+ _require_source_and_authorization(state, source_revision)
49
+ task = find_task(state, task_id)
50
+ if task.get("status") not in {"READY", "REWORK"}:
51
+ raise LifecycleError("invalid-task-status", f"task {task_id} is not launchable")
52
+ _require_dependencies_accepted(state, task)
53
+ _require_parallel_capacity(state)
54
+ attempt = next_available_attempt(state_path, state, task)
55
+ gate_receipts = validate_controller_gates(
56
+ state_path,
57
+ state,
58
+ task,
59
+ phase="pre-launch",
60
+ operation_id=operation_id,
61
+ attempt=attempt,
62
+ )
63
+ _mark_task_running(state, task, attempt, reason)
64
+ record_gate_receipts(task, gate_receipts)
65
+ commit_state(
66
+ state_path,
67
+ state,
68
+ operation_id=operation_id,
69
+ event_type="task-started",
70
+ payload={"taskId": task_id, "attempt": attempt, "reason": reason},
71
+ )
72
+ return status(state_path)
73
+
74
+
75
+ def commit_task_result(
76
+ state_path: Path,
77
+ *,
78
+ task_id: str,
79
+ operation_id: str,
80
+ expected_revision: int,
81
+ source_revision: str,
82
+ result_path: str,
83
+ model_usage_receipt_path: str | None = None,
84
+ budget_targets_path: str | None = None,
85
+ reason: str,
86
+ ) -> dict[str, Any]:
87
+ state = _mutable_state(state_path, operation_id, expected_revision)
88
+ if state["phase"] != "RUNNING":
89
+ raise LifecycleError("invalid-phase", "task result requires RUNNING phase")
90
+ if state.get("sourceRevision") != source_revision:
91
+ raise LifecycleError("source-revision-mismatch", "source revision mismatch")
92
+ task = find_task(state, task_id)
93
+ if task.get("status") != "RUNNING":
94
+ raise LifecycleError("invalid-task-status", f"task {task_id} is not RUNNING")
95
+ gate_receipts = validate_controller_gates(
96
+ state_path,
97
+ state,
98
+ task,
99
+ phase="post-attempt",
100
+ operation_id=operation_id,
101
+ attempt=int(task["attempt"]),
102
+ )
103
+ expected_path = artifact_path(task, "result", int(task["attempt"]))
104
+ if normalize_repo_path(result_path) != expected_path:
105
+ raise LifecycleError("artifact-path-mismatch", "task result path does not match frozen template")
106
+ root = package_root(state_path, state)
107
+ result = read_json_object(root / expected_path, label="task result")
108
+ identity = artifact_identity(root, expected_path, result)
109
+ validate_task_result(state, task, result, identity)
110
+ model_usage_identity = None
111
+ if model_usage_receipt_required(task):
112
+ if model_usage_receipt_path is None:
113
+ raise LifecycleError(
114
+ "model-usage-receipt-required",
115
+ "task result requires a model usage receipt for the attempt model route",
116
+ )
117
+ usage_path = normalize_repo_path(model_usage_receipt_path, label="model usage receipt")
118
+ receipt = read_json_object(root / usage_path, label="model usage receipt")
119
+ model_usage_identity = artifact_identity(root, usage_path, receipt)
120
+ validation = validate_task_model_usage_receipt(
121
+ state,
122
+ task,
123
+ receipt,
124
+ budget_targets=_read_budget_targets(root, budget_targets_path),
125
+ )
126
+ task["modelUsageReceipt"] = {
127
+ **model_usage_identity,
128
+ "operationId": receipt["operationId"],
129
+ "host": receipt["host"],
130
+ "modelClass": receipt["modelClass"],
131
+ "usage": dict(receipt["usage"]),
132
+ "validation": {
133
+ "status": validation["status"],
134
+ "receiptDigest": validation["receiptDigest"],
135
+ "routeDecisionDigest": validation.get("routeDecisionDigest"),
136
+ },
137
+ }
138
+ elif model_usage_receipt_path is not None:
139
+ raise LifecycleError(
140
+ "unexpected-model-usage-receipt",
141
+ "task attempt does not require a model usage receipt",
142
+ )
143
+ task["result"] = identity
144
+ task["status"] = "VERIFYING"
145
+ task["lastReason"] = reason
146
+ record_gate_receipts(task, gate_receipts)
147
+ state["phase"] = "STEP_REVIEW"
148
+ commit_state(
149
+ state_path,
150
+ state,
151
+ operation_id=operation_id,
152
+ event_type="task-result-committed",
153
+ payload={
154
+ "taskId": task_id,
155
+ "attempt": task["attempt"],
156
+ "result": identity,
157
+ "modelUsageReceipt": model_usage_identity,
158
+ "reason": reason,
159
+ },
160
+ )
161
+ return status(state_path)
162
+
163
+
164
+ def accept_task(
165
+ state_path: Path,
166
+ *,
167
+ task_id: str,
168
+ operation_id: str,
169
+ expected_revision: int,
170
+ review_path: str,
171
+ implementation_audit_path: str | None = None,
172
+ reason: str,
173
+ ) -> dict[str, Any]:
174
+ state = _mutable_state(state_path, operation_id, expected_revision)
175
+ if state["phase"] != "STEP_REVIEW":
176
+ raise LifecycleError("invalid-phase", "task acceptance requires STEP_REVIEW phase")
177
+ task = find_task(state, task_id)
178
+ if task.get("status") != "VERIFYING":
179
+ raise LifecycleError("invalid-task-status", f"task {task_id} is not VERIFYING")
180
+ gate_receipts = validate_controller_gates(
181
+ state_path,
182
+ state,
183
+ task,
184
+ phase="pre-acceptance",
185
+ operation_id=operation_id,
186
+ attempt=int(task["attempt"]),
187
+ )
188
+ expected_path = artifact_path(task, "review", int(task["attempt"]))
189
+ if normalize_repo_path(review_path) != expected_path:
190
+ raise LifecycleError("artifact-path-mismatch", "task review path does not match frozen template")
191
+ root = package_root(state_path, state)
192
+ review = read_json_object(root / expected_path, label="task review")
193
+ identity = artifact_identity(root, expected_path, review)
194
+ validate_task_review(state, task, review)
195
+ result = _read_committed_result(root, task)
196
+ ownership_receipt = _validate_task_write_scope(state, task, result)
197
+ implementation_audit = _validate_implementation_audit(
198
+ state_path,
199
+ state,
200
+ task,
201
+ implementation_audit_path=implementation_audit_path,
202
+ )
203
+ record_gate_receipts(task, gate_receipts)
204
+ task["ownershipReceipt"] = ownership_receipt
205
+ if implementation_audit is not None:
206
+ task["implementationAuditReport"] = implementation_audit
207
+ _mark_task_accepted(state, task, review, identity, reason)
208
+ commit_state(
209
+ state_path,
210
+ state,
211
+ operation_id=operation_id,
212
+ event_type="task-accepted",
213
+ payload={"taskId": task_id, "attempt": task["attempt"], "review": task["review"], "reason": reason},
214
+ )
215
+ return status(state_path)
216
+
217
+
218
+ def _validate_implementation_audit(
219
+ state_path: Path,
220
+ state: dict[str, Any],
221
+ task: dict[str, Any],
222
+ *,
223
+ implementation_audit_path: str | None,
224
+ ) -> dict[str, Any] | None:
225
+ required = task_implementation_audit_required(state_path, state, task)
226
+ if implementation_audit_path is None:
227
+ if required:
228
+ raise LifecycleError(
229
+ "implementation-audit-required",
230
+ "task acceptance requires an accepted implementation audit report",
231
+ {"taskId": task.get("id")},
232
+ )
233
+ return None
234
+ return validate_task_implementation_audit_artifact(state_path, state, task, implementation_audit_path)
235
+
236
+
237
+ def _mutable_state(
238
+ state_path: Path,
239
+ operation_id: str,
240
+ expected_revision: int,
241
+ ) -> dict[str, Any]:
242
+ return load_for_update(state_path, operation_id=operation_id, expected_revision=expected_revision)
243
+
244
+
245
+ def _require_source_and_authorization(state: dict[str, Any], source_revision: str) -> None:
246
+ if state.get("sourceRevision") != source_revision:
247
+ raise LifecycleError("source-revision-mismatch", "source revision mismatch")
248
+ authorization = state.get("authorization")
249
+ if not isinstance(authorization, dict) or authorization.get("granted") is not True:
250
+ raise LifecycleError("authorization-required", "execution authorization is required")
251
+
252
+
253
+ def _require_dependencies_accepted(state: dict[str, Any], task: dict[str, Any]) -> None:
254
+ accepted = {item.get("id") for item in state["tasks"] if item.get("status") == "ACCEPTED"}
255
+ missing = sorted(set(task.get("dependsOn", [])).difference(accepted))
256
+ if missing:
257
+ raise LifecycleError(
258
+ "task-dependencies-missing",
259
+ f"task {task.get('id')} dependencies are not accepted",
260
+ {"missing": missing},
261
+ )
262
+
263
+
264
+ def _require_parallel_capacity(state: dict[str, Any]) -> None:
265
+ running = sum(
266
+ 1
267
+ for item in state["tasks"]
268
+ if item.get("status") in {"RUNNING", "VALIDATING", "VERIFYING"}
269
+ )
270
+ max_parallel = int(state.get("budgets", {}).get("maxParallelTasks", 1))
271
+ if running >= max_parallel:
272
+ raise LifecycleError("parallelism-budget-exhausted", "maxParallelTasks budget reached")
273
+
274
+
275
+ def _mark_task_running(
276
+ state: dict[str, Any],
277
+ task: dict[str, Any],
278
+ attempt: int,
279
+ reason: str,
280
+ ) -> None:
281
+ task["attempt"] = attempt
282
+ task["status"] = "RUNNING"
283
+ task["usageIterations"] = []
284
+ task["controllerGateReceipts"] = []
285
+ task.pop("result", None)
286
+ task.pop("review", None)
287
+ task.pop("modelUsageReceipt", None)
288
+ task["attemptStartedAt"] = now_iso()
289
+ task["attemptBaseRevision"] = state.get("sourceRevision")
290
+ task["attemptDeadlineAt"] = deadline_after(
291
+ task["attemptStartedAt"],
292
+ int(state.get("budgets", {}).get("maxTaskWallSeconds", 3600)),
293
+ )
294
+ task["lastReason"] = reason
295
+ if isinstance(task.get("modelRoute"), dict) and task["modelRoute"]:
296
+ validate_attempt_model_route(task)
297
+ task["attemptModelRoute"] = {
298
+ **task["modelRoute"],
299
+ "attempt": attempt,
300
+ }
301
+ state["phase"] = "RUNNING"
302
+
303
+
304
+ def _read_budget_targets(root: Path, budget_targets_path: str | None) -> dict[str, Any] | None:
305
+ if budget_targets_path is None:
306
+ return None
307
+ return read_json_object(
308
+ root / normalize_repo_path(budget_targets_path, label="budget targets"),
309
+ label="budget targets",
310
+ )
311
+
312
+
313
+ def _mark_task_accepted(
314
+ state: dict[str, Any],
315
+ task: dict[str, Any],
316
+ review: dict[str, Any],
317
+ identity: dict[str, Any],
318
+ reason: str,
319
+ ) -> None:
320
+ task["review"] = {
321
+ **identity,
322
+ "reviewId": review["reviewId"],
323
+ "reviewer": review["reviewer"]["id"],
324
+ "reviewerRunId": review["reviewer"]["runId"],
325
+ "surface": review["reviewer"]["surface"],
326
+ "verdict": review["verdict"],
327
+ }
328
+ task["status"] = "ACCEPTED"
329
+ task["lastReason"] = reason
330
+ task.pop("attemptStartedAt", None)
331
+ task.pop("attemptDeadlineAt", None)
332
+ unlock_ready_tasks(state)
333
+ state["phase"] = "RUNNING" if ready_tasks(state) else "FINAL_AUDIT"
334
+
335
+
336
+ def _read_committed_result(root: Path, task: dict[str, Any]) -> dict[str, Any]:
337
+ result_identity = task.get("result")
338
+ if not isinstance(result_identity, dict) or not isinstance(result_identity.get("path"), str):
339
+ raise LifecycleError("missing-task-result", "task acceptance requires committed result")
340
+ return read_json_object(root / normalize_repo_path(result_identity["path"]), label="task result")
341
+
342
+
343
+ def _validate_task_write_scope(
344
+ state: dict[str, Any],
345
+ task: dict[str, Any],
346
+ result: dict[str, Any],
347
+ ) -> dict[str, Any]:
348
+ changed_files = result.get("changedFiles")
349
+ if not isinstance(changed_files, list) or not all(isinstance(path, str) for path in changed_files):
350
+ raise LifecycleError("task-result-invalid", "task result changedFiles must be a list of strings")
351
+ writes = [
352
+ normalize_repo_path(path)
353
+ for path in task.get("writes", [])
354
+ if isinstance(path, str)
355
+ ]
356
+ if not writes:
357
+ if changed_files and (state.get("manifestPath") or state.get("packetSet")):
358
+ raise LifecycleError(
359
+ "task-write-scope-missing",
360
+ "adopted task state has changed files but no write scope",
361
+ {"taskId": task.get("id"), "changedFiles": changed_files},
362
+ )
363
+ return {
364
+ "schemaVersion": "agent-task-ownership-receipt.v1",
365
+ "status": "SKIPPED_NO_WRITE_SCOPE",
366
+ "runId": state.get("runId"),
367
+ "packageId": state.get("packageId"),
368
+ "taskId": task.get("id"),
369
+ "attempt": task.get("attempt"),
370
+ "planDigest": state.get("planDigest"),
371
+ "sourceRevision": state.get("sourceRevision"),
372
+ "changedFileCount": len(changed_files),
373
+ "entries": [],
374
+ "blockers": [],
375
+ }
376
+ policy = state.get("writePolicy", {}) if isinstance(state.get("writePolicy"), dict) else {}
377
+ forbidden_roots = [
378
+ normalize_repo_path(path)
379
+ for path in policy.get("forbiddenWrites", [])
380
+ if isinstance(path, str)
381
+ ]
382
+ read_only_roots = [
383
+ normalize_repo_path(path)
384
+ for path in policy.get("readOnly", [])
385
+ if isinstance(path, str)
386
+ ]
387
+ entries: list[dict[str, Any]] = []
388
+ for raw_path in sorted(set(changed_files)):
389
+ path = normalize_repo_path(raw_path)
390
+ forbidden = [root for root in forbidden_roots if is_under_repo_path(path, root)]
391
+ read_only = [root for root in read_only_roots if is_under_repo_path(path, root)]
392
+ owned = [root for root in writes if is_under_repo_path(path, root)]
393
+ if forbidden:
394
+ entries.append({"path": path, "category": "forbidden", "matched": forbidden})
395
+ elif read_only:
396
+ entries.append({"path": path, "category": "read-only", "matched": read_only})
397
+ elif owned:
398
+ entries.append({"path": path, "category": "task-owned", "matched": owned})
399
+ else:
400
+ entries.append({"path": path, "category": "unowned"})
401
+ blockers = [
402
+ entry
403
+ for entry in entries
404
+ if entry["category"] in {"forbidden", "read-only", "unowned"}
405
+ ]
406
+ receipt = {
407
+ "schemaVersion": "agent-task-ownership-receipt.v1",
408
+ "status": "PASS" if not blockers else "FAIL",
409
+ "runId": state.get("runId"),
410
+ "packageId": state.get("packageId"),
411
+ "taskId": task.get("id"),
412
+ "attempt": task.get("attempt"),
413
+ "planDigest": state.get("planDigest"),
414
+ "sourceRevision": state.get("sourceRevision"),
415
+ "changedFileCount": len(entries),
416
+ "entries": entries,
417
+ "blockers": blockers,
418
+ }
419
+ if blockers:
420
+ raise LifecycleError("task-ownership-violation", "task result changed files are outside task write scope", {"ownership": receipt})
421
+ return receipt
@@ -0,0 +1,17 @@
1
+ """Worktree isolation receipt helpers."""
2
+
3
+ from agent_lifecycle.worktree.isolation import (
4
+ build_attempt_isolation_receipt,
5
+ build_worktree_writeback_receipt,
6
+ validate_attempt_isolation_receipt,
7
+ validate_worktree_writeback_receipt,
8
+ validate_worktree_policy,
9
+ )
10
+
11
+ __all__ = [
12
+ "build_attempt_isolation_receipt",
13
+ "build_worktree_writeback_receipt",
14
+ "validate_attempt_isolation_receipt",
15
+ "validate_worktree_writeback_receipt",
16
+ "validate_worktree_policy",
17
+ ]