agentbundle 0.8.0__tar.gz → 0.9.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 (125) hide show
  1. {agentbundle-0.8.0 → agentbundle-0.9.0}/PKG-INFO +2 -2
  2. {agentbundle-0.8.0 → agentbundle-0.9.0}/README.md +1 -1
  3. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/adapter.toml +45 -14
  4. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_copilot.py +8 -9
  5. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_cursor.py +7 -7
  6. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_gemini.py +7 -7
  7. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +3 -3
  8. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract.py +4 -4
  9. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_scope.py +8 -7
  10. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_v07.py +7 -4
  11. agentbundle-0.9.0/agentbundle/build/tests/test_shared_prefix_contract.py +111 -0
  12. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/cli.py +29 -0
  13. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/diff.py +34 -8
  14. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/init_state.py +33 -64
  15. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/install.py +284 -50
  16. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/reconcile.py +1 -1
  17. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/uninstall.py +87 -32
  18. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/upgrade.py +58 -13
  19. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/config.py +268 -155
  20. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/safety.py +16 -12
  21. agentbundle-0.9.0/agentbundle/statelock.py +148 -0
  22. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/version.py +1 -1
  23. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle.egg-info/PKG-INFO +2 -2
  24. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle.egg-info/SOURCES.txt +2 -0
  25. {agentbundle-0.8.0 → agentbundle-0.9.0}/pyproject.toml +1 -1
  26. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/__init__.py +0 -0
  27. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/__main__.py +0 -0
  28. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/adapter.schema.json +0 -0
  29. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/install-defaults.toml +0 -0
  30. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/install-marker.py +0 -0
  31. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/pack.schema.json +0 -0
  32. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  33. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  34. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/_data/profile.schema.json +0 -0
  35. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/__init__.py +0 -0
  36. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/__main__.py +0 -0
  37. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapter_root_bins.py +0 -0
  38. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/__init__.py +0 -0
  39. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/claude_code.py +0 -0
  40. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/codex.py +0 -0
  41. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/copilot.py +0 -0
  42. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/cursor.py +0 -0
  43. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/gemini.py +0 -0
  44. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro.py +0 -0
  45. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  46. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
  47. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/contract.py +0 -0
  48. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/lint_packs.py +0 -0
  49. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/main.py +0 -0
  50. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/phase_order.py +0 -0
  51. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/__init__.py +0 -0
  52. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  53. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  54. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  55. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/direct_directory.py +0 -0
  56. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  57. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/hook_id.py +0 -0
  58. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  59. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  60. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/merge_json.py +0 -0
  61. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/projections/user_merge_json.py +0 -0
  62. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/scope_rails.py +0 -0
  63. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/self_host.py +0 -0
  64. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/shared_libs.py +0 -0
  65. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/target_resolver.py +0 -0
  66. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/__init__.py +0 -0
  67. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  68. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  69. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  70. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  71. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  72. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  73. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  74. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  75. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  76. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  77. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  78. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  79. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  80. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  81. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  82. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  83. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  84. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  85. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  86. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  87. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  88. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  89. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  90. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  91. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  92. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_security.py +0 -0
  93. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  94. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_self_host_fixture_guard.py +0 -0
  95. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_self_host_recipe_config.py +0 -0
  96. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  97. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  98. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  99. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  100. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_validate.py +0 -0
  101. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_writers_emit_lf.py +0 -0
  102. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/user_libs.py +0 -0
  103. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/build/validate.py +0 -0
  104. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/catalogue.py +0 -0
  105. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/categories.py +0 -0
  106. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/__init__.py +0 -0
  107. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/_common.py +0 -0
  108. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/_drop_warning.py +0 -0
  109. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/adapt.py +0 -0
  110. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/config.py +0 -0
  111. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/list_packs.py +0 -0
  112. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/list_profiles.py +0 -0
  113. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/list_targets.py +0 -0
  114. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/profile.py +0 -0
  115. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/render.py +0 -0
  116. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/scaffold.py +0 -0
  117. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/commands/validate.py +0 -0
  118. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/render.py +0 -0
  119. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/scope.py +0 -0
  120. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/source_defaults.py +0 -0
  121. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle/user_config.py +0 -0
  122. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  123. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle.egg-info/entry_points.txt +0 -0
  124. {agentbundle-0.8.0 → agentbundle-0.9.0}/agentbundle.egg-info/top_level.txt +0 -0
  125. {agentbundle-0.8.0 → agentbundle-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -46,7 +46,7 @@ agentbundle install --pack research --scope user
46
46
 
47
47
  User-scope packs land in your home directory, not the repo — they're yours, not the team's, and they're there in every project you open.
48
48
 
49
- The install auto-detects your agent (`--adapter` overrides). To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
49
+ The install auto-detects your agent (`--adapter` overrides). Multi-IDE? Install the same pack for each agent at the same scope — they coexist, and the agents that read `.agents/skills/` (codex, cursor, gemini, copilot) share one skill copy instead of fighting over it. To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
50
50
 
51
51
  ## More commands
52
52
 
@@ -28,7 +28,7 @@ agentbundle install --pack research --scope user
28
28
 
29
29
  User-scope packs land in your home directory, not the repo — they're yours, not the team's, and they're there in every project you open.
30
30
 
31
- The install auto-detects your agent (`--adapter` overrides). To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
31
+ The install auto-detects your agent (`--adapter` overrides). Multi-IDE? Install the same pack for each agent at the same scope — they coexist, and the agents that read `.agents/skills/` (codex, cursor, gemini, copilot) share one skill copy instead of fighting over it. To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
32
32
 
33
33
  ## More commands
34
34
 
@@ -84,7 +84,13 @@
84
84
  # `_append_layout_section` step (append-if-exists / never-create / never-
85
85
  # overwrite) into an adopter-owned `agentbundle-layout.toml`. Additive +
86
86
  # optional — packs pinned below v0.16 build and validate unchanged.
87
- version = "0.16"
87
+ # RFC-0052 / ADR-0039+0040 (v0.17): the `[contract.shared-prefixes]` registry
88
+ # (below) classifies each allowed-prefix as `shared` (a path more than one
89
+ # adapter reads) or — by absence — `private`, and a shared prefix declares its
90
+ # reader cohort. cursor/gemini/copilot route the `skill` primitive to the shared
91
+ # `.agents/skills/` home (joining codex) and gain it in `allowed-prefixes` at
92
+ # both scopes; their agent/hook/command projection stays native.
93
+ version = "0.17"
88
94
 
89
95
  # Sibling schemas this contract references — pack metadata and the
90
96
  # per-pack Claude-plugin manifest. Defined by spec AC #3 + #4.
@@ -92,6 +98,16 @@ version = "0.16"
92
98
  pack = "pack.schema.json"
93
99
  plugin-manifest = "plugin-manifest.schema.json"
94
100
 
101
+ # Shared-prefix registry (RFC-0052 / ADR-0039). Each key is a `shared` prefix —
102
+ # a path more than one shipped adapter reads — mapped to its reader cohort (the
103
+ # shipped adapters that read it). Every prefix NOT listed here is `private`
104
+ # (adapter-exclusive). The install-time cross-adapter disclosure names a shared
105
+ # prefix's *other* shipped cohort members; the footprint gate co-owns a shared
106
+ # path across same-pack rows. A future adapter joins a cohort by one line here.
107
+ [contract.shared-prefixes]
108
+ ".agents/skills/" = ["codex", "cursor", "gemini", "copilot"]
109
+ ".kiro/skills/" = ["kiro-ide", "kiro-cli"]
110
+
95
111
  # ---------------------------------------------------------------------------
96
112
  # Primitive types — source paths within a pack's .apm/ directory
97
113
  # ---------------------------------------------------------------------------
@@ -477,7 +493,10 @@ allowed-prefixes.user = [".kiro/", ".agentbundle/"]
477
493
  [[adapter.copilot.projection]]
478
494
  primitive = "skill"
479
495
  mode = "direct-directory"
480
- target-path = ".github/skills/"
496
+ # RFC-0052 / ADR-0040: cohort skill home. Copilot reads `.agents/skills/`
497
+ # (project) and `~/.agents/skills/` (personal); routing skills there lets the
498
+ # whole cohort (codex, cursor, gemini, copilot) share one co-owned copy.
499
+ target-path = ".agents/skills/"
481
500
  on-conflict = "prompt-then-preserve"
482
501
 
483
502
  [[adapter.copilot.projection]]
@@ -517,12 +536,14 @@ repo = "."
517
536
  user = "~"
518
537
  # RFC-0024 + copilot-skills-and-web: repo-scope skills at `.github/skills/`,
519
538
  # agents at `.github/agents/`, hook wiring + bodies at `.github/hooks/`.
520
- allowed-prefixes.repo = [".github/skills/", ".github/agents/", ".github/hooks/"]
521
- # User-scope home is `~/.copilot/` the prefix the install rewrite produces
522
- # from the adapter's repo-relpaths. `.agentbundle/` carries the install
523
- # state file (`~/.agentbundle/state.toml`), same as every other user-scope-
524
- # capable adapter (claude-code, codex).
525
- allowed-prefixes.user = [".copilot/skills/", ".copilot/agents/", ".copilot/hooks/", ".agentbundle/"]
539
+ # RFC-0052: skills route to the shared `.agents/skills/` home; agents + hooks
540
+ # stay native under `.github/` (ADR-0040 moves only the skill primitive).
541
+ allowed-prefixes.repo = [".agents/skills/", ".github/skills/", ".github/agents/", ".github/hooks/"]
542
+ # User-scope home is `~/.copilot/` for agents/hooks (the prefix the install
543
+ # rewrite produces) and `~/.agents/skills/` for skills (scope-agnostic, no
544
+ # rewrite). `.agentbundle/` carries the install state file
545
+ # (`~/.agentbundle/state.toml`), same as every other user-scope-capable adapter.
546
+ allowed-prefixes.user = [".agents/skills/", ".copilot/skills/", ".copilot/agents/", ".copilot/hooks/", ".agentbundle/"]
526
547
 
527
548
  # ---------------------------------------------------------------------------
528
549
  # Codex adapter — 5 projections (one per primitive)
@@ -596,7 +617,10 @@ allowed-prefixes.user = [".agents/skills/", ".codex/", ".agentbundle/"]
596
617
  [[adapter.cursor.projection]]
597
618
  primitive = "skill"
598
619
  mode = "direct-directory"
599
- target-path = ".cursor/skills/"
620
+ # RFC-0052 / ADR-0040: cohort skill home. Cursor reads `.agents/skills/` and
621
+ # `~/.agents/skills/` alongside `.cursor/skills/`; route skills there to share
622
+ # one co-owned copy across the cohort.
623
+ target-path = ".agents/skills/"
600
624
  on-conflict = "prompt-then-preserve"
601
625
 
602
626
  [[adapter.cursor.projection]]
@@ -650,8 +674,10 @@ mode = "dropped"
650
674
  [adapter.cursor.scope]
651
675
  repo = "."
652
676
  user = "~"
653
- allowed-prefixes.repo = [".cursor/", ".agentbundle/"]
654
- allowed-prefixes.user = [".cursor/", ".agentbundle/"]
677
+ # RFC-0052: skills route to the shared `.agents/skills/` home (both scopes);
678
+ # agents/hooks stay under `.cursor/` (ADR-0040 moves only the skill primitive).
679
+ allowed-prefixes.repo = [".agents/skills/", ".cursor/", ".agentbundle/"]
680
+ allowed-prefixes.user = [".agents/skills/", ".cursor/", ".agentbundle/"]
655
681
 
656
682
  # ---------------------------------------------------------------------------
657
683
  # Gemini adapter — full-parity native adapter (RFC-0027 / ADR-0016 /
@@ -685,7 +711,10 @@ allowed-prefixes.user = [".cursor/", ".agentbundle/"]
685
711
  [[adapter.gemini.projection]]
686
712
  primitive = "skill"
687
713
  mode = "direct-directory"
688
- target-path = ".gemini/skills/"
714
+ # RFC-0052 / ADR-0040: cohort skill home. Gemini CLI reads `.agents/skills/`
715
+ # and `~/.agents/skills/`, and its docs now make the alias precedence-winning
716
+ # over `.gemini/skills/`; route skills there to share one co-owned copy.
717
+ target-path = ".agents/skills/"
689
718
  on-conflict = "prompt-then-preserve"
690
719
 
691
720
  [[adapter.gemini.projection]]
@@ -744,8 +773,10 @@ mode = "dropped"
744
773
  [adapter.gemini.scope]
745
774
  repo = "."
746
775
  user = "~"
747
- allowed-prefixes.repo = [".gemini/", ".agentbundle/"]
748
- allowed-prefixes.user = [".gemini/", ".agentbundle/"]
776
+ # RFC-0052: skills route to the shared `.agents/skills/` home (both scopes);
777
+ # agents/hooks stay under `.gemini/` (ADR-0040 moves only the skill primitive).
778
+ allowed-prefixes.repo = [".agents/skills/", ".gemini/", ".agentbundle/"]
779
+ allowed-prefixes.user = [".agents/skills/", ".gemini/", ".agentbundle/"]
749
780
 
750
781
  # ---------------------------------------------------------------------------
751
782
  # Frontmatter mappings — rewrite rules applied when projecting a primitive
@@ -42,11 +42,9 @@ class CopilotAdapterTests(unittest.TestCase):
42
42
  cls.contract = load_contract(CONTRACT_PATH)
43
43
 
44
44
  def test_skill_projects_as_first_class_skill_md(self) -> None:
45
- # v0.11 (docs/specs/copilot-skills-and-web): `skill` flips
46
- # `instruction-file` `direct-directory`. Copilot reads
47
- # `.github/skills/<name>/SKILL.md` and accepts our canonical Claude
48
- # SKILL.md verbatim, so the source tree is copied byte-for-byte (no
49
- # `.instructions.md` wrapper, no injected `applyTo` frontmatter).
45
+ # RFC-0052 / ADR-0040: copilot `skill` routes to the shared cohort home
46
+ # `.agents/skills/<name>/SKILL.md` (joining codex, cursor, gemini).
47
+ # Agents/hooks/hook-wiring remain under `.github/`.
50
48
  with tempfile.TemporaryDirectory() as tmp:
51
49
  tmp_path = Path(tmp)
52
50
  pack = _seed_pack(tmp_path)
@@ -55,7 +53,7 @@ class CopilotAdapterTests(unittest.TestCase):
55
53
  source_body = (pack / ".apm" / "skills" / "foo" / "SKILL.md").read_text(
56
54
  encoding="utf-8"
57
55
  )
58
- output_path = out / ".github" / "skills" / "foo" / "SKILL.md"
56
+ output_path = out / ".agents" / "skills" / "foo" / "SKILL.md"
59
57
  self.assertTrue(output_path.exists())
60
58
  self.assertEqual(output_path.read_text(encoding="utf-8"), source_body)
61
59
  # No legacy instruction-file output.
@@ -65,19 +63,20 @@ class CopilotAdapterTests(unittest.TestCase):
65
63
  # AC4: re-projection sweeps a stale skill dir, and the sweep is bounded
66
64
  # to the skill target — it never touches sibling `.github/agents/` or
67
65
  # `.github/hooks/` content.
66
+ # RFC-0052: skill target is now `.agents/skills/` (shared cohort home).
68
67
  with tempfile.TemporaryDirectory() as tmp:
69
68
  tmp_path = Path(tmp)
70
69
  pack = _seed_pack(tmp_path)
71
70
  out = tmp_path / "out"
72
71
  project(pack, self.contract, out)
73
- self.assertTrue((out / ".github" / "skills" / "foo").exists())
72
+ self.assertTrue((out / ".agents" / "skills" / "foo").exists())
74
73
  agents_before = sorted((out / ".github" / "agents").iterdir())
75
74
  hooks_before = sorted((out / ".github" / "hooks").iterdir())
76
75
  # Rename the source skill; re-project. The old name is now orphaned.
77
76
  (pack / ".apm" / "skills" / "foo").rename(pack / ".apm" / "skills" / "renamed")
78
77
  project(pack, self.contract, out)
79
- self.assertFalse((out / ".github" / "skills" / "foo").exists())
80
- self.assertTrue((out / ".github" / "skills" / "renamed").exists())
78
+ self.assertFalse((out / ".agents" / "skills" / "foo").exists())
79
+ self.assertTrue((out / ".agents" / "skills" / "renamed").exists())
81
80
  # Sibling agent/hook trees are untouched by the skill sweep.
82
81
  self.assertEqual(sorted((out / ".github" / "agents").iterdir()), agents_before)
83
82
  self.assertEqual(sorted((out / ".github" / "hooks").iterdir()), hooks_before)
@@ -54,10 +54,10 @@ class CursorContractTests(unittest.TestCase):
54
54
  """AC1 — contract bumped to 0.11 by cursor-full-parity; subsequently
55
55
  0.12 (copilot-skills-and-web), 0.13 (docs/specs/gemini-full-parity),
56
56
  0.14 (docs/specs/enriched-pack-manifest), 0.15
57
- (docs/specs/kiro-cli-agent-skill-resources), then 0.16
57
+ (docs/specs/kiro-cli-agent-skill-resources), then 0.17
58
58
  (docs/specs/consolidated-pack-layout).
59
59
  Name preserved to keep the diff small."""
60
- self.assertEqual(self.contract["contract"]["version"], "0.16")
60
+ self.assertEqual(self.contract["contract"]["version"], "0.17")
61
61
 
62
62
  def test_cursor_block_projects_five_primitives(self) -> None:
63
63
  """AC2 — the five standard primitives are in the projection array."""
@@ -68,7 +68,7 @@ class CursorContractTests(unittest.TestCase):
68
68
  {"skill", "agent", "hook-body", "hook-wiring", "command"},
69
69
  )
