agentbundle 0.7.0__tar.gz → 0.8.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 (123) hide show
  1. {agentbundle-0.7.0 → agentbundle-0.8.0}/PKG-INFO +12 -12
  2. {agentbundle-0.7.0 → agentbundle-0.8.0}/README.md +11 -11
  3. agentbundle-0.8.0/agentbundle/_data/install-defaults.toml +10 -0
  4. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/codex.py +1 -1
  5. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/cursor.py +2 -2
  6. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/gemini.py +2 -2
  7. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro.py +1 -1
  8. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro_ide.py +1 -1
  9. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/main.py +4 -4
  10. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/codex_agent_toml.py +1 -1
  11. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/copilot_agent_md.py +1 -1
  12. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/copilot_hooks_json.py +1 -1
  13. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/gemini_command_toml.py +1 -1
  14. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/kiro_ide_hook.py +1 -1
  15. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/merge_into_agent_json.py +1 -1
  16. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/merge_json.py +1 -1
  17. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/user_merge_json.py +1 -1
  18. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/self_host.py +106 -22
  19. agentbundle-0.8.0/agentbundle/build/tests/test_self_host_fixture_guard.py +88 -0
  20. agentbundle-0.8.0/agentbundle/build/tests/test_self_host_recipe_config.py +113 -0
  21. agentbundle-0.8.0/agentbundle/build/tests/test_writers_emit_lf.py +169 -0
  22. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/cli.py +40 -4
  23. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/_common.py +119 -0
  24. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/config.py +6 -0
  25. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/install.py +30 -6
  26. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_packs.py +20 -30
  27. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_profiles.py +12 -16
  28. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/upgrade.py +10 -1
  29. agentbundle-0.8.0/agentbundle/source_defaults.py +342 -0
  30. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/user_config.py +53 -5
  31. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/version.py +1 -1
  32. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/PKG-INFO +12 -12
  33. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/SOURCES.txt +5 -0
  34. {agentbundle-0.7.0 → agentbundle-0.8.0}/pyproject.toml +1 -1
  35. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/__init__.py +0 -0
  36. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/__main__.py +0 -0
  37. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/adapter.schema.json +0 -0
  38. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/adapter.toml +0 -0
  39. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/install-marker.py +0 -0
  40. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/pack.schema.json +0 -0
  41. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  42. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  43. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/profile.schema.json +0 -0
  44. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/__init__.py +0 -0
  45. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/__main__.py +0 -0
  46. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapter_root_bins.py +0 -0
  47. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/__init__.py +0 -0
  48. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/claude_code.py +0 -0
  49. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/copilot.py +0 -0
  50. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  51. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/contract.py +0 -0
  52. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/lint_packs.py +0 -0
  53. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/phase_order.py +0 -0
  54. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/__init__.py +0 -0
  55. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/direct_directory.py +0 -0
  56. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/hook_id.py +0 -0
  57. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/scope_rails.py +0 -0
  58. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/shared_libs.py +0 -0
  59. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/target_resolver.py +0 -0
  60. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/__init__.py +0 -0
  61. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  62. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  63. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  64. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
  65. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
  66. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  67. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  68. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  69. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
  70. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  71. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  72. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  73. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  74. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract.py +0 -0
  75. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
  76. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
  77. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  78. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  79. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  80. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  81. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  82. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  83. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  84. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  85. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  86. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  87. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  88. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  89. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  90. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  91. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  92. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  93. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_security.py +0 -0
  94. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  95. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  96. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  97. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  98. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  99. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_validate.py +0 -0
  100. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/user_libs.py +0 -0
  101. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/validate.py +0 -0
  102. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/catalogue.py +0 -0
  103. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/categories.py +0 -0
  104. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/__init__.py +0 -0
  105. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/_drop_warning.py +0 -0
  106. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/adapt.py +0 -0
  107. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/diff.py +0 -0
  108. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/init_state.py +0 -0
  109. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_targets.py +0 -0
  110. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/profile.py +0 -0
  111. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/reconcile.py +0 -0
  112. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/render.py +0 -0
  113. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/scaffold.py +0 -0
  114. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/uninstall.py +0 -0
  115. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/validate.py +0 -0
  116. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/config.py +0 -0
  117. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/render.py +0 -0
  118. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/safety.py +0 -0
  119. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/scope.py +0 -0
  120. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  121. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/entry_points.txt +0 -0
  122. {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/top_level.txt +0 -0
  123. {agentbundle-0.7.0 → agentbundle-0.8.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.8.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). 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). 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
