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,140 @@
1
+ """Budget exceeded policy validation and deterministic action selection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+
9
+ ALLOWED_BUDGET_ACTIONS = {
10
+ "continue-same-route",
11
+ "reroute-cheaper",
12
+ "reroute-stronger",
13
+ "split-task",
14
+ "abort",
15
+ }
16
+ BUDGET_MODES = {"metered", "subscription", "local"}
17
+ CRITICAL_DOWNGRADE_ACTIONS = {"reroute-cheaper"}
18
+
19
+
20
+ def validate_budget_exceeded_policy(policy: dict[str, Any]) -> dict[str, Any]:
21
+ if policy.get("schemaVersion") != "agent-lifecycle-budget-exceeded-policy.v1":
22
+ raise LifecycleError("invalid-budget-policy", "unsupported budget exceeded policy schema")
23
+ mode = _required_str(policy, "mode")
24
+ if mode not in {"manual", "auto"}:
25
+ raise LifecycleError("invalid-budget-policy", "mode must be manual or auto", {"mode": mode})
26
+ allowed_actions = _required_string_list(policy, "allowedActions")
27
+ unknown = sorted(set(allowed_actions) - ALLOWED_BUDGET_ACTIONS)
28
+ if unknown:
29
+ raise LifecycleError("invalid-budget-policy", "allowedActions contains unsupported actions", {"actions": unknown})
30
+ if not allowed_actions:
31
+ raise LifecycleError("invalid-budget-policy", "allowedActions must not be empty")
32
+ forbid_critical = policy.get("forbidDowngradeForCriticalReview")
33
+ if not isinstance(forbid_critical, bool):
34
+ raise LifecycleError("invalid-budget-policy", "forbidDowngradeForCriticalReview must be boolean")
35
+ max_auto = policy.get("maxAutoReroutesPerTask")
36
+ if not isinstance(max_auto, int) or isinstance(max_auto, bool) or max_auto < 0:
37
+ raise LifecycleError("invalid-budget-policy", "maxAutoReroutesPerTask must be a non-negative integer")
38
+ default_auto = policy.get("defaultAutoAction")
39
+ if mode == "auto" and default_auto not in allowed_actions:
40
+ raise LifecycleError("invalid-budget-policy", "defaultAutoAction must be one of allowedActions")
41
+ budget_modes = policy.get("budgetModes")
42
+ if not isinstance(budget_modes, dict):
43
+ raise LifecycleError("invalid-budget-policy", "budgetModes must be an object")
44
+ mode_checks = [_budget_mode_check(name, budget_modes.get(name)) for name in sorted(BUDGET_MODES)]
45
+ if any(item["status"] == "FAIL" for item in mode_checks):
46
+ raise LifecycleError("invalid-budget-policy", "budget mode caps are invalid", {"checks": mode_checks})
47
+ return {
48
+ "schemaVersion": "agent-lifecycle-budget-exceeded-policy-validation.v1",
49
+ "status": "PASS",
50
+ "mode": mode,
51
+ "allowedActions": allowed_actions,
52
+ "policyDigest": canonical_digest(policy),
53
+ "checks": mode_checks,
54
+ }
55
+
56
+
57
+ def select_auto_budget_action(
58
+ policy: dict[str, Any],
59
+ *,
60
+ task: dict[str, Any],
61
+ route_decision: dict[str, Any],
62
+ ) -> str:
63
+ validation = validate_budget_exceeded_policy(policy)
64
+ if validation["mode"] != "auto":
65
+ raise LifecycleError("invalid-budget-policy", "auto action selection requires auto mode")
66
+ reroutes = int(task.get("budgetAutoReroutes", 0))
67
+ if reroutes >= int(policy["maxAutoReroutesPerTask"]):
68
+ raise LifecycleError("budget-auto-reroute-limit", "max auto reroutes per task exhausted")
69
+ action = str(policy["defaultAutoAction"])
70
+ if (
71
+ policy.get("forbidDowngradeForCriticalReview") is True
72
+ and route_decision.get("criticalReview") is True
73
+ and action in CRITICAL_DOWNGRADE_ACTIONS
74
+ ):
75
+ for candidate in ("reroute-stronger", "split-task", "continue-same-route", "abort"):
76
+ if candidate in policy["allowedActions"]:
77
+ return candidate
78
+ raise LifecycleError("budget-critical-downgrade", "critical review cannot auto-downgrade to a cheaper route")
79
+ return action
80
+
81
+
82
+ def _budget_mode_check(mode: str, config: Any) -> dict[str, Any]:
83
+ if not isinstance(config, dict):
84
+ return {"id": f"{mode}-cap-config", "status": "FAIL", "reason": "missing"}
85
+ if mode == "metered":
86
+ cap = config.get("budgetCapUsd")
87
+ threshold = config.get("meteredAskThreshold")
88
+ has_cap = _positive_number(cap)
89
+ threshold_present = threshold is not None
90
+ threshold_valid = (
91
+ not threshold_present
92
+ or (_positive_number(threshold) and has_cap and float(threshold) < float(cap))
93
+ )
94
+ return {
95
+ "id": "metered-usd-cap",
96
+ "status": "PASS" if has_cap and threshold_valid else "FAIL",
97
+ "hasBudgetCapUsd": has_cap,
98
+ "advisoryThresholdEnabled": threshold_present and threshold_valid,
99
+ "advisoryOnly": threshold_present,
100
+ }
101
+ if "meteredAskThreshold" in config:
102
+ return {
103
+ "id": f"{mode}-resource-caps",
104
+ "status": "FAIL",
105
+ "reason": "meteredAskThreshold is only valid for metered budget mode",
106
+ }
107
+ max_invocations = config.get("maxInvocations")
108
+ has_invocations = isinstance(max_invocations, int) and not isinstance(max_invocations, bool) and max_invocations > 0
109
+ has_tokens = (
110
+ isinstance(config.get("maxBillableTokens"), int)
111
+ and not isinstance(config.get("maxBillableTokens"), bool)
112
+ and config["maxBillableTokens"] > 0
113
+ )
114
+ has_wall = _positive_number(config.get("maxWallSeconds"))
115
+ return {
116
+ "id": f"{mode}-resource-caps",
117
+ "status": "PASS" if has_invocations and (has_tokens or has_wall) else "FAIL",
118
+ "hasMaxInvocations": has_invocations,
119
+ "hasTokenOrWallCap": has_tokens or has_wall,
120
+ }
121
+
122
+
123
+ def _required_str(payload: dict[str, Any], key: str) -> str:
124
+ value = payload.get(key)
125
+ if not isinstance(value, str) or not value:
126
+ raise LifecycleError("invalid-budget-policy", f"{key} is required")
127
+ return value
128
+
129
+
130
+ def _required_string_list(payload: dict[str, Any], key: str) -> list[str]:
131
+ value = payload.get(key)
132
+ if not isinstance(value, list) or any(not isinstance(item, str) or not item for item in value):
133
+ raise LifecycleError("invalid-budget-policy", f"{key} must be a list of non-empty strings")
134
+ if len(value) != len(set(value)):
135
+ raise LifecycleError("invalid-budget-policy", f"{key} contains duplicates")
136
+ return list(value)
137
+
138
+
139
+ def _positive_number(value: Any) -> bool:
140
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0
@@ -0,0 +1,70 @@
1
+ """Budget decision receipt construction helpers."""
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 build_budget_decision_receipt(
11
+ *,
12
+ state: dict[str, Any],
13
+ task: dict[str, Any],
14
+ route_decision: dict[str, Any],
15
+ usage_receipt: dict[str, Any],
16
+ usage_identity: dict[str, Any],
17
+ policy: dict[str, Any],
18
+ selected_action: str,
19
+ validation: dict[str, Any],
20
+ expected_workflow_revision: int,
21
+ operator_identity_hash: str | None = None,
22
+ prior_route_digest: str | None = None,
23
+ usage_receipt_digest: str | None = None,
24
+ policy_digest: str | None = None,
25
+ overrun_reason: str | None = None,
26
+ next_route_decision: dict[str, Any] | None = None,
27
+ next_route_identity: dict[str, Any] | None = None,
28
+ split_packet_identity: dict[str, Any] | None = None,
29
+ cap_deltas: dict[str, Any] | None = None,
30
+ ) -> dict[str, Any]:
31
+ receipt = {
32
+ "schemaVersion": "agent-lifecycle-budget-decision-receipt.v1",
33
+ "runId": state["runId"],
34
+ "packageId": state["packageId"],
35
+ "taskId": task["id"],
36
+ "attemptId": f"{task['id']}:attempt-{task.get('attempt')}",
37
+ "attempt": task.get("attempt"),
38
+ "sourceRevision": state["sourceRevision"],
39
+ "priorRouteDecisionDigest": prior_route_digest or route_digest(route_decision),
40
+ "usageReceiptDigest": usage_receipt_digest or canonical_digest(usage_receipt),
41
+ "usageReceipt": usage_identity,
42
+ "overrunReason": overrun_reason or overrun_reason_from_validation(validation),
43
+ "decisionMode": policy["mode"],
44
+ "selectedAction": selected_action,
45
+ "allowedActions": list(policy["allowedActions"]),
46
+ "operatorIdentityHash": operator_identity_hash,
47
+ "policyDigest": policy_digest or canonical_digest(policy),
48
+ "nextRouteDecisionDigest": route_digest(next_route_decision) if next_route_decision is not None else None,
49
+ "splitPacketIdentity": split_packet_identity,
50
+ "capDeltas": dict(cap_deltas or {}),
51
+ "resumePhase": "RUNNING",
52
+ "expectedWorkflowRevision": expected_workflow_revision,
53
+ "productionPromotionClaimed": False,
54
+ }
55
+ if next_route_identity is not None:
56
+ receipt["nextRouteDecision"] = next_route_identity
57
+ return receipt
58
+
59
+
60
+ def route_digest(route_decision: dict[str, Any]) -> str:
61
+ return str(route_decision.get("decisionDigest") or canonical_digest(route_decision))
62
+
63
+
64
+ def overrun_reason_from_validation(validation: dict[str, Any]) -> str:
65
+ failed = [
66
+ str(check.get("id"))
67
+ for check in validation.get("checks", [])
68
+ if isinstance(check, dict) and check.get("status") == "FAIL"
69
+ ]
70
+ return ",".join(failed) or "budget-overrun"
@@ -0,0 +1,284 @@
1
+ """Workflow gate helpers for the optional bug-forensics profile."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.audit.proof_integrity import validate_fix_impact_receipt
8
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
9
+ from agent_lifecycle.quality.bug_forensics import (
10
+ build_bug_forensics_profile,
11
+ validate_bug_forensics_profile,
12
+ validate_bug_reproduction_receipt,
13
+ validate_failure_fingerprint,
14
+ validate_hypothesis_ledger,
15
+ validate_regression_proof_receipt,
16
+ )
17
+ from agent_lifecycle.quality.cross_check import validate_cross_check_receipt
18
+ from agent_lifecycle.quality.failure_classification import (
19
+ HIGH_RISK_FAILURE_CLASSES,
20
+ validate_failure_classification_receipt,
21
+ )
22
+
23
+ BUG_FORENSICS_GATE_RECEIPT_SCHEMA = "agent-bug-forensics-gate-receipt.v1"
24
+ BUG_FORENSICS_GATE_VALIDATION_SCHEMA = "agent-bug-forensics-gate-validation.v1"
25
+
26
+
27
+ def bug_forensics_activated(task: dict[str, Any]) -> bool:
28
+ """Return true only when the task explicitly enables the optional profile."""
29
+
30
+ if not isinstance(task, dict):
31
+ return False
32
+ if task.get("qualityProfile") == "bug-forensics":
33
+ return True
34
+ profiles = task.get("qualityProfiles")
35
+ if isinstance(profiles, list) and "bug-forensics" in profiles:
36
+ return True
37
+ config = task.get("bugForensics")
38
+ return isinstance(config, dict) and config.get("enabled") is True
39
+
40
+
41
+ def build_bug_forensics_gate_receipt(
42
+ *,
43
+ task: dict[str, Any],
44
+ profile: dict[str, Any] | None = None,
45
+ reproduction_receipt: dict[str, Any] | None = None,
46
+ failure_fingerprint: dict[str, Any] | None = None,
47
+ failure_classification: dict[str, Any] | None = None,
48
+ flake_signal: dict[str, Any] | None = None,
49
+ hypothesis_ledger: dict[str, Any] | None = None,
50
+ regression_proof: dict[str, Any] | None = None,
51
+ fix_impact_receipt: dict[str, Any] | None = None,
52
+ cross_check_receipt: dict[str, Any] | None = None,
53
+ ) -> dict[str, Any]:
54
+ """Build a gate receipt for an optional bug-forensics run."""
55
+
56
+ selected_profile = profile or build_bug_forensics_profile()
57
+ activated = bug_forensics_activated(task)
58
+ blockers: list[dict[str, Any]] = []
59
+ validations: dict[str, Any] = {"profile": validate_bug_forensics_profile(selected_profile)}
60
+ if validations["profile"]["status"] != "PASS":
61
+ blockers.append({"code": "bug-forensics-profile-invalid", "validation": validations["profile"]})
62
+
63
+ if not activated:
64
+ status = "SKIPPED"
65
+ else:
66
+ _collect_active_gate_blockers(
67
+ blockers,
68
+ validations,
69
+ task=task,
70
+ reproduction_receipt=reproduction_receipt,
71
+ failure_fingerprint=failure_fingerprint,
72
+ failure_classification=failure_classification,
73
+ flake_signal=flake_signal,
74
+ hypothesis_ledger=hypothesis_ledger,
75
+ regression_proof=regression_proof,
76
+ fix_impact_receipt=fix_impact_receipt,
77
+ cross_check_receipt=cross_check_receipt,
78
+ )
79
+ status = "PASS" if not blockers else "FAIL"
80
+
81
+ evidence = {
82
+ "reproduction": _digest_ref(reproduction_receipt, "receiptDigest"),
83
+ "failureFingerprint": _digest_ref(failure_fingerprint, "fingerprintDigest"),
84
+ "failureClassification": _digest_ref(failure_classification, "classificationDigest"),
85
+ "flakeSignal": _flake_signal_summary(flake_signal),
86
+ "hypothesisLedger": _digest_ref(hypothesis_ledger, "ledgerDigest"),
87
+ "regressionProof": _digest_ref(regression_proof, "proofDigest"),
88
+ "fixImpact": _digest_ref(fix_impact_receipt, "impactDigest"),
89
+ "crossCheck": _digest_ref(cross_check_receipt, "receiptDigest"),
90
+ "validations": validations,
91
+ }
92
+ body = {
93
+ "schemaVersion": BUG_FORENSICS_GATE_RECEIPT_SCHEMA,
94
+ "status": status,
95
+ "taskId": task.get("taskId") or task.get("id"),
96
+ "profileId": selected_profile.get("profileId", "bug-forensics"),
97
+ "activated": activated,
98
+ "chainVerified": activated and status == "PASS",
99
+ "evidence": evidence,
100
+ "blockers": blockers,
101
+ "phase2Deferred": list(selected_profile.get("phase2Deferred", [])),
102
+ "productionPromotionClaimed": False,
103
+ }
104
+ return {**body, "receiptDigest": canonical_digest(body)}
105
+
106
+
107
+ def validate_bug_forensics_gate_receipt(receipt: dict[str, Any]) -> dict[str, Any]:
108
+ blockers: list[dict[str, Any]] = []
109
+ if not isinstance(receipt, dict):
110
+ raise LifecycleError("invalid-bug-forensics-gate-receipt", "bug-forensics gate receipt must be an object")
111
+ if receipt.get("schemaVersion") != BUG_FORENSICS_GATE_RECEIPT_SCHEMA:
112
+ blockers.append({"code": "bug-forensics-gate-schema-invalid"})
113
+ gate_status = receipt.get("status")
114
+ if gate_status not in {"PASS", "FAIL", "SKIPPED"}:
115
+ blockers.append({"code": "bug-forensics-gate-status-invalid", "status": gate_status})
116
+ if not isinstance(receipt.get("profileId"), str) or not receipt["profileId"]:
117
+ blockers.append({"code": "bug-forensics-gate-profile-id-invalid"})
118
+ if not isinstance(receipt.get("activated"), bool):
119
+ blockers.append({"code": "bug-forensics-gate-activation-invalid"})
120
+ if not isinstance(receipt.get("chainVerified"), bool):
121
+ blockers.append({"code": "bug-forensics-gate-chain-invalid"})
122
+ if receipt.get("activated") is True and gate_status == "PASS" and receipt.get("chainVerified") is not True:
123
+ blockers.append({"code": "bug-forensics-gate-chain-not-verified"})
124
+ if receipt.get("activated") is True and gate_status == "SKIPPED":
125
+ blockers.append({"code": "bug-forensics-gate-skipped-while-activated"})
126
+ if not isinstance(receipt.get("evidence"), dict):
127
+ blockers.append({"code": "bug-forensics-gate-evidence-invalid"})
128
+ if not isinstance(receipt.get("blockers"), list) or not all(isinstance(item, dict) for item in receipt.get("blockers", [])):
129
+ blockers.append({"code": "bug-forensics-gate-blockers-invalid"})
130
+ if receipt.get("productionPromotionClaimed") is not False:
131
+ blockers.append({"code": "bug-forensics-gate-production-claim"})
132
+ expected_digest = canonical_digest({key: value for key, value in receipt.items() if key != "receiptDigest"})
133
+ if receipt.get("receiptDigest") != expected_digest:
134
+ blockers.append({"code": "bug-forensics-gate-digest-mismatch"})
135
+ if gate_status == "FAIL" and not receipt.get("blockers"):
136
+ blockers.append({"code": "bug-forensics-gate-fail-without-blockers"})
137
+ body = {
138
+ "schemaVersion": BUG_FORENSICS_GATE_VALIDATION_SCHEMA,
139
+ "status": "PASS" if not blockers else "FAIL",
140
+ "gateStatus": gate_status if isinstance(gate_status, str) else None,
141
+ "blockers": blockers,
142
+ "receiptDigest": receipt.get("receiptDigest"),
143
+ }
144
+ return {**body, "validationDigest": canonical_digest(body)}
145
+
146
+
147
+ def require_bug_forensics_gate_pass(validation: dict[str, Any]) -> dict[str, Any]:
148
+ if validation.get("status") != "PASS" or validation.get("gateStatus") not in {"PASS", "SKIPPED"}:
149
+ raise LifecycleError("bug-forensics-gate-validation-failed", "bug-forensics gate did not pass", {"validation": validation})
150
+ return validation
151
+
152
+
153
+ def _collect_active_gate_blockers(
154
+ blockers: list[dict[str, Any]],
155
+ validations: dict[str, Any],
156
+ *,
157
+ task: dict[str, Any],
158
+ reproduction_receipt: dict[str, Any] | None,
159
+ failure_fingerprint: dict[str, Any] | None,
160
+ failure_classification: dict[str, Any] | None,
161
+ flake_signal: dict[str, Any] | None,
162
+ hypothesis_ledger: dict[str, Any] | None,
163
+ regression_proof: dict[str, Any] | None,
164
+ fix_impact_receipt: dict[str, Any] | None,
165
+ cross_check_receipt: dict[str, Any] | None,
166
+ ) -> None:
167
+ for key, value, validator, missing_code in [
168
+ ("reproduction", reproduction_receipt, validate_bug_reproduction_receipt, "bug-forensics-reproduction-missing"),
169
+ ("failureFingerprint", failure_fingerprint, validate_failure_fingerprint, "bug-forensics-fingerprint-missing"),
170
+ ("hypothesisLedger", hypothesis_ledger, validate_hypothesis_ledger, "bug-forensics-hypothesis-ledger-missing"),
171
+ ("regressionProof", regression_proof, validate_regression_proof_receipt, "bug-forensics-regression-proof-missing"),
172
+ ]:
173
+ if value is None:
174
+ blockers.append({"code": missing_code})
175
+ continue
176
+ validation = validator(value)
177
+ validations[key] = validation
178
+ if validation.get("status") != "PASS":
179
+ blockers.append({"code": f"{key}-invalid", "validation": validation})
180
+ if fix_impact_receipt is None:
181
+ blockers.append({"code": "bug-forensics-fix-impact-missing"})
182
+ else:
183
+ validation = validate_fix_impact_receipt(fix_impact_receipt)
184
+ validations["fixImpact"] = validation
185
+ if validation.get("status") != "PASS":
186
+ blockers.append({"code": "bug-forensics-fix-impact-invalid", "validation": validation})
187
+ if failure_fingerprint is not None and regression_proof is not None:
188
+ before = regression_proof.get("before") if isinstance(regression_proof, dict) else {}
189
+ if isinstance(before, dict) and before.get("fingerprintDigest") != failure_fingerprint.get("fingerprintDigest"):
190
+ blockers.append({"code": "bug-forensics-fingerprint-regression-mismatch"})
191
+ if _classification_required(task) and failure_classification is None:
192
+ blockers.append({"code": "bug-forensics-failure-classification-missing"})
193
+ if failure_classification is not None:
194
+ validation = validate_failure_classification_receipt(failure_classification)
195
+ validations["failureClassification"] = validation
196
+ if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
197
+ blockers.append({"code": "bug-forensics-failure-classification-invalid", "validation": validation})
198
+ if flake_signal is not None:
199
+ validations["flakeSignal"] = _validate_flake_signal(flake_signal)
200
+ if validations["flakeSignal"]["status"] != "PASS":
201
+ blockers.append({"code": "bug-forensics-flake-signal-invalid", "validation": validations["flakeSignal"]})
202
+ if _cross_check_required(task, failure_classification):
203
+ if cross_check_receipt is None:
204
+ blockers.append({"code": "bug-forensics-cross-check-missing"})
205
+ else:
206
+ validation = validate_cross_check_receipt(cross_check_receipt)
207
+ validations["crossCheck"] = validation
208
+ if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
209
+ blockers.append({"code": "bug-forensics-cross-check-invalid", "validation": validation})
210
+ elif cross_check_receipt is not None:
211
+ validation = validate_cross_check_receipt(cross_check_receipt)
212
+ validations["crossCheck"] = validation
213
+ if validation.get("status") != "PASS":
214
+ blockers.append({"code": "bug-forensics-cross-check-invalid", "validation": validation})
215
+
216
+
217
+ def _cross_check_required(task: dict[str, Any], failure_classification: dict[str, Any] | None = None) -> bool:
218
+ if task.get("blockingCrossCheckRequired") is True or task.get("crossCheckRequired") is True:
219
+ return True
220
+ config = task.get("bugForensics")
221
+ if isinstance(config, dict) and config.get("crossCheckRequired") is True:
222
+ return True
223
+ if _high_risk_classification(task, failure_classification):
224
+ return True
225
+ return False
226
+
227
+
228
+ def _classification_required(task: dict[str, Any]) -> bool:
229
+ config = task.get("bugForensics")
230
+ if isinstance(config, dict) and config.get("requireFailureClassification") is True:
231
+ return True
232
+ return task.get("requireFailureClassification") is True
233
+
234
+
235
+ def _high_risk_classification(task: dict[str, Any], failure_classification: dict[str, Any] | None) -> bool:
236
+ if not isinstance(failure_classification, dict):
237
+ return False
238
+ if failure_classification.get("failureClass") not in HIGH_RISK_FAILURE_CLASSES:
239
+ return False
240
+ risks = task.get("riskFlags", {})
241
+ active_security = bool(risks.get("security")) if isinstance(risks, dict) else "security" in risks if isinstance(risks, list) else False
242
+ return task.get("sddTier") == "S2" or active_security
243
+
244
+
245
+ def _digest_ref(value: dict[str, Any] | None, key: str) -> dict[str, Any] | None:
246
+ if value is None:
247
+ return None
248
+ return {"schemaVersion": value.get("schemaVersion"), "digest": value.get(key)}
249
+
250
+
251
+ def _flake_signal_summary(value: dict[str, Any] | None) -> dict[str, Any] | None:
252
+ if value is None:
253
+ return None
254
+ return {
255
+ "status": value.get("status") or value.get("flakeStatus"),
256
+ "runs": value.get("runs"),
257
+ "failures": value.get("failures"),
258
+ }
259
+
260
+
261
+ def _validate_flake_signal(value: dict[str, Any]) -> dict[str, Any]:
262
+ blockers: list[dict[str, Any]] = []
263
+ status = value.get("status") or value.get("flakeStatus")
264
+ if status not in {"stable-fail", "stable-pass", "flaky", "inconclusive"}:
265
+ blockers.append({"code": "flake-signal-status-invalid", "status": status})
266
+ for field in ("runs", "failures"):
267
+ if field in value and (not isinstance(value[field], int) or isinstance(value[field], bool) or value[field] < 0):
268
+ blockers.append({"code": "flake-signal-count-invalid", "field": field})
269
+ body = {
270
+ "schemaVersion": "agent-flake-signal-validation.v1",
271
+ "status": "PASS" if not blockers else "FAIL",
272
+ "flakeStatus": status if isinstance(status, str) else None,
273
+ "blockers": blockers,
274
+ }
275
+ return {**body, "validationDigest": canonical_digest(body)}
276
+
277
+
278
+ __all__ = [
279
+ "BUG_FORENSICS_GATE_RECEIPT_SCHEMA",
280
+ "build_bug_forensics_gate_receipt",
281
+ "bug_forensics_activated",
282
+ "require_bug_forensics_gate_pass",
283
+ "validate_bug_forensics_gate_receipt",
284
+ ]
@@ -0,0 +1,45 @@
1
+ """Compatibility facade for the standalone workflow controller API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agent_lifecycle.workflow.finalization import finalize_run
6
+ from agent_lifecycle.workflow.lineage import check_lineage
7
+ from agent_lifecycle.workflow.budget_decisions import (
8
+ apply_budget_decision,
9
+ pause_for_budget_decision,
10
+ select_auto_budget_action,
11
+ validate_budget_exceeded_policy,
12
+ )
13
+ from agent_lifecycle.workflow.query import next_action, status
14
+ from agent_lifecycle.workflow.plan_adoption import adopt_plan, start_execution
15
+ from agent_lifecycle.workflow.run_transitions import (
16
+ block_run,
17
+ pause_for_external_action,
18
+ resolve_blocker,
19
+ resume_external_action,
20
+ )
21
+ from agent_lifecycle.workflow.task_transitions import (
22
+ accept_task,
23
+ commit_task_result,
24
+ start_task,
25
+ )
26
+
27
+ __all__ = [
28
+ "accept_task",
29
+ "adopt_plan",
30
+ "apply_budget_decision",
31
+ "block_run",
32
+ "check_lineage",
33
+ "commit_task_result",
34
+ "finalize_run",
35
+ "next_action",
36
+ "pause_for_budget_decision",
37
+ "pause_for_external_action",
38
+ "resolve_blocker",
39
+ "resume_external_action",
40
+ "select_auto_budget_action",
41
+ "start_execution",
42
+ "start_task",
43
+ "status",
44
+ "validate_budget_exceeded_policy",
45
+ ]
@@ -0,0 +1,50 @@
1
+ """Append-only workflow event log."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agent_lifecycle.contracts import LifecycleError, canonical_bytes
10
+ from agent_lifecycle.workflow.state import now_iso
11
+
12
+
13
+ def event_log_path(state_path: Path, state: dict[str, Any]) -> Path:
14
+ raw = state.get("eventLog")
15
+ if not isinstance(raw, str) or not raw:
16
+ raise LifecycleError("invalid-workflow-state", "eventLog is required")
17
+ raw_path = Path(raw)
18
+ if raw_path.is_absolute():
19
+ raise LifecycleError("invalid-workflow-state", "eventLog must be relative")
20
+ package_root = state.get("packageRoot")
21
+ if isinstance(package_root, str) and package_root:
22
+ return (state_path.parent / package_root / raw_path).resolve()
23
+ return state_path.parent / raw_path
24
+
25
+
26
+ def append_event(
27
+ *,
28
+ state_path: Path,
29
+ state: dict[str, Any],
30
+ operation_id: str,
31
+ event_type: str,
32
+ payload: dict[str, Any],
33
+ ) -> dict[str, Any]:
34
+ event = {
35
+ "schemaVersion": "agent-workflow-event.v1",
36
+ "runId": state.get("runId"),
37
+ "packageId": state.get("packageId"),
38
+ "stateRevision": state["stateRevision"],
39
+ "operationId": operation_id,
40
+ "eventType": event_type,
41
+ "payload": payload,
42
+ "recordedAt": now_iso(),
43
+ }
44
+ path = event_log_path(state_path, state)
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ with path.open("ab") as handle:
47
+ handle.write(canonical_bytes(event) + b"\n")
48
+ handle.flush()
49
+ os.fsync(handle.fileno())
50
+ return event
@@ -0,0 +1,74 @@
1
+ """Final-proof integration for proof-integrity receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.audit.proof_integrity import (
8
+ require_proof_integrity_pass,
9
+ validate_proof_integrity_receipt,
10
+ )
11
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
12
+
13
+
14
+ def proof_integrity_required(state: dict[str, Any], final_audit: dict[str, Any] | None = None) -> bool:
15
+ """Return whether the current run explicitly requires proof-integrity evidence."""
16
+
17
+ policy = state.get("proofIntegrityPolicy")
18
+ if state.get("proofIntegrityRequired") is True:
19
+ return True
20
+ if isinstance(policy, dict) and policy.get("mode") in {"required", "bug-forensics", "strict"}:
21
+ return True
22
+ if isinstance(final_audit, dict):
23
+ if final_audit.get("proofIntegrityRequired") is True:
24
+ return True
25
+ evidence = final_audit.get("proofIntegrityEvidence")
26
+ if isinstance(evidence, dict) and evidence.get("required") is True:
27
+ return True
28
+ return False
29
+
30
+
31
+ def validate_final_proof_integrity(
32
+ *,
33
+ state: dict[str, Any],
34
+ final_audit: dict[str, Any],
35
+ receipt: dict[str, Any] | None,
36
+ final_proof: dict[str, Any] | None = None,
37
+ required: bool | None = None,
38
+ new_run: bool = True,
39
+ ) -> dict[str, Any] | None:
40
+ """Validate optional final-proof integrity evidence and fail closed when required."""
41
+
42
+ must_validate = proof_integrity_required(state, final_audit) if required is None else required
43
+ if receipt is None:
44
+ if must_validate:
45
+ raise LifecycleError(
46
+ "proof-integrity-receipt-missing",
47
+ "final proof requires proof-integrity receipt",
48
+ )
49
+ return None
50
+ validation = validate_proof_integrity_receipt(
51
+ receipt,
52
+ state=state,
53
+ final_audit=final_audit,
54
+ final_proof=final_proof,
55
+ new_run=new_run,
56
+ )
57
+ require_proof_integrity_pass(validation)
58
+ if isinstance(final_proof, dict) and isinstance(final_proof.get("proofIntegrity"), dict):
59
+ _validate_embedded_identity(final_proof["proofIntegrity"], receipt)
60
+ return validation
61
+
62
+
63
+ def _validate_embedded_identity(proof_integrity: dict[str, Any], receipt: dict[str, Any]) -> None:
64
+ receipt_identity = proof_integrity.get("receipt")
65
+ if not isinstance(receipt_identity, dict):
66
+ raise LifecycleError("proof-integrity-final-proof-mismatch", "final proof proofIntegrity.receipt must be an object")
67
+ if receipt_identity.get("sha256") != canonical_digest(receipt):
68
+ raise LifecycleError("proof-integrity-final-proof-mismatch", "final proof proofIntegrity receipt digest mismatch")
69
+ validation = proof_integrity.get("validation")
70
+ if not isinstance(validation, dict) or validation.get("receiptDigest") != receipt.get("receiptDigest"):
71
+ raise LifecycleError("proof-integrity-final-proof-mismatch", "final proof proofIntegrity validation mismatch")
72
+
73
+
74
+ __all__ = ["proof_integrity_required", "validate_final_proof_integrity"]