70
70
  self.assertEqual(prims["skill"]["mode"], "direct-directory")
71
- self.assertEqual(prims["skill"]["target-path"], ".cursor/skills/")
71
+ self.assertEqual(prims["skill"]["target-path"], ".agents/skills/")
72
72
  self.assertEqual(prims["agent"]["mode"], "direct-file")
73
73
  self.assertEqual(prims["agent"]["target-path"], ".cursor/agents/")
74
74
  self.assertEqual(
@@ -109,7 +109,7 @@ class CursorContractTests(unittest.TestCase):
109
109
  scope = self.contract["adapter"]["cursor"]["scope"]
110
110
  self.assertEqual(scope["repo"], ".")
111
111
  self.assertEqual(scope["user"], "~")
112
- self.assertEqual(scope["allowed-prefixes"]["repo"], [".cursor/", ".agentbundle/"])
112
+ self.assertEqual(scope["allowed-prefixes"]["repo"], [".agents/skills/", ".cursor/", ".agentbundle/"])
113
113
  self.assertEqual(
114
114
  scope["allowed-prefixes"]["repo"], scope["allowed-prefixes"]["user"]
115
115
  )
@@ -147,7 +147,7 @@ class CursorProjectionTests(unittest.TestCase):
147
147
  (pack / ".apm" / "hooks" / "on-start.py").write_text("#!py", encoding="utf-8")
148
148
  out = tmp_path / "out"
149
149
  cursor.project(pack, self.contract, out)
150
- self.assertTrue((out / ".cursor" / "skills" / "my-skill" / "SKILL.md").exists())
150
+ self.assertTrue((out / ".agents" / "skills" / "my-skill" / "SKILL.md").exists())
151
151
  self.assertTrue((out / ".cursor" / "commands" / "do-thing.md").exists())
152
152
  self.assertTrue((out / ".cursor" / "hooks" / "on-start.py").exists())
153
153
 
@@ -332,10 +332,10 @@ class CursorProjectionTests(unittest.TestCase):
332
332
  os.symlink(secret, skill / "leak.txt")
333
333
  out = tmp_path / "out"
334
334
  cursor.project(pack, self.contract, out)
335
- leaked = out / ".cursor" / "skills" / "s" / "leak.txt"
335
+ leaked = out / ".agents" / "skills" / "s" / "leak.txt"
336
336
  self.assertFalse(leaked.is_symlink(), "nested symlink must not be reproduced")
337
337
  self.assertFalse(leaked.exists(), "nested symlink target must not be copied")
338
- self.assertTrue((out / ".cursor" / "skills" / "s" / "SKILL.md").exists())
338
+ self.assertTrue((out / ".agents" / "skills" / "s" / "SKILL.md").exists())
339
339
 
340
340
 
341
341
  class CursorInstallDispatchTests(unittest.TestCase):
@@ -151,9 +151,9 @@ class GeminiContractTests(unittest.TestCase):
151
151
  cls.contract = load_contract(CONTRACT_PATH)
152
152
 
153
153
  def test_contract_version_is_0_14(self) -> None:
154
- """Contract version is 0.16 (docs/specs/consolidated-pack-layout
154
+ """Contract version is 0.17 (docs/specs/consolidated-pack-layout
155
155
  bumped it from kiro-cli-agent-skill-resources' 0.15). Name preserved."""
156
- self.assertEqual(self.contract["contract"]["version"], "0.16")
156
+ self.assertEqual(self.contract["contract"]["version"], "0.17")
157
157
 
158
158
  def test_gemini_block_projects_five_primitives(self) -> None:
159
159
  """AC2 — five standard primitives with their gemini targets."""
@@ -161,7 +161,7 @@ class GeminiContractTests(unittest.TestCase):
161
161
  prims = {e["primitive"]: e for e in block["projection"]}
162
162
  self.assertEqual(set(prims), {"skill", "agent", "hook-body", "hook-wiring", "command"})
163
163
  self.assertEqual(prims["skill"]["mode"], "direct-directory")
164
- self.assertEqual(prims["skill"]["target-path"], ".gemini/skills/")
164
+ self.assertEqual(prims["skill"]["target-path"], ".agents/skills/")
165
165
  self.assertEqual(prims["agent"]["mode"], "direct-file")
166
166
  self.assertEqual(prims["agent"]["target-path"], ".gemini/agents/")
167
167
  self.assertEqual(prims["agent"]["frontmatter-mapping"], "gemini-agent-frontmatter")
@@ -210,7 +210,7 @@ class GeminiContractTests(unittest.TestCase):
210
210
  scope = self.contract["adapter"]["gemini"]["scope"]
211
211
  self.assertEqual(scope["repo"], ".")
212
212
  self.assertEqual(scope["user"], "~")
213
- self.assertEqual(scope["allowed-prefixes"]["repo"], [".gemini/", ".agentbundle/"])
213
+ self.assertEqual(scope["allowed-prefixes"]["repo"], [".agents/skills/", ".gemini/", ".agentbundle/"])
214
214
  self.assertEqual(scope["allowed-prefixes"]["repo"], scope["allowed-prefixes"]["user"])
215
215
 
216
216
  def test_frontmatter_mapping_keeps_tools_and_model(self) -> None:
@@ -254,7 +254,7 @@ class GeminiProjectionTests(unittest.TestCase):
254
254
  (pack / ".apm" / "hooks" / "on-start.py").write_text("#!py", encoding="utf-8")
255
255
  out = tmp_path / "out"
256
256
  gemini.project(pack, self.contract, out)
257
- self.assertTrue((out / ".gemini" / "skills" / "my-skill" / "SKILL.md").exists())
257
+ self.assertTrue((out / ".agents" / "skills" / "my-skill" / "SKILL.md").exists())
258
258
  self.assertTrue((out / ".gemini" / "commands" / "do-thing.toml").exists())
259
259
  self.assertTrue((out / ".gemini" / "hooks" / "on-start.py").exists())
260
260
 
@@ -361,9 +361,9 @@ class GeminiProjectionTests(unittest.TestCase):
361
361
  os.symlink(secret, skill / "leak.txt")
362
362
  out = tmp_path / "out"
363
363
  gemini.project(pack, self.contract, out)
364
- leaked = out / ".gemini" / "skills" / "s" / "leak.txt"
364
+ leaked = out / ".agents" / "skills" / "s" / "leak.txt"
365
365
  self.assertFalse(leaked.exists())
366
- self.assertTrue((out / ".gemini" / "skills" / "s" / "SKILL.md").exists())
366
+ self.assertTrue((out / ".agents" / "skills" / "s" / "SKILL.md").exists())
367
367
 
368
368
 
369
369
  # ===========================================================================
@@ -172,13 +172,13 @@ class KiroIdeAdapterTests(unittest.TestCase):
172
172
  self.assertIn(".kiro.hook", target_repo)
173
173
 
174
174
  def test_contract_version_is_0_9(self) -> None:
175
- """Contract version is 0.16 (docs/specs/consolidated-pack-layout,
175
+ """Contract version is 0.17 (docs/specs/consolidated-pack-layout,
176
176
  atop kiro-cli-agent-skill-resources' 0.15 and enriched-pack-manifest's 0.14).
177
177
  Name preserved to keep the diff small."""
178
178
  self.assertEqual(
179
179
  self.contract["contract"]["version"],
180
- "0.16",
181
- "adapter.toml [contract] version must be '0.16' after consolidated-pack-layout",
180
+ "0.17",
181
+ "adapter.toml [contract] version must be '0.17' after consolidated-pack-layout",
182
182
  )
183
183
 
184
184
  def test_kiro_ide_hook_projects_with_flat_prefix_path(self) -> None:
@@ -393,7 +393,7 @@ class FrontmatterTableTests(unittest.TestCase):
393
393
  if p["primitive"] == "skill"
394
394
  )
395
395
  self.assertEqual(skill["mode"], "direct-directory")
396
- self.assertEqual(skill["target-path"], ".github/skills/")
396
+ self.assertEqual(skill["target-path"], ".agents/skills/")
397
397
  self.assertNotIn("frontmatter-default", skill)
398
398
 
399
399
  def test_frontmatter_mapping_and_default_are_structurally_distinct(self) -> None:
@@ -452,7 +452,7 @@ class ContractV05Tests(unittest.TestCase):
452
452
  self.schema = _load_schema()
453
453
 
454
454
  def test_contract_version_is_v05(self) -> None:
455
- """tomllib.loads of adapter.toml returns contract.version == "0.16"
455
+ """tomllib.loads of adapter.toml returns contract.version == "0.17"
456
456
  (bumped from kiro-cli-agent-skill-resources' "0.15" by
457
457
  docs/specs/consolidated-pack-layout: pack.toml gains an optional
458
458
  scope-keyed [pack.layout] table). Class/method names preserved to
@@ -460,8 +460,8 @@ class ContractV05Tests(unittest.TestCase):
460
460
  """
461
461
  self.assertEqual(
462
462
  self.contract["contract"]["version"],
463
- "0.16",
464
- "adapter.toml [contract] version must be '0.16' after consolidated-pack-layout",
463
+ "0.17",
464
+ "adapter.toml [contract] version must be '0.17' after consolidated-pack-layout",
465
465
  )
466
466
 
467
467
  def test_claude_code_install_routes_includes_apm(self) -> None:
@@ -89,20 +89,21 @@ class OtherAdaptersOmitScopeTests(unittest.TestCase):
89
89
  scope = contract["adapter"]["copilot"].get("scope")
90
90
  self.assertIsNotNone(scope, "copilot [scope] block missing")
91
91
  self.assertEqual(scope["repo"], ".")
92
- # v0.11 (docs/specs/copilot-skills-and-web): copilot `skill` projects as
93
- # first-class `direct-directory` SKILL.md under `.github/skills/`, so the
94
- # repo skill prefix is `.github/skills/` (was `.github/instructions/`).
95
- # Agent + hook homes under `.github/` are unchanged.
92
+ # RFC-0052 / ADR-0040: copilot skill routes to the shared `.agents/skills/`
93
+ # cohort home (prepended to the list); agents + hooks stay under `.github/`.
94
+ # The `.github/skills/` prefix is retained to allow the path-jail to admit
95
+ # any existing `.github/skills/` files written by the previous contract.
96
96
  self.assertEqual(
97
97
  scope["allowed-prefixes"]["repo"],
98
- [".github/skills/", ".github/agents/", ".github/hooks/"],
98
+ [".agents/skills/", ".github/skills/", ".github/agents/", ".github/hooks/"],
99
99
  )
100
- # User scope: `~/.copilot/{skills,agents,hooks}/` + `.agentbundle/`
101
- # (the install state-file home, same as every other user-capable adapter).
100
+ # User scope: RFC-0052 prepends `.agents/skills/` to the user list.
101
+ # `.copilot/skills/` is kept for path-jail compat; `.agentbundle/` stays.
102
102
  self.assertEqual(scope["user"], "~")
103
103
  self.assertEqual(
104
104
  scope["allowed-prefixes"]["user"],
105
105
  [
106
+ ".agents/skills/",
106
107
  ".copilot/skills/",
107
108
  ".copilot/agents/",
108
109
  ".copilot/hooks/",
@@ -83,17 +83,20 @@ class TestContractV07(unittest.TestCase):
83
83
  copilot_scope = self.contract["adapter"]["copilot"].get("scope")
84
84
  self.assertIsNotNone(copilot_scope, "copilot scope table missing")
85
85
  self.assertEqual(copilot_scope["repo"], ".")
86
- # v0.10 (copilot-full-parity) made copilot user-scope-capable; v0.11
87
- # (copilot-skills-and-web) flips `skill` to `.github/skills/` SKILL.md, so
88
- # the repo skill prefix is `.github/skills/` (was `.github/instructions/`).
86
+ # RFC-0052 / ADR-0040: copilot skill now routes to `.agents/skills/`
87
+ # (shared cohort home, prepended); agents + hooks stay under `.github/`.
88
+ # `.github/skills/` is kept for path-jail compat with existing files.
89
89
  self.assertEqual(
90
90
  copilot_scope["allowed-prefixes"]["repo"],
91
- [".github/skills/", ".github/agents/", ".github/hooks/"],
91
+ [".agents/skills/", ".github/skills/", ".github/agents/", ".github/hooks/"],
92
92
  )
93
93
  self.assertEqual(copilot_scope["user"], "~")
94
+ # RFC-0052: `.agents/skills/` prepended to user list too.
95
+ # `.copilot/skills/` retained for path-jail compat.
94
96
  self.assertEqual(
95
97
  copilot_scope["allowed-prefixes"]["user"],
96
98
  [
99
+ ".agents/skills/",
97
100
  ".copilot/skills/",
98
101
  ".copilot/agents/",
99
102
  ".copilot/hooks/",
@@ -0,0 +1,111 @@
1
+ """T5 (RFC-0052 / ADR-0039+0040): the `shared` prefix class + cohort routing.
2
+
3
+ The adapter contract gains a `[contract.shared-prefixes]` registry mapping each
4
+ shared prefix to its reader cohort (shipped adapters); every other prefix is
5
+ `private` by derivation. cursor/gemini/copilot route the `skill` primitive to
6
+ the shared `.agents/skills/` home (joining codex) and include it in
7
+ `allowed-prefixes` at both scopes. The two contract copies agree byte-for-byte.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import tomllib
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+
17
+ _REPO_ROOT = Path(__file__).resolve().parents[5]
18
+ _DATA_COPY = (
19
+ _REPO_ROOT
20
+ / "packages/agentbundle/agentbundle/_data/adapter.toml"
21
+ )
22
+ _DOCS_COPY = _REPO_ROOT / "docs/contracts/adapter.toml"
23
+
24
+
25
+ def _contract() -> dict:
26
+ return tomllib.loads(_DATA_COPY.read_text(encoding="utf-8"))
27
+
28
+
29
+ class SharedPrefixRegistryTests(unittest.TestCase):
30
+ def setUp(self) -> None:
31
+ self.contract = _contract()
32
+ self.shared = self.contract["contract"]["shared-prefixes"]
33
+
34
+ def test_contract_version_bumped(self) -> None:
35
+ self.assertEqual(self.contract["contract"]["version"], "0.17")
36
+
37
+ def test_agents_skills_shared_with_full_cohort(self) -> None:
38
+ self.assertEqual(
39
+ sorted(self.shared[".agents/skills/"]),
40
+ ["codex", "copilot", "cursor", "gemini"],
41
+ )
42
+
43
+ def test_kiro_skills_shared_with_kiro_cohort(self) -> None:
44
+ self.assertEqual(
45
+ sorted(self.shared[".kiro/skills/"]),
46
+ ["kiro-cli", "kiro-ide"],
47
+ )
48
+
49
+ def test_only_known_shared_prefixes_declared(self) -> None:
50
+ # Everything not in the registry is `private` by derivation; the
51
+ # registry itself holds exactly the two shared prefixes.
52
+ self.assertEqual(
53
+ set(self.shared), {".agents/skills/", ".kiro/skills/"}
54
+ )
55
+
56
+ def test_private_prefix_not_in_registry(self) -> None:
57
+ # A representative private prefix (claude-code's `.claude/`) is absent.
58
+ self.assertNotIn(".claude/", self.shared)
59
+
60
+
61
+ class CohortSkillRoutingTests(unittest.TestCase):
62
+ def setUp(self) -> None:
63
+ self.contract = _contract()
64
+
65
+ def _skill_target(self, adapter: str) -> str:
66
+ for p in self.contract["adapter"][adapter]["projection"]:
67
+ if p.get("primitive") == "skill":
68
+ return p["target-path"]
69
+ raise AssertionError(f"no skill projection for {adapter}")
70
+
71
+ def test_cohort_skill_targets_shared_home(self) -> None:
72
+ for adapter in ("codex", "cursor", "gemini", "copilot"):
73
+ self.assertEqual(
74
+ self._skill_target(adapter), ".agents/skills/",
75
+ f"{adapter} skill must route to the shared .agents/skills/ home",
76
+ )
77
+
78
+ def test_cohort_allowed_prefixes_include_shared_home_both_scopes(self) -> None:
79
+ for adapter in ("cursor", "gemini", "copilot"):
80
+ scope = self.contract["adapter"][adapter]["scope"]
81
+ for which in ("repo", "user"):
82
+ self.assertIn(
83
+ ".agents/skills/", scope["allowed-prefixes"][which],
84
+ f"{adapter}.{which} must admit the shared skill prefix",
85
+ )
86
+
87
+ def test_cohort_agents_stay_native(self) -> None:
88
+ # ADR-0040 moves only the skill primitive; agents stay native.
89
+ for adapter, native in (
90
+ ("cursor", ".cursor/agents/"),
91
+ ("gemini", ".gemini/agents/"),
92
+ ("copilot", ".github/agents/"),
93
+ ):
94
+ agent = next(
95
+ p for p in self.contract["adapter"][adapter]["projection"]
96
+ if p.get("primitive") == "agent"
97
+ )
98
+ self.assertEqual(agent["target-path"], native)
99
+
100
+
101
+ class ContractMirrorTests(unittest.TestCase):
102
+ def test_data_and_docs_agree_byte_for_byte(self) -> None:
103
+ self.assertEqual(
104
+ _DATA_COPY.read_bytes(),
105
+ _DOCS_COPY.read_bytes(),
106
+ "the _data/ and docs/contracts/ adapter.toml copies must be identical",
107
+ )
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()
@@ -406,6 +406,15 @@ def _build_parser() -> argparse.ArgumentParser:
406
406
  sp.add_argument("pack_path", help="Path to the pack to diff against.")
407
407
  sp.add_argument("--root", default=".")
408
408
  sp.add_argument("--scope", choices=("repo", "user"))
409
+ sp.add_argument(
410
+ "--adapter",
411
+ choices=_shipped_adapters_choices(),
412
+ help=(
413
+ "Disambiguate when the pack is installed for multiple adapters at "
414
+ "the resolved scope (RFC-0052). Inferred when the pack has a single "
415
+ "adapter row; required when it has more than one."
416
+ ),
417
+ )
409
418
  sp.set_defaults(func=_lazy("diff"))
410
419
 
411
420
  # --- upgrade --- (--scope disambiguator)
@@ -436,6 +445,15 @@ def _build_parser() -> argparse.ArgumentParser:
436
445
  )
437
446
  sp.add_argument("--root", default=".")
438
447
  sp.add_argument("--scope", choices=("repo", "user"))
448
+ sp.add_argument(
449
+ "--adapter",
450
+ choices=_shipped_adapters_choices(),
451
+ help=(
452
+ "Disambiguate when the pack is installed for multiple adapters at "
453
+ "the resolved scope (RFC-0052). Inferred when the pack has a single "
454
+ "adapter row; required when it has more than one."
455
+ ),
456
+ )
439
457
  sp.add_argument(
440
458
  "--yes",
441
459
  action="store_true",
@@ -464,6 +482,17 @@ def _build_parser() -> argparse.ArgumentParser:
464
482
  sp.add_argument("--pack", required=True)
465
483
  sp.add_argument("--root", default=".")
466
484
  sp.add_argument("--scope", choices=("repo", "user"))
485
+ sp.add_argument(
486
+ "--adapter",
487
+ choices=_shipped_adapters_choices(),
488
+ help=(
489
+ "Disambiguate when the pack is installed for multiple adapters at "
490
+ "the resolved scope (RFC-0052). Inferred when the pack has a single "
491
+ "adapter row; required when it has more than one. Only the named "
492
+ "adapter row is uninstalled; shared files survive while a sibling "
493
+ "row still owns them."
494
+ ),
495
+ )
467
496
  sp.add_argument(
468
497
  "--yes",
469
498
  action="store_true",