@@ -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),
@@ -0,0 +1,88 @@
1
+ """The tests/fixtures fixture-overwrite guard lives in the CLI handler.
2
+
3
+ `build-self` overwrites the working tree; pointing `--packs-dir` into
4
+ `tests/fixtures/` would overwrite it with fixture data. That guard used to live
5
+ only in the Makefile, so the make-free `python -m agentbundle.build self` entry
6
+ (the only one available on Windows) bypassed it. It now lives in `cmd_self` via
7
+ `_refuse_fixture_packs_dir`, honouring the same `ALLOW_FIXTURE_PACKS` override.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ import unittest
15
+ from pathlib import Path
16
+ from unittest import mock
17
+
18
+ import agentbundle.build.self_host as sh
19
+ from agentbundle.build.self_host import _refuse_fixture_packs_dir
20
+
21
+ _FIXTURE_DIR = Path("/repo/packages/agentbundle/tests/fixtures/packs")
22
+ _REAL_DIR = Path("/repo/packs")
23
+
24
+ # Empty string is falsy, so this simulates "unset" for the guard regardless of
25
+ # the ambient environment; "1" simulates the override being set.
26
+ _ENV_UNSET = {"ALLOW_FIXTURE_PACKS": ""}
27
+ _ENV_SET = {"ALLOW_FIXTURE_PACKS": "1"}
28
+
29
+
30
+ class RefuseFixturePacksDirTest(unittest.TestCase):
31
+ def test_real_write_into_fixtures_refuses(self):
32
+ with mock.patch.dict(os.environ, _ENV_UNSET):
33
+ rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=False)
34
+ self.assertEqual(rc, 2)
35
+
36
+ def test_env_override_proceeds(self):
37
+ with mock.patch.dict(os.environ, _ENV_SET):
38
+ rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=False)
39
+ self.assertIsNone(rc)
40
+
41
+ def test_dry_run_is_never_guarded(self):
42
+ with mock.patch.dict(os.environ, _ENV_UNSET):
43
+ rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=True)
44
+ self.assertIsNone(rc)
45
+
46
+ def test_non_fixtures_dir_proceeds(self):
47
+ with mock.patch.dict(os.environ, _ENV_UNSET):
48
+ rc = _refuse_fixture_packs_dir(_REAL_DIR, dry_run=False)
49
+ self.assertIsNone(rc)
50
+
51
+
52
+ class CmdSelfGuardWiringTest(unittest.TestCase):
53
+ def test_cmd_self_refuses_and_does_not_run(self):
54
+ args = argparse.Namespace(
55
+ output_dir=".",
56
+ packs_dir=str(_FIXTURE_DIR),
57
+ dry_run=False,
58
+ force=False,
59
+ no_symlink=False,
60
+ )
61
+ with mock.patch.dict(os.environ, _ENV_UNSET), mock.patch.object(
62
+ sh, "run_self_host"
63
+ ) as run:
64
+ rc = sh.cmd_self(args)
65
+ self.assertEqual(rc, 2)
66
+ run.assert_not_called()
67
+
68
+ def test_cmd_self_refuses_relative_fixtures_path(self):
69
+ # cmd_self resolves the packs-dir first; a *relative* tests/fixtures
70
+ # path must still match after resolve() + as_posix() (the real input
71
+ # shape, which the pure-helper tests above bypass).
72
+ args = argparse.Namespace(
73
+ output_dir=".",
74
+ packs_dir="agentbundle/build/tests/fixtures/packs",
75
+ dry_run=False,
76
+ force=False,
77
+ no_symlink=False,
78
+ )
79
+ with mock.patch.dict(os.environ, _ENV_UNSET), mock.patch.object(
80
+ sh, "run_self_host"
81
+ ) as run:
82
+ rc = sh.cmd_self(args)
83
+ self.assertEqual(rc, 2)
84
+ run.assert_not_called()
85
+
86
+
87
+ if __name__ == "__main__":
88
+ unittest.main()