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,486 @@
1
+ """Generic external dialect import pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from agent_lifecycle.context.rendering import estimate_tokens
11
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest, sha256_hex
12
+ from agent_lifecycle.imports.dialect_profiles import (
13
+ DEFAULT_PROFILE_ID,
14
+ external_dialect_profile,
15
+ require_external_profile_pass,
16
+ validate_external_dialect_profile,
17
+ )
18
+ from agent_lifecycle.imports.planning import (
19
+ DEFAULT_MAX_INPUT_BYTES,
20
+ DEFAULT_TARGET_TOKENS,
21
+ PLANNING_IMPORT_RESULT_SCHEMA,
22
+ validate_import_result,
23
+ )
24
+ from agent_lifecycle.planning import validate_plan_manifest
25
+
26
+ EXTERNAL_IMPORT_VALIDATION_SCHEMA = "agent-external-dialect-import-validation.v1"
27
+ MAX_REQUIREMENTS = 8
28
+ MAX_REQUIREMENT_CHARS = 180
29
+
30
+ _POSIX_USER_PREFIX = "/" "Users" "/"
31
+ _POSIX_VOLUME_PREFIX = "/" "Volumes" "/"
32
+ _LOCAL_PATH_RE = re.compile(
33
+ r"(" + re.escape(_POSIX_USER_PREFIX) + r"|" + re.escape(_POSIX_VOLUME_PREFIX) + r"|[A-Za-z]:[\\/])\S+"
34
+ )
35
+ _SECRET_ASSIGNMENT_RE = re.compile(
36
+ r"(?i)\b([A-Z0-9_]*(TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z0-9_]*)\s*[:=]\s*[^\s,}\]]+"
37
+ )
38
+
39
+
40
+ def import_external_dialect(
41
+ source_path: Path,
42
+ *,
43
+ family: str,
44
+ profile_id: str = DEFAULT_PROFILE_ID,
45
+ package_id: str = "external-dialect-import",
46
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
47
+ target_tokens: int = DEFAULT_TARGET_TOKENS,
48
+ ) -> dict[str, Any]:
49
+ """Map a generic external dialect file into an untrusted ALK draft."""
50
+
51
+ _check_positive_cap(max_input_bytes, "maxInputBytes")
52
+ _check_positive_cap(target_tokens, "targetTokens")
53
+ profile = external_dialect_profile(
54
+ family,
55
+ profile_id=profile_id,
56
+ max_input_bytes=max_input_bytes,
57
+ target_tokens=target_tokens,
58
+ )
59
+ require_external_profile_pass(validate_external_dialect_profile(profile))
60
+ blockers: list[dict[str, Any]] = []
61
+ data = _read_source(source_path, max_input_bytes=max_input_bytes, blockers=blockers)
62
+ text = _decode_source(data, blockers) if data else ""
63
+ parsed = _parse_input(text, blockers) if text else {}
64
+ mapping = _map_family(family, parsed, text, blockers)
65
+ candidate = _candidate_plan(
66
+ mapping,
67
+ package_id=package_id,
68
+ source_label=source_path.name,
69
+ source_digest=sha256_hex(data),
70
+ profile=profile,
71
+ ) if not blockers else None
72
+ if candidate is None and not blockers:
73
+ blockers.append({"code": "external-dialect-requirements-missing"})
74
+ body = {
75
+ "schemaVersion": PLANNING_IMPORT_RESULT_SCHEMA,
76
+ "status": "PASS",
77
+ "enabledByDefault": False,
78
+ "activationMode": "explicit-command",
79
+ "sourceTrusted": False,
80
+ "candidateLifecycleStatus": "DRAFT_REQUIRES_REVIEW" if candidate else "BLOCKED",
81
+ "reviewGates": ["plan check", "independent audit", "freeze approval"],
82
+ "resourceCaps": {"maxInputBytes": max_input_bytes, "targetTokens": target_tokens},
83
+ "source": {"label": source_path.name, "digest": sha256_hex(data), "inputBytes": len(data)},
84
+ "nativeDialectProfileDigest": profile["profileDigest"],
85
+ "dialectProfile": _profile_summary(profile),
86
+ "externalDialect": _external_summary(mapping),
87
+ "candidatePlan": candidate,
88
+ "requiresReview": True,
89
+ "auditRequired": True,
90
+ "freezeBlocked": True,
91
+ "blockers": blockers,
92
+ }
93
+ body["estimatedTokens"] = estimate_tokens(body)
94
+ if body["estimatedTokens"] > target_tokens:
95
+ body["blockers"].append(
96
+ {
97
+ "code": "external-dialect-target-tokens-exceeded",
98
+ "estimatedTokens": body["estimatedTokens"],
99
+ "targetTokens": target_tokens,
100
+ }
101
+ )
102
+ if body["blockers"]:
103
+ body["status"] = "FAIL"
104
+ return {**body, "importDigest": canonical_digest(body)}
105
+
106
+
107
+ def validate_external_import_result(result: dict[str, Any]) -> dict[str, Any]:
108
+ blockers: list[dict[str, Any]] = []
109
+ planning_validation = validate_import_result(result)
110
+ if planning_validation.get("status") != "PASS":
111
+ blockers.extend(planning_validation.get("blockers", []))
112
+ external = result.get("externalDialect")
113
+ if not isinstance(external, dict):
114
+ blockers.append({"code": "external-dialect-summary-missing"})
115
+ else:
116
+ if external.get("sourceTrusted") is not False:
117
+ blockers.append({"code": "external-dialect-source-trusted"})
118
+ if external.get("family") not in {"workflow", "agent"}:
119
+ blockers.append({"code": "external-dialect-family-invalid"})
120
+ if external.get("executesInput") is not False:
121
+ blockers.append({"code": "external-dialect-execution-enabled"})
122
+ if external.get("portableProviderDefaults") is not False:
123
+ blockers.append({"code": "external-dialect-provider-default"})
124
+ candidate = result.get("candidatePlan")
125
+ if isinstance(candidate, dict):
126
+ external_state = candidate.get("externalImport") if isinstance(candidate.get("externalImport"), dict) else {}
127
+ if external_state.get("sourceTrusted") is not False:
128
+ blockers.append({"code": "external-dialect-candidate-source-trusted"})
129
+ if external_state.get("executesInput") is not False:
130
+ blockers.append({"code": "external-dialect-candidate-executes-input"})
131
+ try:
132
+ validate_plan_manifest(candidate)
133
+ except LifecycleError as exc:
134
+ blockers.append({"code": "external-dialect-candidate-invalid", "reason": exc.code})
135
+ body = {
136
+ "schemaVersion": EXTERNAL_IMPORT_VALIDATION_SCHEMA,
137
+ "status": "PASS" if not blockers else "FAIL",
138
+ "family": external.get("family") if isinstance(external, dict) else None,
139
+ "candidateDigest": canonical_digest(candidate) if isinstance(candidate, dict) else None,
140
+ "requiresReview": result.get("requiresReview") is True,
141
+ "auditRequired": result.get("auditRequired") is True,
142
+ "freezeBlocked": result.get("freezeBlocked") is True,
143
+ "blockers": blockers,
144
+ }
145
+ return {**body, "validationDigest": canonical_digest(body)}
146
+
147
+
148
+ def require_external_import_pass(validation: dict[str, Any]) -> dict[str, Any]:
149
+ if validation.get("status") != "PASS":
150
+ raise LifecycleError(
151
+ "external-dialect-import-validation-failed",
152
+ "external dialect import validation failed",
153
+ {"validation": validation},
154
+ )
155
+ return validation
156
+
157
+
158
+ def _read_source(source_path: Path, *, max_input_bytes: int, blockers: list[dict[str, Any]]) -> bytes:
159
+ if not source_path.is_file():
160
+ blockers.append({"code": "external-dialect-source-missing", "sourceLabel": source_path.name})
161
+ return b""
162
+ size = source_path.stat().st_size
163
+ if size > max_input_bytes:
164
+ blockers.append({"code": "external-dialect-input-cap-exceeded", "inputBytes": size, "cap": max_input_bytes})
165
+ return b""
166
+ return source_path.read_bytes()
167
+
168
+
169
+ def _decode_source(data: bytes, blockers: list[dict[str, Any]]) -> str:
170
+ try:
171
+ return data.decode("utf-8")
172
+ except UnicodeDecodeError:
173
+ blockers.append({"code": "external-dialect-decode-failed"})
174
+ return ""
175
+
176
+
177
+ def _parse_input(text: str, blockers: list[dict[str, Any]]) -> dict[str, Any]:
178
+ try:
179
+ parsed = json.loads(text)
180
+ except json.JSONDecodeError:
181
+ return _parse_yaml_like(text)
182
+ if not isinstance(parsed, dict):
183
+ blockers.append({"code": "external-dialect-input-not-object"})
184
+ return {}
185
+ return parsed
186
+
187
+
188
+ def _parse_yaml_like(text: str) -> dict[str, Any]:
189
+ parsed: dict[str, Any] = {}
190
+ current_key: str | None = None
191
+ for raw_line in text.splitlines():
192
+ line = raw_line.rstrip()
193
+ stripped = line.strip()
194
+ if not stripped or stripped.startswith("#"):
195
+ continue
196
+ if not line.startswith((" ", "\t")) and ":" in stripped:
197
+ key, value = stripped.split(":", 1)
198
+ current_key = _clean_key(key)
199
+ parsed[current_key] = _parse_scalar(value.strip()) if value.strip() else []
200
+ continue
201
+ if current_key and stripped.startswith("- "):
202
+ value = stripped[2:].strip()
203
+ if not isinstance(parsed.get(current_key), list):
204
+ parsed[current_key] = []
205
+ parsed[current_key].append(_parse_inline_mapping(value))
206
+ continue
207
+ if current_key and ":" in stripped:
208
+ key, value = stripped.split(":", 1)
209
+ if not isinstance(parsed.get(current_key), dict):
210
+ parsed[current_key] = {}
211
+ nested_key = key.strip() if current_key in {"env", "environment"} else _clean_key(key)
212
+ parsed[current_key][nested_key] = _parse_scalar(value.strip())
213
+ return parsed
214
+
215
+
216
+ def _parse_inline_mapping(value: str) -> Any:
217
+ if ":" not in value:
218
+ return _parse_scalar(value)
219
+ key, raw = value.split(":", 1)
220
+ return {_clean_key(key): _parse_scalar(raw.strip())}
221
+
222
+
223
+ def _parse_scalar(value: str) -> Any:
224
+ stripped = value.strip().strip("\"'")
225
+ if stripped.lower() in {"true", "false"}:
226
+ return stripped.lower() == "true"
227
+ return stripped
228
+
229
+
230
+ def _map_family(family: str, parsed: dict[str, Any], text: str, blockers: list[dict[str, Any]]) -> dict[str, Any]:
231
+ if family == "workflow":
232
+ return _map_workflow(parsed, text)
233
+ if family == "agent":
234
+ return _map_agent(parsed, text)
235
+ blockers.append({"code": "external-dialect-family-invalid", "family": family})
236
+ return {}
237
+
238
+
239
+ def _map_workflow(parsed: dict[str, Any], text: str) -> dict[str, Any]:
240
+ steps = _list_from(parsed, "steps", "jobs", "tasks")
241
+ validations = _list_from(parsed, "validation", "validations", "checks")
242
+ requirements = [
243
+ f"Review imported workflow step {index + 1}: {_safe_summary(step)}."
244
+ for index, step in enumerate(steps[:MAX_REQUIREMENTS])
245
+ ]
246
+ if not requirements:
247
+ requirements = _requirements_from_lines(text, prefix="Review imported workflow instruction")
248
+ return {
249
+ "family": "workflow",
250
+ "title": _title(parsed, "Imported workflow draft"),
251
+ "requirements": requirements,
252
+ "workHints": [{"kind": "workflow-step", "summary": _safe_summary(step)} for step in steps[:MAX_REQUIREMENTS]],
253
+ "validationHints": [_safe_summary(item) for item in validations[:MAX_REQUIREMENTS]],
254
+ "hostLocalHints": {},
255
+ "redactions": _redaction_summary(parsed),
256
+ }
257
+
258
+
259
+ def _map_agent(parsed: dict[str, Any], text: str) -> dict[str, Any]:
260
+ policies = _list_from(parsed, "policies", "policy", "rules", "instructions")
261
+ requirements = []
262
+ role = _first_string(parsed, "role", "description", "purpose")
263
+ if role:
264
+ requirements.append(f"Review imported agent role hint before converting it into ALK task scope: {_safe_summary(role)}.")
265
+ requirements.extend(f"Review imported agent policy hint {index + 1}: {_safe_summary(item)}." for index, item in enumerate(policies[:MAX_REQUIREMENTS]))
266
+ if not requirements:
267
+ requirements = _requirements_from_lines(text, prefix="Review imported agent instruction")
268
+ return {
269
+ "family": "agent",
270
+ "title": _title(parsed, "Imported agent draft"),
271
+ "requirements": requirements[:MAX_REQUIREMENTS],
272
+ "workHints": [],
273
+ "validationHints": [],
274
+ "hostLocalHints": _host_local_hints(parsed),
275
+ "redactions": _redaction_summary(parsed),
276
+ }
277
+
278
+
279
+ def _candidate_plan(
280
+ mapping: dict[str, Any],
281
+ *,
282
+ package_id: str,
283
+ source_label: str,
284
+ source_digest: str,
285
+ profile: dict[str, Any],
286
+ ) -> dict[str, Any] | None:
287
+ requirements = [
288
+ {"id": f"R-EXT-{index + 1}", "description": _truncate(item, MAX_REQUIREMENT_CHARS)}
289
+ for index, item in enumerate(mapping.get("requirements", [])[:MAX_REQUIREMENTS])
290
+ if isinstance(item, str) and item.strip()
291
+ ]
292
+ if not requirements:
293
+ return None
294
+ criteria = [
295
+ {
296
+ "id": f"AC-EXT-{index + 1}",
297
+ "requirementIds": [requirement["id"]],
298
+ "evidenceIds": [f"EV-EXT-{index + 1}"],
299
+ "statement": f"Imported {mapping['family']} requirement {index + 1} is reviewed before freeze.",
300
+ }
301
+ for index, requirement in enumerate(requirements)
302
+ ]
303
+ evidence = [
304
+ {"id": f"EV-EXT-{index + 1}", "description": "Independent review evidence for imported external dialect content."}
305
+ for index, _ in enumerate(requirements)
306
+ ]
307
+ return {
308
+ "schemaVersion": "agent-plan-manifest.v1",
309
+ "status": "DRAFT",
310
+ "planRevision": 1,
311
+ "package": {
312
+ "id": _safe_identifier(package_id) or "external-dialect-import",
313
+ "title": mapping["title"],
314
+ "workspaceRoot": ".",
315
+ "artifactRoot": "imported",
316
+ "root": ".",
317
+ "planArtifactRoot": "imported",
318
+ },
319
+ "author": {"id": "external-dialect-import", "surface": "agent-lifecycle", "runId": source_digest[:16]},
320
+ "baseRevision": {"ref": "UNRESOLVED", "sha": "UNRESOLVED"},
321
+ "importState": {
322
+ "sourceLabel": source_label,
323
+ "sourceDigest": source_digest,
324
+ "nativeDialectProfileDigest": profile["profileDigest"],
325
+ "dialect": _profile_summary(profile),
326
+ "requiresReview": True,
327
+ "auditRequired": True,
328
+ "freezeBlocked": True,
329
+ },
330
+ "externalImport": {
331
+ "family": mapping["family"],
332
+ "sourceTrusted": False,
333
+ "executesInput": False,
334
+ "portableProviderDefaults": False,
335
+ "workHints": mapping.get("workHints", []),
336
+ "validationHints": mapping.get("validationHints", []),
337
+ "hostLocalHints": mapping.get("hostLocalHints", {}),
338
+ "redactions": mapping.get("redactions", {}),
339
+ },
340
+ "specification": {
341
+ "tier": "S2",
342
+ "intent": "to-be",
343
+ "status": "DRAFT",
344
+ "source": "external-dialect-import",
345
+ "requirements": requirements,
346
+ },
347
+ "readOnly": [],
348
+ "forbiddenWrites": [".git"],
349
+ "leadOwned": [],
350
+ "workstreams": [
351
+ {
352
+ "id": "WS-EXTERNAL-REVIEW",
353
+ "title": "Review imported external draft",
354
+ "owner": "import-reviewer",
355
+ "dependsOn": [],
356
+ "writes": [],
357
+ "acceptanceIds": [item["id"] for item in criteria],
358
+ "evidenceIds": [item["id"] for item in evidence],
359
+ }
360
+ ],
361
+ "acceptance": {
362
+ "criteria": criteria,
363
+ "evidence": evidence,
364
+ "releaseGate": "External dialect imports require ALK plan review and explicit freeze before implementation.",
365
+ "qualityFloor": "Imported workflow or agent content cannot replace source-of-truth ALK artifacts.",
366
+ },
367
+ }
368
+
369
+
370
+ def _external_summary(mapping: dict[str, Any]) -> dict[str, Any]:
371
+ return {
372
+ "family": mapping.get("family"),
373
+ "sourceTrusted": False,
374
+ "executesInput": False,
375
+ "portableProviderDefaults": False,
376
+ "workHintCount": len(mapping.get("workHints", [])),
377
+ "validationHintCount": len(mapping.get("validationHints", [])),
378
+ "hostLocalHintKeys": sorted(mapping.get("hostLocalHints", {}).keys()),
379
+ "redactions": mapping.get("redactions", {}),
380
+ }
381
+
382
+
383
+ def _profile_summary(profile: dict[str, Any]) -> dict[str, Any]:
384
+ return {
385
+ "schemaVersion": profile.get("schemaVersion"),
386
+ "dialectId": profile.get("dialectId"),
387
+ "dialectKind": profile.get("dialectKind"),
388
+ "family": profile.get("family"),
389
+ "profileId": profile.get("profileId"),
390
+ "sourceTrusted": profile.get("sourceTrusted"),
391
+ "profileDigest": profile.get("profileDigest"),
392
+ }
393
+
394
+
395
+ def _host_local_hints(parsed: dict[str, Any]) -> dict[str, Any]:
396
+ hints: dict[str, Any] = {}
397
+ for key in ("provider", "model", "auth"):
398
+ value = parsed.get(key)
399
+ if value not in (None, "", []):
400
+ hints[key] = _redacted_value(value)
401
+ env = parsed.get("environment", parsed.get("env", {}))
402
+ if isinstance(env, dict):
403
+ hints["environmentKeys"] = sorted(str(key) for key in env)
404
+ tools = _list_from(parsed, "tools", "tooling", "capabilities")
405
+ if tools:
406
+ hints["toolHints"] = [_redacted_value(item) for item in tools]
407
+ return hints
408
+
409
+
410
+ def _redacted_value(value: Any) -> dict[str, Any]:
411
+ return {"redacted": True, "valueDigest": canonical_digest({"value": str(value)})}
412
+
413
+
414
+ def _redaction_summary(parsed: dict[str, Any]) -> dict[str, Any]:
415
+ keys = []
416
+ for key in parsed:
417
+ lowered = str(key).lower()
418
+ if lowered in {"provider", "model", "auth", "env", "environment", "tools"} or any(marker in lowered for marker in ("token", "secret", "password")):
419
+ keys.append(str(key))
420
+ return {"redactedOrHostLocalKeys": sorted(keys), "secretValuesStored": False}
421
+
422
+
423
+ def _requirements_from_lines(text: str, *, prefix: str) -> list[str]:
424
+ requirements = []
425
+ for line in text.splitlines():
426
+ stripped = line.strip()
427
+ if stripped.startswith(("- ", "* ")):
428
+ requirements.append(f"{prefix}: {_safe_summary(stripped[2:])}.")
429
+ elif re.match(r"^\d+\.\s+", stripped):
430
+ numbered_text = re.sub(r"^\d+\.\s+", "", stripped)
431
+ requirements.append(f"{prefix}: {_safe_summary(numbered_text)}.")
432
+ return requirements[:MAX_REQUIREMENTS]
433
+
434
+
435
+ def _list_from(parsed: dict[str, Any], *keys: str) -> list[Any]:
436
+ for key in keys:
437
+ value = parsed.get(key)
438
+ if isinstance(value, list):
439
+ return value
440
+ if isinstance(value, dict):
441
+ return [value]
442
+ if isinstance(value, str) and value.strip():
443
+ return [value]
444
+ return []
445
+
446
+
447
+ def _first_string(parsed: dict[str, Any], *keys: str) -> str | None:
448
+ for key in keys:
449
+ value = parsed.get(key)
450
+ if isinstance(value, str) and value.strip():
451
+ return value
452
+ return None
453
+
454
+
455
+ def _title(parsed: dict[str, Any], fallback: str) -> str:
456
+ value = _first_string(parsed, "title", "name", "workflow", "agent")
457
+ return _safe_summary(value) if value else fallback
458
+
459
+
460
+ def _safe_summary(value: Any) -> str:
461
+ if isinstance(value, dict):
462
+ text = " ".join(f"{_clean_key(str(key))} {_safe_summary(item)}" for key, item in sorted(value.items()))
463
+ else:
464
+ text = str(value)
465
+ text = _LOCAL_PATH_RE.sub("[redacted-path]", text)
466
+ text = _SECRET_ASSIGNMENT_RE.sub(r"\1=[redacted]", text)
467
+ text = re.sub(r"\s+", " ", text).strip()
468
+ return _truncate(text, MAX_REQUIREMENT_CHARS)
469
+
470
+
471
+ def _safe_identifier(value: str) -> str:
472
+ lowered = value.strip().lower()
473
+ return re.sub(r"[^a-z0-9_.-]+", "-", lowered).strip("-")[:80]
474
+
475
+
476
+ def _clean_key(value: str) -> str:
477
+ return re.sub(r"[^A-Za-z0-9_.-]+", "-", value.strip()).strip("-").lower()
478
+
479
+
480
+ def _truncate(value: str, limit: int) -> str:
481
+ return value[:limit]
482
+
483
+
484
+ def _check_positive_cap(value: int, field: str) -> None:
485
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
486
+ raise LifecycleError("invalid-resource-cap", f"{field} must be a positive integer", {"field": field})
@@ -0,0 +1,28 @@
1
+ """Generic workflow-family external dialect import."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.imports.dialect_profiles import DEFAULT_PROFILE_ID
9
+ from agent_lifecycle.imports.external_dialects import import_external_dialect
10
+ from agent_lifecycle.imports.planning import DEFAULT_MAX_INPUT_BYTES, DEFAULT_TARGET_TOKENS
11
+
12
+
13
+ def import_external_workflow(
14
+ source_path: Path,
15
+ *,
16
+ profile_id: str = DEFAULT_PROFILE_ID,
17
+ package_id: str = "external-workflow-import",
18
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
19
+ target_tokens: int = DEFAULT_TARGET_TOKENS,
20
+ ) -> dict[str, Any]:
21
+ return import_external_dialect(
22
+ source_path,
23
+ family="workflow",
24
+ profile_id=profile_id,
25
+ package_id=package_id,
26
+ max_input_bytes=max_input_bytes,
27
+ target_tokens=target_tokens,
28
+ )
@@ -0,0 +1,111 @@
1
+ """Deterministic Markdown file and folder import helpers."""
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, sha256_hex
9
+ from agent_lifecycle.imports.planning import (
10
+ DEFAULT_MAX_INPUT_BYTES,
11
+ DEFAULT_TARGET_TOKENS,
12
+ import_planning_text,
13
+ )
14
+
15
+ MARKDOWN_COLLECTION_SCHEMA = "agent-markdown-source-collection.v1"
16
+ DEFAULT_MAX_MARKDOWN_FILES = 32
17
+
18
+
19
+ def import_markdown_collection(
20
+ source_path: Path,
21
+ *,
22
+ package_id: str = "markdown-import",
23
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
24
+ target_tokens: int = DEFAULT_TARGET_TOKENS,
25
+ max_files: int = DEFAULT_MAX_MARKDOWN_FILES,
26
+ dialect_profile: dict[str, Any] | None = None,
27
+ ) -> dict[str, Any]:
28
+ """Import one Markdown file or a directory of Markdown files as draft input."""
29
+
30
+ collection = collect_markdown_collection(source_path, max_input_bytes=max_input_bytes, max_files=max_files)
31
+ text = collection.pop("_combinedText", "")
32
+ result = import_planning_text(
33
+ text,
34
+ source_label=collection["sourceLabel"],
35
+ package_id=package_id,
36
+ max_input_bytes=max_input_bytes,
37
+ target_tokens=target_tokens,
38
+ dialect_profile=dialect_profile,
39
+ )
40
+ result["markdownCollection"] = collection
41
+ if collection["blockers"]:
42
+ result["blockers"].extend(collection["blockers"])
43
+ result["status"] = "FAIL"
44
+ result["candidateLifecycleStatus"] = "BLOCKED"
45
+ result["candidatePlan"] = None
46
+ result["importDigest"] = canonical_digest({key: value for key, value in result.items() if key != "importDigest"})
47
+ return result
48
+
49
+
50
+ def collect_markdown_collection(
51
+ source_path: Path,
52
+ *,
53
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
54
+ max_files: int = DEFAULT_MAX_MARKDOWN_FILES,
55
+ ) -> dict[str, Any]:
56
+ if max_files <= 0:
57
+ raise LifecycleError("invalid-resource-cap", "maxFiles must be a positive integer", {"field": "maxFiles"})
58
+ if source_path.is_file():
59
+ files = [source_path]
60
+ root = source_path.parent
61
+ source_kind = "file"
62
+ elif source_path.is_dir():
63
+ root = source_path
64
+ files = sorted(path for path in source_path.rglob("*.md") if path.is_file())
65
+ source_kind = "directory"
66
+ else:
67
+ files = []
68
+ root = source_path.parent
69
+ source_kind = "missing"
70
+ blockers: list[dict[str, Any]] = []
71
+ if not files:
72
+ blockers.append({"code": "markdown-collection-empty", "sourceLabel": source_path.name})
73
+ if len(files) > max_files:
74
+ blockers.append({"code": "markdown-collection-file-cap-exceeded", "fileCount": len(files), "cap": max_files})
75
+ files = files[:max_files]
76
+
77
+ entries: list[dict[str, Any]] = []
78
+ combined_parts: list[str] = []
79
+ total_input_bytes = 0
80
+ for path in files:
81
+ rel = path.relative_to(root).as_posix() if source_kind == "directory" else path.name
82
+ data = path.read_bytes()
83
+ total_input_bytes += len(data)
84
+ entries.append({"label": rel, "digest": sha256_hex(data), "inputBytes": len(data)})
85
+ try:
86
+ text = data.decode("utf-8")
87
+ except UnicodeDecodeError:
88
+ blockers.append({"code": "markdown-collection-decode-failed", "sourceLabel": rel})
89
+ text = ""
90
+ combined_parts.append(f"# Source: {rel}\n\n{text.strip()}\n")
91
+
92
+ if total_input_bytes > max_input_bytes:
93
+ blockers.append({"code": "markdown-collection-input-cap-exceeded", "inputBytes": total_input_bytes, "cap": max_input_bytes})
94
+ combined_text = ""
95
+ else:
96
+ combined_text = "\n".join(combined_parts).strip()
97
+
98
+ body = {
99
+ "schemaVersion": MARKDOWN_COLLECTION_SCHEMA,
100
+ "sourceKind": source_kind,
101
+ "sourceLabel": source_path.name,
102
+ "ordering": "lexical-relative-posix",
103
+ "fileCount": len(entries),
104
+ "totalInputBytes": total_input_bytes,
105
+ "resourceCaps": {"maxInputBytes": max_input_bytes, "maxFiles": max_files},
106
+ "files": entries,
107
+ "blockers": blockers,
108
+ }
109
+ body["collectionDigest"] = canonical_digest(body)
110
+ body["_combinedText"] = combined_text
111
+ return body
@@ -0,0 +1,49 @@
1
+ """OpenSpec planning import profile."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.imports.planning import (
9
+ DEFAULT_MAX_INPUT_BYTES,
10
+ DEFAULT_TARGET_TOKENS,
11
+ import_planning_input,
12
+ planning_dialect_profile,
13
+ )
14
+
15
+
16
+ def openspec_profile(
17
+ *,
18
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
19
+ target_tokens: int = DEFAULT_TARGET_TOKENS,
20
+ ) -> dict[str, Any]:
21
+ return planning_dialect_profile(
22
+ dialect_id="openspec-planning",
23
+ dialect_kind="openspec",
24
+ markers=["openspec", "spec", "proposal", "capability", "change"],
25
+ mapping={
26
+ "title": ["h1", "title", "spec name", "proposal"],
27
+ "requirements": ["requirements", "acceptance", "capabilities", "changes"],
28
+ "reviewHints": ["risks", "validation", "migration"],
29
+ },
30
+ max_input_bytes=max_input_bytes,
31
+ target_tokens=target_tokens,
32
+ )
33
+
34
+
35
+ def import_openspec_planning(
36
+ source_path: Path,
37
+ *,
38
+ package_id: str = "openspec-import",
39
+ max_input_bytes: int = DEFAULT_MAX_INPUT_BYTES,
40
+ target_tokens: int = DEFAULT_TARGET_TOKENS,
41
+ ) -> dict[str, Any]:
42
+ profile = openspec_profile(max_input_bytes=max_input_bytes, target_tokens=target_tokens)
43
+ return import_planning_input(
44
+ source_path,
45
+ package_id=package_id,
46
+ max_input_bytes=max_input_bytes,
47
+ target_tokens=target_tokens,
48
+ dialect_profile=profile,
49
+ )