agentbundle 0.7.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.7.0 → agentbundle-0.9.0}/PKG-INFO +12 -12
  2. {agentbundle-0.7.0 → agentbundle-0.9.0}/README.md +11 -11
  3. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/adapter.toml +45 -14
  4. agentbundle-0.9.0/agentbundle/_data/install-defaults.toml +10 -0
  5. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/codex.py +1 -1
  6. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/cursor.py +2 -2
  7. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/gemini.py +2 -2
  8. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro.py +1 -1
  9. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro_ide.py +1 -1
  10. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/main.py +4 -4
  11. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/codex_agent_toml.py +1 -1
  12. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/copilot_agent_md.py +1 -1
  13. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/copilot_hooks_json.py +1 -1
  14. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/gemini_command_toml.py +1 -1
  15. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/kiro_ide_hook.py +1 -1
  16. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/merge_into_agent_json.py +1 -1
  17. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/merge_json.py +1 -1
  18. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/user_merge_json.py +1 -1
  19. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/self_host.py +106 -22
  20. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_copilot.py +8 -9
  21. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_cursor.py +7 -7
  22. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_gemini.py +7 -7
  23. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +3 -3
  24. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract.py +4 -4
  25. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_scope.py +8 -7
  26. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_v07.py +7 -4
  27. agentbundle-0.9.0/agentbundle/build/tests/test_self_host_fixture_guard.py +88 -0
  28. agentbundle-0.9.0/agentbundle/build/tests/test_self_host_recipe_config.py +113 -0
  29. agentbundle-0.9.0/agentbundle/build/tests/test_shared_prefix_contract.py +111 -0
  30. agentbundle-0.9.0/agentbundle/build/tests/test_writers_emit_lf.py +169 -0
  31. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/cli.py +69 -4
  32. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/_common.py +119 -0
  33. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/config.py +6 -0
  34. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/diff.py +34 -8
  35. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/init_state.py +33 -64
  36. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/install.py +314 -56
  37. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/list_packs.py +20 -30
  38. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/list_profiles.py +12 -16
  39. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/reconcile.py +1 -1
  40. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/uninstall.py +87 -32
  41. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/upgrade.py +68 -14
  42. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/config.py +268 -155
  43. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/safety.py +16 -12
  44. agentbundle-0.9.0/agentbundle/source_defaults.py +342 -0
  45. agentbundle-0.9.0/agentbundle/statelock.py +148 -0
  46. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/user_config.py +53 -5
  47. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/version.py +1 -1
  48. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle.egg-info/PKG-INFO +12 -12
  49. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle.egg-info/SOURCES.txt +7 -0
  50. {agentbundle-0.7.0 → agentbundle-0.9.0}/pyproject.toml +1 -1
  51. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/__init__.py +0 -0
  52. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/__main__.py +0 -0
  53. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/adapter.schema.json +0 -0
  54. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/install-marker.py +0 -0
  55. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/pack.schema.json +0 -0
  56. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  57. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  58. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/_data/profile.schema.json +0 -0
  59. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/__init__.py +0 -0
  60. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/__main__.py +0 -0
  61. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapter_root_bins.py +0 -0
  62. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/__init__.py +0 -0
  63. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/claude_code.py +0 -0
  64. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/copilot.py +0 -0
  65. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  66. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/contract.py +0 -0
  67. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/lint_packs.py +0 -0
  68. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/phase_order.py +0 -0
  69. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/__init__.py +0 -0
  70. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/direct_directory.py +0 -0
  71. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/projections/hook_id.py +0 -0
  72. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/scope_rails.py +0 -0
  73. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/shared_libs.py +0 -0
  74. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/target_resolver.py +0 -0
  75. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/__init__.py +0 -0
  76. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  77. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  78. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  79. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  80. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  81. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  82. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  83. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  84. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  85. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  86. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  87. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  88. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  89. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  90. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  91. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  92. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  93. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  94. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  95. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  96. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  97. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  98. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  99. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  100. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  101. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_security.py +0 -0
  102. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  103. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  104. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  105. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  106. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  107. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/tests/test_validate.py +0 -0
  108. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/user_libs.py +0 -0
  109. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/build/validate.py +0 -0
  110. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/catalogue.py +0 -0
  111. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/categories.py +0 -0
  112. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/__init__.py +0 -0
  113. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/_drop_warning.py +0 -0
  114. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/adapt.py +0 -0
  115. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/list_targets.py +0 -0
  116. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/profile.py +0 -0
  117. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/render.py +0 -0
  118. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/scaffold.py +0 -0
  119. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/commands/validate.py +0 -0
  120. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/render.py +0 -0
  121. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle/scope.py +0 -0
  122. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  123. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle.egg-info/entry_points.txt +0 -0
  124. {agentbundle-0.7.0 → agentbundle-0.9.0}/agentbundle.egg-info/top_level.txt +0 -0
  125. {agentbundle-0.7.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.7.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
@@ -33,37 +33,37 @@ pip install agentbundle
33
33
  **Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
34
34
 
35
35
  ```bash
36
- agentbundle install --pack core git+https://github.com/eugenelim/agent-ready-repo
36
+ agentbundle install --pack core
37
37
  ```
38
38
 
39
- It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
39
+ No catalogue argument needed: it defaults to the agent-ready-repo catalogue. It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
40
40
 
41
41
  **Install for yourself, everywhere** — so a pack follows you across every project, with no per-repo setup:
42
42
 
43
43
  ```bash
44
- agentbundle install --pack research git+https://github.com/eugenelim/agent-ready-repo --scope user
44
+ agentbundle install --pack research --scope user
45
45
  ```
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
- A catalogue is a git URL or a local path, and the install auto-detects your agent (`--adapter` overrides).
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
 
53
53
  ```bash
54
- # See what a catalogue offers
55
- agentbundle list-packs git+https://github.com/eugenelim/agent-ready-repo
56
- agentbundle list-profiles git+https://github.com/eugenelim/agent-ready-repo
54
+ # See what the catalogue offers (bare uses the default; or name one explicitly)
55
+ agentbundle list-packs
56
+ agentbundle list-profiles
57
57
 
58
58
  # Install a whole curated profile — a single-scope set of packs — in one command
59
- agentbundle install --profile inception git+https://github.com/eugenelim/agent-ready-repo
59
+ agentbundle install --profile inception
60
60
 
61
61
  # Preview any install without writing a file
62
- agentbundle install --pack core git+https://github.com/eugenelim/agent-ready-repo --dry-run
62
+ agentbundle install --pack core --dry-run
63
63
 
64
64
  # Upgrade to the version the catalogue ships — shows installed → target, asks first
65
- agentbundle upgrade --pack core git+https://github.com/eugenelim/agent-ready-repo
66
- agentbundle upgrade --pack core git+https://github.com/eugenelim/agent-ready-repo --yes # skip the prompt (CI)
65
+ agentbundle upgrade --pack core
66
+ agentbundle upgrade --pack core --yes # skip the prompt (CI)
67
67
 
68
68
  # Uninstall — previews remove (Tier-1) vs keep (your edits), asks first
69
69
  agentbundle uninstall --pack core --dry-run
@@ -15,37 +15,37 @@ pip install agentbundle
15
15
  **Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
16
16
 
17
17
  ```bash
18
- agentbundle install --pack core git+https://github.com/eugenelim/agent-ready-repo
18
+ agentbundle install --pack core
19
19
  ```
20
20
 
21
- It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
21
+ No catalogue argument needed: it defaults to the agent-ready-repo catalogue. It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
22
22
 
23
23
  **Install for yourself, everywhere** — so a pack follows you across every project, with no per-repo setup:
24
24
 
25
25
  ```bash
26
- agentbundle install --pack research git+https://github.com/eugenelim/agent-ready-repo --scope user
26
+ agentbundle install --pack research --scope user
27
27
  ```
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
- A catalogue is a git URL or a local path, and the install auto-detects your agent (`--adapter` overrides).
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
 
35
35
  ```bash
36
- # See what a catalogue offers
37
- agentbundle list-packs git+https://github.com/eugenelim/agent-ready-repo
38
- agentbundle list-profiles git+https://github.com/eugenelim/agent-ready-repo
36
+ # See what the catalogue offers (bare uses the default; or name one explicitly)
37
+ agentbundle list-packs
38
+ agentbundle list-profiles
39
39
 
40
40
  # Install a whole curated profile — a single-scope set of packs — in one command
41
- agentbundle install --profile inception git+https://github.com/eugenelim/agent-ready-repo
41
+ agentbundle install --profile inception
42
42
 
43
43
  # Preview any install without writing a file
44
- agentbundle install --pack core git+https://github.com/eugenelim/agent-ready-repo --dry-run
44
+ agentbundle install --pack core --dry-run
45
45
 
46
46
  # Upgrade to the version the catalogue ships — shows installed → target, asks first
47
- agentbundle upgrade --pack core git+https://github.com/eugenelim/agent-ready-repo
48
- agentbundle upgrade --pack core git+https://github.com/eugenelim/agent-ready-repo --yes # skip the prompt (CI)
47
+ agentbundle upgrade --pack core
48
+ agentbundle upgrade --pack core --yes # skip the prompt (CI)
49
49
 
50
50
  # Uninstall — previews remove (Tier-1) vs keep (your edits), asks first
51
51
  agentbundle uninstall --pack core --dry-run
@@ -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
@@ -0,0 +1,10 @@
1
+ # Packaged install default — layer 4 of the catalogue-source resolution chain
2
+ # (RFC-0046 / ADR-0036). When `agentbundle install`/`upgrade` is run with no
3
+ # `catalogue` positional and layers 1–3 (explicit arg, user config, editable
4
+ # detection) yield nothing, this source is used.
5
+ #
6
+ # Private-fork pattern: blank the `source` value below (or delete this file
7
+ # entirely) to disable the packaged GitHub default — layers 1–3 still apply, so
8
+ # an editable clone or a `config set source` still resolves.
9
+ [defaults]
10
+ source = "git+https://github.com/eugenelim/agent-ready-repo"
@@ -72,7 +72,7 @@ def project_packs(pack_paths: list[Path], contract: dict, output_root: Path) ->
72
72
  dir=str(agents_md.parent),
73
73
  )
