agentbundle 0.3.1__tar.gz → 0.4.0__tar.gz
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-0.3.1 → agentbundle-0.4.0}/PKG-INFO +25 -9
- {agentbundle-0.3.1 → agentbundle-0.4.0}/README.md +23 -7
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/adapter.toml +9 -1
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/pack.schema.json +38 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/plugin-manifest.derived.schema.json +10 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/plugin-manifest.schema.json +11 -1
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/main.py +108 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/self_host.py +20 -1
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_cursor.py +3 -2
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_gemini.py +18 -4
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +4 -4
- agentbundle-0.4.0/agentbundle/build/tests/test_architect_design_reviewer_projection.py +88 -0
- agentbundle-0.4.0/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +74 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract.py +8 -8
- agentbundle-0.4.0/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +94 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema.py +150 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +69 -0
- agentbundle-0.4.0/agentbundle/build/tests/test_projectable_subset.py +188 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_self_host_check.py +39 -4
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/validate.py +10 -1
- agentbundle-0.4.0/agentbundle/categories.py +60 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/list_packs.py +17 -1
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/validate.py +20 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/PKG-INFO +25 -9
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/SOURCES.txt +5 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/pyproject.toml +2 -2
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/__main__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/adapter.schema.json +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/install-marker.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/__main__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapter_root_bins.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/claude_code.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/codex.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/copilot.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/cursor.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/gemini.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/contract.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/lint_packs.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/phase_order.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/direct_directory.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/hook_id.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/merge_json.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/user_merge_json.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/scope_rails.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/shared_libs.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/target_resolver.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pipeline.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_security.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_validate.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/user_libs.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/catalogue.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/cli.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/__init__.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/_common.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/_drop_warning.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/adapt.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/config.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/diff.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/init_state.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/install.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/list_targets.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/reconcile.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/render.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/scaffold.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/uninstall.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/upgrade.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/config.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/render.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/safety.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/scope.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/user_config.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/version.py +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/dependency_links.txt +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/entry_points.txt +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/top_level.txt +0 -0
- {agentbundle-0.3.1 → agentbundle-0.4.0}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentbundle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent.
|
|
5
5
|
Author-email: eugenelim <eugenelim@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0 OR MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/eugenelim/agent-ready-repo
|
|
8
8
|
Project-URL: Source, https://github.com/eugenelim/agent-ready-repo
|
|
9
|
-
Project-URL: Documentation, https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/how-to/install-agentbundle-from-clone.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/install-agentbundle-from-clone.md
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -60,17 +60,33 @@ A catalogue is a git URL or a local path. Installs auto-detect your agent; pass
|
|
|
60
60
|
|
|
61
61
|
```text
|
|
62
62
|
my-pack/
|
|
63
|
-
pack.toml # name, version, adapter-contract, install scope
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
pack.toml # name, version, adapter-contract, install scope,
|
|
64
|
+
# plus rich metadata (license, maintainers, links,
|
|
65
|
+
# categories, keywords) and a README pointer
|
|
66
|
+
.claude-plugin/
|
|
67
|
+
plugin.json # Claude Code plugin manifest (hand-authored)
|
|
68
|
+
README.md # the pack's portable doc — projected with the pack
|
|
69
|
+
.apm/ # primitives — projected by the build pipeline
|
|
70
|
+
skills/<name>/
|
|
71
|
+
SKILL.md # the skill body; one folder per skill
|
|
72
|
+
references/ # progressive-disclosure docs, loaded on demand
|
|
73
|
+
assets/ # templates the skill copies into the repo
|
|
74
|
+
agents/<name>.md # subagents
|
|
75
|
+
hooks/<name>.py # lifecycle hooks
|
|
76
|
+
seeds/ # files scaffolded into the adopter repo
|
|
69
77
|
```
|
|
70
78
|
|
|
79
|
+
`pack.toml` is the **single source of truth** for a pack's metadata. Declare
|
|
80
|
+
`license`, `[[pack.maintainers]]`, `[pack.links]`, `categories`, and
|
|
81
|
+
`keywords` once; the build projects the cleanly-mappable subset — plus the
|
|
82
|
+
pack's `README.md` — into each distribution route's manifest (the `plugin.json`
|
|
83
|
+
/ `marketplace.json` entry), so the catalogue describes each pack richly rather
|
|
84
|
+
than with a single sentence. Extra fields stay in `pack.toml`; the projection
|
|
85
|
+
is deliberately lossy per tool.
|
|
86
|
+
|
|
71
87
|
Point a catalogue URI (a git URL or a local path) at the repo that holds your packs. Then `validate` a pack against the adapter contract, `render` it to preview the projection, and `install` it into a target repo. `scaffold` drops a pack's seeds into a fresh directory to start from. The build pipeline (`agentbundle.build`) is the same engine `make build` runs.
|
|
72
88
|
|
|
73
|
-
See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/how-to/author-a-skill.md).
|
|
89
|
+
See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
|
|
74
90
|
|
|
75
91
|
## Credentials
|
|
76
92
|
|
|
@@ -42,17 +42,33 @@ A catalogue is a git URL or a local path. Installs auto-detect your agent; pass
|
|
|
42
42
|
|
|
43
43
|
```text
|
|
44
44
|
my-pack/
|
|
45
|
-
pack.toml # name, version, adapter-contract, install scope
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
pack.toml # name, version, adapter-contract, install scope,
|
|
46
|
+
# plus rich metadata (license, maintainers, links,
|
|
47
|
+
# categories, keywords) and a README pointer
|
|
48
|
+
.claude-plugin/
|
|
49
|
+
plugin.json # Claude Code plugin manifest (hand-authored)
|
|
50
|
+
README.md # the pack's portable doc — projected with the pack
|
|
51
|
+
.apm/ # primitives — projected by the build pipeline
|
|
52
|
+
skills/<name>/
|
|
53
|
+
SKILL.md # the skill body; one folder per skill
|
|
54
|
+
references/ # progressive-disclosure docs, loaded on demand
|
|
55
|
+
assets/ # templates the skill copies into the repo
|
|
56
|
+
agents/<name>.md # subagents
|
|
57
|
+
hooks/<name>.py # lifecycle hooks
|
|
58
|
+
seeds/ # files scaffolded into the adopter repo
|
|
51
59
|
```
|
|
52
60
|
|
|
61
|
+
`pack.toml` is the **single source of truth** for a pack's metadata. Declare
|
|
62
|
+
`license`, `[[pack.maintainers]]`, `[pack.links]`, `categories`, and
|
|
63
|
+
`keywords` once; the build projects the cleanly-mappable subset — plus the
|
|
64
|
+
pack's `README.md` — into each distribution route's manifest (the `plugin.json`
|
|
65
|
+
/ `marketplace.json` entry), so the catalogue describes each pack richly rather
|
|
66
|
+
than with a single sentence. Extra fields stay in `pack.toml`; the projection
|
|
67
|
+
is deliberately lossy per tool.
|
|
68
|
+
|
|
53
69
|
Point a catalogue URI (a git URL or a local path) at the repo that holds your packs. Then `validate` a pack against the adapter contract, `render` it to preview the projection, and `install` it into a target repo. `scaffold` drops a pack's seeds into a fresh directory to start from. The build pipeline (`agentbundle.build`) is the same engine `make build` runs.
|
|
54
70
|
|
|
55
|
-
See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/how-to/author-a-skill.md).
|
|
71
|
+
See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
|
|
56
72
|
|
|
57
73
|
## Credentials
|
|
58
74
|
|
|
@@ -63,7 +63,15 @@
|
|
|
63
63
|
# kept and name-mapped; tier-preserving model map. hook-body lands under
|
|
64
64
|
# `.gemini/hooks/` (the cursor model — single prefix at both scopes). kiro-ide-hook
|
|
65
65
|
# dropped. Distribution-only (not in SELF_HOST_ADAPTERS).
|
|
66
|
-
|
|
66
|
+
# RFC-0031 / ADR-0021 / docs/specs/enriched-pack-manifest (v0.14): pack.toml
|
|
67
|
+
# becomes the rich metadata source of truth (optional readme / display_name /
|
|
68
|
+
# license / maintainers / links / categories / keywords / [pack.metadata.<tool>]
|
|
69
|
+
# / [pack].catalogue). The build projects the cleanly-projectable subset into
|
|
70
|
+
# the claude-plugins + apm routes (plugin.json / marketplace.json entry) plus
|
|
71
|
+
# each pack's README. All new fields are optional — packs pinned below v0.14
|
|
72
|
+
# build and validate unchanged. Declaration + projection only (no @catalogue/pack
|
|
73
|
+
# resolution, no search, no persisted index).
|
|
74
|
+
version = "0.14"
|
|
67
75
|
|
|
68
76
|
# Sibling schemas this contract references — pack metadata and the
|
|
69
77
|
# per-pack Claude-plugin manifest. Defined by spec AC #3 + #4.
|
|
@@ -9,6 +9,44 @@
|
|
|
9
9
|
"name": {"type": "string"},
|
|
10
10
|
"version": {"type": "string"},
|
|
11
11
|
"description": {"type": "string"},
|
|
12
|
+
"readme": {"type": "string"},
|
|
13
|
+
"display_name": {"type": "string"},
|
|
14
|
+
"license": {"type": "string"},
|
|
15
|
+
"catalogue": {"type": "string"},
|
|
16
|
+
"categories": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {"type": "string"},
|
|
19
|
+
"maxItems": 5
|
|
20
|
+
},
|
|
21
|
+
"keywords": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {"type": "string"},
|
|
24
|
+
"maxItems": 5
|
|
25
|
+
},
|
|
26
|
+
"maintainers": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["name"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"name": {"type": "string"},
|
|
33
|
+
"email": {"type": "string"},
|
|
34
|
+
"url": {"type": "string"}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"links": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"homepage": {"type": "string"},
|
|
42
|
+
"repository": {"type": "string"},
|
|
43
|
+
"documentation": {"type": "string"},
|
|
44
|
+
"changelog": {"type": "string"},
|
|
45
|
+
"issues": {"type": "string"},
|
|
46
|
+
"icon": {"type": "string"}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"metadata": {"type": "object"},
|
|
12
50
|
"adapter-contract": {
|
|
13
51
|
"type": "object",
|
|
14
52
|
"properties": {
|
{agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/plugin-manifest.derived.schema.json
RENAMED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
"type": "array",
|
|
15
15
|
"items": {"type": "string"}
|
|
16
16
|
},
|
|
17
|
+
"author": {"type": "string"},
|
|
18
|
+
"license": {"type": "string"},
|
|
19
|
+
"homepage": {"type": "string"},
|
|
20
|
+
"repository": {"type": "string"},
|
|
21
|
+
"keywords": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {"type": "string"}
|
|
24
|
+
},
|
|
25
|
+
"category": {"type": "string"},
|
|
26
|
+
"displayName": {"type": "string"},
|
|
17
27
|
"hooks": {
|
|
18
28
|
"type": "object",
|
|
19
29
|
"properties": {
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
"agents": {
|
|
14
14
|
"type": "array",
|
|
15
15
|
"items": {"type": "string"}
|
|
16
|
-
}
|
|
16
|
+
},
|
|
17
|
+
"author": {"type": "string"},
|
|
18
|
+
"license": {"type": "string"},
|
|
19
|
+
"homepage": {"type": "string"},
|
|
20
|
+
"repository": {"type": "string"},
|
|
21
|
+
"keywords": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {"type": "string"}
|
|
24
|
+
},
|
|
25
|
+
"category": {"type": "string"},
|
|
26
|
+
"displayName": {"type": "string"}
|
|
17
27
|
}
|
|
18
28
|
}
|
|
@@ -137,6 +137,23 @@ def _read_install_marker_template() -> bytes:
|
|
|
137
137
|
return (REPO_ROOT / "packages" / "agentbundle" / "templates" / "install-marker.py").read_bytes()
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def _project_pack_readme(pack_path: Path, per_pack_output: Path) -> None:
|
|
141
|
+
"""Copy a pack's ``README.md`` into its per-pack dist route, if present.
|
|
142
|
+
|
|
143
|
+
enriched-pack-manifest T5: the README is the sole portable per-pack doc,
|
|
144
|
+
and the manifest's ``readme = "README.md"`` pointer resolves relative to
|
|
145
|
+
the route directory. A pack without a README projects none and does not
|
|
146
|
+
error (the ``readme`` field is then simply absent / unresolved — never a
|
|
147
|
+
build failure). ``follow_symlinks=False`` mirrors the pack.toml copy so a
|
|
148
|
+
symlinked README is not dereferenced into ``dist/`` at build time.
|
|
149
|
+
"""
|
|
150
|
+
readme_src = pack_path / "README.md"
|
|
151
|
+
if readme_src.is_file():
|
|
152
|
+
shutil.copy2(
|
|
153
|
+
readme_src, per_pack_output / "README.md", follow_symlinks=False
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
140
157
|
def validate_derived_plugin_manifest_dict(manifest: dict, label: str = "<derived>") -> None:
|
|
141
158
|
"""Validate an in-memory derived plugin manifest dict against the derived schema.
|
|
142
159
|
|
|
@@ -161,6 +178,78 @@ def validate_derived_plugin_manifest(plugin_json_path: Path) -> None:
|
|
|
161
178
|
manifest = json.loads(plugin_json_path.read_text(encoding="utf-8"))
|
|
162
179
|
validate_derived_plugin_manifest_dict(manifest, label=str(plugin_json_path))
|
|
163
180
|
|
|
181
|
+
|
|
182
|
+
def derive_projectable_subset(pack_toml: dict) -> dict:
|
|
183
|
+
"""Map a parsed ``pack.toml`` to the projectable plugin-manifest subset.
|
|
184
|
+
|
|
185
|
+
enriched-pack-manifest (RFC-0031 / ADR-0021): ``pack.toml`` is the rich
|
|
186
|
+
metadata source of truth; the build projects a *lossy*, schema-compliant
|
|
187
|
+
subset into the claude-plugins + apm routes (the ``plugin.json`` /
|
|
188
|
+
``marketplace.json`` entry). Fixed mapping:
|
|
189
|
+
|
|
190
|
+
- ``author`` ← first ``[[pack.maintainers]]``, rendered
|
|
191
|
+
``"Name <email>"`` (name alone when no email).
|
|
192
|
+
- ``license`` ← ``[pack].license`` (verbatim).
|
|
193
|
+
- ``homepage`` ← ``[pack.links].homepage`` (verbatim).
|
|
194
|
+
- ``repository`` ← ``[pack.links].repository`` (verbatim).
|
|
195
|
+
- ``keywords`` ← ``[pack].keywords`` (string entries, verbatim).
|
|
196
|
+
- ``category`` ← ``categories[0]``.
|
|
197
|
+
- ``displayName`` ← ``[pack].display_name``.
|
|
198
|
+
|
|
199
|
+
**Emit-only-when-present** is the load-bearing invariant: a key appears in
|
|
200
|
+
the output only when its source field is present and non-empty, so a
|
|
201
|
+
legacy ``pack.toml`` declaring none of the enriched fields yields ``{}``
|
|
202
|
+
and the projected manifest is byte-identical to the pre-enrichment output
|
|
203
|
+
(legacy-invariance AC). This is a pure function — no I/O, no schema read.
|
|
204
|
+
"""
|
|
205
|
+
pack = pack_toml.get("pack", {})
|
|
206
|
+
if not isinstance(pack, dict):
|
|
207
|
+
return {}
|
|
208
|
+
out: dict = {}
|
|
209
|
+
|
|
210
|
+
maintainers = pack.get("maintainers")
|
|
211
|
+
if isinstance(maintainers, list) and maintainers:
|
|
212
|
+
first = maintainers[0]
|
|
213
|
+
if isinstance(first, dict):
|
|
214
|
+
name = first.get("name")
|
|
215
|
+
email = first.get("email")
|
|
216
|
+
if isinstance(name, str) and name:
|
|
217
|
+
if isinstance(email, str) and email:
|
|
218
|
+
out["author"] = f"{name} <{email}>"
|
|
219
|
+
else:
|
|
220
|
+
out["author"] = name
|
|
221
|
+
|
|
222
|
+
license_ = pack.get("license")
|
|
223
|
+
if isinstance(license_, str) and license_:
|
|
224
|
+
out["license"] = license_
|
|
225
|
+
|
|
226
|
+
links = pack.get("links")
|
|
227
|
+
if isinstance(links, dict):
|
|
228
|
+
homepage = links.get("homepage")
|
|
229
|
+
if isinstance(homepage, str) and homepage:
|
|
230
|
+
out["homepage"] = homepage
|
|
231
|
+
repository = links.get("repository")
|
|
232
|
+
if isinstance(repository, str) and repository:
|
|
233
|
+
out["repository"] = repository
|
|
234
|
+
|
|
235
|
+
keywords = pack.get("keywords")
|
|
236
|
+
if isinstance(keywords, list):
|
|
237
|
+
kws = [k for k in keywords if isinstance(k, str) and k]
|
|
238
|
+
if kws:
|
|
239
|
+
out["keywords"] = kws
|
|
240
|
+
|
|
241
|
+
categories = pack.get("categories")
|
|
242
|
+
if isinstance(categories, list) and categories:
|
|
243
|
+
first_cat = categories[0]
|
|
244
|
+
if isinstance(first_cat, str) and first_cat:
|
|
245
|
+
out["category"] = first_cat
|
|
246
|
+
|
|
247
|
+
display_name = pack.get("display_name")
|
|
248
|
+
if isinstance(display_name, str) and display_name:
|
|
249
|
+
out["displayName"] = display_name
|
|
250
|
+
|
|
251
|
+
return out
|
|
252
|
+
|
|
164
253
|
# The three RFC-0001 recipes that plain `make build` invokes.
|
|
165
254
|
# RFC-0002 recipes (per-pack-overlay, composite-agents-md,
|
|
166
255
|
# composite-marketplace) fire only under --self.
|
|
@@ -392,6 +481,15 @@ def _run_per_pack_single(
|
|
|
392
481
|
derived["hooks"] = {
|
|
393
482
|
"SessionStart": [{"command": _SESSION_START_COMMAND}]
|
|
394
483
|
}
|
|
484
|
+
# enriched-pack-manifest: merge the projectable metadata subset derived
|
|
485
|
+
# from this pack's pack.toml (emit-only-when-present, so a legacy pack
|
|
486
|
+
# adds no keys and the output stays byte-identical).
|
|
487
|
+
pack_toml_for_subset = pack.path / "pack.toml"
|
|
488
|
+
if pack_toml_for_subset.exists():
|
|
489
|
+
pack_meta = tomllib.loads(
|
|
490
|
+
pack_toml_for_subset.read_text(encoding="utf-8")
|
|
491
|
+
)
|
|
492
|
+
derived.update(derive_projectable_subset(pack_meta))
|
|
395
493
|
# Validate the derived manifest IN MEMORY before writing to disk
|
|
396
494
|
# (Blocker-3: pre-write validation so a synthesis bug never lands
|
|
397
495
|
# a malformed plugin.json in dist/).
|
|
@@ -411,6 +509,12 @@ def _run_per_pack_single(
|
|
|
411
509
|
if pack_toml_src.exists():
|
|
412
510
|
shutil.copy2(pack_toml_src, per_pack_output / "pack.toml", follow_symlinks=False)
|
|
413
511
|
|
|
512
|
+
# enriched-pack-manifest T5: project the pack's README.md into the route so
|
|
513
|
+
# the manifest's `readme = "README.md"` pointer resolves. The README is the
|
|
514
|
+
# sole portable per-pack doc. follow_symlinks=False mirrors the pack.toml
|
|
515
|
+
# copy's posture (a symlinked README is not dereferenced into dist/).
|
|
516
|
+
_project_pack_readme(pack.path, per_pack_output)
|
|
517
|
+
|
|
414
518
|
# Project the canonical install-marker.py writer into scripts/.
|
|
415
519
|
scripts_dir = per_pack_output / ".claude-plugin" / "scripts"
|
|
416
520
|
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -478,6 +582,10 @@ def _run_per_pack_apm(recipe: Recipe, packs: list[Pack], output_dir: Path) -> di
|
|
|
478
582
|
follow_symlinks=False,
|
|
479
583
|
)
|
|
480
584
|
|
|
585
|
+
# enriched-pack-manifest T5: project the pack's README into the APM
|
|
586
|
+
# route too (the sole portable per-pack doc; same posture as above).
|
|
587
|
+
_project_pack_readme(pack.path, per_pack_output)
|
|
588
|
+
|
|
481
589
|
# Issue #190 / RFC-0001 §595: ship the pack's seeds/ inside the APM
|
|
482
590
|
# package so the governance content travels with the pack on the APM
|
|
483
591
|
# route. symlinks=True preserves a seed symlink as a symlink rather
|
|
@@ -39,6 +39,7 @@ import shutil
|
|
|
39
39
|
import stat
|
|
40
40
|
import subprocess
|
|
41
41
|
import sys
|
|
42
|
+
import tomllib
|
|
42
43
|
import tempfile
|
|
43
44
|
from pathlib import Path
|
|
44
45
|
|
|
@@ -47,6 +48,7 @@ from agentbundle.build.contract import load as load_contract
|
|
|
47
48
|
from agentbundle.build.main import (
|
|
48
49
|
CONTRACT_PATH,
|
|
49
50
|
REPO_ROOT,
|
|
51
|
+
derive_projectable_subset,
|
|
50
52
|
discover_packs,
|
|
51
53
|
validate_pack_uniqueness,
|
|
52
54
|
)
|
|
@@ -480,6 +482,14 @@ def _project_seeds(packs_dir: Path, output_root: Path) -> dict[Path, Path]:
|
|
|
480
482
|
# For re-install / self-host against this repo, Manual targets
|
|
481
483
|
# exist and are preserved.
|
|
482
484
|
for relative, src in seen.items():
|
|
485
|
+
# Guides are repo-owned and reach adopters via `deliver_seeds` at
|
|
486
|
+
# install, not via self-host projection. Never scaffold the
|
|
487
|
+
# by-quadrant guide tree here: the seed stays by-quadrant for
|
|
488
|
+
# adopters, but writing it during self-host litters a repo that
|
|
489
|
+
# owns its guides (e.g. organized by pack) with untracked
|
|
490
|
+
# `docs/guides/<quadrant>/README.md` on every build-self run.
|
|
491
|
+
if relative.as_posix().startswith("docs/guides/"):
|
|
492
|
+
continue
|
|
483
493
|
if _is_excluded(relative) and (output_root / relative).exists():
|
|
484
494
|
# Manual file on disk — leave it alone. The seed is
|
|
485
495
|
# placeholder; the on-disk file is the adopter's
|
|
@@ -507,7 +517,16 @@ def _aggregate_marketplace(
|
|
|
507
517
|
continue
|
|
508
518
|
manifest = pack_path / ".claude-plugin" / "plugin.json"
|
|
509
519
|
if manifest.exists():
|
|
510
|
-
|
|
520
|
+
entry = json.loads(manifest.read_text(encoding="utf-8"))
|
|
521
|
+
# enriched-pack-manifest: surface the projectable metadata subset
|
|
522
|
+
# (author / license / links / keywords / category / displayName)
|
|
523
|
+
# derived from pack.toml, so the catalogue entry is described
|
|
524
|
+
# richly. Emit-only-when-present keeps legacy entries byte-identical.
|
|
525
|
+
pack_meta = tomllib.loads(
|
|
526
|
+
(pack_path / "pack.toml").read_text(encoding="utf-8")
|
|
527
|
+
)
|
|
528
|
+
entry.update(derive_projectable_subset(pack_meta))
|
|
529
|
+
entries.append(entry)
|
|
511
530
|
target = output_root / ".claude-plugin" / "marketplace.json"
|
|
512
531
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
513
532
|
payload = {
|
|
@@ -52,9 +52,10 @@ class CursorContractTests(unittest.TestCase):
|
|
|
52
52
|
|
|
53
53
|
def test_contract_version_is_0_11(self) -> None:
|
|
54
54
|
"""AC1 — contract bumped to 0.11 by cursor-full-parity; subsequently
|
|
55
|
-
0.12 (copilot-skills-and-web)
|
|
55
|
+
0.12 (copilot-skills-and-web), 0.13 (docs/specs/gemini-full-parity),
|
|
56
|
+
then 0.14 (docs/specs/enriched-pack-manifest).
|
|
56
57
|
Name preserved to keep the diff small."""
|
|
57
|
-
self.assertEqual(self.contract["contract"]["version"], "0.
|
|
58
|
+
self.assertEqual(self.contract["contract"]["version"], "0.14")
|
|
58
59
|
|
|
59
60
|
def test_cursor_block_projects_five_primitives(self) -> None:
|
|
60
61
|
"""AC2 — the five standard primitives are in the projection array."""
|
|
@@ -150,9 +150,10 @@ class GeminiContractTests(unittest.TestCase):
|
|
|
150
150
|
def setUpClass(cls) -> None:
|
|
151
151
|
cls.contract = load_contract(CONTRACT_PATH)
|
|
152
152
|
|
|
153
|
-
def
|
|
154
|
-
"""
|
|
155
|
-
|
|
153
|
+
def test_contract_version_is_0_14(self) -> None:
|
|
154
|
+
"""Contract version is 0.14 (docs/specs/enriched-pack-manifest bumped it
|
|
155
|
+
from gemini-full-parity's 0.13). Renamed from the 0_13 method."""
|
|
156
|
+
self.assertEqual(self.contract["contract"]["version"], "0.14")
|
|
156
157
|
|
|
157
158
|
def test_gemini_block_projects_five_primitives(self) -> None:
|
|
158
159
|
"""AC2 — five standard primitives with their gemini targets."""
|
|
@@ -591,9 +592,18 @@ class GeminiAllPacksAdmissibleTests(unittest.TestCase):
|
|
|
591
592
|
from agentbundle.commands.install import _resolve_target_adapter
|
|
592
593
|
|
|
593
594
|
pack_dirs = sorted(p for p in self.PACKS_DIR.iterdir() if (p / "pack.toml").exists())
|
|
594
|
-
self.
|
|
595
|
+
self.assertTrue(pack_dirs, "no packs discovered under packs/ — pack lookup is broken")
|
|
596
|
+
# Count-independent by design: don't pin the number of packs (every new
|
|
597
|
+
# pack would break an unrelated adapter test). Assert gemini resolution
|
|
598
|
+
# for the packs that *support* gemini — a pack with no allowed-adapters
|
|
599
|
+
# list admits any shipped adapter; a pack with a list must name gemini.
|
|
600
|
+
# Packs that constrain to a list without gemini are skipped, not failed.
|
|
601
|
+
# `checked` is the non-vacuity guard the old hard count used to be.
|
|
602
|
+
checked = 0
|
|
595
603
|
for pack_dir in pack_dirs:
|
|
596
604
|
allowed = self._allowed_adapters(pack_dir)
|
|
605
|
+
if allowed is not None and "gemini" not in allowed:
|
|
606
|
+
continue
|
|
597
607
|
for scope in ("repo", "user"):
|
|
598
608
|
resolved = _resolve_target_adapter(
|
|
599
609
|
pack_dir,
|
|
@@ -607,6 +617,10 @@ class GeminiAllPacksAdmissibleTests(unittest.TestCase):
|
|
|
607
617
|
resolved, "gemini",
|
|
608
618
|
f"{pack_dir.name} @ {scope}: --adapter gemini was not admitted",
|
|
609
619
|
)
|
|
620
|
+
checked += 1
|
|
621
|
+
self.assertTrue(
|
|
622
|
+
checked, "no pack exercised gemini admission — the check ran vacuously"
|
|
623
|
+
)
|
|
610
624
|
|
|
611
625
|
|
|
612
626
|
if __name__ == "__main__":
|
|
@@ -134,13 +134,13 @@ class KiroIdeAdapterTests(unittest.TestCase):
|
|
|
134
134
|
self.assertIn(".kiro.hook", target_repo)
|
|
135
135
|
|
|
136
136
|
def test_contract_version_is_0_9(self) -> None:
|
|
137
|
-
"""Contract version is 0.
|
|
138
|
-
|
|
137
|
+
"""Contract version is 0.14 (docs/specs/enriched-pack-manifest, atop
|
|
138
|
+
gemini-full-parity's 0.13 and copilot-skills-and-web's 0.12).
|
|
139
139
|
Name preserved to keep the diff small."""
|
|
140
140
|
self.assertEqual(
|
|
141
141
|
self.contract["contract"]["version"],
|
|
142
|
-
"0.
|
|
143
|
-
"adapter.toml [contract] version must be '0.
|
|
142
|
+
"0.14",
|
|
143
|
+
"adapter.toml [contract] version must be '0.14' after enriched-pack-manifest",
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
def test_kiro_ide_hook_projects_with_flat_prefix_path(self) -> None:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Durable projection check for architect's `design-reviewer` subagent (RFC-0032).
|
|
2
|
+
|
|
3
|
+
architect-design-reviewer spec AC10: the agent must project across **all seven**
|
|
4
|
+
adapters architect declares in `allowed-adapters`. The per-adapter projection
|
|
5
|
+
*mechanism* is covered elsewhere with synthetic agents; this test pins the
|
|
6
|
+
*real* architect agent across every route so a future adapter change that drops
|
|
7
|
+
it fails here. It also lands RFC-0032's deferred `kiro-cli` confirmation
|
|
8
|
+
(`kiro-cli` is the one adapter architect ships that the research pack does not).
|
|
9
|
+
|
|
10
|
+
Naming varies per adapter (codex emits `.toml`, copilot `.agent.md`, kiro remaps
|
|
11
|
+
frontmatter, …), so the assertion globs `design-reviewer*` under the projected
|
|
12
|
+
output rather than hard-coding one extension.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import tempfile
|
|
18
|
+
import tomllib
|
|
19
|
+
import unittest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from agentbundle.build.adapters import ADAPTERS
|
|
23
|
+
from agentbundle.build.contract import load as load_contract
|
|
24
|
+
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
26
|
+
CONTRACT_PATH = REPO_ROOT / "docs" / "contracts" / "adapter.toml"
|
|
27
|
+
ARCHITECT_PACK = REPO_ROOT / "packs" / "architect"
|
|
28
|
+
AGENT_NAME = "design-reviewer"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _architect_allowed_adapters() -> list[str]:
|
|
32
|
+
data = tomllib.loads((ARCHITECT_PACK / "pack.toml").read_text(encoding="utf-8"))
|
|
33
|
+
return list(data["pack"]["install"]["allowed-adapters"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ArchitectDesignReviewerProjectionTests(unittest.TestCase):
|
|
37
|
+
@classmethod
|
|
38
|
+
def setUpClass(cls) -> None:
|
|
39
|
+
cls.contract = load_contract(CONTRACT_PATH)
|
|
40
|
+
cls.adapters = _architect_allowed_adapters()
|
|
41
|
+
|
|
42
|
+
def test_source_agent_present(self) -> None:
|
|
43
|
+
self.assertTrue(
|
|
44
|
+
(ARCHITECT_PACK / ".apm" / "agents" / f"{AGENT_NAME}.md").exists(),
|
|
45
|
+
"architect must ship .apm/agents/design-reviewer.md",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def test_allowed_adapters_are_the_expected_seven(self) -> None:
|
|
49
|
+
# Pin the seven so this test (and AC10's "all seven") stays honest if the
|
|
50
|
+
# list ever changes — update deliberately, with the projection re-checked.
|
|
51
|
+
self.assertEqual(
|
|
52
|
+
set(self.adapters),
|
|
53
|
+
{"claude-code", "codex", "copilot", "kiro-ide", "kiro-cli", "cursor", "gemini"},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def test_design_reviewer_projects_for_every_allowed_adapter(self) -> None:
|
|
57
|
+
for adapter in self.adapters:
|
|
58
|
+
with self.subTest(adapter=adapter):
|
|
59
|
+
self.assertIn(adapter, ADAPTERS, f"no projector registered for {adapter!r}")
|
|
60
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
61
|
+
out = Path(tmp) / "out"
|
|
62
|
+
ADAPTERS[adapter](ARCHITECT_PACK, self.contract, out)
|
|
63
|
+
hits = list(out.rglob(f"{AGENT_NAME}*"))
|
|
64
|
+
self.assertTrue(
|
|
65
|
+
hits,
|
|
66
|
+
f"{adapter}: design-reviewer agent did not project under {out}",
|
|
67
|
+
)
|
|
68
|
+
self.assertTrue(
|
|
69
|
+
any("agents" in h.parts for h in hits),
|
|
70
|
+
f"{adapter}: design-reviewer projected but not under an agents/ route: {hits}",
|
|
71
|
+
)
|
|
72
|
+
if adapter == "cursor":
|
|
73
|
+
# cursor encodes the read-only contract as a `readonly`
|
|
74
|
+
# frontmatter flag (it drops the source `tools:` allowlist
|
|
75
|
+
# and derives the flag for a non-mutating agent). Assert it
|
|
76
|
+
# survives projection so AC5's read-only guarantee holds at
|
|
77
|
+
# the one target that represents it as a flag rather than a
|
|
78
|
+
# tools list — the design-reviewer flags, never rewrites.
|
|
79
|
+
agent_hit = next(h for h in hits if "agents" in h.parts)
|
|
80
|
+
self.assertIn(
|
|
81
|
+
"readonly",
|
|
82
|
+
agent_hit.read_text(encoding="utf-8"),
|
|
83
|
+
"cursor: design-reviewer must project with the readonly flag",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Rubric-parity guard for architect's `design-reviewer` subagent (RFC-0032).
|
|
2
|
+
|
|
3
|
+
The `design-reviewer` agent inlines a condensed copy of `architect-review`'s
|
|
4
|
+
verdict scheme, severity glossary, and 🔧/🧭 mechanical-judgment taxonomy
|
|
5
|
+
(agents bundle no `references/`, so the rubric is inlined — the pack's
|
|
6
|
+
duplication-over-DRY stance). The *prose* is free to be condensed, but three
|
|
7
|
+
token sets are a **cross-skill interop contract**: `architect-design`'s
|
|
8
|
+
convergence loop consumes mechanical/judgment-tagged findings and routes on the
|
|
9
|
+
verdict + severity vocabulary, so those tokens must stay identical wherever they
|
|
10
|
+
appear. This is the same reason `tools/lint-knowledge-surface-parity.py` guards
|
|
11
|
+
the knowledge-surface taxonomy despite the duplication principle.
|
|
12
|
+
|
|
13
|
+
This test is the guard the `design-reviewer-rubric-drift` backlog item asked
|
|
14
|
+
for. The canonical constants below are an **explicit allowlist** of the
|
|
15
|
+
interop tokens: a clean *rename or drop* in any carrier (e.g. retitling
|
|
16
|
+
`SHIP IT` in `architect-review/SKILL.md`) removes the token from that file,
|
|
17
|
+
fails the per-file assertion, and forces the rename to be reconciled across the
|
|
18
|
+
constant *and* every carrier in one change — that clean-rename drift is the
|
|
19
|
+
realistic case and what this catches. Two limits, by design: a reword that
|
|
20
|
+
keeps the token as a substring (`MAJOR REWRITE` → `MAJOR REWRITE REQUIRED`) is
|
|
21
|
+
not caught (AC2's one-time byte-faithful diff covers strict wording), and a
|
|
22
|
+
*newly added* verdict/glyph must be added to the constants here by hand — the
|
|
23
|
+
allowlist does not auto-discover vocabulary growth.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import unittest
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
32
|
+
ARCHITECT = REPO_ROOT / "packs" / "architect"
|
|
33
|
+
|
|
34
|
+
# The interop vocabulary, canonical. (Prose around these may be condensed in a
|
|
35
|
+
# copy; the tokens themselves may not drift.)
|
|
36
|
+
VERDICTS = ("SHIP IT", "SHIP WITH CHANGES", "MAJOR REWRITE", "WRONG ARTIFACT")
|
|
37
|
+
SEVERITY_GLYPHS = ("🟥", "🟧", "🟨", "⚪")
|
|
38
|
+
TAXONOMY_GLYPHS = ("🔧", "🧭")
|
|
39
|
+
ALL_TOKENS = VERDICTS + SEVERITY_GLYPHS + TAXONOMY_GLYPHS
|
|
40
|
+
|
|
41
|
+
# Every carrier of the rubric vocabulary. architect-review is canonical (its
|
|
42
|
+
# SKILL.md + rubric-well-architected.md own the verdict / severity / taxonomy);
|
|
43
|
+
# the design-reviewer agent is the inlined copy. All must carry every token.
|
|
44
|
+
CARRIERS = (
|
|
45
|
+
ARCHITECT / ".apm" / "skills" / "architect-review" / "SKILL.md",
|
|
46
|
+
ARCHITECT / ".apm" / "skills" / "architect-review" / "references" / "rubric-well-architected.md",
|
|
47
|
+
ARCHITECT / ".apm" / "agents" / "design-reviewer.md",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ArchitectRubricParityTests(unittest.TestCase):
|
|
52
|
+
def test_every_carrier_exists(self) -> None:
|
|
53
|
+
for path in CARRIERS:
|
|
54
|
+
with self.subTest(path=str(path)):
|
|
55
|
+
self.assertTrue(path.exists(), f"rubric carrier missing: {path}")
|
|
56
|
+
|
|
57
|
+
def test_interop_tokens_consistent_across_carriers(self) -> None:
|
|
58
|
+
for path in CARRIERS:
|
|
59
|
+
text = path.read_text(encoding="utf-8")
|
|
60
|
+
for token in ALL_TOKENS:
|
|
61
|
+
with self.subTest(carrier=path.name, token=token):
|
|
62
|
+
self.assertIn(
|
|
63
|
+
token,
|
|
64
|
+
text,
|
|
65
|
+
f"{path.name} is missing interop token {token!r} — the "
|
|
66
|
+
f"design-reviewer agent and architect-review must share "
|
|
67
|
+
f"one verdict / severity / mechanical-judgment vocabulary; "
|
|
68
|
+
f"reconcile the rename across all carriers and the "
|
|
69
|
+
f"canonical constants in this test.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
unittest.main()
|