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.
Files changed (99) hide show
  1. agentbundle/__init__.py +14 -0
  2. agentbundle/__main__.py +5 -0
  3. agentbundle/_data/adapter.schema.json +270 -0
  4. agentbundle/_data/adapter.toml +584 -0
  5. agentbundle/_data/install-marker.py +1099 -0
  6. agentbundle/_data/pack.schema.json +152 -0
  7. agentbundle/_data/plugin-manifest.derived.schema.json +33 -0
  8. agentbundle/_data/plugin-manifest.schema.json +18 -0
  9. agentbundle/build/__init__.py +206 -0
  10. agentbundle/build/__main__.py +8 -0
  11. agentbundle/build/adapter_root_bins.py +336 -0
  12. agentbundle/build/adapters/__init__.py +46 -0
  13. agentbundle/build/adapters/claude_code.py +142 -0
  14. agentbundle/build/adapters/codex.py +227 -0
  15. agentbundle/build/adapters/copilot.py +149 -0
  16. agentbundle/build/adapters/kiro.py +608 -0
  17. agentbundle/build/adapters/kiro_cli.py +53 -0
  18. agentbundle/build/adapters/kiro_ide.py +275 -0
  19. agentbundle/build/contract.py +20 -0
  20. agentbundle/build/lint_packs.py +555 -0
  21. agentbundle/build/main.py +596 -0
  22. agentbundle/build/phase_order.py +40 -0
  23. agentbundle/build/projections/__init__.py +13 -0
  24. agentbundle/build/projections/codex_agent_toml.py +232 -0
  25. agentbundle/build/projections/copilot_agent_md.py +206 -0
  26. agentbundle/build/projections/copilot_hooks_json.py +142 -0
  27. agentbundle/build/projections/direct_directory.py +41 -0
  28. agentbundle/build/projections/hook_id.py +27 -0
  29. agentbundle/build/projections/kiro_ide_hook.py +256 -0
  30. agentbundle/build/projections/merge_into_agent_json.py +264 -0
  31. agentbundle/build/projections/merge_json.py +58 -0
  32. agentbundle/build/projections/user_merge_json.py +324 -0
  33. agentbundle/build/scope_rails.py +728 -0
  34. agentbundle/build/self_host.py +1486 -0
  35. agentbundle/build/shared_libs.py +309 -0
  36. agentbundle/build/target_resolver.py +85 -0
  37. agentbundle/build/tests/__init__.py +0 -0
  38. agentbundle/build/tests/test_adapter_claude_code.py +275 -0
  39. agentbundle/build/tests/test_adapter_codex.py +699 -0
  40. agentbundle/build/tests/test_adapter_copilot.py +91 -0
  41. agentbundle/build/tests/test_adapter_kiro.py +449 -0
  42. agentbundle/build/tests/test_adapter_kiro_alias.py +105 -0
  43. agentbundle/build/tests/test_adapter_kiro_cli.py +102 -0
  44. agentbundle/build/tests/test_adapter_kiro_ide.py +173 -0
  45. agentbundle/build/tests/test_adapter_root_bins_projection.py +429 -0
  46. agentbundle/build/tests/test_build_ships_seeds.py +78 -0
  47. agentbundle/build/tests/test_contract.py +582 -0
  48. agentbundle/build/tests/test_contract_scope.py +224 -0
  49. agentbundle/build/tests/test_contract_v07.py +191 -0
  50. agentbundle/build/tests/test_contract_v08.py +230 -0
  51. agentbundle/build/tests/test_direct_directory_cleanup.py +65 -0
  52. agentbundle/build/tests/test_end_to_end_build.py +227 -0
  53. agentbundle/build/tests/test_lint_agents_md_legacy_block.py +135 -0
  54. agentbundle/build/tests/test_lint_agents_md_risk_block.py +116 -0
  55. agentbundle/build/tests/test_lint_packs.py +703 -0
  56. agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +176 -0
  57. agentbundle/build/tests/test_pack_schema.py +265 -0
  58. agentbundle/build/tests/test_pack_schema_allowed_adapters.py +258 -0
  59. agentbundle/build/tests/test_pack_schema_install.py +305 -0
  60. agentbundle/build/tests/test_pipeline.py +272 -0
  61. agentbundle/build/tests/test_plugin_manifest_schema.py +327 -0
  62. agentbundle/build/tests/test_projections_merge_json.py +148 -0
  63. agentbundle/build/tests/test_scope_rails.py +398 -0
  64. agentbundle/build/tests/test_security.py +97 -0
  65. agentbundle/build/tests/test_self_host_check.py +2100 -0
  66. agentbundle/build/tests/test_shared_libs_projection.py +415 -0
  67. agentbundle/build/tests/test_shipped_packs_v07_declarations.py +100 -0
  68. agentbundle/build/tests/test_shipped_packs_v08_declarations.py +80 -0
  69. agentbundle/build/tests/test_validate.py +250 -0
  70. agentbundle/build/validate.py +141 -0
  71. agentbundle/catalogue.py +164 -0
  72. agentbundle/cli.py +486 -0
  73. agentbundle/commands/__init__.py +5 -0
  74. agentbundle/commands/_common.py +174 -0
  75. agentbundle/commands/_drop_warning.py +329 -0
  76. agentbundle/commands/adapt.py +343 -0
  77. agentbundle/commands/config.py +125 -0
  78. agentbundle/commands/diff.py +211 -0
  79. agentbundle/commands/init_state.py +279 -0
  80. agentbundle/commands/install.py +3026 -0
  81. agentbundle/commands/list_packs.py +170 -0
  82. agentbundle/commands/list_targets.py +23 -0
  83. agentbundle/commands/reconcile.py +161 -0
  84. agentbundle/commands/render.py +165 -0
  85. agentbundle/commands/scaffold.py +69 -0
  86. agentbundle/commands/uninstall.py +294 -0
  87. agentbundle/commands/upgrade.py +699 -0
  88. agentbundle/commands/validate.py +688 -0
  89. agentbundle/config.py +747 -0
  90. agentbundle/render.py +123 -0
  91. agentbundle/safety.py +633 -0
  92. agentbundle/scope.py +319 -0
  93. agentbundle/user_config.py +284 -0
  94. agentbundle/version.py +49 -0
  95. agentbundle-0.2.0.dist-info/METADATA +37 -0
  96. agentbundle-0.2.0.dist-info/RECORD +99 -0
  97. agentbundle-0.2.0.dist-info/WHEEL +5 -0
  98. agentbundle-0.2.0.dist-info/entry_points.txt +2 -0
  99. 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"))