agentbundle 0.2.0__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.
- agentbundle/__init__.py +14 -0
- agentbundle/__main__.py +5 -0
- agentbundle/_data/adapter.schema.json +270 -0
- agentbundle/_data/adapter.toml +584 -0
- agentbundle/_data/install-marker.py +1099 -0
- agentbundle/_data/pack.schema.json +152 -0
- agentbundle/_data/plugin-manifest.derived.schema.json +33 -0
- agentbundle/_data/plugin-manifest.schema.json +18 -0
- agentbundle/build/__init__.py +206 -0
- agentbundle/build/__main__.py +8 -0
- agentbundle/build/adapter_root_bins.py +336 -0
- agentbundle/build/adapters/__init__.py +46 -0
- agentbundle/build/adapters/claude_code.py +142 -0
- agentbundle/build/adapters/codex.py +227 -0
- agentbundle/build/adapters/copilot.py +149 -0
- agentbundle/build/adapters/kiro.py +608 -0
- agentbundle/build/adapters/kiro_cli.py +53 -0
- agentbundle/build/adapters/kiro_ide.py +275 -0
- agentbundle/build/contract.py +20 -0
- agentbundle/build/lint_packs.py +555 -0
- agentbundle/build/main.py +596 -0
- agentbundle/build/phase_order.py +40 -0
- agentbundle/build/projections/__init__.py +13 -0
- agentbundle/build/projections/codex_agent_toml.py +232 -0
- agentbundle/build/projections/copilot_agent_md.py +206 -0
- agentbundle/build/projections/copilot_hooks_json.py +142 -0
- agentbundle/build/projections/direct_directory.py +41 -0
- agentbundle/build/projections/hook_id.py +27 -0
- agentbundle/build/projections/kiro_ide_hook.py +256 -0
- agentbundle/build/projections/merge_into_agent_json.py +264 -0
- agentbundle/build/projections/merge_json.py +58 -0
- agentbundle/build/projections/user_merge_json.py +324 -0
- agentbundle/build/scope_rails.py +728 -0
- agentbundle/build/self_host.py +1486 -0
- agentbundle/build/shared_libs.py +309 -0
- agentbundle/build/target_resolver.py +85 -0
- agentbundle/build/tests/__init__.py +0 -0
- agentbundle/build/tests/test_adapter_claude_code.py +275 -0
- agentbundle/build/tests/test_adapter_codex.py +699 -0
- agentbundle/build/tests/test_adapter_copilot.py +91 -0
- agentbundle/build/tests/test_adapter_kiro.py +449 -0
- agentbundle/build/tests/test_adapter_kiro_alias.py +105 -0
- agentbundle/build/tests/test_adapter_kiro_cli.py +102 -0
- agentbundle/build/tests/test_adapter_kiro_ide.py +173 -0
- agentbundle/build/tests/test_adapter_root_bins_projection.py +429 -0
- agentbundle/build/tests/test_build_ships_seeds.py +78 -0
- agentbundle/build/tests/test_contract.py +582 -0
- agentbundle/build/tests/test_contract_scope.py +224 -0
- agentbundle/build/tests/test_contract_v07.py +191 -0
- agentbundle/build/tests/test_contract_v08.py +230 -0
- agentbundle/build/tests/test_direct_directory_cleanup.py +65 -0
- agentbundle/build/tests/test_end_to_end_build.py +227 -0
- agentbundle/build/tests/test_lint_agents_md_legacy_block.py +135 -0
- agentbundle/build/tests/test_lint_agents_md_risk_block.py +116 -0
- agentbundle/build/tests/test_lint_packs.py +703 -0
- agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +176 -0
- agentbundle/build/tests/test_pack_schema.py +265 -0
- agentbundle/build/tests/test_pack_schema_allowed_adapters.py +258 -0
- agentbundle/build/tests/test_pack_schema_install.py +305 -0
- agentbundle/build/tests/test_pipeline.py +272 -0
- agentbundle/build/tests/test_plugin_manifest_schema.py +327 -0
- agentbundle/build/tests/test_projections_merge_json.py +148 -0
- agentbundle/build/tests/test_scope_rails.py +398 -0
- agentbundle/build/tests/test_security.py +97 -0
- agentbundle/build/tests/test_self_host_check.py +2100 -0
- agentbundle/build/tests/test_shared_libs_projection.py +415 -0
- agentbundle/build/tests/test_shipped_packs_v07_declarations.py +100 -0
- agentbundle/build/tests/test_shipped_packs_v08_declarations.py +80 -0
- agentbundle/build/tests/test_validate.py +250 -0
- agentbundle/build/validate.py +141 -0
- agentbundle/catalogue.py +164 -0
- agentbundle/cli.py +486 -0
- agentbundle/commands/__init__.py +5 -0
- agentbundle/commands/_common.py +174 -0
- agentbundle/commands/_drop_warning.py +329 -0
- agentbundle/commands/adapt.py +343 -0
- agentbundle/commands/config.py +125 -0
- agentbundle/commands/diff.py +211 -0
- agentbundle/commands/init_state.py +279 -0
- agentbundle/commands/install.py +3026 -0
- agentbundle/commands/list_packs.py +170 -0
- agentbundle/commands/list_targets.py +23 -0
- agentbundle/commands/reconcile.py +161 -0
- agentbundle/commands/render.py +165 -0
- agentbundle/commands/scaffold.py +69 -0
- agentbundle/commands/uninstall.py +294 -0
- agentbundle/commands/upgrade.py +699 -0
- agentbundle/commands/validate.py +688 -0
- agentbundle/config.py +747 -0
- agentbundle/render.py +123 -0
- agentbundle/safety.py +633 -0
- agentbundle/scope.py +319 -0
- agentbundle/user_config.py +284 -0
- agentbundle/version.py +49 -0
- agentbundle-0.2.0.dist-info/METADATA +37 -0
- agentbundle-0.2.0.dist-info/RECORD +99 -0
- agentbundle-0.2.0.dist-info/WHEEL +5 -0
- agentbundle-0.2.0.dist-info/entry_points.txt +2 -0
- agentbundle-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""kiro-ide adapter — projects primitives for the Kiro VS Code-fork IDE.
|
|
2
|
+
|
|
3
|
+
Targets the Kiro IDE, not the `kiro` CLI binary. Key differences from kiro-cli:
|
|
4
|
+
|
|
5
|
+
- Agents project as `.md` (YAML frontmatter + body), loaded by the IDE
|
|
6
|
+
via gray-matter. The `kiro-ide-agent-frontmatter-v0.9` mapping applies IDE
|
|
7
|
+
tool ids (read_file, grep_search, etc.) — not CLI short-names.
|
|
8
|
+
- `hook-wiring` is DROPPED. The IDE loader silently drops any agent carrying
|
|
9
|
+
a `hooks` key (RFC-0022 E2). Use kiro-ide-hook instead.
|
|
10
|
+
- `kiro-ide-hook` is ACTIVATED. Flat projection path confirmed by Q6 probe
|
|
11
|
+
(no-recursion, yes-extension-filter, 2026-06-01, Kiro 0.12.224):
|
|
12
|
+
`.kiro/hooks/<pack>--<name>.kiro.hook`.
|
|
13
|
+
|
|
14
|
+
The deprecated `kiro` adapter maps to this module (T4).
|
|
15
|
+
|
|
16
|
+
Phase order (from phase_order.PHASE_ORDER):
|
|
17
|
+
hook-body → agent → kiro-ide-hook → command → skill
|
|
18
|
+
(hook-wiring is skipped — dropped for kiro-ide).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Iterator
|
|
27
|
+
|
|
28
|
+
from agentbundle.build.phase_order import PHASE_ORDER as _PHASE_ORDER
|
|
29
|
+
from agentbundle.build.projections.direct_directory import sweep_orphans
|
|
30
|
+
from agentbundle.build.projections.kiro_ide_hook import project as kiro_ide_hook_project
|
|
31
|
+
|
|
32
|
+
# Import frontmatter helpers from kiro.py (pure functions, no side effects).
|
|
33
|
+
from agentbundle.build.adapters.kiro import (
|
|
34
|
+
_split_frontmatter,
|
|
35
|
+
_parse_frontmatter,
|
|
36
|
+
_apply_mapping,
|
|
37
|
+
_project_direct_directory,
|
|
38
|
+
_project_direct_file,
|
|
39
|
+
_project_direct_file_template,
|
|
40
|
+
_resolve_kiro_hook_body_target_dir as _resolve_hook_body_target_dir_from_kiro,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_ADAPTER = "kiro-ide"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def project(pack_path: Path, contract: dict, output_root: Path) -> None:
|
|
47
|
+
"""Single-pack convenience wrapper. Delegates to `project_packs`."""
|
|
48
|
+
project_packs([pack_path], contract, output_root)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def project_packs(pack_paths: list[Path], contract: dict, output_root: Path) -> None:
|
|
52
|
+
"""Project every pack in `pack_paths` using the kiro-ide adapter block."""
|
|
53
|
+
for pack_path in pack_paths:
|
|
54
|
+
_project_single(pack_path, contract, output_root)
|
|
55
|
+
_sweep_skill_orphans(pack_paths, contract, output_root)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _skill_direct_directory_target(contract: dict, output_root: Path) -> Path | None:
|
|
59
|
+
adapter_block = contract["adapter"][_ADAPTER]
|
|
60
|
+
for entry in adapter_block.get("projection", []):
|
|
61
|
+
if entry.get("primitive") == "skill" and entry.get("mode") == "direct-directory":
|
|
62
|
+
return output_root / entry["target-path"].rstrip("/")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _sweep_skill_orphans(pack_paths: list[Path], contract: dict, output_root: Path) -> None:
|
|
67
|
+
target_dir = _skill_direct_directory_target(contract, output_root)
|
|
68
|
+
if target_dir is None:
|
|
69
|
+
return
|
|
70
|
+
skill_source_path = contract["primitive"]["skill"]["source-path"].rstrip("/")
|
|
71
|
+
expected_names: set[str] = set()
|
|
72
|
+
for pack_path in pack_paths:
|
|
73
|
+
source_dir = pack_path / skill_source_path
|
|
74
|
+
if not source_dir.exists():
|
|
75
|
+
continue
|
|
76
|
+
for entry in source_dir.iterdir():
|
|
77
|
+
if entry.is_dir():
|
|
78
|
+
expected_names.add(entry.name)
|
|
79
|
+
sweep_orphans(target_dir, expected_names)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _iter_primitives(contract: dict) -> Iterator[str]:
|
|
83
|
+
"""Yield kiro-ide's projected primitive names in phase order.
|
|
84
|
+
|
|
85
|
+
Skips hook-wiring (dropped for kiro-ide) and any dropped table-form
|
|
86
|
+
entries. kiro-ide-hook is included when its mode is not dropped.
|
|
87
|
+
"""
|
|
88
|
+
adapter_block = contract["adapter"][_ADAPTER]
|
|
89
|
+
array_form = {e["primitive"]: e for e in adapter_block.get("projection", [])}
|
|
90
|
+
table_form = adapter_block.get("projections", {}) or {}
|
|
91
|
+
|
|
92
|
+
for primitive_name in _PHASE_ORDER:
|
|
93
|
+
if primitive_name in array_form:
|
|
94
|
+
if array_form[primitive_name].get("mode") == "dropped":
|
|
95
|
+
continue
|
|
96
|
+
yield primitive_name
|
|
97
|
+
elif primitive_name in table_form:
|
|
98
|
+
rule = table_form[primitive_name]
|
|
99
|
+
effective_mode = rule.get("mode")
|
|
100
|
+
if isinstance(effective_mode, dict):
|
|
101
|
+
effective_mode = effective_mode.get("repo")
|
|
102
|
+
if effective_mode == "dropped":
|
|
103
|
+
continue
|
|
104
|
+
yield primitive_name
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _project_single(pack_path: Path, contract: dict, output_root: Path) -> None:
|
|
108
|
+
adapter_block = contract["adapter"][_ADAPTER]
|
|
109
|
+
array_form = {e["primitive"]: e for e in adapter_block.get("projection", [])}
|
|
110
|
+
table_form = adapter_block.get("projections", {}) or {}
|
|
111
|
+
|
|
112
|
+
for primitive_name in _iter_primitives(contract):
|
|
113
|
+
# kiro-ide-hook: source dir is implicit (.apm/kiro-ide-hooks/),
|
|
114
|
+
# dispatch to the dedicated projector directly.
|
|
115
|
+
if primitive_name == "kiro-ide-hook":
|
|
116
|
+
rule = table_form.get("kiro-ide-hook", {})
|
|
117
|
+
_dispatch_kiro_ide_hook(pack_path, output_root, rule, contract)
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
prim_def = contract["primitive"].get(primitive_name)
|
|
121
|
+
if prim_def is None:
|
|
122
|
+
continue
|
|
123
|
+
source_dir = pack_path / prim_def["source-path"].rstrip("/")
|
|
124
|
+
if not source_dir.exists():
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
if primitive_name in array_form:
|
|
128
|
+
rule = array_form[primitive_name]
|
|
129
|
+
_dispatch_array_form(primitive_name, source_dir, output_root, rule, contract)
|
|
130
|
+
else:
|
|
131
|
+
rule = table_form[primitive_name]
|
|
132
|
+
_dispatch_table_form(primitive_name, source_dir, output_root, rule)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _dispatch_array_form(
|
|
136
|
+
primitive_name: str,
|
|
137
|
+
source_dir: Path,
|
|
138
|
+
output_root: Path,
|
|
139
|
+
rule: dict,
|
|
140
|
+
contract: dict,
|
|
141
|
+
) -> None:
|
|
142
|
+
mode = rule["mode"]
|
|
143
|
+
if mode == "direct-directory":
|
|
144
|
+
_project_direct_directory(source_dir, output_root / rule["target-path"].rstrip("/"))
|
|
145
|
+
elif mode == "direct-file":
|
|
146
|
+
if primitive_name == "agent":
|
|
147
|
+
_project_agent_as_md(source_dir, output_root, rule, contract)
|
|
148
|
+
else:
|
|
149
|
+
_project_direct_file(source_dir, output_root, rule["target-path"])
|
|
150
|
+
else:
|
|
151
|
+
raise ValueError(f"kiro-ide: unhandled array-form mode {mode!r} for {primitive_name}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _dispatch_table_form(
|
|
155
|
+
primitive_name: str,
|
|
156
|
+
source_dir: Path,
|
|
157
|
+
output_root: Path,
|
|
158
|
+
rule: dict,
|
|
159
|
+
) -> None:
|
|
160
|
+
mode = rule.get("mode")
|
|
161
|
+
effective_mode = mode["repo"] if isinstance(mode, dict) else mode
|
|
162
|
+
|
|
163
|
+
if effective_mode == "direct-file":
|
|
164
|
+
target = rule.get("target")
|
|
165
|
+
target_template = target.get("repo") if isinstance(target, dict) else target
|
|
166
|
+
if target_template:
|
|
167
|
+
_project_direct_file_template(source_dir, output_root, target_template)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _dispatch_kiro_ide_hook(
|
|
171
|
+
pack_path: Path,
|
|
172
|
+
output_root: Path,
|
|
173
|
+
rule: dict,
|
|
174
|
+
contract: dict,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Delegate kiro-ide-hook projection to the dedicated projector.
|
|
177
|
+
|
|
178
|
+
Uses the flat target path confirmed by Q6 probe:
|
|
179
|
+
`.kiro/hooks/<pack>--<name>.kiro.hook`
|
|
180
|
+
"""
|
|
181
|
+
target = rule.get("target")
|
|
182
|
+
if isinstance(target, dict):
|
|
183
|
+
target_template = target.get("repo")
|
|
184
|
+
else:
|
|
185
|
+
target_template = target
|
|
186
|
+
if not target_template:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Resolve hook-body target dir from the kiro-ide adapter block.
|
|
190
|
+
# The kiro-ide block has hook-body in the array form (tools/hooks/)
|
|
191
|
+
# so we look there first.
|
|
192
|
+
adapter_block = contract.get("adapter", {}).get(_ADAPTER, {})
|
|
193
|
+
hook_body_target_dir = "tools/hooks"
|
|
194
|
+
for entry in adapter_block.get("projection", []):
|
|
195
|
+
if entry.get("primitive") == "hook-body" and entry.get("mode") == "direct-file":
|
|
196
|
+
hook_body_target_dir = entry.get("target-path", "tools/hooks/").rstrip("/")
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
kiro_ide_hook_project(
|
|
200
|
+
pack_path,
|
|
201
|
+
output_root,
|
|
202
|
+
target_template=target_template,
|
|
203
|
+
hook_body_target_dir=hook_body_target_dir,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Agent .md → .md rewrite (kiro-ide specific)
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _project_agent_as_md(
|
|
213
|
+
source_dir: Path,
|
|
214
|
+
output_root: Path,
|
|
215
|
+
rule: dict,
|
|
216
|
+
contract: dict,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Read `.apm/agents/<name>.md` and emit `.kiro/agents/<name>.md`.
|
|
219
|
+
|
|
220
|
+
The source format is YAML-style frontmatter (--- fence) + markdown body.
|
|
221
|
+
The output preserves the markdown format but rewrites frontmatter fields
|
|
222
|
+
through the `kiro-ide-agent-frontmatter-v0.9` mapping table (IDE tool ids,
|
|
223
|
+
model aliases). No CLI-only keys (hooks, allowedTools, toolsSettings,
|
|
224
|
+
mcpServers) appear in the output — these are not in the mapping table
|
|
225
|
+
and would cause the IDE loader to silently drop the agent.
|
|
226
|
+
"""
|
|
227
|
+
target_dir = output_root / rule["target-path"].rstrip("/")
|
|
228
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
mapping_name = rule.get("frontmatter-mapping")
|
|
231
|
+
mapping = (
|
|
232
|
+
contract.get("frontmatter-mapping", {}).get(mapping_name, {})
|
|
233
|
+
if mapping_name
|
|
234
|
+
else {}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
for entry in sorted(source_dir.iterdir()):
|
|
238
|
+
if not (entry.is_file() and entry.suffix == ".md"):
|
|
239
|
+
continue
|
|
240
|
+
frontmatter, body = _split_frontmatter(entry.read_text(encoding="utf-8"))
|
|
241
|
+
rewritten = _apply_mapping(frontmatter, mapping)
|
|
242
|
+
|
|
243
|
+
# Ensure name is present (derived from filename if not in frontmatter).
|
|
244
|
+
agent_name = rewritten.get("name") or entry.stem
|
|
245
|
+
rewritten["name"] = agent_name
|
|
246
|
+
|
|
247
|
+
output_text = _serialize_frontmatter_md(rewritten) + body
|
|
248
|
+
destination = target_dir / entry.name # preserves .md extension
|
|
249
|
+
destination.write_text(output_text, encoding="utf-8")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _serialize_frontmatter_md(fields: dict[str, Any]) -> str:
|
|
253
|
+
"""Emit a YAML frontmatter block for a kiro-ide .md agent file.
|
|
254
|
+
|
|
255
|
+
Produces `--- ... ---\n` wrapping. Strings are plain scalars unless they
|
|
256
|
+
contain YAML-special characters, in which case they are double-quoted.
|
|
257
|
+
Lists are emitted as YAML flow sequences: `[item1, item2]`. The gray-matter
|
|
258
|
+
parser in the Kiro IDE accepts both styles.
|
|
259
|
+
"""
|
|
260
|
+
lines = ["---"]
|
|
261
|
+
for key, value in fields.items():
|
|
262
|
+
if isinstance(value, list):
|
|
263
|
+
items = ", ".join(str(v) for v in value)
|
|
264
|
+
lines.append(f"{key}: [{items}]")
|
|
265
|
+
elif isinstance(value, str):
|
|
266
|
+
# Quote strings that contain YAML-special characters.
|
|
267
|
+
if any(c in value for c in ':#{}[]|>&*!,\'"'):
|
|
268
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
269
|
+
lines.append(f'{key}: "{escaped}"')
|
|
270
|
+
else:
|
|
271
|
+
lines.append(f"{key}: {value}")
|
|
272
|
+
else:
|
|
273
|
+
lines.append(f"{key}: {value}")
|
|
274
|
+
lines.append("---")
|
|
275
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Contract loader.
|
|
2
|
+
|
|
3
|
+
Reads `docs/contracts/adapter.toml` via `tomllib` and
|
|
4
|
+
returns a dict. No validation logic — `validate.py` does that, and
|
|
5
|
+
the CLI's `validate` subcommand wires the two together.
|
|
6
|
+
|
|
7
|
+
The loader exposes a single function `load(path)` so adapters and
|
|
8
|
+
the build pipeline share the same input shape.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import tomllib
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load(path: str | Path) -> dict:
|
|
18
|
+
"""Load a TOML file from disk and return its parsed contents."""
|
|
19
|
+
contents = Path(path).read_bytes()
|
|
20
|
+
return tomllib.loads(contents.decode("utf-8"))
|