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,72 @@
1
+ """Validate portable plan manifest structure and write ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+ from agent_lifecycle.contracts.paths import is_under_repo_path, normalize_repo_path
9
+ from agent_lifecycle.planning.completeness import require_plan_completeness_pass, validate_plan_completeness
10
+
11
+
12
+ def validate_plan_manifest(
13
+ manifest: dict[str, Any],
14
+ *,
15
+ require_completeness: bool = False,
16
+ completeness_profile: dict[str, Any] | None = None,
17
+ ) -> dict[str, Any]:
18
+ _require_package(manifest)
19
+ workstreams = _workstreams(manifest)
20
+ overlaps = _write_overlaps(workstreams)
21
+ if overlaps:
22
+ raise LifecycleError("plan-write-overlap", "workstream write sets overlap", {"overlaps": overlaps})
23
+ payload: dict[str, Any] = {
24
+ "schemaVersion": "agent-plan-validation.v1",
25
+ "packageId": manifest["package"]["id"],
26
+ "planRevision": manifest.get("planRevision"),
27
+ "status": manifest.get("status"),
28
+ "workstreamCount": len(workstreams),
29
+ "planDigest": canonical_digest(manifest),
30
+ }
31
+ if require_completeness:
32
+ completeness = validate_plan_completeness(manifest, profile=completeness_profile)
33
+ require_plan_completeness_pass(completeness)
34
+ payload["completeness"] = completeness
35
+ return payload
36
+
37
+
38
+ def _require_package(manifest: dict[str, Any]) -> None:
39
+ package = manifest.get("package")
40
+ if not isinstance(package, dict) or not isinstance(package.get("id"), str):
41
+ raise LifecycleError("invalid-plan-manifest", "package.id is required")
42
+ if manifest.get("status") not in {"DRAFT", "REOPENED", "FROZEN"}:
43
+ raise LifecycleError("invalid-plan-manifest", "plan status is invalid")
44
+ if not isinstance(manifest.get("planRevision"), int):
45
+ raise LifecycleError("invalid-plan-manifest", "planRevision is required")
46
+
47
+
48
+ def _workstreams(manifest: dict[str, Any]) -> list[dict[str, Any]]:
49
+ workstreams = manifest.get("workstreams")
50
+ if not isinstance(workstreams, list) or not workstreams:
51
+ raise LifecycleError("invalid-plan-manifest", "workstreams are required")
52
+ return [item for item in workstreams if isinstance(item, dict)]
53
+
54
+
55
+ def _write_overlaps(workstreams: list[dict[str, Any]]) -> list[dict[str, str]]:
56
+ seen: list[tuple[str, str]] = []
57
+ overlaps: list[dict[str, str]] = []
58
+ for workstream in workstreams:
59
+ owner = workstream.get("id")
60
+ for raw_path in workstream.get("writes", []):
61
+ path = normalize_repo_path(raw_path)
62
+ overlaps.extend(_overlaps(owner, path, seen))
63
+ seen.append((owner, path))
64
+ return overlaps
65
+
66
+
67
+ def _overlaps(owner: str, path: str, seen: list[tuple[str, str]]) -> list[dict[str, str]]:
68
+ return [
69
+ {"leftOwner": prior_owner, "left": prior_path, "rightOwner": owner, "right": path}
70
+ for prior_owner, prior_path in seen
71
+ if prior_owner != owner and (is_under_repo_path(path, prior_path) or is_under_repo_path(prior_path, path))
72
+ ]
@@ -0,0 +1,37 @@
1
+ """Lifecycle policy proposal helpers."""
2
+
3
+ from agent_lifecycle.policy.apply import apply_policy_proposal, build_tuned_policy
4
+ from agent_lifecycle.policy.adaptive_lifecycle import (
5
+ build_adaptive_lifecycle_decision,
6
+ require_adaptive_lifecycle_decision_pass,
7
+ small_model_packet_eligibility,
8
+ validate_adaptive_lifecycle_decision,
9
+ )
10
+ from agent_lifecycle.policy.proposals import (
11
+ build_policy_proposal,
12
+ build_policy_summary,
13
+ require_policy_proposal_pass,
14
+ )
15
+ from agent_lifecycle.policy.quality_floor import quality_floor_mode, resolve_quality_floor
16
+ from agent_lifecycle.policy.runtime_receipts import (
17
+ build_runtime_policy_receipt,
18
+ require_runtime_policy_receipt_pass,
19
+ validate_runtime_policy_receipt,
20
+ )
21
+
22
+ __all__ = [
23
+ "apply_policy_proposal",
24
+ "build_adaptive_lifecycle_decision",
25
+ "build_policy_proposal",
26
+ "build_policy_summary",
27
+ "build_runtime_policy_receipt",
28
+ "build_tuned_policy",
29
+ "quality_floor_mode",
30
+ "require_adaptive_lifecycle_decision_pass",
31
+ "require_policy_proposal_pass",
32
+ "require_runtime_policy_receipt_pass",
33
+ "resolve_quality_floor",
34
+ "small_model_packet_eligibility",
35
+ "validate_adaptive_lifecycle_decision",
36
+ "validate_runtime_policy_receipt",
37
+ ]
@@ -0,0 +1,416 @@
1
+ """Adaptive lifecycle policy decisions from provider-neutral inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+ from agent_lifecycle.metrics.recommendations import validate_lifecycle_baselines
9
+ from agent_lifecycle.policy.quality_floor import MODES, max_mode, mode_index, resolve_quality_floor
10
+ from agent_lifecycle.quality.failure_classification import FAILURE_CLASSES, HIGH_RISK_FAILURE_CLASSES
11
+
12
+ ADAPTIVE_REQUEST_SCHEMA = "agent-adaptive-lifecycle-policy-request.v1"
13
+ ADAPTIVE_DECISION_SCHEMA = "agent-adaptive-lifecycle-policy-decision.v1"
14
+ ADAPTIVE_VALIDATION_SCHEMA = "agent-adaptive-lifecycle-policy-decision-validation.v1"
15
+ RESOURCE_BASIS = "tokens-and-resources"
16
+ SDD_TIERS = {"S0", "S1", "S2"}
17
+ BUDGET_MODES = {"local", "subscription", "metered"}
18
+ FLAKE_STATUSES = {"stable-fail", "stable-pass", "flaky", "inconclusive"}
19
+ FORBIDDEN_NEUTRAL_KEYS = {
20
+ "apikey",
21
+ "auth",
22
+ "authheader",
23
+ "credential",
24
+ "credentials",
25
+ "model",
26
+ "modelid",
27
+ "modelname",
28
+ "password",
29
+ "provider",
30
+ "providerid",
31
+ "providermodel",
32
+ "providermodelhash",
33
+ "providername",
34
+ "secret",
35
+ }
36
+
37
+
38
+ def build_adaptive_lifecycle_decision(request: dict[str, Any], baseline_profile: dict[str, Any]) -> dict[str, Any]:
39
+ """Build a deterministic advisory-or-opt-in lifecycle mode decision."""
40
+
41
+ normalized, blockers = _normalize_request(request)
42
+ blockers.extend(_neutrality_blockers(request))
43
+ blockers.extend(_monetary_blockers(request, budget_mode=normalized["budgetMode"]))
44
+ baseline_validation = validate_lifecycle_baselines(baseline_profile)
45
+ if baseline_validation["status"] != "PASS":
46
+ blockers.extend({"code": "adaptive-baseline-invalid", "blocker": item} for item in baseline_validation["blockers"])
47
+
48
+ floor_decision = resolve_quality_floor(
49
+ task_shape=normalized["taskShape"],
50
+ baseline_profile=baseline_profile,
51
+ sdd_tier=normalized["sddTier"],
52
+ risk_flags=normalized["riskFlags"],
53
+ required_evidence=normalized["requiredEvidence"],
54
+ )
55
+ if floor_decision["status"] != "PASS":
56
+ blockers.extend({"code": "adaptive-quality-floor-invalid", "blocker": item} for item in floor_decision["blockers"])
57
+
58
+ recommended, reasons = _recommended_mode(normalized, baseline_profile, floor_decision["qualityFloor"])
59
+ apply_automatically = normalized["automaticSelectionEnabled"] and not blockers
60
+ body = {
61
+ "schemaVersion": ADAPTIVE_DECISION_SCHEMA,
62
+ "status": "PASS" if not blockers else "FAIL",
63
+ "requestId": normalized["requestId"],
64
+ "taskShape": normalized["taskShape"],
65
+ "sddTier": normalized["sddTier"],
66
+ "currentMode": normalized["currentMode"],
67
+ "recommendedMode": recommended,
68
+ "selectedMode": recommended,
69
+ "qualityFloor": floor_decision["qualityFloor"],
70
+ "qualityFloorPreserved": mode_index(recommended) >= mode_index(floor_decision["qualityFloor"]),
71
+ "automaticSelectionEnabled": normalized["automaticSelectionEnabled"],
72
+ "applyAutomatically": apply_automatically,
73
+ "advisoryOnly": not apply_automatically,
74
+ "reasonCodes": [*floor_decision["reasonCodes"], *reasons],
75
+ "neutralInputs": _neutral_inputs(normalized, monetary_keys=_monetary_keys(request)),
76
+ "resourceBasis": RESOURCE_BASIS,
77
+ "monetaryFieldsUsed": False,
78
+ "providerModelNamesInCore": False,
79
+ "blockers": blockers,
80
+ "qualityFloorDecision": floor_decision,
81
+ "baselineValidation": baseline_validation,
82
+ "requestDigest": canonical_digest(request),
83
+ "baselineProfileDigest": canonical_digest(baseline_profile),
84
+ "productionPromotionClaimed": False,
85
+ }
86
+ return {**body, "decisionDigest": canonical_digest(body)}
87
+
88
+
89
+ def validate_adaptive_lifecycle_decision(decision: dict[str, Any]) -> dict[str, Any]:
90
+ if not isinstance(decision, dict):
91
+ raise LifecycleError("invalid-adaptive-lifecycle-decision", "adaptive lifecycle decision must be an object")
92
+ blockers: list[dict[str, Any]] = []
93
+ if decision.get("schemaVersion") != ADAPTIVE_DECISION_SCHEMA:
94
+ blockers.append({"code": "adaptive-decision-schema-invalid"})
95
+ if decision.get("status") not in {"PASS", "FAIL"}:
96
+ blockers.append({"code": "adaptive-decision-status-invalid"})
97
+ floor = decision.get("qualityFloor")
98
+ selected = decision.get("selectedMode")
99
+ recommended = decision.get("recommendedMode")
100
+ for field, value in [("qualityFloor", floor), ("selectedMode", selected), ("recommendedMode", recommended)]:
101
+ if value not in MODES:
102
+ blockers.append({"code": "adaptive-decision-mode-invalid", "field": field, "mode": value})
103
+ if floor in MODES and selected in MODES and mode_index(selected) < mode_index(floor):
104
+ blockers.append({"code": "adaptive-decision-selected-below-floor", "selectedMode": selected, "qualityFloor": floor})
105
+ if floor in MODES and recommended in MODES and mode_index(recommended) < mode_index(floor):
106
+ blockers.append({"code": "adaptive-decision-recommended-below-floor", "recommendedMode": recommended, "qualityFloor": floor})
107
+ if decision.get("qualityFloorPreserved") is not True:
108
+ blockers.append({"code": "adaptive-decision-quality-floor-not-preserved"})
109
+ if decision.get("resourceBasis") != RESOURCE_BASIS:
110
+ blockers.append({"code": "adaptive-decision-resource-basis-invalid"})
111
+ if decision.get("monetaryFieldsUsed") is not False:
112
+ blockers.append({"code": "adaptive-decision-monetary-fields-used"})
113
+ if decision.get("providerModelNamesInCore") is not False:
114
+ blockers.append({"code": "adaptive-decision-provider-model-core"})
115
+ if decision.get("productionPromotionClaimed") is not False:
116
+ blockers.append({"code": "adaptive-decision-production-claim"})
117
+ if not isinstance(decision.get("automaticSelectionEnabled"), bool):
118
+ blockers.append({"code": "adaptive-decision-auto-flag-invalid"})
119
+ if not isinstance(decision.get("applyAutomatically"), bool):
120
+ blockers.append({"code": "adaptive-decision-apply-flag-invalid"})
121
+ if not isinstance(decision.get("advisoryOnly"), bool):
122
+ blockers.append({"code": "adaptive-decision-advisory-flag-invalid"})
123
+ if decision.get("applyAutomatically") is True and decision.get("automaticSelectionEnabled") is not True:
124
+ blockers.append({"code": "adaptive-decision-auto-apply-without-opt-in"})
125
+ if decision.get("applyAutomatically") is True and decision.get("advisoryOnly") is True:
126
+ blockers.append({"code": "adaptive-decision-auto-apply-advisory"})
127
+ embedded_blockers = decision.get("blockers")
128
+ if not isinstance(embedded_blockers, list) or not all(isinstance(item, dict) for item in embedded_blockers):
129
+ blockers.append({"code": "adaptive-decision-blockers-invalid"})
130
+ embedded_blockers = []
131
+ if decision.get("status") == "PASS" and embedded_blockers:
132
+ blockers.append({"code": "adaptive-decision-pass-with-blockers"})
133
+ if decision.get("status") == "FAIL" and not embedded_blockers:
134
+ blockers.append({"code": "adaptive-decision-fail-without-blockers"})
135
+ expected_digest = canonical_digest({key: value for key, value in decision.items() if key != "decisionDigest"})
136
+ if decision.get("decisionDigest") != expected_digest:
137
+ blockers.append({"code": "adaptive-decision-digest-mismatch"})
138
+ body = {
139
+ "schemaVersion": ADAPTIVE_VALIDATION_SCHEMA,
140
+ "status": "PASS" if not blockers else "FAIL",
141
+ "decisionStatus": decision.get("status") if isinstance(decision.get("status"), str) else None,
142
+ "recommendedMode": recommended if recommended in MODES else None,
143
+ "selectedMode": selected if selected in MODES else None,
144
+ "qualityFloor": floor if floor in MODES else None,
145
+ "qualityFloorPreserved": decision.get("qualityFloorPreserved") is True,
146
+ "applyAutomatically": decision.get("applyAutomatically") is True,
147
+ "advisoryOnly": decision.get("advisoryOnly") is True,
148
+ "blockers": blockers,
149
+ "decisionDigest": decision.get("decisionDigest") if isinstance(decision.get("decisionDigest"), str) else None,
150
+ }
151
+ return {**body, "validationDigest": canonical_digest(body)}
152
+
153
+
154
+ def require_adaptive_lifecycle_decision_pass(validation: dict[str, Any]) -> dict[str, Any]:
155
+ if validation.get("status") != "PASS" or validation.get("decisionStatus") != "PASS":
156
+ raise LifecycleError("adaptive-lifecycle-decision-validation-failed", "adaptive lifecycle decision did not pass", {"validation": validation})
157
+ return validation
158
+
159
+
160
+ def small_model_packet_eligibility(decision: dict[str, Any]) -> dict[str, Any]:
161
+ """Return whether a small-model packet surface is allowed by the decision."""
162
+
163
+ validation = validate_adaptive_lifecycle_decision(decision)
164
+ blockers = []
165
+ if validation.get("status") != "PASS" or validation.get("decisionStatus") != "PASS":
166
+ blockers.append({"code": "small-model-adaptive-decision-invalid", "validation": validation})
167
+ floor = decision.get("qualityFloor")
168
+ if floor not in {"light", "standard"}:
169
+ blockers.append({"code": "small-model-quality-floor-blocked", "qualityFloor": floor})
170
+ body = {
171
+ "status": "PASS" if not blockers else "FAIL",
172
+ "decisionDigest": decision.get("decisionDigest"),
173
+ "qualityFloor": floor,
174
+ "recommendedMode": decision.get("recommendedMode"),
175
+ "smallModelPacketEligible": not blockers,
176
+ "advisoryOnly": decision.get("advisoryOnly") is True,
177
+ "blockers": blockers,
178
+ }
179
+ return {**body, "eligibilityDigest": canonical_digest(body)}
180
+
181
+
182
+ def _normalize_request(request: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
183
+ blockers: list[dict[str, Any]] = []
184
+ if not isinstance(request, dict):
185
+ raise LifecycleError("invalid-adaptive-lifecycle-request", "adaptive lifecycle request must be an object")
186
+ if request.get("schemaVersion") != ADAPTIVE_REQUEST_SCHEMA:
187
+ blockers.append({"code": "adaptive-request-schema-invalid"})
188
+ task_shape = _string(request.get("taskShape"), default="feature")
189
+ if "taskShape" not in request:
190
+ blockers.append({"code": "adaptive-request-task-shape-missing"})
191
+ sdd_tier = _string(request.get("sddTier"), default="S1")
192
+ if sdd_tier not in SDD_TIERS:
193
+ blockers.append({"code": "adaptive-request-sdd-tier-invalid", "sddTier": sdd_tier})
194
+ sdd_tier = "S1"
195
+ risk_flags = _risk_list(request.get("riskFlags"))
196
+ required_evidence = _string_list(request.get("requiredEvidence", []))
197
+ prior_attempts = _non_negative_int(request.get("priorAttempts", 0), field="priorAttempts", blockers=blockers)
198
+ context_tokens = _non_negative_int(request.get("contextTokens", 0), field="contextTokens", blockers=blockers)
199
+ resource_caps = _resource_caps(request.get("resourceCaps", {}), blockers)
200
+ failure_signals = _failure_signals(request.get("failureSignals", {}), blockers)
201
+ budget_mode = _string(request.get("budgetMode"), default="local")
202
+ if budget_mode not in BUDGET_MODES:
203
+ blockers.append({"code": "adaptive-request-budget-mode-invalid", "budgetMode": budget_mode})
204
+ budget_mode = "local"
205
+ current_mode = request.get("currentMode")
206
+ if current_mode is not None and current_mode not in MODES:
207
+ blockers.append({"code": "adaptive-request-current-mode-invalid", "currentMode": current_mode})
208
+ current_mode = None
209
+ automatic = request.get("automaticSelectionEnabled", False)
210
+ if not isinstance(automatic, bool):
211
+ blockers.append({"code": "adaptive-request-auto-flag-invalid"})
212
+ automatic = False
213
+ return (
214
+ {
215
+ "requestId": request.get("requestId") if isinstance(request.get("requestId"), str) and request.get("requestId") else None,
216
+ "taskShape": task_shape,
217
+ "sddTier": sdd_tier,
218
+ "riskFlags": risk_flags,
219
+ "requiredEvidence": required_evidence,
220
+ "priorAttempts": prior_attempts,
221
+ "contextTokens": context_tokens,
222
+ "resourceCaps": resource_caps,
223
+ "failureSignals": failure_signals,
224
+ "budgetMode": budget_mode,
225
+ "currentMode": current_mode,
226
+ "automaticSelectionEnabled": automatic,
227
+ },
228
+ blockers,
229
+ )
230
+
231
+
232
+ def _recommended_mode(normalized: dict[str, Any], baseline_profile: dict[str, Any], quality_floor: str) -> tuple[str, list[str]]:
233
+ reasons: list[str] = []
234
+ shape = {}
235
+ shapes = baseline_profile.get("taskShapes") if isinstance(baseline_profile, dict) else {}
236
+ if isinstance(shapes, dict) and isinstance(shapes.get(normalized["taskShape"]), dict):
237
+ shape = shapes[normalized["taskShape"]]
238
+ default_mode = shape.get("defaultMode") if shape.get("defaultMode") in MODES else "standard"
239
+ target = max_mode(default_mode, quality_floor)
240
+ reasons.append(f"default-mode-{default_mode}")
241
+ if normalized["priorAttempts"] >= 2:
242
+ target = max_mode(target, "strict")
243
+ reasons.append("retry-escalation")
244
+ failure_signals = normalized.get("failureSignals", {})
245
+ failure_class = failure_signals.get("failureClass")
246
+ if failure_class in {"api-contract", "serialization", "permission", "migration", "performance"}:
247
+ target = max_mode(target, "standard")
248
+ reasons.append(f"failure-class-{failure_class}-escalation")
249
+ if failure_class in HIGH_RISK_FAILURE_CLASSES or failure_class == "flaky-test" or failure_signals.get("flakeStatus") == "flaky":
250
+ target = max_mode(target, "strict")
251
+ reasons.append(f"failure-class-{failure_class or 'flake'}-strict-escalation")
252
+ if failure_signals.get("validationStatus") in {"FAIL", "ERROR", "BLOCKED", "REJECTED"}:
253
+ target = max_mode(target, "standard")
254
+ reasons.append("validation-failure-escalation")
255
+ if int(failure_signals.get("retryCount", 0)) >= 2 or int(failure_signals.get("remediationLoops", 0)) >= 2:
256
+ target = max_mode(target, "strict")
257
+ reasons.append("failure-loop-strict-escalation")
258
+ if normalized["currentMode"] in MODES and (normalized["priorAttempts"] > 0 or failure_signals):
259
+ raised = max_mode(target, normalized["currentMode"])
260
+ if raised != target:
261
+ reasons.append("no-downgrade-after-failure")
262
+ target = raised
263
+ if normalized["contextTokens"] >= 64000:
264
+ target = max_mode(target, "strict")
265
+ reasons.append("large-context-escalation")
266
+ elif normalized["contextTokens"] >= 32000:
267
+ target = max_mode(target, "standard")
268
+ reasons.append("medium-context-floor")
269
+ caps = normalized["resourceCaps"]
270
+ if caps.get("maxInvocations") == 1 and quality_floor in {"light", "standard"} and not _high_risk(normalized, shape):
271
+ target = quality_floor
272
+ reasons.append("resource-cap-tight")
273
+ return max_mode(target, quality_floor), reasons
274
+
275
+
276
+ def _neutral_inputs(normalized: dict[str, Any], *, monetary_keys: list[str]) -> dict[str, Any]:
277
+ caps = normalized["resourceCaps"]
278
+ return {
279
+ "taskShape": normalized["taskShape"],
280
+ "sddTier": normalized["sddTier"],
281
+ "riskFlags": list(normalized["riskFlags"]),
282
+ "requiredEvidence": list(normalized["requiredEvidence"]),
283
+ "priorAttempts": normalized["priorAttempts"],
284
+ "contextTokens": normalized["contextTokens"],
285
+ "resourceCaps": {key: caps[key] for key in sorted(caps) if key in {"maxInvocations", "maxWallSeconds", "maxBillableTokens"}},
286
+ "failureSignals": dict(normalized["failureSignals"]),
287
+ "budgetMode": normalized["budgetMode"],
288
+ "automaticSelectionEnabled": normalized["automaticSelectionEnabled"],
289
+ "monetaryMetadataKeys": monetary_keys,
290
+ }
291
+
292
+
293
+ def _resource_caps(value: Any, blockers: list[dict[str, Any]]) -> dict[str, Any]:
294
+ if value is None:
295
+ return {}
296
+ if not isinstance(value, dict):
297
+ blockers.append({"code": "adaptive-request-resource-caps-invalid"})
298
+ return {}
299
+ caps = dict(value)
300
+ for field in ("maxInvocations", "maxWallSeconds", "maxBillableTokens"):
301
+ if field in caps:
302
+ caps[field] = _non_negative_int(caps[field], field=f"resourceCaps.{field}", blockers=blockers)
303
+ return caps
304
+
305
+
306
+ def _failure_signals(value: Any, blockers: list[dict[str, Any]]) -> dict[str, Any]:
307
+ if value in (None, {}):
308
+ return {}
309
+ if not isinstance(value, dict):
310
+ blockers.append({"code": "adaptive-request-failure-signals-invalid"})
311
+ return {}
312
+ failure_class = value.get("failureClass")
313
+ if failure_class is not None and failure_class not in FAILURE_CLASSES:
314
+ blockers.append({"code": "adaptive-request-failure-class-invalid", "failureClass": failure_class})
315
+ failure_class = None
316
+ confidence = value.get("confidence")
317
+ if confidence is not None and confidence not in {"LOW", "MEDIUM", "HIGH"}:
318
+ blockers.append({"code": "adaptive-request-failure-confidence-invalid", "confidence": confidence})
319
+ confidence = None
320
+ flake_status = value.get("flakeStatus")
321
+ if flake_status is not None and flake_status not in FLAKE_STATUSES:
322
+ blockers.append({"code": "adaptive-request-flake-status-invalid", "flakeStatus": flake_status})
323
+ flake_status = None
324
+ validation_status = value.get("validationStatus")
325
+ if validation_status is not None and not isinstance(validation_status, str):
326
+ blockers.append({"code": "adaptive-request-validation-status-invalid"})
327
+ validation_status = None
328
+ classification_digest = value.get("classificationDigest")
329
+ if classification_digest is not None and not _is_digest(classification_digest):
330
+ blockers.append({"code": "adaptive-request-classification-digest-invalid"})
331
+ classification_digest = None
332
+ return {
333
+ "failureClass": failure_class,
334
+ "confidence": confidence,
335
+ "retryCount": _non_negative_int(value.get("retryCount", 0), field="failureSignals.retryCount", blockers=blockers),
336
+ "remediationLoops": _non_negative_int(value.get("remediationLoops", 0), field="failureSignals.remediationLoops", blockers=blockers),
337
+ "validationStatus": validation_status,
338
+ "flakeStatus": flake_status,
339
+ "classificationDigest": classification_digest,
340
+ }
341
+
342
+
343
+ def _monetary_blockers(value: Any, *, budget_mode: str) -> list[dict[str, Any]]:
344
+ blockers: list[dict[str, Any]] = []
345
+ for path, item in _walk(value):
346
+ key = path.rsplit(".", 1)[-1]
347
+ if _is_monetary_key(key) and item is not None and budget_mode != "metered":
348
+ blockers.append({"code": "adaptive-request-monetary-field-not-metered", "path": path})
349
+ return blockers
350
+
351
+
352
+ def _monetary_keys(value: Any) -> list[str]:
353
+ return sorted({path for path, item in _walk(value) if _is_monetary_key(path.rsplit(".", 1)[-1]) and item is not None})
354
+
355
+
356
+ def _neutrality_blockers(value: Any) -> list[dict[str, Any]]:
357
+ blockers = []
358
+ for path, _item in _walk(value):
359
+ key = path.rsplit(".", 1)[-1]
360
+ normalized = key.replace("_", "").replace("-", "").lower()
361
+ if normalized in FORBIDDEN_NEUTRAL_KEYS:
362
+ blockers.append({"code": "adaptive-request-provider-model-key", "path": path})
363
+ return blockers
364
+
365
+
366
+ def _walk(value: Any, prefix: str = "$") -> list[tuple[str, Any]]:
367
+ rows: list[tuple[str, Any]] = []
368
+ if isinstance(value, dict):
369
+ for key, item in value.items():
370
+ path = f"{prefix}.{key}"
371
+ rows.append((path, item))
372
+ rows.extend(_walk(item, path))
373
+ elif isinstance(value, list):
374
+ for index, item in enumerate(value):
375
+ rows.extend(_walk(item, f"{prefix}[{index}]"))
376
+ return rows
377
+
378
+
379
+ def _is_monetary_key(key: str) -> bool:
380
+ normalized = key.replace("_", "").replace("-", "").lower()
381
+ return "usd" in normalized or "cost" in normalized
382
+
383
+
384
+ def _is_digest(value: Any) -> bool:
385
+ return isinstance(value, str) and len(value) == 64 and all(char in "0123456789abcdef" for char in value)
386
+
387
+
388
+ def _risk_list(value: Any) -> list[str]:
389
+ if isinstance(value, dict):
390
+ return sorted(str(key) for key, active in value.items() if active)
391
+ if isinstance(value, list):
392
+ return sorted({item for item in value if isinstance(item, str) and item})
393
+ return []
394
+
395
+
396
+ def _string_list(value: Any) -> list[str]:
397
+ if not isinstance(value, list):
398
+ return []
399
+ return sorted({item for item in value if isinstance(item, str) and item})
400
+
401
+
402
+ def _string(value: Any, *, default: str) -> str:
403
+ return value if isinstance(value, str) and value else default
404
+
405
+
406
+ def _non_negative_int(value: Any, *, field: str, blockers: list[dict[str, Any]]) -> int:
407
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
408
+ blockers.append({"code": "adaptive-request-non-negative-int-invalid", "field": field, "value": value})
409
+ return 0
410
+ return value
411
+
412
+
413
+ def _high_risk(normalized: dict[str, Any], shape: dict[str, Any]) -> bool:
414
+ if shape.get("highRisk") is True:
415
+ return True
416
+ return bool(set(normalized["riskFlags"]).intersection({"security", "contracts", "adapter", "architecture", "release"}))
@@ -0,0 +1,44 @@
1
+ """Explicit write path for approved lifecycle policy proposals."""
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, write_json_create
9
+
10
+ APPLY_RESULT_SCHEMA = "agent-lifecycle-policy-apply-result.v1"
11
+ TUNED_POLICY_SCHEMA = "agent-lifecycle-tuned-policy.v1"
12
+
13
+
14
+ def build_tuned_policy(proposal: dict[str, Any]) -> dict[str, Any]:
15
+ if proposal.get("applyAllowed") is not True:
16
+ raise LifecycleError("policy-apply-not-allowed", "policy proposal is not safe to apply", {"proposal": proposal})
17
+ changes = [item for item in proposal.get("candidateChanges", []) if isinstance(item, dict) and item.get("applies") is True]
18
+ body = {
19
+ "schemaVersion": TUNED_POLICY_SCHEMA,
20
+ "status": "PASS",
21
+ "sourceProposalDigest": proposal.get("proposalDigest") or canonical_digest(proposal),
22
+ "changes": changes,
23
+ "rollback": proposal.get("rollback"),
24
+ "qualityConstraints": proposal.get("qualityConstraints"),
25
+ "productionPromotionClaimed": False,
26
+ }
27
+ return {**body, "policyDigest": canonical_digest(body)}
28
+
29
+
30
+ def apply_policy_proposal(proposal: dict[str, Any], output_path: Path) -> dict[str, Any]:
31
+ policy = build_tuned_policy(proposal)
32
+ payload = write_json_create(output_path, policy)
33
+ body = {
34
+ "schemaVersion": APPLY_RESULT_SCHEMA,
35
+ "status": "PASS",
36
+ "outputPath": output_path.as_posix(),
37
+ "outputBytes": len(payload),
38
+ "outputDigest": canonical_digest(policy),
39
+ "proposalDigest": proposal.get("proposalDigest") or canonical_digest(proposal),
40
+ "changed": bool(policy["changes"]),
41
+ "liveCallsStarted": False,
42
+ "productionPromotionClaimed": False,
43
+ }
44
+ return {**body, "applyDigest": canonical_digest(body)}