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,189 @@
1
+ """Build read-only lifecycle policy tuning proposals."""
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 import summarize_regression_signals
9
+ from agent_lifecycle.policy.quality_floor import is_downgrade, protected_work
10
+
11
+ PROPOSAL_SCHEMA = "agent-lifecycle-policy-proposal.v1"
12
+ SUMMARY_SCHEMA = "agent-lifecycle-policy-summary.v1"
13
+ RECOMMENDATION_SCHEMA = "agent-lifecycle-recommendation.v1"
14
+
15
+
16
+ def build_policy_proposal(
17
+ recommendation: dict[str, Any],
18
+ *,
19
+ regression_signals: list[dict[str, Any]] | None = None,
20
+ risk_flags: list[str] | None = None,
21
+ proposal_id: str = "lifecycle-policy-proposal",
22
+ ) -> dict[str, Any]:
23
+ blockers = _recommendation_blockers(recommendation)
24
+ regression_summary = summarize_regression_signals(regression_signals)
25
+ if regression_summary["status"] == "FAIL":
26
+ blockers.extend(regression_summary["blockers"])
27
+ if blockers:
28
+ body = _base_proposal(proposal_id, recommendation, regression_summary, status="FAIL")
29
+ body.update({"applyAllowed": False, "refusalReasons": blockers, "candidateChanges": []})
30
+ return _with_summary_and_digest(body)
31
+
32
+ before = str(recommendation.get("currentMode") or recommendation.get("recommendedMode") or "standard")
33
+ after = str(recommendation.get("recommendedMode") or before)
34
+ protected = protected_work(recommendation, risk_flags=risk_flags)
35
+ refusing = _refusal_reasons(recommendation, regression_summary, before=before, after=after, protected=protected)
36
+ candidate_changes = [_candidate_change(recommendation, before=before, after=after, applies=not refusing)]
37
+ body = _base_proposal(proposal_id, recommendation, regression_summary, status="PASS")
38
+ body.update(
39
+ {
40
+ "applyAllowed": not refusing,
41
+ "refusalReasons": refusing,
42
+ "candidateChanges": candidate_changes,
43
+ "expectedBenefit": _expected_benefit(recommendation, before=before, after=after),
44
+ "qualityConstraints": _quality_constraints(recommendation, protected=protected),
45
+ "rollback": _rollback(candidate_changes),
46
+ }
47
+ )
48
+ return _with_summary_and_digest(body)
49
+
50
+
51
+ def build_policy_summary(proposal: dict[str, Any]) -> dict[str, Any]:
52
+ return {
53
+ "schemaVersion": SUMMARY_SCHEMA,
54
+ "latestUserIntent": "Tune lifecycle policy only when it reduces overhead without lowering required quality.",
55
+ "activeDecisions": [
56
+ f"status={proposal.get('status')}",
57
+ f"applyAllowed={proposal.get('applyAllowed')}",
58
+ f"proposalId={proposal.get('proposalId')}",
59
+ ],
60
+ "openBlockers": list(proposal.get("refusalReasons", [])),
61
+ "acceptedEvidence": [
62
+ {
63
+ "id": "policy-proposal",
64
+ "status": proposal.get("status"),
65
+ "applyAllowed": proposal.get("applyAllowed") is True,
66
+ }
67
+ ],
68
+ "changedFiles": [],
69
+ "nextRequiredAction": "review proposal and apply only with explicit approval",
70
+ "doNotDo": [
71
+ "Do not auto-apply policy changes.",
72
+ "Do not reduce validation for protected task classes.",
73
+ ],
74
+ "applyAllowed": proposal.get("applyAllowed") is True,
75
+ "candidateChanges": proposal.get("candidateChanges", [])[:8],
76
+ "qualityConstraints": proposal.get("qualityConstraints", {}),
77
+ }
78
+
79
+
80
+ def require_policy_proposal_pass(proposal: dict[str, Any]) -> dict[str, Any]:
81
+ if proposal.get("status") == "FAIL":
82
+ raise LifecycleError("policy-proposal-failed", "policy proposal validation failed", {"proposal": proposal})
83
+ return proposal
84
+
85
+
86
+ def _recommendation_blockers(recommendation: dict[str, Any]) -> list[dict[str, Any]]:
87
+ blockers: list[dict[str, Any]] = []
88
+ if recommendation.get("schemaVersion") != RECOMMENDATION_SCHEMA:
89
+ blockers.append({"code": "policy-recommendation-schema"})
90
+ if recommendation.get("status") != "PASS":
91
+ blockers.append({"code": "policy-recommendation-status", "status": recommendation.get("status")})
92
+ if recommendation.get("advisoryOnly") is not True or recommendation.get("autoApply") is not False:
93
+ blockers.append({"code": "policy-recommendation-safety-flags"})
94
+ if recommendation.get("qualityFloorPreserved") is not True:
95
+ blockers.append({"code": "policy-recommendation-quality-floor"})
96
+ return blockers
97
+
98
+
99
+ def _refusal_reasons(
100
+ recommendation: dict[str, Any],
101
+ regression_summary: dict[str, Any],
102
+ *,
103
+ before: str,
104
+ after: str,
105
+ protected: bool,
106
+ ) -> list[dict[str, Any]]:
107
+ reasons: list[dict[str, Any]] = []
108
+ if recommendation.get("confidence") == "LOW":
109
+ reasons.append({"code": "policy-low-confidence"})
110
+ if before == after:
111
+ reasons.append({"code": "policy-no-change", "mode": before})
112
+ if protected and is_downgrade(before, after):
113
+ reasons.append({"code": "policy-protected-downgrade", "before": before, "after": after})
114
+ if regression_summary["status"] == "BLOCK":
115
+ reasons.append({"code": "policy-regression-signals", "count": len(regression_summary["blockingSignals"])})
116
+ return reasons
117
+
118
+
119
+ def _base_proposal(
120
+ proposal_id: str,
121
+ recommendation: dict[str, Any],
122
+ regression_summary: dict[str, Any],
123
+ *,
124
+ status: str,
125
+ ) -> dict[str, Any]:
126
+ return {
127
+ "schemaVersion": PROPOSAL_SCHEMA,
128
+ "status": status,
129
+ "proposalId": proposal_id,
130
+ "taskShape": recommendation.get("taskShape"),
131
+ "sourceRecommendationDigest": recommendation.get("recommendationDigest") or canonical_digest(recommendation),
132
+ "regressionSignals": regression_summary,
133
+ "advisoryOnly": True,
134
+ "autoApply": False,
135
+ "productionPromotionClaimed": False,
136
+ }
137
+
138
+
139
+ def _candidate_change(recommendation: dict[str, Any], *, before: str, after: str, applies: bool) -> dict[str, Any]:
140
+ task_shape = recommendation.get("taskShape") or "feature"
141
+ return {
142
+ "path": f"taskShapes.{task_shape}.defaultMode",
143
+ "before": before,
144
+ "after": after,
145
+ "applies": applies,
146
+ }
147
+
148
+
149
+ def _expected_benefit(recommendation: dict[str, Any], *, before: str, after: str) -> dict[str, Any]:
150
+ stats = recommendation.get("statistics") if isinstance(recommendation.get("statistics"), dict) else {}
151
+ selected_signal = stats.get("selectedSignal") if isinstance(stats.get("selectedSignal"), dict) else {}
152
+ totals = stats.get("totals") if isinstance(stats.get("totals"), dict) else {}
153
+ pipeline = totals.get("pipelineCompliance", {}) if isinstance(totals.get("pipelineCompliance"), dict) else {}
154
+ coordination = totals.get("coordination", {}) if isinstance(totals.get("coordination"), dict) else {}
155
+ return {
156
+ "kind": "reduce-process-overhead" if is_downgrade(before, after) else "preserve-or-increase-quality",
157
+ "processOverheadTokens": int(pipeline.get("tokens", 0)) + int(coordination.get("tokens", 0)),
158
+ "pipelineTokenShare": (stats.get("ratios") or {}).get("pipelineTokenShare", 0.0),
159
+ "localAverageTokens": selected_signal.get("averageTokens"),
160
+ "localSuccessRate": selected_signal.get("successRate"),
161
+ }
162
+
163
+
164
+ def _quality_constraints(recommendation: dict[str, Any], *, protected: bool) -> dict[str, Any]:
165
+ return {
166
+ "qualityFloor": recommendation.get("qualityFloor"),
167
+ "qualityFloorPreserved": True,
168
+ "protectedWork": protected,
169
+ "nonNegotiable": [
170
+ "acceptance evidence",
171
+ "final proof",
172
+ "security review for security-sensitive work",
173
+ "release review for release work",
174
+ "adapter promotion evidence for adapter maturity claims",
175
+ ],
176
+ }
177
+
178
+
179
+ def _rollback(changes: list[dict[str, Any]]) -> dict[str, Any]:
180
+ return {
181
+ "strategy": "restore previous policy values and rerun recommendation",
182
+ "restore": [{"path": item["path"], "value": item["before"]} for item in changes],
183
+ "requiresReview": True,
184
+ }
185
+
186
+
187
+ def _with_summary_and_digest(body: dict[str, Any]) -> dict[str, Any]:
188
+ body["compactSummary"] = build_policy_summary(body)
189
+ return {**body, "proposalDigest": canonical_digest(body)}
@@ -0,0 +1,144 @@
1
+ """Quality-floor helpers for lifecycle policy proposals and decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import canonical_digest
8
+ from agent_lifecycle.metrics.costs import DEFAULT_MODE_LIMITS
9
+
10
+ MODES = tuple(DEFAULT_MODE_LIMITS)
11
+ PROTECTED_TASK_SHAPES = {"adapter", "architecture", "release"}
12
+ PROTECTED_RISKS = {"security", "contracts", "adapter", "architecture", "release", "migration", "dataMigration", "S2"}
13
+ QUALITY_FLOOR_DECISION_SCHEMA = "agent-lifecycle-quality-floor-decision.v1"
14
+ REQUIRED_EVIDENCE_FLOORS = {
15
+ "adapter-promotion": "release",
16
+ "production-promotion": "release",
17
+ "release-proof": "release",
18
+ "bug-forensics": "strict",
19
+ "cross-check": "strict",
20
+ "proof-integrity": "strict",
21
+ "sandbox": "strict",
22
+ "security-review": "strict",
23
+ }
24
+
25
+
26
+ def mode_index(mode: str | None) -> int:
27
+ return MODES.index(mode) if mode in MODES else MODES.index("standard")
28
+
29
+
30
+ def is_downgrade(before: str | None, after: str | None) -> bool:
31
+ return mode_index(after) < mode_index(before)
32
+
33
+
34
+ def max_mode(*modes: str | None) -> str:
35
+ valid = [mode for mode in modes if mode in MODES]
36
+ if not valid:
37
+ return "standard"
38
+ return max(valid, key=mode_index)
39
+
40
+
41
+ def quality_floor_mode(
42
+ *,
43
+ task_shape: str,
44
+ baseline_profile: dict[str, Any],
45
+ sdd_tier: str | None = None,
46
+ risk_flags: list[str] | dict[str, Any] | None = None,
47
+ required_evidence: list[str] | None = None,
48
+ ) -> str:
49
+ """Return the minimum safe lifecycle mode for the given neutral inputs."""
50
+
51
+ return resolve_quality_floor(
52
+ task_shape=task_shape,
53
+ baseline_profile=baseline_profile,
54
+ sdd_tier=sdd_tier,
55
+ risk_flags=risk_flags,
56
+ required_evidence=required_evidence,
57
+ )["qualityFloor"]
58
+
59
+
60
+ def resolve_quality_floor(
61
+ *,
62
+ task_shape: str,
63
+ baseline_profile: dict[str, Any],
64
+ sdd_tier: str | None = None,
65
+ risk_flags: list[str] | dict[str, Any] | None = None,
66
+ required_evidence: list[str] | None = None,
67
+ ) -> dict[str, Any]:
68
+ """Build a deterministic quality-floor receipt from baseline policy inputs."""
69
+
70
+ blockers: list[dict[str, Any]] = []
71
+ reasons: list[str] = []
72
+ risks = _risk_list(risk_flags)
73
+ evidence = sorted({item for item in (required_evidence or []) if isinstance(item, str) and item})
74
+ shapes = baseline_profile.get("taskShapes") if isinstance(baseline_profile, dict) else None
75
+ shape_config = shapes.get(task_shape) if isinstance(shapes, dict) and isinstance(shapes.get(task_shape), dict) else None
76
+ floor = "standard"
77
+ min_mode: str | None = None
78
+ if shape_config is None:
79
+ blockers.append({"code": "quality-floor-task-shape-missing", "taskShape": task_shape})
80
+ reasons.append("task-shape-missing")
81
+ else:
82
+ min_mode = shape_config.get("minMode") if isinstance(shape_config.get("minMode"), str) else None
83
+ if min_mode not in MODES:
84
+ blockers.append({"code": "quality-floor-min-mode-invalid", "taskShape": task_shape, "mode": min_mode})
85
+ reasons.append("task-shape-min-mode-invalid")
86
+ else:
87
+ floor = min_mode
88
+ reasons.append(f"task-shape-min-mode-{floor}")
89
+ if shape_config.get("highRisk") is True:
90
+ floor = max_mode(floor, "strict")
91
+ reasons.append("task-shape-high-risk")
92
+
93
+ risk_floors = baseline_profile.get("riskFloors") if isinstance(baseline_profile, dict) else None
94
+ if not isinstance(risk_floors, dict):
95
+ blockers.append({"code": "quality-floor-risk-floors-invalid"})
96
+ risk_floors = {}
97
+ for risk in [*risks, sdd_tier]:
98
+ if isinstance(risk, str) and risk in risk_floors:
99
+ risk_floor = risk_floors[risk]
100
+ if risk_floor in MODES:
101
+ floor = max_mode(floor, str(risk_floor))
102
+ reasons.append(f"risk-floor-{risk}-{risk_floor}")
103
+ else:
104
+ blockers.append({"code": "quality-floor-risk-mode-invalid", "risk": risk, "mode": risk_floor})
105
+ for item in evidence:
106
+ evidence_floor = REQUIRED_EVIDENCE_FLOORS.get(item)
107
+ if evidence_floor is not None:
108
+ floor = max_mode(floor, evidence_floor)
109
+ reasons.append(f"evidence-floor-{item}-{evidence_floor}")
110
+
111
+ body = {
112
+ "schemaVersion": QUALITY_FLOOR_DECISION_SCHEMA,
113
+ "status": "PASS" if not blockers else "FAIL",
114
+ "taskShape": task_shape,
115
+ "sddTier": sdd_tier,
116
+ "riskFlags": risks,
117
+ "requiredEvidence": evidence,
118
+ "minMode": min_mode,
119
+ "qualityFloor": floor,
120
+ "reasonCodes": reasons,
121
+ "blockers": blockers,
122
+ "baselineProfileDigest": canonical_digest(baseline_profile),
123
+ "productionPromotionClaimed": False,
124
+ }
125
+ return {**body, "floorDigest": canonical_digest(body)}
126
+
127
+
128
+ def protected_work(recommendation: dict[str, Any], risk_flags: list[str] | None = None) -> bool:
129
+ task_shape = recommendation.get("taskShape")
130
+ quality_floor = recommendation.get("qualityFloor")
131
+ risks = set(risk_flags or [])
132
+ if isinstance(task_shape, str) and task_shape in PROTECTED_TASK_SHAPES:
133
+ return True
134
+ if isinstance(quality_floor, str) and quality_floor in {"strict", "release"}:
135
+ return True
136
+ return bool(risks.intersection(PROTECTED_RISKS))
137
+
138
+
139
+ def _risk_list(risk_flags: list[str] | dict[str, Any] | None) -> list[str]:
140
+ if isinstance(risk_flags, dict):
141
+ return sorted(str(key) for key, value in risk_flags.items() if value)
142
+ if isinstance(risk_flags, list):
143
+ return sorted({item for item in risk_flags if isinstance(item, str) and item})
144
+ return []
@@ -0,0 +1,160 @@
1
+ """Runtime policy receipts for enforceable or advisory host decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+
9
+ RUNTIME_POLICY_RECEIPT_SCHEMA = "agent-runtime-policy-receipt.v1"
10
+ RUNTIME_POLICY_RECEIPT_VALIDATION_SCHEMA = "agent-runtime-policy-receipt-validation.v1"
11
+
12
+ RUNTIME_ACTIONS = {"ALLOW", "DENY", "ASK"}
13
+ ENFORCEMENT_MODES = {"enforced", "advisory"}
14
+
15
+
16
+ def build_runtime_policy_receipt(
17
+ *,
18
+ policy_id: str,
19
+ action: str,
20
+ subject: dict[str, Any],
21
+ adapter_evidence: dict[str, Any],
22
+ enforcement_mode: str = "advisory",
23
+ evidence_ids: list[str] | None = None,
24
+ ) -> dict[str, Any]:
25
+ """Build a receipt without claiming enforcement unless evidence proves it."""
26
+
27
+ mode = _enum(enforcement_mode, ENFORCEMENT_MODES, label="enforcementMode")
28
+ body = {
29
+ "schemaVersion": RUNTIME_POLICY_RECEIPT_SCHEMA,
30
+ "status": "PASS",
31
+ "policyId": _required_string(policy_id, label="policyId"),
32
+ "action": _enum(action, RUNTIME_ACTIONS, label="action"),
33
+ "subject": _object(subject, label="subject"),
34
+ "adapterEvidence": _object(adapter_evidence, label="adapterEvidence"),
35
+ "enforcementMode": mode,
36
+ "enforcementClaimed": mode == "enforced",
37
+ "advisoryOnly": mode == "advisory",
38
+ "decisionRecordedBeforeExecution": adapter_evidence.get("decisionRecordedBeforeExecution") is True,
39
+ "evidenceIds": _string_list(evidence_ids or [], label="evidenceIds", allow_empty=True),
40
+ "blockers": [],
41
+ "productionPromotionClaimed": False,
42
+ }
43
+ blockers = _runtime_policy_blockers(body)
44
+ body["status"] = "PASS" if not blockers else "FAIL"
45
+ body["blockers"] = blockers
46
+ return {**body, "receiptDigest": canonical_digest(body)}
47
+
48
+
49
+ def validate_runtime_policy_receipt(receipt: dict[str, Any]) -> dict[str, Any]:
50
+ if not isinstance(receipt, dict):
51
+ raise LifecycleError("invalid-runtime-policy-receipt", "runtime policy receipt must be an object")
52
+ blockers: list[dict[str, Any]] = []
53
+ if receipt.get("schemaVersion") != RUNTIME_POLICY_RECEIPT_SCHEMA:
54
+ blockers.append({"code": "runtime-policy-receipt-schema-invalid"})
55
+ if receipt.get("status") not in {"PASS", "FAIL"}:
56
+ blockers.append({"code": "runtime-policy-receipt-status-invalid"})
57
+ if not isinstance(receipt.get("policyId"), str) or not receipt["policyId"]:
58
+ blockers.append({"code": "runtime-policy-id-missing"})
59
+ if receipt.get("action") not in RUNTIME_ACTIONS:
60
+ blockers.append({"code": "runtime-policy-action-invalid", "action": receipt.get("action")})
61
+ if not isinstance(receipt.get("subject"), dict) or not receipt["subject"]:
62
+ blockers.append({"code": "runtime-policy-subject-invalid"})
63
+ if not isinstance(receipt.get("adapterEvidence"), dict) or not receipt["adapterEvidence"]:
64
+ blockers.append({"code": "runtime-policy-adapter-evidence-invalid"})
65
+ if receipt.get("enforcementMode") not in ENFORCEMENT_MODES:
66
+ blockers.append({"code": "runtime-policy-enforcement-mode-invalid"})
67
+ if not isinstance(receipt.get("enforcementClaimed"), bool):
68
+ blockers.append({"code": "runtime-policy-enforcement-claim-invalid"})
69
+ if not isinstance(receipt.get("advisoryOnly"), bool):
70
+ blockers.append({"code": "runtime-policy-advisory-flag-invalid"})
71
+ if not isinstance(receipt.get("decisionRecordedBeforeExecution"), bool):
72
+ blockers.append({"code": "runtime-policy-decision-timing-invalid"})
73
+ if not isinstance(receipt.get("evidenceIds", []), list) or not all(
74
+ isinstance(item, str) and item for item in receipt.get("evidenceIds", [])
75
+ ):
76
+ blockers.append({"code": "runtime-policy-evidence-ids-invalid"})
77
+ if not isinstance(receipt.get("blockers", []), list) or not all(
78
+ isinstance(item, dict) for item in receipt.get("blockers", [])
79
+ ):
80
+ blockers.append({"code": "runtime-policy-blockers-invalid"})
81
+ if receipt.get("productionPromotionClaimed") is not False:
82
+ blockers.append({"code": "runtime-policy-production-claim"})
83
+ semantic_blockers = _runtime_policy_blockers(receipt) if not blockers else []
84
+ if not blockers:
85
+ if receipt.get("blockers", []) != semantic_blockers:
86
+ blockers.append({"code": "runtime-policy-blockers-mismatch"})
87
+ blockers.extend(semantic_blockers)
88
+ expected_status = "PASS" if not semantic_blockers else "FAIL"
89
+ if receipt.get("status") != expected_status:
90
+ blockers.append({"code": "runtime-policy-status-mismatch"})
91
+ expected_digest = canonical_digest({key: value for key, value in receipt.items() if key != "receiptDigest"})
92
+ if receipt.get("receiptDigest") != expected_digest:
93
+ blockers.append({"code": "runtime-policy-receipt-digest-mismatch"})
94
+ body = {
95
+ "schemaVersion": RUNTIME_POLICY_RECEIPT_VALIDATION_SCHEMA,
96
+ "status": "PASS" if not blockers else "FAIL",
97
+ "receiptStatus": receipt.get("status") if isinstance(receipt.get("status"), str) else None,
98
+ "policyId": receipt.get("policyId") if isinstance(receipt.get("policyId"), str) else None,
99
+ "action": receipt.get("action") if isinstance(receipt.get("action"), str) else None,
100
+ "enforcementMode": receipt.get("enforcementMode") if isinstance(receipt.get("enforcementMode"), str) else None,
101
+ "enforcementClaimed": receipt.get("enforcementClaimed") if isinstance(receipt.get("enforcementClaimed"), bool) else None,
102
+ "blockers": blockers,
103
+ "receiptDigest": receipt.get("receiptDigest") if isinstance(receipt.get("receiptDigest"), str) else None,
104
+ }
105
+ return {**body, "validationDigest": canonical_digest(body)}
106
+
107
+
108
+ def require_runtime_policy_receipt_pass(validation: dict[str, Any]) -> dict[str, Any]:
109
+ if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
110
+ raise LifecycleError("runtime-policy-receipt-validation-failed", "runtime policy receipt did not pass", {"validation": validation})
111
+ return validation
112
+
113
+
114
+ def _runtime_policy_blockers(receipt: dict[str, Any]) -> list[dict[str, Any]]:
115
+ blockers: list[dict[str, Any]] = []
116
+ mode = receipt.get("enforcementMode")
117
+ enforcement_claimed = receipt.get("enforcementClaimed") is True
118
+ advisory_only = receipt.get("advisoryOnly") is True
119
+ evidence = receipt.get("adapterEvidence") if isinstance(receipt.get("adapterEvidence"), dict) else {}
120
+ pre_execution = evidence.get("preExecutionEnforcement") is True
121
+ decision_before_execution = receipt.get("decisionRecordedBeforeExecution") is True
122
+ if mode == "enforced" and not enforcement_claimed:
123
+ blockers.append({"code": "runtime-policy-enforcement-flag-mismatch"})
124
+ if mode == "advisory" and not advisory_only:
125
+ blockers.append({"code": "runtime-policy-advisory-flag-mismatch"})
126
+ if enforcement_claimed and advisory_only:
127
+ blockers.append({"code": "runtime-policy-enforcement-and-advisory"})
128
+ if enforcement_claimed and not pre_execution:
129
+ blockers.append({"code": "runtime-policy-enforcement-unproven"})
130
+ if enforcement_claimed and not decision_before_execution:
131
+ blockers.append({"code": "runtime-policy-decision-not-pre-execution"})
132
+ if not enforcement_claimed and mode == "enforced":
133
+ blockers.append({"code": "runtime-policy-mode-unproven"})
134
+ if mode == "advisory" and evidence.get("postFactumOnly") is True and enforcement_claimed:
135
+ blockers.append({"code": "runtime-policy-post-factum-enforcement-claim"})
136
+ return blockers
137
+
138
+
139
+ def _object(value: Any, *, label: str) -> dict[str, Any]:
140
+ if not isinstance(value, dict) or not value:
141
+ raise LifecycleError("invalid-runtime-policy-receipt", f"{label} must be a non-empty object")
142
+ return dict(value)
143
+
144
+
145
+ def _required_string(value: Any, *, label: str) -> str:
146
+ if not isinstance(value, str) or not value:
147
+ raise LifecycleError("invalid-runtime-policy-receipt", f"{label} is required")
148
+ return value
149
+
150
+
151
+ def _string_list(value: Any, *, label: str, allow_empty: bool = False) -> list[str]:
152
+ if not isinstance(value, list) or (not allow_empty and not value) or not all(isinstance(item, str) and item for item in value):
153
+ raise LifecycleError("invalid-runtime-policy-receipt", f"{label} must be a list of strings")
154
+ return list(value)
155
+
156
+
157
+ def _enum(value: Any, allowed: set[str], *, label: str) -> str:
158
+ if value not in allowed:
159
+ raise LifecycleError("invalid-runtime-policy-receipt", f"{label} is unsupported", {label: value})
160
+ return str(value)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,89 @@
1
+ """Optional quality packs and behavior checks."""
2
+
3
+ from agent_lifecycle.quality.cross_check import (
4
+ build_cross_check_profile,
5
+ build_cross_check_receipt,
6
+ require_cross_check_profile_pass,
7
+ require_cross_check_receipt_pass,
8
+ validate_cross_check_profile,
9
+ validate_cross_check_receipt,
10
+ )
11
+ from agent_lifecycle.quality.bug_forensics import (
12
+ build_bug_forensics_profile,
13
+ build_bug_reproduction_receipt,
14
+ build_failure_fingerprint,
15
+ build_hypothesis_ledger,
16
+ build_regression_proof_receipt,
17
+ require_bug_forensics_profile_pass,
18
+ require_bug_reproduction_pass,
19
+ require_hypothesis_ledger_pass,
20
+ require_regression_proof_pass,
21
+ validate_bug_forensics_profile,
22
+ validate_bug_reproduction_receipt,
23
+ validate_failure_fingerprint,
24
+ validate_hypothesis_ledger,
25
+ validate_regression_proof_receipt,
26
+ )
27
+ from agent_lifecycle.quality.bug_forensics_advisor import (
28
+ BUG_FORENSICS_ADVISORY_SCHEMA,
29
+ BUG_FORENSICS_PROFILE_ID,
30
+ bug_forensics_recommended,
31
+ build_bug_forensics_advisory,
32
+ )
33
+ from agent_lifecycle.quality.bug_forensics_recipes import (
34
+ build_bug_forensics_recipe_library,
35
+ require_bug_forensics_recipe_pass,
36
+ validate_bug_forensics_recipe_library,
37
+ )
38
+ from agent_lifecycle.quality.failure_classification import (
39
+ FAILURE_CLASSES,
40
+ build_failure_classification_receipt,
41
+ require_failure_classification_pass,
42
+ validate_failure_classification_receipt,
43
+ )
44
+ from agent_lifecycle.quality.packs import (
45
+ build_default_quality_pack,
46
+ require_behavior_checks_pass,
47
+ require_quality_pack_pass,
48
+ run_behavior_checks,
49
+ validate_quality_pack,
50
+ )
51
+
52
+ __all__ = [
53
+ "build_cross_check_profile",
54
+ "build_bug_forensics_profile",
55
+ "build_bug_forensics_advisory",
56
+ "build_bug_forensics_recipe_library",
57
+ "build_bug_reproduction_receipt",
58
+ "build_cross_check_receipt",
59
+ "build_failure_fingerprint",
60
+ "build_failure_classification_receipt",
61
+ "build_hypothesis_ledger",
62
+ "build_regression_proof_receipt",
63
+ "build_default_quality_pack",
64
+ "require_bug_forensics_profile_pass",
65
+ "require_bug_forensics_recipe_pass",
66
+ "require_bug_reproduction_pass",
67
+ "require_cross_check_profile_pass",
68
+ "require_cross_check_receipt_pass",
69
+ "require_failure_classification_pass",
70
+ "require_hypothesis_ledger_pass",
71
+ "require_regression_proof_pass",
72
+ "require_behavior_checks_pass",
73
+ "require_quality_pack_pass",
74
+ "bug_forensics_recommended",
75
+ "run_behavior_checks",
76
+ "validate_bug_forensics_profile",
77
+ "validate_bug_forensics_recipe_library",
78
+ "validate_bug_reproduction_receipt",
79
+ "validate_cross_check_profile",
80
+ "validate_cross_check_receipt",
81
+ "validate_failure_classification_receipt",
82
+ "validate_failure_fingerprint",
83
+ "validate_hypothesis_ledger",
84
+ "validate_regression_proof_receipt",
85
+ "validate_quality_pack",
86
+ "FAILURE_CLASSES",
87
+ "BUG_FORENSICS_ADVISORY_SCHEMA",
88
+ "BUG_FORENSICS_PROFILE_ID",
89
+ ]