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.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- 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
|
+
)
|