74
74
  try:
75
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
75
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
76
76
  handle.write(stripped)
77
77
  os.replace(tmp_path, agents_md)
78
78
  except BaseException:
@@ -251,7 +251,7 @@ def _project_agent_as_md(
251
251
  out_fm["readonly"] = readonly
252
252
 
253
253
  output_text = _serialize_frontmatter_md(out_fm) + body
254
- (target_dir / entry.name).write_text(output_text, encoding="utf-8")
254
+ (target_dir / entry.name).write_text(output_text, encoding="utf-8", newline="\n")
255
255
 
256
256
 
257
257
  def _derive_readonly(frontmatter: dict[str, Any]) -> bool | None:
@@ -357,7 +357,7 @@ def _project_hooks_json(source_dir: Path, output_root: Path, rule: dict) -> None
357
357
  target_path.parent.mkdir(parents=True, exist_ok=True)
358
358
  target_path.write_text(
359
359
  json.dumps(existing, indent=2, sort_keys=True) + "\n",
360
- encoding="utf-8",
360
+ encoding="utf-8", newline="\n",
361
361
  )
362
362
 
363
363
 
@@ -220,7 +220,7 @@ def _project_agent_as_md(
220
220
  frontmatter, body = _split_frontmatter(entry.read_text(encoding="utf-8"))
221
221
  out_fm = _apply_mapping(frontmatter, mapping, entry.stem)
222
222
  output_text = _serialize_frontmatter_md(out_fm) + body
223
- (target_dir / entry.name).write_text(output_text, encoding="utf-8")
223
+ (target_dir / entry.name).write_text(output_text, encoding="utf-8", newline="\n")
224
224
 
225
225
 
226
226
  def _apply_mapping(
@@ -386,7 +386,7 @@ def _project_settings_json(
386
386
  target_path.parent.mkdir(parents=True, exist_ok=True)
387
387
  target_path.write_text(
388
388
  json.dumps(existing, indent=2, sort_keys=True) + "\n",
389
- encoding="utf-8",
389
+ encoding="utf-8", newline="\n",
390
390
  )
391
391
 
392
392
 
@@ -297,7 +297,7 @@ def _project_agent_as_json(
297
297
  destination = target_dir / (entry.stem + ".json")
298
298
  destination.write_text(
299
299
  json.dumps(agent_json, indent=2, sort_keys=False) + "\n",
300
- encoding="utf-8",
300
+ encoding="utf-8", newline="\n",
301
301
  )
302
302
 
303
303
 
@@ -255,7 +255,7 @@ def _project_agent_as_md(
255
255
 
256
256
  output_text = _serialize_frontmatter_md(rewritten) + body
257
257
  destination = target_dir / entry.name # preserves .md extension
258
- destination.write_text(output_text, encoding="utf-8")
258
+ destination.write_text(output_text, encoding="utf-8", newline="\n")
259
259
 
260
260
 
261
261
  def _serialize_frontmatter_md(fields: dict[str, Any]) -> str:
@@ -498,7 +498,7 @@ def _run_per_pack_single(
498
498
  )
499
499
  destination.write_text(
500
500
  json.dumps(derived, indent=2, sort_keys=False) + "\n",
501
- encoding="utf-8",
501
+ encoding="utf-8", newline="\n",
502
502
  )
503
503
  # Defence-in-depth: re-validate the written file against the schema
504
504
  # to catch any serialise/parse divergence introduced by json.dumps.
@@ -547,7 +547,7 @@ def _run_per_pack_apm(recipe: Recipe, packs: list[Pack], output_dir: Path) -> di
547
547
  pack_metadata = tomllib.loads((pack.path / "pack.toml").read_text(encoding="utf-8"))
548
548
  (per_pack_output / "apm.yml").write_text(
549
549
  _render_apm_yml(pack_metadata.get("pack", {})),
550
- encoding="utf-8",
550
+ encoding="utf-8", newline="\n",
551
551
  )
552
552
  apm_source = pack.path / ".apm"
553
553
  if apm_source.exists():
@@ -568,7 +568,7 @@ def _run_per_pack_apm(recipe: Recipe, packs: list[Pack], output_dir: Path) -> di
568
568
  (hooks_dir / "install-marker.py").write_bytes(writer_bytes)
569
569
  (hooks_dir / "install-marker.json").write_text(
570
570
  json.dumps(_APM_INSTALL_MARKER_HOOK_JSON, indent=2) + "\n",
571
- encoding="utf-8",
571
+ encoding="utf-8", newline="\n",
572
572
  )
573
573
 
574
574
  # Project pack.toml verbatim. The writer reads it for
@@ -630,7 +630,7 @@ def _run_aggregate(recipe: Recipe, output_dir: Path) -> dict:
630
630
  output_path.parent.mkdir(parents=True, exist_ok=True)
631
631
  output_path.write_text(
632
632
  json.dumps({"plugins": entries}, indent=2, sort_keys=True) + "\n",
633
- encoding="utf-8",
633
+ encoding="utf-8", newline="\n",
634
634
  )
635
635
  return {"recipe": recipe.name, "type": recipe.type, "entries": len(entries)}
636
636
 
@@ -331,4 +331,4 @@ def project_codex_agent_toml(
331
331
  )
332
332
  toml_lines = _emit_fields(rewritten, body)
333
333
  destination = target_dir / (entry.stem + ".toml")
334
- destination.write_text("\n".join(toml_lines) + "\n", encoding="utf-8")
334
+ destination.write_text("\n".join(toml_lines) + "\n", encoding="utf-8", newline="\n")
@@ -208,4 +208,4 @@ def project_copilot_agent_md(
208
208
  _validate_tools(frontmatter["tools"])
209
209
  emitted["tools"] = frontmatter["tools"]
210
210
  destination = target_dir / (entry.stem + ".agent.md")
211
- destination.write_text(_emit(emitted, body), encoding="utf-8")
211
+ destination.write_text(_emit(emitted, body), encoding="utf-8", newline="\n")
@@ -138,5 +138,5 @@ def project_copilot_hooks_json(
138
138
  destination = target_dir / (entry.stem + ".json")
139
139
  destination.write_text(
140
140
  json.dumps(document, indent=2, sort_keys=False) + "\n",
141
- encoding="utf-8",
141
+ encoding="utf-8", newline="\n",
142
142
  )
@@ -227,4 +227,4 @@ def project_gemini_command_toml(
227
227
 
228
228
  destination = target_root / relative.with_suffix(".toml")
229
229
  destination.parent.mkdir(parents=True, exist_ok=True)
230
- destination.write_text("\n".join(lines) + "\n", encoding="utf-8")
230
+ destination.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
@@ -191,7 +191,7 @@ def project(
191
191
  # POSIX-friendliness.
192
192
  target_path.write_text(
193
193
  json.dumps(body, indent=2, sort_keys=False) + "\n",
194
- encoding="utf-8",
194
+ encoding="utf-8", newline="\n",
195
195
  )
196
196
 
197
197
 
@@ -243,7 +243,7 @@ def _atomic_write(target_path: Path, data: dict) -> None:
243
243
  serialised = json.dumps(data, indent=2, sort_keys=False) + "\n"
244
244
  with tempfile.NamedTemporaryFile(
245
245
  mode="w",
246
- encoding="utf-8",
246
+ encoding="utf-8", newline="\n",
247
247
  dir=str(target_path.parent),
248
248
  prefix=target_path.name + ".",
249
249
  suffix=".tmp",
@@ -54,5 +54,5 @@ def project_merge_json(source_dir: Path, output_root: Path, rule: dict) -> None:
54
54
  target_path.parent.mkdir(parents=True, exist_ok=True)
55
55
  target_path.write_text(
56
56
  json.dumps(existing, indent=2, sort_keys=True) + "\n",
57
- encoding="utf-8",
57
+ encoding="utf-8", newline="\n",
58
58
  )
@@ -296,7 +296,7 @@ def _atomic_write(target_path: Path, data: dict) -> None:
296
296
  serialised = json.dumps(data, indent=2, sort_keys=False) + "\n"
297
297
  with tempfile.NamedTemporaryFile(
298
298
  mode="w",
299
- encoding="utf-8",
299
+ encoding="utf-8", newline="\n",
300
300
  dir=str(target_path.parent),
301
301
  prefix=target_path.name + ".",
302
302
  suffix=".tmp",
@@ -24,7 +24,9 @@ consumer.
24
24
  Self-host scope (see docs/specs/self-hosting/spec.md § Phased rollout):
25
25
  the `SELF_HOST_ADAPTERS` allow-list runs `claude-code` and `codex`.
26
26
  Kiro and Copilot stay distribution-only so self-host does not project
27
- `.kiro/` or `.github/instructions/`.
27
+ `.kiro/` or `.github/instructions/`. Both `SELF_HOST_ADAPTERS` and
28
+ `SELF_HOST_PACKS` are sourced from `recipes/self-host.toml` (see the
29
+ `_DEFAULT_*` block below); the values named here are the current defaults.
28
30
  """
29
31
 
30
32
  from __future__ import annotations
@@ -79,27 +81,76 @@ TARGET_PATHS = (
79
81
  Path("AGENTS.md"),
80
82
  )
81
83
 
82
- # Self-host allow-list (see self-hosting spec § Phased rollout).
83
- # Kiro and Copilot remain in the contract for distribution builds but
84
- # are excluded from the self-host runner.
85
- SELF_HOST_ADAPTERS: tuple[str, ...] = ("claude-code", "codex")
86
-
87
- # Self-host *pack* allow-list. This repo is the catalogue's home, not
88
- # an adopter `make build-self` should only project the in-house
89
- # packs into the working tree. User-scope-default packs (architect,
90
- # atlassian, contracts, converters, credential-brokers, figma) are
91
- # advertised via `marketplace.json` but their primitives don't belong
92
- # in this repo's `.claude/skills/` (or future `.kiro/skills/`) tree.
93
- # `monorepo-extras` is repo-only by metadata but the bundle is not a
94
- # canonical monorepo consumer of `new-package`, so it's left out too.
95
- # `_aggregate_marketplace` intentionally ignores this filter — the
96
- # catalogue advertises every pack.
97
- SELF_HOST_PACKS: tuple[str, ...] = (
84
+ # Built-in fallbacks for the self-host pack / adapter allow-lists. The
85
+ # authoritative values live in `recipes/self-host.toml` (read at import below);
86
+ # these are used only when that recipe is missing a key or can't be read, so
87
+ # module import stays total. Kiro and Copilot remain in the contract for
88
+ # distribution builds but are excluded from the self-host runner. This repo is
89
+ # the catalogue's home, not an adopter, so `make build-self` only projects the
90
+ # in-house packs; `_aggregate_marketplace` intentionally ignores the pack
91
+ # filter the catalogue advertises every pack. See the recipe for the full
92
+ # rationale behind the pack selection.
93
+ _DEFAULT_SELF_HOST_ADAPTERS: tuple[str, ...] = ("claude-code", "codex")
94
+ _DEFAULT_SELF_HOST_PACKS: tuple[str, ...] = (
98
95
  "core",
99
96
  "governance-extras",
100
97
  "user-guide-diataxis",
101
98
  )
102
99
 
100
+ _SELF_HOST_RECIPE = "self-host"
101
+
102
+
103
+ def _read_recipe_text(name: str) -> str | None:
104
+ """Return the text of `recipes/<name>.toml`, or None if unreadable.
105
+
106
+ Mirrors `build.main`'s recipe resolution: filesystem first (dev / editable
107
+ install), then `importlib.resources` (zipapp, where the package lives inside
108
+ a `.pyz` archive that `Path.exists()` cannot traverse).
109
+ """
110
+ recipe_path = Path(__file__).resolve().parent / "recipes" / f"{name}.toml"
111
+ if recipe_path.exists():
112
+ return recipe_path.read_text(encoding="utf-8")
113
+ try:
114
+ from importlib.resources import files
115
+
116
+ resource = files("agentbundle.build").joinpath(f"recipes/{name}.toml")
117
+ if resource.is_file():
118
+ return resource.read_text(encoding="utf-8")
119
+ except (FileNotFoundError, ModuleNotFoundError):
120
+ pass
121
+ return None
122
+
123
+
124
+ def _extract_self_host_lists(
125
+ recipe: dict,
126
+ ) -> tuple[tuple[str, ...], tuple[str, ...]]:
127
+ """Pull `(packs, adapters)` from a parsed self-host recipe, substituting the
128
+ built-in defaults for any list that is absent or empty."""
129
+ body = recipe.get("recipe", {})
130
+ packs = tuple(body.get("packs", {}).get("include", ())) or _DEFAULT_SELF_HOST_PACKS
131
+ adapters = (
132
+ tuple(body.get("adapters", {}).get("targets", ()))
133
+ or _DEFAULT_SELF_HOST_ADAPTERS
134
+ )
135
+ return packs, adapters
136
+
137
+
138
+ def _load_self_host_lists() -> tuple[tuple[str, ...], tuple[str, ...]]:
139
+ """Read the self-host allow-lists from the recipe. Total: any read or parse
140
+ failure falls back to the built-in defaults so module import never raises."""
141
+ try:
142
+ text = _read_recipe_text(_SELF_HOST_RECIPE)
143
+ if text is None:
144
+ return _DEFAULT_SELF_HOST_PACKS, _DEFAULT_SELF_HOST_ADAPTERS
145
+ return _extract_self_host_lists(tomllib.loads(text))
146
+ except Exception:
147
+ # Unreadable (non-UTF-8 / permission / IO) or malformed recipe — fall
148
+ # back so module import is total (AC3).
149
+ return _DEFAULT_SELF_HOST_PACKS, _DEFAULT_SELF_HOST_ADAPTERS
150
+
151
+
152
+ SELF_HOST_PACKS, SELF_HOST_ADAPTERS = _load_self_host_lists()
153
+
103
154
 
104
155
  def _filter_self_host_packs(pack_paths: list[Path]) -> list[Path]:
105
156
  """Return the subset of *pack_paths* whose directory name is in
@@ -195,7 +246,7 @@ def resolve_markers(
195
246
  text,
196
247
  )
197
248
  if replaced != text:
198
- path.write_text(replaced, encoding="utf-8")
249
+ path.write_text(replaced, encoding="utf-8", newline="\n")
199
250
  modified += 1
200
251
  return modified
201
252
 
@@ -270,7 +321,7 @@ def _compose_agents_md(
270
321
  body = body_path.read_text(encoding="utf-8").replace("\r\n", "\n")
271
322
  if body and not body.endswith("\n"):
272
323
  body += "\n"
273
- target_path.write_text(body, encoding="utf-8")
324
+ target_path.write_text(body, encoding="utf-8", newline="\n")
274
325
 
275
326
  if footer_path.exists():
276
327
  text = target_path.read_text(encoding="utf-8")
@@ -279,7 +330,7 @@ def _compose_agents_md(
279
330
  text += "\n"
280
331
  if footer and not footer.endswith("\n"):
281
332
  footer += "\n"
282
- target_path.write_text(text + footer, encoding="utf-8")
333
+ target_path.write_text(text + footer, encoding="utf-8", newline="\n")
283
334
  return target_path
284
335
 
285
336
 
@@ -329,6 +380,7 @@ EXCLUDED_PATTERNS: tuple[str, ...] = (
329
380
  "README.md", # root-level; nested README.md not excluded
330
381
  "LICENSE-*",
331
382
  ".gitignore",
383
+ ".gitattributes",
332
384
  ".github/**",
333
385
  "AGENTS.local.md",
334
386
  "AGENTS.md", # root-level; nested AGENTS.md not excluded
@@ -535,7 +587,7 @@ def _aggregate_marketplace(
535
587
  }
536
588
  target.write_text(
537
589
  json.dumps(payload, indent=2, sort_keys=True) + "\n",
538
- encoding="utf-8",
590
+ encoding="utf-8", newline="\n",
539
591
  )
540
592
  return target
541
593
 
@@ -1463,10 +1515,42 @@ def run_build_check_drift_gates(
1463
1515
  return 0
1464
1516
 
1465
1517
 
1518
+ def _refuse_fixture_packs_dir(packs_dir: Path, *, dry_run: bool) -> int | None:
1519
+ """Refuse a real-write self-host whose `packs_dir` points into
1520
+ `tests/fixtures/` (which would overwrite the working tree with fixture
1521
+ data), unless `ALLOW_FIXTURE_PACKS` is set.
1522
+
1523
+ This is the cross-platform home of the guard that used to live only in the
1524
+ Makefile `build-self` recipe — so the make-free entry
1525
+ `python -m agentbundle.build self` (the only way to run build-self on
1526
+ Windows) is protected too. Returns a non-zero exit code to refuse, or
1527
+ `None` to proceed. Dry-run writes to a shadow temp dir, so it is never
1528
+ guarded (matching the `run_self_host` dirty-tree check). `as_posix()`
1529
+ normalises separators so the match is Windows-safe.
1530
+ """
1531
+ if dry_run or os.environ.get("ALLOW_FIXTURE_PACKS"):
1532
+ return None
1533
+ # Trailing slash mirrors the historical Makefile glob `*tests/fixtures/*`
1534
+ # exactly — so a sibling like `my-tests/fixtures-backup/` doesn't over-match.
1535
+ if "tests/fixtures/" in packs_dir.as_posix():
1536
+ print(
1537
+ "self-host: refusing — --packs-dir points into tests/fixtures/; "
1538
+ "this would overwrite your working tree with fixture data. Set "
1539
+ "ALLOW_FIXTURE_PACKS=1 to override, or use --packs-dir packs.",
1540
+ file=sys.stderr,
1541
+ )
1542
+ return 2
1543
+ return None
1544
+
1545
+
1466
1546
  def cmd_self(args) -> int:
1547
+ packs_dir = Path(args.packs_dir).resolve()
1548
+ refusal = _refuse_fixture_packs_dir(packs_dir, dry_run=args.dry_run)
1549
+ if refusal is not None:
1550
+ return refusal
1467
1551
  return run_self_host(
1468
1552
  working_tree=Path(args.output_dir).resolve(),
1469
- packs_dir=Path(args.packs_dir).resolve(),
1553
+ packs_dir=packs_dir,
1470
1554
  dry_run=args.dry_run,
1471
1555
  force=args.force,
1472
1556
  no_symlink=getattr(args, "no_symlink", False),