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,318 @@
1
+ """Neutral failure classification receipts for escalation decisions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
9
+
10
+ FAILURE_CLASSIFICATION_SCHEMA = "agent-failure-classification-receipt.v1"
11
+ FAILURE_CLASSIFICATION_VALIDATION_SCHEMA = "agent-failure-classification-validation.v1"
12
+
13
+ FAILURE_CLASSES = (
14
+ "edge-case",
15
+ "api-contract",
16
+ "serialization",
17
+ "race",
18
+ "permission",
19
+ "migration",
20
+ "performance",
21
+ "flaky-test",
22
+ "security-bug",
23
+ "unknown",
24
+ )
25
+ CONFIDENCE_LEVELS = {"LOW", "MEDIUM", "HIGH"}
26
+ HIGH_RISK_FAILURE_CLASSES = {"security-bug", "race"}
27
+ FORBIDDEN_CORE_KEYS = {
28
+ "provider",
29
+ "providerid",
30
+ "providermodel",
31
+ "providermodelhash",
32
+ "providername",
33
+ "modelid",
34
+ "modelname",
35
+ }
36
+
37
+ CLASS_PATTERNS: dict[str, tuple[str, ...]] = {
38
+ "security-bug": ("security", "vulnerability", "xss", "csrf", "sql injection", "token leak", "secret leak"),
39
+ "race": ("race", "deadlock", "concurrent", "mutex", "thread", "lock contention", "async ordering"),
40
+ "flaky-test": ("flaky", "intermittent", "non-deterministic", "rerun passed", "passes on retry"),
41
+ "permission": ("permissionerror", "permission denied", "forbidden", "access denied", "unauthorized"),
42
+ "performance": ("timeout", "latency", "slow", "performance", "oom", "out of memory", "cpu", "memory"),
43
+ "migration": ("migration", "schema version", "database version", "alembic", "migrate"),
44
+ "serialization": ("serialize", "deserialize", "serialization", "jsondecode", "json decode", "yaml", "encoding"),
45
+ "api-contract": ("contract", "schema mismatch", "status code", "http ", "expected", "assertionerror"),
46
+ "edge-case": ("none", "null", "nil", "empty", "missing", "boundary", "indexerror", "out of range"),
47
+ }
48
+
49
+
50
+ def build_failure_classification_receipt(
51
+ *,
52
+ failure: dict[str, Any],
53
+ evidence: list[dict[str, Any]] | None = None,
54
+ failure_fingerprint: dict[str, Any] | None = None,
55
+ flake_signal: dict[str, Any] | None = None,
56
+ evidence_ids: list[str] | None = None,
57
+ declared_class: str | None = None,
58
+ ) -> dict[str, Any]:
59
+ """Classify a failure using neutral evidence, not provider or model names."""
60
+
61
+ failure_body = _object(failure, "failure")
62
+ evidence_rows = _evidence_rows(evidence, failure_body=failure_body, flake_signal=flake_signal)
63
+ blockers = _neutrality_blockers({"failure": failure_body, "evidence": evidence_rows, "flakeSignal": flake_signal})
64
+ selected, matched = _classify(failure_body, evidence_rows, flake_signal=flake_signal, declared_class=declared_class)
65
+ confidence = _confidence(selected, matched, failure_fingerprint=failure_fingerprint, flake_signal=flake_signal, declared_class=declared_class)
66
+ if declared_class is not None and declared_class not in FAILURE_CLASSES:
67
+ blockers.append({"code": "failure-classification-declared-class-invalid", "failureClass": declared_class})
68
+ body = {
69
+ "schemaVersion": FAILURE_CLASSIFICATION_SCHEMA,
70
+ "status": "PASS" if not blockers else "FAIL",
71
+ "failureClass": selected,
72
+ "confidence": confidence,
73
+ "highRiskFailureClass": selected in HIGH_RISK_FAILURE_CLASSES,
74
+ "evidence": {
75
+ "evidenceBacked": bool(evidence_rows),
76
+ "matchedSignals": matched,
77
+ "evidenceCount": len(evidence_rows),
78
+ "flakeStatus": _flake_status(flake_signal),
79
+ },
80
+ "failureFingerprintDigest": _optional_digest(failure_fingerprint, "fingerprintDigest"),
81
+ "evidenceIds": _string_list(evidence_ids or [], allow_empty=True, label="evidenceIds"),
82
+ "reasonCodes": _reason_codes(selected, confidence, matched, declared_class=declared_class),
83
+ "providerModelNamesInCore": False,
84
+ "productionPromotionClaimed": False,
85
+ "blockers": blockers,
86
+ }
87
+ return {**body, "classificationDigest": canonical_digest(body)}
88
+
89
+
90
+ def validate_failure_classification_receipt(receipt: dict[str, Any]) -> dict[str, Any]:
91
+ if not isinstance(receipt, dict):
92
+ raise LifecycleError("invalid-failure-classification-receipt", "failure classification receipt must be an object")
93
+ blockers: list[dict[str, Any]] = []
94
+ if receipt.get("schemaVersion") != FAILURE_CLASSIFICATION_SCHEMA:
95
+ blockers.append({"code": "failure-classification-schema-invalid"})
96
+ status = receipt.get("status")
97
+ if status not in {"PASS", "FAIL"}:
98
+ blockers.append({"code": "failure-classification-status-invalid", "status": status})
99
+ failure_class = receipt.get("failureClass")
100
+ if failure_class not in FAILURE_CLASSES:
101
+ blockers.append({"code": "failure-classification-class-invalid", "failureClass": failure_class})
102
+ confidence = receipt.get("confidence")
103
+ if confidence not in CONFIDENCE_LEVELS:
104
+ blockers.append({"code": "failure-classification-confidence-invalid", "confidence": confidence})
105
+ if failure_class == "unknown" and confidence != "LOW":
106
+ blockers.append({"code": "failure-classification-unknown-confidence"})
107
+ evidence = receipt.get("evidence")
108
+ if not isinstance(evidence, dict):
109
+ blockers.append({"code": "failure-classification-evidence-invalid"})
110
+ else:
111
+ if evidence.get("evidenceBacked") is not True:
112
+ blockers.append({"code": "failure-classification-evidence-missing"})
113
+ if not isinstance(evidence.get("matchedSignals", []), list):
114
+ blockers.append({"code": "failure-classification-matched-signals-invalid"})
115
+ if receipt.get("failureFingerprintDigest") is not None and not _is_digest(receipt.get("failureFingerprintDigest")):
116
+ blockers.append({"code": "failure-classification-fingerprint-digest-invalid"})
117
+ _check_string_list(receipt.get("evidenceIds", []), "failure-classification-evidence-ids-invalid", blockers)
118
+ if receipt.get("providerModelNamesInCore") is not False:
119
+ blockers.append({"code": "failure-classification-provider-model-core"})
120
+ if receipt.get("productionPromotionClaimed") is not False:
121
+ blockers.append({"code": "failure-classification-production-claim"})
122
+ embedded_blockers = receipt.get("blockers")
123
+ if not isinstance(embedded_blockers, list) or not all(isinstance(item, dict) for item in embedded_blockers):
124
+ blockers.append({"code": "failure-classification-blockers-invalid"})
125
+ embedded_blockers = []
126
+ if status == "PASS" and embedded_blockers:
127
+ blockers.append({"code": "failure-classification-pass-with-blockers"})
128
+ if status == "FAIL" and not embedded_blockers:
129
+ blockers.append({"code": "failure-classification-fail-without-blockers"})
130
+ expected_digest = canonical_digest({key: value for key, value in receipt.items() if key != "classificationDigest"})
131
+ if receipt.get("classificationDigest") != expected_digest:
132
+ blockers.append({"code": "failure-classification-digest-mismatch"})
133
+ body = {
134
+ "schemaVersion": FAILURE_CLASSIFICATION_VALIDATION_SCHEMA,
135
+ "status": "PASS" if not blockers else "FAIL",
136
+ "receiptStatus": status if isinstance(status, str) else None,
137
+ "failureClass": failure_class if failure_class in FAILURE_CLASSES else None,
138
+ "confidence": confidence if confidence in CONFIDENCE_LEVELS else None,
139
+ "blockers": blockers,
140
+ "classificationDigest": receipt.get("classificationDigest"),
141
+ }
142
+ return {**body, "validationDigest": canonical_digest(body)}
143
+
144
+
145
+ def require_failure_classification_pass(validation: dict[str, Any]) -> dict[str, Any]:
146
+ if validation.get("status") != "PASS" or validation.get("receiptStatus") != "PASS":
147
+ raise LifecycleError("failure-classification-validation-failed", "failure classification did not pass", {"validation": validation})
148
+ return validation
149
+
150
+
151
+ def _classify(
152
+ failure: dict[str, Any],
153
+ evidence: list[dict[str, Any]],
154
+ *,
155
+ flake_signal: dict[str, Any] | None,
156
+ declared_class: str | None,
157
+ ) -> tuple[str, list[dict[str, Any]]]:
158
+ search_payload: dict[str, Any] = {"failure": failure, "evidence": evidence}
159
+ if flake_signal is not None:
160
+ search_payload["flakeSignal"] = flake_signal
161
+ text = _search_text(search_payload)
162
+ matches: list[dict[str, Any]] = []
163
+ if _flake_status(flake_signal) == "flaky":
164
+ matches.append({"failureClass": "flaky-test", "signal": "flake-status"})
165
+ for failure_class, patterns in CLASS_PATTERNS.items():
166
+ for pattern in patterns:
167
+ if pattern in text:
168
+ matches.append({"failureClass": failure_class, "signal": pattern})
169
+ if declared_class in FAILURE_CLASSES:
170
+ matches.append({"failureClass": declared_class, "signal": "declared-class"})
171
+ if not matches:
172
+ return "unknown", []
173
+ selected = sorted(matches, key=lambda item: (_class_priority(str(item["failureClass"])), str(item["signal"])))[0]
174
+ return str(selected["failureClass"]), matches
175
+
176
+
177
+ def _confidence(
178
+ selected: str,
179
+ matches: list[dict[str, Any]],
180
+ *,
181
+ failure_fingerprint: dict[str, Any] | None,
182
+ flake_signal: dict[str, Any] | None,
183
+ declared_class: str | None,
184
+ ) -> str:
185
+ if selected == "unknown":
186
+ return "LOW"
187
+ if selected == "flaky-test" and _flake_status(flake_signal) == "flaky":
188
+ return "HIGH"
189
+ selected_matches = [item for item in matches if item.get("failureClass") == selected and item.get("signal") != "declared-class"]
190
+ if failure_fingerprint is not None and _optional_digest(failure_fingerprint, "fingerprintDigest") is not None:
191
+ return "HIGH"
192
+ if len(selected_matches) >= 2:
193
+ return "HIGH"
194
+ if selected_matches:
195
+ return "MEDIUM"
196
+ return "LOW" if declared_class == selected else "MEDIUM"
197
+
198
+
199
+ def _reason_codes(selected: str, confidence: str, matches: list[dict[str, Any]], *, declared_class: str | None) -> list[str]:
200
+ reasons = [f"failure-class-{selected}", f"confidence-{confidence.lower()}"]
201
+ if declared_class is not None:
202
+ reasons.append("declared-class-present")
203
+ reasons.extend(f"signal-{item['signal']}" for item in matches[:8] if isinstance(item.get("signal"), str))
204
+ return reasons
205
+
206
+
207
+ def _class_priority(failure_class: str) -> int:
208
+ order = {
209
+ "security-bug": 0,
210
+ "race": 1,
211
+ "flaky-test": 2,
212
+ "permission": 3,
213
+ "performance": 4,
214
+ "migration": 5,
215
+ "serialization": 6,
216
+ "api-contract": 7,
217
+ "edge-case": 8,
218
+ "unknown": 9,
219
+ }
220
+ return order.get(failure_class, 99)
221
+
222
+
223
+ def _evidence_rows(
224
+ evidence: list[dict[str, Any]] | None,
225
+ *,
226
+ failure_body: dict[str, Any],
227
+ flake_signal: dict[str, Any] | None,
228
+ ) -> list[dict[str, Any]]:
229
+ if evidence is None:
230
+ rows: list[dict[str, Any]] = [{"source": "failure", "fields": sorted(failure_body)}]
231
+ if flake_signal is not None:
232
+ rows.append({"source": "flake-signal", "status": _flake_status(flake_signal)})
233
+ return rows
234
+ if not isinstance(evidence, list):
235
+ raise LifecycleError("invalid-failure-classification-receipt", "evidence must be an array")
236
+ if any(not isinstance(item, dict) or not item for item in evidence):
237
+ raise LifecycleError("invalid-failure-classification-receipt", "evidence items must be non-empty objects")
238
+ return [dict(item) for item in evidence]
239
+
240
+
241
+ def _object(value: Any, label: str) -> dict[str, Any]:
242
+ if not isinstance(value, dict) or not value:
243
+ raise LifecycleError("invalid-failure-classification-receipt", f"{label} must be a non-empty object")
244
+ return dict(value)
245
+
246
+
247
+ def _string_list(value: Any, *, allow_empty: bool, label: str) -> list[str]:
248
+ if not isinstance(value, list):
249
+ raise LifecycleError("invalid-failure-classification-receipt", f"{label} must be an array")
250
+ if not value and not allow_empty:
251
+ raise LifecycleError("invalid-failure-classification-receipt", f"{label} must not be empty")
252
+ if not all(isinstance(item, str) and item for item in value):
253
+ raise LifecycleError("invalid-failure-classification-receipt", f"{label} must contain non-empty strings")
254
+ return list(value)
255
+
256
+
257
+ def _check_string_list(value: Any, code: str, blockers: list[dict[str, Any]]) -> None:
258
+ if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
259
+ blockers.append({"code": code})
260
+
261
+
262
+ def _optional_digest(value: dict[str, Any] | None, key: str) -> str | None:
263
+ if value is None:
264
+ return None
265
+ digest = value.get(key)
266
+ if digest is None:
267
+ return None
268
+ if not _is_digest(digest):
269
+ raise LifecycleError("invalid-failure-classification-receipt", f"{key} must be a sha256 digest")
270
+ return str(digest)
271
+
272
+
273
+ def _is_digest(value: Any) -> bool:
274
+ return isinstance(value, str) and len(value) == 64 and all(char in "0123456789abcdef" for char in value)
275
+
276
+
277
+ def _flake_status(value: dict[str, Any] | None) -> str | None:
278
+ if not isinstance(value, dict):
279
+ return None
280
+ status = value.get("status") or value.get("flakeStatus")
281
+ return status if status in {"stable-fail", "stable-pass", "flaky", "inconclusive"} else None
282
+
283
+
284
+ def _search_text(value: Any) -> str:
285
+ return json.dumps(value, sort_keys=True, ensure_ascii=True, default=str).lower()
286
+
287
+
288
+ def _neutrality_blockers(value: Any) -> list[dict[str, Any]]:
289
+ blockers: list[dict[str, Any]] = []
290
+ for path, _item in _walk(value):
291
+ key = path.rsplit(".", 1)[-1]
292
+ normalized = key.replace("_", "").replace("-", "").lower()
293
+ if normalized in FORBIDDEN_CORE_KEYS:
294
+ blockers.append({"code": "failure-classification-provider-model-key", "path": path})
295
+ return blockers
296
+
297
+
298
+ def _walk(value: Any, prefix: str = "$") -> list[tuple[str, Any]]:
299
+ rows: list[tuple[str, Any]] = []
300
+ if isinstance(value, dict):
301
+ for key, item in value.items():
302
+ path = f"{prefix}.{key}"
303
+ rows.append((path, item))
304
+ rows.extend(_walk(item, path))
305
+ elif isinstance(value, list):
306
+ for index, item in enumerate(value):
307
+ rows.extend(_walk(item, f"{prefix}[{index}]"))
308
+ return rows
309
+
310
+
311
+ __all__ = [
312
+ "FAILURE_CLASSES",
313
+ "FAILURE_CLASSIFICATION_SCHEMA",
314
+ "HIGH_RISK_FAILURE_CLASSES",
315
+ "build_failure_classification_receipt",
316
+ "require_failure_classification_pass",
317
+ "validate_failure_classification_receipt",
318
+ ]
@@ -0,0 +1,265 @@
1
+ """Opt-in quality pack contracts and fixture-based behavior checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+
9
+ QUALITY_PACK_SCHEMA = "agent-optional-quality-pack.v1"
10
+ QUALITY_PACK_VALIDATION_SCHEMA = "agent-optional-quality-pack-validation.v1"
11
+ BEHAVIOR_CHECK_RUN_SCHEMA = "agent-behavior-check-run.v1"
12
+ FIXTURE_SCHEMA = "agent-behavior-check-fixture.v1"
13
+ RESOURCE_CAP_KEYS = {
14
+ "maxArtifacts",
15
+ "maxInputBytes",
16
+ "maxInputTokens",
17
+ "maxInvocations",
18
+ "maxOutputTokens",
19
+ "maxWallSeconds",
20
+ }
21
+ SIGNAL_NAMES = (
22
+ "completion",
23
+ "goal",
24
+ "budget",
25
+ "eventCapture",
26
+ "externalAction",
27
+ "review",
28
+ )
29
+ NEGATIVE_STATUSES = {"FAIL", "BLOCKED"}
30
+
31
+
32
+ def build_default_quality_pack() -> dict[str, Any]:
33
+ """Return the built-in optional quality pack manifest."""
34
+
35
+ return {
36
+ "schemaVersion": QUALITY_PACK_SCHEMA,
37
+ "packId": "optional-quality-observability",
38
+ "status": "OPTIONAL",
39
+ "enabledByDefault": False,
40
+ "activationMode": "opt-in",
41
+ "canonicalLifecycleCommandsChanged": False,
42
+ "providerSpecificCoreDependency": False,
43
+ "productionPromotionClaimed": False,
44
+ "defaultCommandFootprint": {
45
+ "extraCommands": 0,
46
+ "extraLiveCalls": 0,
47
+ "extraRequiredArtifacts": 0,
48
+ },
49
+ "commands": [
50
+ {
51
+ "name": "quality pack-check",
52
+ "purpose": "validate an optional quality pack manifest",
53
+ "inputSchemas": [QUALITY_PACK_SCHEMA],
54
+ "expectedEvidence": [QUALITY_PACK_VALIDATION_SCHEMA],
55
+ "resourceCaps": {"maxInputBytes": 32768, "maxOutputTokens": 2048},
56
+ },
57
+ {
58
+ "name": "quality behavior-check",
59
+ "purpose": "check lifecycle outcome fixtures",
60
+ "inputSchemas": [FIXTURE_SCHEMA],
61
+ "expectedEvidence": [BEHAVIOR_CHECK_RUN_SCHEMA],
62
+ "resourceCaps": {"maxArtifacts": 16, "maxInputBytes": 65536, "maxOutputTokens": 4096},
63
+ },
64
+ ],
65
+ }
66
+
67
+
68
+ def validate_quality_pack(manifest: dict[str, Any]) -> dict[str, Any]:
69
+ """Validate one optional quality pack without enabling it."""
70
+
71
+ if not isinstance(manifest, dict):
72
+ raise LifecycleError("invalid-quality-pack", "quality pack manifest must be an object")
73
+ blockers: list[dict[str, Any]] = []
74
+ if manifest.get("schemaVersion") != QUALITY_PACK_SCHEMA:
75
+ blockers.append({"code": "quality-pack-schema-invalid", "message": "unsupported quality pack schemaVersion"})
76
+ _required_string(manifest, "packId", blockers)
77
+ if manifest.get("status") != "OPTIONAL":
78
+ blockers.append({"code": "quality-pack-not-optional", "message": "quality pack status must be OPTIONAL"})
79
+ if manifest.get("enabledByDefault") is not False:
80
+ blockers.append({"code": "quality-pack-default-enabled", "message": "quality packs must be disabled by default"})
81
+ if manifest.get("activationMode") != "opt-in":
82
+ blockers.append({"code": "quality-pack-not-opt-in", "message": "quality pack activation must be opt-in"})
83
+ if manifest.get("canonicalLifecycleCommandsChanged") is not False:
84
+ blockers.append({"code": "quality-pack-command-drift", "message": "canonical lifecycle commands must remain unchanged"})
85
+ if manifest.get("providerSpecificCoreDependency") is not False:
86
+ blockers.append({"code": "quality-pack-provider-core-dependency", "message": "quality packs cannot add provider-specific core dependencies"})
87
+ if manifest.get("productionPromotionClaimed") is not False:
88
+ blockers.append({"code": "quality-pack-promotion-overclaim", "message": "quality packs cannot claim production promotion"})
89
+ _validate_default_footprint(manifest.get("defaultCommandFootprint"), blockers)
90
+ commands = manifest.get("commands")
91
+ if not isinstance(commands, list) or not commands:
92
+ blockers.append({"code": "quality-pack-command-missing", "message": "quality pack must declare opt-in commands"})
93
+ commands = []
94
+ seen: set[str] = set()
95
+ for index, command in enumerate(commands):
96
+ _validate_command(command, index, blockers, seen)
97
+ body = {
98
+ "schemaVersion": QUALITY_PACK_VALIDATION_SCHEMA,
99
+ "status": "PASS" if not blockers else "FAIL",
100
+ "packId": manifest.get("packId"),
101
+ "commandCount": len(commands),
102
+ "defaultEnabled": manifest.get("enabledByDefault"),
103
+ "blockers": blockers,
104
+ "productionPromotionClaimed": False,
105
+ }
106
+ return {**body, "packDigest": canonical_digest(manifest), "validationDigest": canonical_digest(body)}
107
+
108
+
109
+ def require_quality_pack_pass(payload: dict[str, Any]) -> dict[str, Any]:
110
+ if payload.get("status") == "FAIL":
111
+ raise LifecycleError("quality-pack-validation-failed", "quality pack validation failed", {"validation": payload})
112
+ return payload
113
+
114
+
115
+ def run_behavior_checks(pack_manifest: dict[str, Any], fixtures: list[dict[str, Any]]) -> dict[str, Any]:
116
+ """Run small, deterministic lifecycle outcome checks."""
117
+
118
+ pack_validation = validate_quality_pack(pack_manifest)
119
+ checks: list[dict[str, Any]] = []
120
+ blockers: list[dict[str, Any]] = []
121
+ if pack_validation["status"] == "FAIL":
122
+ blockers.append({"code": "behavior-check-pack-invalid", "message": "quality pack validation failed"})
123
+ if not fixtures:
124
+ blockers.append({"code": "behavior-check-fixtures-missing", "message": "at least one fixture is required"})
125
+ for index, fixture in enumerate(fixtures):
126
+ checks.append(_evaluate_fixture(fixture, index, blockers))
127
+ failed_expectations = [item for item in checks if item["expectationStatus"] == "FAIL"]
128
+ if failed_expectations:
129
+ blockers.append(
130
+ {
131
+ "code": "behavior-check-expectation-failed",
132
+ "fixtureIds": [item["fixtureId"] for item in failed_expectations],
133
+ }
134
+ )
135
+ body = {
136
+ "schemaVersion": BEHAVIOR_CHECK_RUN_SCHEMA,
137
+ "status": "PASS" if not blockers else "FAIL",
138
+ "packId": pack_manifest.get("packId"),
139
+ "packDigest": canonical_digest(pack_manifest),
140
+ "fixtureCount": len(fixtures),
141
+ "passedExpectationCount": len([item for item in checks if item["expectationStatus"] == "PASS"]),
142
+ "failedExpectationCount": len(failed_expectations),
143
+ "checks": checks,
144
+ "blockers": blockers,
145
+ "productionPromotionClaimed": False,
146
+ }
147
+ return {**body, "runDigest": canonical_digest(body)}
148
+
149
+
150
+ def require_behavior_checks_pass(payload: dict[str, Any]) -> dict[str, Any]:
151
+ if payload.get("status") == "FAIL":
152
+ raise LifecycleError("behavior-check-failed", "behavior checks failed", {"validation": payload})
153
+ return payload
154
+
155
+
156
+ def _validate_command(command: object, index: int, blockers: list[dict[str, Any]], seen: set[str]) -> None:
157
+ if not isinstance(command, dict):
158
+ blockers.append({"code": "quality-pack-command-invalid", "index": index, "message": "command must be an object"})
159
+ return
160
+ name = command.get("name")
161
+ if not isinstance(name, str) or not name:
162
+ blockers.append({"code": "quality-pack-command-name-missing", "index": index})
163
+ elif name in seen:
164
+ blockers.append({"code": "quality-pack-command-duplicate", "command": name})
165
+ else:
166
+ seen.add(name)
167
+ _required_string(command, "purpose", blockers, index=index)
168
+ _required_string_list(command.get("inputSchemas"), "inputSchemas", blockers, index=index)
169
+ _required_string_list(command.get("expectedEvidence"), "expectedEvidence", blockers, index=index)
170
+ _validate_resource_caps(command.get("resourceCaps"), blockers, index=index)
171
+
172
+
173
+ def _validate_default_footprint(value: object, blockers: list[dict[str, Any]]) -> None:
174
+ if not isinstance(value, dict):
175
+ blockers.append({"code": "quality-pack-default-footprint-missing", "message": "default footprint is required"})
176
+ return
177
+ for key in ("extraCommands", "extraLiveCalls", "extraRequiredArtifacts"):
178
+ if value.get(key) != 0:
179
+ blockers.append({"code": "quality-pack-default-footprint-nonzero", "field": key})
180
+
181
+
182
+ def _validate_resource_caps(value: object, blockers: list[dict[str, Any]], *, index: int | None = None) -> None:
183
+ if not isinstance(value, dict) or not value:
184
+ blockers.append({"code": "quality-pack-resource-caps-missing", "index": index})
185
+ return
186
+ supported = [key for key in value if key in RESOURCE_CAP_KEYS]
187
+ if not supported:
188
+ blockers.append({"code": "quality-pack-resource-caps-unsupported", "index": index})
189
+ for key in supported:
190
+ number = value.get(key)
191
+ if not isinstance(number, (int, float)) or isinstance(number, bool) or number <= 0:
192
+ blockers.append({"code": "quality-pack-resource-cap-invalid", "index": index, "field": key})
193
+
194
+
195
+ def _evaluate_fixture(fixture: dict[str, Any], index: int, blockers: list[dict[str, Any]]) -> dict[str, Any]:
196
+ fixture_blockers: list[dict[str, Any]] = []
197
+ if not isinstance(fixture, dict):
198
+ blockers.append({"code": "behavior-check-fixture-invalid", "index": index})
199
+ return {
200
+ "fixtureId": f"fixture-{index}",
201
+ "expectedOutcome": None,
202
+ "actualOutcome": "FAIL",
203
+ "expectationStatus": "FAIL",
204
+ "blockerCodes": ["behavior-check-fixture-invalid"],
205
+ }
206
+ if fixture.get("schemaVersion") != FIXTURE_SCHEMA:
207
+ fixture_blockers.append({"code": "behavior-check-fixture-schema-invalid"})
208
+ fixture_id = fixture.get("fixtureId")
209
+ if not isinstance(fixture_id, str) or not fixture_id:
210
+ fixture_id = f"fixture-{index}"
211
+ fixture_blockers.append({"code": "behavior-check-fixture-id-missing"})
212
+ expected = fixture.get("expectedOutcome")
213
+ if expected not in {"PASS", "FAIL", "BLOCKED"}:
214
+ fixture_blockers.append({"code": "behavior-check-expected-outcome-invalid"})
215
+ expected = "FAIL"
216
+ signals = fixture.get("signals")
217
+ if not isinstance(signals, dict):
218
+ signals = {}
219
+ fixture_blockers.append({"code": "behavior-check-signals-missing"})
220
+ signal_blockers = _signal_blockers(signals)
221
+ for item in fixture_blockers:
222
+ blockers.append({"fixtureId": fixture_id, **item})
223
+ actual = "FAIL" if signal_blockers or fixture_blockers else "PASS"
224
+ if any(item.get("status") == "BLOCKED" for item in signals.values() if isinstance(item, dict)):
225
+ actual = "BLOCKED"
226
+ expectation_status = "PASS" if actual == expected else "FAIL"
227
+ return {
228
+ "fixtureId": fixture_id,
229
+ "expectedOutcome": expected,
230
+ "actualOutcome": actual,
231
+ "expectationStatus": expectation_status,
232
+ "signalCount": len(signals),
233
+ "blockerCodes": [item["code"] for item in [*fixture_blockers, *signal_blockers]],
234
+ }
235
+
236
+
237
+ def _signal_blockers(signals: dict[str, Any]) -> list[dict[str, Any]]:
238
+ blockers: list[dict[str, Any]] = []
239
+ for name in SIGNAL_NAMES:
240
+ signal = signals.get(name)
241
+ if signal is None:
242
+ continue
243
+ if not isinstance(signal, dict):
244
+ blockers.append({"code": "behavior-check-signal-invalid", "signal": name})
245
+ continue
246
+ status = signal.get("status")
247
+ if status in NEGATIVE_STATUSES:
248
+ blockers.append({"code": f"behavior-check-{name}-blocked", "status": status})
249
+ return blockers
250
+
251
+
252
+ def _required_string(value: dict[str, Any], key: str, blockers: list[dict[str, Any]], *, index: int | None = None) -> None:
253
+ if not isinstance(value.get(key), str) or not value.get(key):
254
+ item = {"code": f"quality-pack-{key}-missing"}
255
+ if index is not None:
256
+ item["index"] = index
257
+ blockers.append(item)
258
+
259
+
260
+ def _required_string_list(value: object, key: str, blockers: list[dict[str, Any]], *, index: int | None = None) -> None:
261
+ if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value):
262
+ item = {"code": f"quality-pack-{key}-invalid"}
263
+ if index is not None:
264
+ item["index"] = index
265
+ blockers.append(item)
@@ -0,0 +1,37 @@
1
+ """Read-only reporting views over existing lifecycle artifacts."""
2
+
3
+ from agent_lifecycle.reporting.event_feed import build_workflow_event_feed
4
+ from agent_lifecycle.reporting.change_summary import build_change_summary_receipt
5
+ from agent_lifecycle.reporting.progress_bridge import (
6
+ build_progress_bridge_config,
7
+ build_progress_bridge_receipt,
8
+ render_progress_bridge_terminal,
9
+ )
10
+ from agent_lifecycle.reporting.progress_hooks import (
11
+ build_progress_hook_policy,
12
+ build_progress_hook_receipt,
13
+ write_progress_hook_receipt,
14
+ )
15
+ from agent_lifecycle.reporting.progress_view import build_lifecycle_progress_view, build_lifecycle_progress_watch
16
+ from agent_lifecycle.reporting.progress_terminal import render_goal_view_terminal, render_progress_terminal
17
+ from agent_lifecycle.reporting.status_view import build_status_view, require_status_view_pass
18
+ from agent_lifecycle.reporting.usage_export import render_usage_export_json, render_usage_export_table
19
+
20
+ __all__ = [
21
+ "build_change_summary_receipt",
22
+ "build_lifecycle_progress_view",
23
+ "build_lifecycle_progress_watch",
24
+ "build_progress_bridge_config",
25
+ "build_progress_bridge_receipt",
26
+ "build_progress_hook_policy",
27
+ "build_progress_hook_receipt",
28
+ "build_status_view",
29
+ "build_workflow_event_feed",
30
+ "render_progress_bridge_terminal",
31
+ "render_goal_view_terminal",
32
+ "render_progress_terminal",
33
+ "render_usage_export_json",
34
+ "render_usage_export_table",
35
+ "require_status_view_pass",
36
+ "write_progress_hook_receipt",
37
+ ]