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.
Files changed (115) hide show
  1. {agentbundle-0.3.1 → agentbundle-0.4.0}/PKG-INFO +25 -9
  2. {agentbundle-0.3.1 → agentbundle-0.4.0}/README.md +23 -7
  3. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/adapter.toml +9 -1
  4. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/pack.schema.json +38 -0
  5. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/plugin-manifest.derived.schema.json +10 -0
  6. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/plugin-manifest.schema.json +11 -1
  7. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/main.py +108 -0
  8. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/self_host.py +20 -1
  9. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_cursor.py +3 -2
  10. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_gemini.py +18 -4
  11. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +4 -4
  12. agentbundle-0.4.0/agentbundle/build/tests/test_architect_design_reviewer_projection.py +88 -0
  13. agentbundle-0.4.0/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +74 -0
  14. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract.py +8 -8
  15. agentbundle-0.4.0/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +94 -0
  16. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema.py +150 -0
  17. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +69 -0
  18. agentbundle-0.4.0/agentbundle/build/tests/test_projectable_subset.py +188 -0
  19. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_self_host_check.py +39 -4
  20. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/validate.py +10 -1
  21. agentbundle-0.4.0/agentbundle/categories.py +60 -0
  22. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/list_packs.py +17 -1
  23. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/validate.py +20 -0
  24. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/PKG-INFO +25 -9
  25. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/SOURCES.txt +5 -0
  26. {agentbundle-0.3.1 → agentbundle-0.4.0}/pyproject.toml +2 -2
  27. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/__init__.py +0 -0
  28. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/__main__.py +0 -0
  29. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/adapter.schema.json +0 -0
  30. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/_data/install-marker.py +0 -0
  31. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/__init__.py +0 -0
  32. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/__main__.py +0 -0
  33. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapter_root_bins.py +0 -0
  34. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/__init__.py +0 -0
  35. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/claude_code.py +0 -0
  36. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/codex.py +0 -0
  37. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/copilot.py +0 -0
  38. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/cursor.py +0 -0
  39. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/gemini.py +0 -0
  40. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro.py +0 -0
  41. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  42. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
  43. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/contract.py +0 -0
  44. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/lint_packs.py +0 -0
  45. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/phase_order.py +0 -0
  46. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/__init__.py +0 -0
  47. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  48. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  49. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  50. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/direct_directory.py +0 -0
  51. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  52. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/hook_id.py +0 -0
  53. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  54. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  55. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/merge_json.py +0 -0
  56. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/projections/user_merge_json.py +0 -0
  57. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/scope_rails.py +0 -0
  58. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/shared_libs.py +0 -0
  59. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/target_resolver.py +0 -0
  60. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/__init__.py +0 -0
  61. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  62. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  63. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  64. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  65. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  66. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  67. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  68. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  69. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
  70. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
  71. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  72. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  73. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  74. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  75. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  76. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  77. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  78. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  79. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  80. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  81. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  82. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  83. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_security.py +0 -0
  84. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  85. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  86. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  87. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  88. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/tests/test_validate.py +0 -0
  89. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/build/user_libs.py +0 -0
  90. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/catalogue.py +0 -0
  91. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/cli.py +0 -0
  92. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/__init__.py +0 -0
  93. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/_common.py +0 -0
  94. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/_drop_warning.py +0 -0
  95. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/adapt.py +0 -0
  96. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/config.py +0 -0
  97. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/diff.py +0 -0
  98. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/init_state.py +0 -0
  99. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/install.py +0 -0
  100. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/list_targets.py +0 -0
  101. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/reconcile.py +0 -0
  102. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/render.py +0 -0
  103. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/scaffold.py +0 -0
  104. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/uninstall.py +0 -0
  105. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/commands/upgrade.py +0 -0
  106. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/config.py +0 -0
  107. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/render.py +0 -0
  108. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/safety.py +0 -0
  109. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/scope.py +0 -0
  110. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/user_config.py +0 -0
  111. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle/version.py +0 -0
  112. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  113. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/entry_points.txt +0 -0
  114. {agentbundle-0.3.1 → agentbundle-0.4.0}/agentbundle.egg-info/top_level.txt +0 -0
  115. {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.1
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
- .apm/
65
- skills/<name>/SKILL.md # one folder per skill
66
- agents/<name>.md # subagents
67
- hooks/<name>.py # lifecycle hooks
68
- seeds/ # files scaffolded into the adopter repo
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
- .apm/
47
- skills/<name>/SKILL.md # one folder per skill
48
- agents/<name>.md # subagents
49
- hooks/<name>.py # lifecycle hooks
50
- seeds/ # files scaffolded into the adopter repo
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
- version = "0.13"
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": {
@@ -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
- entries.append(json.loads(manifest.read_text(encoding="utf-8")))
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) then 0.13 (docs/specs/gemini-full-parity).
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.13")
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 test_contract_version_is_0_13(self) -> None:
154
- """AC13 contract bumped to 0.13."""
155
- self.assertEqual(self.contract["contract"]["version"], "0.13")
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.assertEqual(len(pack_dirs), 11, f"expected 11 packs, found {len(pack_dirs)}")
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.13 (docs/specs/gemini-full-parity, atop
138
- copilot-skills-and-web's 0.12 and RFC-0026 / cursor-full-parity's 0.11).
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.13",
143
- "adapter.toml [contract] version must be '0.13' after gemini-full-parity",
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()