agentbundle 0.20.0__tar.gz → 0.21.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 (164) hide show
  1. {agentbundle-0.20.0 → agentbundle-0.21.0}/PKG-INFO +35 -1
  2. {agentbundle-0.20.0 → agentbundle-0.21.0}/README.md +34 -0
  3. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/catalogue.schema.json +12 -0
  4. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/install-marker.py +28 -35
  5. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/self_host.py +1 -1
  6. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue.py +7 -9
  7. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/archive.py +41 -10
  8. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/build.py +6 -6
  9. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/config.py +63 -3
  10. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/defaults.py +16 -2
  11. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/diagnostics.py +3 -3
  12. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/lint.py +50 -49
  13. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/package.py +29 -19
  14. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/self_host.py +0 -1
  15. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/skill_spec_lint.py +2 -3
  16. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/verify.py +30 -12
  17. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/categories.py +3 -4
  18. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/cli.py +177 -28
  19. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/_common.py +17 -7
  20. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/_drop_warning.py +1 -2
  21. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/adapt.py +9 -4
  22. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_build.py +1 -1
  23. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_lint.py +1 -1
  24. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_package.py +1 -1
  25. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_self_host.py +4 -6
  26. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_sync_defaults.py +2 -2
  27. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_tooling_stub.py +1 -1
  28. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/catalogue_verify.py +1 -1
  29. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/docs.py +1 -1
  30. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/init_state.py +1 -1
  31. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/install.py +92 -65
  32. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/list_installed.py +35 -29
  33. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/list_packs.py +1 -1
  34. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/list_profiles.py +1 -1
  35. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/list_targets.py +0 -1
  36. agentbundle-0.21.0/agentbundle/commands/oplog_cmd.py +98 -0
  37. agentbundle-0.21.0/agentbundle/commands/pack_config_cmd.py +201 -0
  38. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/pack_evals.py +45 -17
  39. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/package_catalogue.py +1 -2
  40. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/reconcile.py +1 -1
  41. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/render.py +1 -2
  42. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/show.py +4 -4
  43. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/uninstall.py +5 -7
  44. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/upgrade.py +102 -103
  45. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/validate.py +1 -4
  46. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/config.py +165 -8
  47. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/https_catalogue.py +34 -38
  48. agentbundle-0.21.0/agentbundle/oplog.py +168 -0
  49. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/render.py +1 -2
  50. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/safety.py +85 -9
  51. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/scope.py +1 -2
  52. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/source_defaults.py +15 -11
  53. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/statelock.py +3 -3
  54. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/user_config.py +5 -11
  55. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/version.py +1 -1
  56. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/PKG-INFO +35 -1
  57. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/SOURCES.txt +3 -0
  58. {agentbundle-0.20.0 → agentbundle-0.21.0}/pyproject.toml +1 -1
  59. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/__init__.py +0 -0
  60. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/__main__.py +0 -0
  61. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/adapter.schema.json +0 -0
  62. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/adapter.toml +0 -0
  63. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/install-defaults.toml +0 -0
  64. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/pack.schema.json +0 -0
  65. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  66. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  67. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/_data/profile.schema.json +0 -0
  68. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/__init__.py +0 -0
  69. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/__main__.py +0 -0
  70. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapter_root_bins.py +0 -0
  71. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/__init__.py +0 -0
  72. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/claude_code.py +0 -0
  73. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/codex.py +0 -0
  74. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/copilot.py +0 -0
  75. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/cursor.py +0 -0
  76. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/gemini.py +0 -0
  77. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/kiro.py +0 -0
  78. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  79. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
  80. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/contract.py +0 -0
  81. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/lint_packs.py +0 -0
  82. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/main.py +0 -0
  83. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/phase_order.py +0 -0
  84. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/__init__.py +0 -0
  85. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  86. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  87. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  88. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/direct_directory.py +0 -0
  89. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  90. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/hook_id.py +0 -0
  91. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  92. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  93. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/merge_json.py +0 -0
  94. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/projections/user_merge_json.py +0 -0
  95. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/composite-agents-md.toml +0 -0
  96. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/composite-marketplace.toml +0 -0
  97. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/marketplace.toml +0 -0
  98. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/per-pack-apm-package.toml +0 -0
  99. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/per-pack-claude-plugin.toml +0 -0
  100. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/per-pack-overlay.toml +0 -0
  101. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/recipes/self-host.toml +0 -0
  102. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/scope_rails.py +0 -0
  103. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/shared_libs.py +0 -0
  104. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/target_resolver.py +0 -0
  105. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/__init__.py +0 -0
  106. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  107. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  108. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  109. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
  110. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
  111. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  112. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  113. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  114. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
  115. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  116. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  117. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  118. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  119. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_contract.py +0 -0
  120. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
  121. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
  122. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  123. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  124. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  125. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  126. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  127. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  128. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  129. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  130. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  131. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  132. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  133. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  134. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  135. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  136. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  137. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  138. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_security.py +0 -0
  139. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  140. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_self_host_fixture_guard.py +0 -0
  141. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_self_host_recipe_config.py +0 -0
  142. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  143. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_shared_prefix_contract.py +0 -0
  144. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  145. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  146. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  147. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_validate.py +0 -0
  148. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/tests/test_writers_emit_lf.py +0 -0
  149. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/user_libs.py +0 -0
  150. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/build/validate.py +0 -0
  151. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/__init__.py +0 -0
  152. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/catalogue_tooling/results.py +0 -0
  153. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/__init__.py +0 -0
  154. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/config.py +0 -0
  155. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/diff.py +0 -0
  156. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/profile.py +0 -0
  157. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/commands/scaffold.py +0 -0
  158. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle/pack_inventory.py +0 -0
  159. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  160. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/entry_points.txt +0 -0
  161. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/requires.txt +0 -0
  162. {agentbundle-0.20.0 → agentbundle-0.21.0}/agentbundle.egg-info/top_level.txt +0 -0
  163. {agentbundle-0.20.0 → agentbundle-0.21.0}/setup.cfg +0 -0
  164. {agentbundle-0.20.0 → agentbundle-0.21.0}/tests/test_linear_primitive.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.20.0
3
+ Version: 0.21.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
@@ -34,6 +34,8 @@ Requires-Dist: pyyaml>=6.0; extra == "lint"
34
34
  pip install agentbundle
35
35
  ```
36
36
 
37
+ Requires Python 3.11+. Runs on macOS, Linux, and Windows.
38
+
37
39
  **Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
38
40
 
39
41
  ```bash
@@ -240,6 +242,38 @@ agentbundle pack evals run --pack <pack-name> --catalogue-root .
240
242
 
241
243
  See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
242
244
 
245
+ ## Catalogue defaults and pack config API
246
+
247
+ **Catalogue defaults** let operators ship sensible starting values for every pack they distribute. Add a `[pack-defaults.<pack-name>]` table to `catalogue.toml` and the defaults are baked in at publish time. They slot into the three-layer cascade: pack-source defaults → operator defaults → user config. A custom install root is also supported via `[catalogue] user-dir = "~/custom/path"`.
248
+
249
+ **Pack scripts** can resolve their user-scope directory and read the merged config at runtime using the `agentbundle.config` API:
250
+
251
+ ```python
252
+ from agentbundle.config import pack_dir, load_pack_config
253
+
254
+ directory = pack_dir("my-pack") # ~/.agentbundle/my-pack/
255
+ config = load_pack_config("my-pack") # merged dict: pack defaults + operator + user
256
+ ```
257
+
258
+ **Operation log** — scripts can append structured JSONL records to `<pack_dir>/ops.jsonl` for lightweight audit trails:
259
+
260
+ ```python
261
+ from agentbundle.oplog import write_entry
262
+
263
+ write_entry("my-pack", "install", src="git+https://example.com/my-pack")
264
+ ```
265
+
266
+ **CLI commands** to read, write, and inspect pack config and operation logs:
267
+
268
+ ```bash
269
+ agentbundle pack-config show my-pack # all config values for a pack
270
+ agentbundle pack-config get my-pack api-key # single value
271
+ agentbundle pack-config set my-pack api-key v # write to user config.toml
272
+
273
+ agentbundle oplog show my-pack # JSONL operation history
274
+ agentbundle oplog clear my-pack # wipe history (asks first)
275
+ ```
276
+
243
277
  ## Credentials
244
278
 
245
279
  `agentbundle` doesn't resolve secrets. Credentialed skills use [`credbroker`](https://pypi.org/project/credbroker/), a standalone resolver that keeps cleartext out of the model's reach.
@@ -12,6 +12,8 @@
12
12
  pip install agentbundle
13
13
  ```
14
14
 
15
+ Requires Python 3.11+. Runs on macOS, Linux, and Windows.
16
+
15
17
  **Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
16
18
 
17
19
  ```bash
@@ -218,6 +220,38 @@ agentbundle pack evals run --pack <pack-name> --catalogue-root .
218
220
 
219
221
  See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
220
222
 
223
+ ## Catalogue defaults and pack config API
224
+
225
+ **Catalogue defaults** let operators ship sensible starting values for every pack they distribute. Add a `[pack-defaults.<pack-name>]` table to `catalogue.toml` and the defaults are baked in at publish time. They slot into the three-layer cascade: pack-source defaults → operator defaults → user config. A custom install root is also supported via `[catalogue] user-dir = "~/custom/path"`.
226
+
227
+ **Pack scripts** can resolve their user-scope directory and read the merged config at runtime using the `agentbundle.config` API:
228
+
229
+ ```python
230
+ from agentbundle.config import pack_dir, load_pack_config
231
+
232
+ directory = pack_dir("my-pack") # ~/.agentbundle/my-pack/
233
+ config = load_pack_config("my-pack") # merged dict: pack defaults + operator + user
234
+ ```
235
+
236
+ **Operation log** — scripts can append structured JSONL records to `<pack_dir>/ops.jsonl` for lightweight audit trails:
237
+
238
+ ```python
239
+ from agentbundle.oplog import write_entry
240
+
241
+ write_entry("my-pack", "install", src="git+https://example.com/my-pack")
242
+ ```
243
+
244
+ **CLI commands** to read, write, and inspect pack config and operation logs:
245
+
246
+ ```bash
247
+ agentbundle pack-config show my-pack # all config values for a pack
248
+ agentbundle pack-config get my-pack api-key # single value
249
+ agentbundle pack-config set my-pack api-key v # write to user config.toml
250
+
251
+ agentbundle oplog show my-pack # JSONL operation history
252
+ agentbundle oplog clear my-pack # wipe history (asks first)
253
+ ```
254
+
221
255
  ## Credentials
222
256
 
223
257
  `agentbundle` doesn't resolve secrets. Credentialed skills use [`credbroker`](https://pypi.org/project/credbroker/), a standalone resolver that keeps cleartext out of the model's reach.
@@ -12,6 +12,14 @@
12
12
  "description": "Schema version — must be 1.",
13
13
  "enum": [1]
14
14
  },
15
+ "pack-defaults": {
16
+ "type": "object",
17
+ "description": "Per-pack configuration defaults baked into install-defaults.toml at build time.",
18
+ "additionalProperties": {
19
+ "type": "object",
20
+ "additionalProperties": { "type": "string" }
21
+ }
22
+ },
15
23
  "catalogue": {
16
24
  "type": "object",
17
25
  "additionalProperties": false,
@@ -25,6 +33,10 @@
25
33
  "package"
26
34
  ],
27
35
  "properties": {
36
+ "user-dir": {
37
+ "type": "string",
38
+ "description": "User-scope pack directory root (must start with ~/). Defaults to ~/.agentbundle when absent."
39
+ },
28
40
  "name": {
29
41
  "type": "string",
30
42
  "description": "Safe identifier — [A-Za-z0-9][A-Za-z0-9_-]*.",
@@ -48,16 +48,12 @@ Exit codes:
48
48
  """
49
49
 
50
50
  import argparse
51
+ import contextlib
51
52
  import datetime
52
53
  import hashlib
53
54
  import json
54
55
  import os
55
56
  import pathlib
56
- import sys
57
- import tempfile
58
- import tomllib
59
- from datetime import timezone
60
-
61
57
 
62
58
  # ---------------------------------------------------------------------------
63
59
  # Vendored pack-name / pack-version shape rules — copied from
@@ -69,6 +65,9 @@ from datetime import timezone
69
65
  # pack.toml would pass unchecked and land phantom TOML lines in the marker.
70
66
  # ---------------------------------------------------------------------------
71
67
  import re as _re
68
+ import sys
69
+ import tempfile
70
+ import tomllib
72
71
 
73
72
  _PACK_NAME_RE = _re.compile(r"^[a-z0-9][a-z0-9-]*$")
74
73
  _PACK_VERSION_RE = _re.compile(
@@ -164,11 +163,11 @@ def _assert_under(target: pathlib.Path, jail: pathlib.Path) -> None:
164
163
  # jail is already resolved; do not call os.path.realpath on it again.
165
164
  try:
166
165
  resolved_target.relative_to(jail)
167
- except ValueError:
166
+ except ValueError as exc:
168
167
  raise ValueError(
169
168
  f"install-marker: marker path {resolved_target} escapes the per-scope jail "
170
169
  f"{jail}; refusing write"
171
- )
170
+ ) from exc
172
171
 
173
172
 
174
173
  def _assert_portable_name(component: str) -> None:
@@ -196,7 +195,7 @@ def _assert_portable_name(component: str) -> None:
196
195
  f"install-marker: refusing path component with control character "
197
196
  f"U+{ord(ch):04X}: {component!r}"
198
197
  )
199
- if component.endswith(".") or component.endswith(" "):
198
+ if component.endswith((".", " ")):
200
199
  raise ValueError(
201
200
  f"install-marker: refusing path component with trailing dot or space: "
202
201
  f"{component!r}"
@@ -294,7 +293,7 @@ def _marker_scope(origin: str) -> str:
294
293
  def _pack_toml(plugin_root: pathlib.Path) -> dict:
295
294
  """Load and return the pack manifest dict from ``pack.toml``."""
296
295
  toml_path = plugin_root / "pack.toml"
297
- with open(toml_path, "rb") as fh:
296
+ with toml_path.open("rb") as fh:
298
297
  return tomllib.load(fh)
299
298
 
300
299
 
@@ -343,12 +342,11 @@ def _marker_path(
343
342
  agentbundle.mkdir(mode=0o700, parents=True, exist_ok=True)
344
343
  resolved_jail = pathlib.Path(os.path.realpath(agentbundle))
345
344
  return agentbundle / ".adapt-install-marker.toml", resolved_jail
346
- else:
347
- # repo scope — project_dir must be set; callers ensure this.
348
- if project_dir is None:
349
- raise ValueError("project_dir required for repo-scope marker")
350
- resolved_jail = pathlib.Path(os.path.realpath(project_dir))
351
- return project_dir / ".adapt-install-marker.toml", resolved_jail
345
+ # repo scope — project_dir must be set; callers ensure this.
346
+ if project_dir is None:
347
+ raise ValueError("project_dir required for repo-scope marker")
348
+ resolved_jail = pathlib.Path(os.path.realpath(project_dir))
349
+ return project_dir / ".adapt-install-marker.toml", resolved_jail
352
350
 
353
351
 
354
352
  def _should_fire(
@@ -606,18 +604,14 @@ def _write_marker(
606
604
  os.write(tmp_fd, content_bytes)
607
605
  os.close(tmp_fd)
608
606
  tmp_fd = -1
609
- os.replace(tmp_name, marker_path)
607
+ pathlib.Path(tmp_name).replace(marker_path)
610
608
  except Exception:
611
609
  # Clean up tempfile on error (best effort).
612
610
  if tmp_fd >= 0:
613
- try:
611
+ with contextlib.suppress(OSError):
614
612
  os.close(tmp_fd)
615
- except OSError:
616
- pass
617
- try:
618
- os.unlink(tmp_name)
619
- except OSError:
620
- pass
613
+ with contextlib.suppress(OSError):
614
+ pathlib.Path(tmp_name).unlink()
621
615
  raise
622
616
 
623
617
 
@@ -640,17 +634,13 @@ def _write_hash(plugin_data: pathlib.Path, current_hash: str) -> None:
640
634
  os.write(tmp_fd, content_bytes)
641
635
  os.close(tmp_fd)
642
636
  tmp_fd = -1
643
- os.replace(tmp_name, hash_path)
637
+ pathlib.Path(tmp_name).replace(hash_path)
644
638
  except Exception:
645
639
  if tmp_fd >= 0:
646
- try:
640
+ with contextlib.suppress(OSError):
647
641
  os.close(tmp_fd)
648
- except OSError:
649
- pass
650
- try:
651
- os.unlink(tmp_name)
652
- except OSError:
653
- pass
642
+ with contextlib.suppress(OSError):
643
+ pathlib.Path(tmp_name).unlink()
654
644
  raise
655
645
 
656
646
 
@@ -892,7 +882,7 @@ def _main_claude_plugins(args: argparse.Namespace) -> int:
892
882
  new_entry: dict = {
893
883
  "name": pack_name,
894
884
  "version": pack_version,
895
- "installed-at": datetime.datetime.now(timezone.utc),
885
+ "installed-at": datetime.datetime.now(datetime.UTC),
896
886
  "install-route": args.install_route,
897
887
  }
898
888
 
@@ -912,7 +902,10 @@ def _main_claude_plugins(args: argparse.Namespace) -> int:
912
902
  except Exception as exc:
913
903
  # Hash write failure is non-fatal for the adopter (the marker was written),
914
904
  # but log it so the next session retries detection rather than silently skipping.
915
- print(f"install-marker: hash write failed (next session will retry): {exc}", file=sys.stderr)
905
+ print(
906
+ f"install-marker: hash write failed (next session will retry): {exc}",
907
+ file=sys.stderr,
908
+ )
916
909
 
917
910
  return 0
918
911
 
@@ -935,7 +928,7 @@ def _main_apm(args: argparse.Namespace) -> int:
935
928
  home = pathlib.Path(home_str)
936
929
 
937
930
  # --- Resolve data directory per AC3 precedence ---
938
- plugin_data = _resolve_data_dir(env)
931
+ plugin_data = _resolve_data_dir(env) # type: ignore[arg-type]
939
932
  if plugin_data is None:
940
933
  # No-match fall-through (no APM data-directory token set); exit 0
941
934
  # without writing marker or hash file (the no-partial-state rail).
@@ -1072,7 +1065,7 @@ def _main_apm(args: argparse.Namespace) -> int:
1072
1065
  new_entry: dict = {
1073
1066
  "name": pack_name,
1074
1067
  "version": pack_version,
1075
- "installed-at": datetime.datetime.now(timezone.utc),
1068
+ "installed-at": datetime.datetime.now(datetime.UTC),
1076
1069
  "install-route": args.install_route,
1077
1070
  }
1078
1071
 
@@ -1482,7 +1482,7 @@ def run_build_check_drift_gates(
1482
1482
  except _PARITY_EXPECTED_EXC as exc:
1483
1483
  source_out = f"<{type(exc).__name__}: {exc}>"
1484
1484
  try:
1485
- template_out: object = template_emit(test_input)
1485
+ template_out: object = template_emit(test_input) # type: ignore[operator]
1486
1486
  except _PARITY_EXPECTED_EXC as exc:
1487
1487
  template_out = f"<{type(exc).__name__}: {exc}>"
1488
1488
  if source_out != template_out:
@@ -103,8 +103,7 @@ def _resolve_https(uri: str) -> Path:
103
103
  atexit.register(shutil.rmtree, str(tmpdir), True)
104
104
  _fetch_and_extract(tarball_url, tmpdir)
105
105
  # The GitHub archive extracts to <repo>-<ref>/ (with '/' → '-' in SHAs).
106
- inner = _find_inner_dir(tmpdir)
107
- return inner
106
+ return _find_inner_dir(tmpdir)
108
107
 
109
108
 
110
109
  def _ref_type(ref: str) -> str:
@@ -148,13 +147,12 @@ def _github_archive_url(owner: str, repo: str, ref: str) -> str:
148
147
  def _fetch_and_extract(url: str, dest: Path) -> None:
149
148
  try:
150
149
  # B310: constant github.com archive base assembled from parsed owner/repo/ref.
151
- with urllib.request.urlopen(url) as resp: # nosec B310
152
- with tarfile.open(fileobj=resp, mode="r|gz") as tf:
153
- # filter="data" rejects unsafe members (absolute paths, ..
154
- # links, devices, setuid bits) — Python 3.12+ default but
155
- # explicit for 3.11 compatibility and to silence the 3.14
156
- # DeprecationWarning. Path-jail is belt; this is braces.
157
- tf.extractall(path=dest, filter="data")
150
+ with urllib.request.urlopen(url) as resp, tarfile.open(fileobj=resp, mode="r|gz") as tf: # nosec B310
151
+ # filter="data" rejects unsafe members (absolute paths, ..
152
+ # links, devices, setuid bits) — Python 3.12+ default but
153
+ # explicit for 3.11 compatibility and to silence the 3.14
154
+ # DeprecationWarning. Path-jail is belt; this is braces.
155
+ tf.extractall(path=dest, filter="data")
158
156
  except urllib.error.URLError as exc:
159
157
  raise CatalogueError(
160
158
  f"Failed to fetch catalogue archive: {url} — {exc.reason}"
@@ -58,7 +58,10 @@ def _check_sha256_sidecar(archive: Path, sha256_file: Path | None) -> list[Diagn
58
58
  return [_err("CAT-V-ARC-001", f"cannot read sha256 sidecar: {exc}")]
59
59
  actual_hex = hashlib.sha256(archive.read_bytes()).hexdigest()
60
60
  if expected_hex != actual_hex:
61
- return [_err("CAT-V-ARC-001", f"sha256 sidecar mismatch: expected {expected_hex}, got {actual_hex}")]
61
+ return [_err(
62
+ "CAT-V-ARC-001",
63
+ f"sha256 sidecar mismatch: expected {expected_hex}, got {actual_hex}",
64
+ )]
62
65
  return []
63
66
 
64
67
 
@@ -74,7 +77,10 @@ def _check_gzip_parseable(archive: Path) -> list[Diagnostic]:
74
77
  def _check_compressed_size(archive: Path) -> list[Diagnostic]:
75
78
  size = archive.stat().st_size
76
79
  if size > _MAX_COMPRESSED_BYTES:
77
- return [_err("CAT-V-ARC-003", f"compressed size {size} exceeds limit {_MAX_COMPRESSED_BYTES}")]
80
+ return [_err(
81
+ "CAT-V-ARC-003",
82
+ f"compressed size {size} exceeds limit {_MAX_COMPRESSED_BYTES}",
83
+ )]
78
84
  return []
79
85
 
80
86
 
@@ -102,7 +108,11 @@ def check_members(members: list[tarfile.TarInfo]) -> list[Diagnostic]:
102
108
  # no device/special files or FIFOs
103
109
  for m in members:
104
110
  if m.isdev() or m.isfifo():
105
- diags.append(_err("CAT-V-ARC-007", f"device/special/FIFO in archive: {m.name!r}", path=m.name))
111
+ diags.append(_err(
112
+ "CAT-V-ARC-007",
113
+ f"device/special/FIFO in archive: {m.name!r}",
114
+ path=m.name,
115
+ ))
106
116
 
107
117
  # AC13: no duplicate members
108
118
  seen: set[str] = set()
@@ -125,7 +135,10 @@ def check_members(members: list[tarfile.TarInfo]) -> list[Diagnostic]:
125
135
 
126
136
  # member count limit
127
137
  if len(members) > _MAX_MEMBERS:
128
- diags.append(_err("CAT-V-ARC-003", f"member count {len(members)} exceeds limit {_MAX_MEMBERS}"))
138
+ diags.append(_err(
139
+ "CAT-V-ARC-003",
140
+ f"member count {len(members)} exceeds limit {_MAX_MEMBERS}",
141
+ ))
129
142
 
130
143
  return diags
131
144
 
@@ -133,7 +146,10 @@ def check_members(members: list[tarfile.TarInfo]) -> list[Diagnostic]:
133
146
  def _check_expanded_size(members: list[tarfile.TarInfo]) -> list[Diagnostic]:
134
147
  total = sum(m.size for m in members if m.isreg())
135
148
  if total > _MAX_EXPANDED_BYTES:
136
- return [_err("CAT-V-ARC-003", f"expanded size {total} exceeds limit {_MAX_EXPANDED_BYTES}")]
149
+ return [_err(
150
+ "CAT-V-ARC-003",
151
+ f"expanded size {total} exceeds limit {_MAX_EXPANDED_BYTES}",
152
+ )]
137
153
  return []
138
154
 
139
155
 
@@ -180,7 +196,10 @@ def _check_manifest_schema(manifest: dict) -> list[Diagnostic]:
180
196
  diags.append(_err("CAT-V-ARC-011", f"manifest schema must be 1 or 2, got {schema!r}"))
181
197
  for field in ("bundle", "release", "generated_at"):
182
198
  if not isinstance(manifest.get(field), str):
183
- diags.append(_err("CAT-V-ARC-011", f"manifest missing required string field {field!r}"))
199
+ diags.append(_err(
200
+ "CAT-V-ARC-011",
201
+ f"manifest missing required string field {field!r}",
202
+ ))
184
203
  if not isinstance(manifest.get("files"), list):
185
204
  diags.append(_err("CAT-V-ARC-011", "manifest 'files' must be a list"))
186
205
  if not isinstance(manifest.get("packs"), list):
@@ -201,11 +220,19 @@ def _check_all_manifest_digests(
201
220
  member_name = f"{prefix}{rel_path}" if prefix else rel_path
202
221
  member = member_map.get(member_name)
203
222
  if member is None:
204
- diags.append(_err("CAT-V-ARC-013", f"manifest declares {rel_path!r} but not in archive", path=rel_path))
223
+ diags.append(_err(
224
+ "CAT-V-ARC-013",
225
+ f"manifest declares {rel_path!r} but not in archive",
226
+ path=rel_path,
227
+ ))
205
228
  continue
206
229
  fobj = tf.extractfile(member)
207
230
  if fobj is None:
208
- diags.append(_err("CAT-V-ARC-012", f"cannot extract {rel_path!r} for digest check", path=rel_path))
231
+ diags.append(_err(
232
+ "CAT-V-ARC-012",
233
+ f"cannot extract {rel_path!r} for digest check",
234
+ path=rel_path,
235
+ ))
209
236
  continue
210
237
  actual_sha = hashlib.sha256(fobj.read()).hexdigest()
211
238
  if actual_sha != expected_sha:
@@ -268,7 +295,10 @@ def _check_min_agentbundle_compat(manifest: dict) -> list[Diagnostic]:
268
295
  except ValueError:
269
296
  return []
270
297
  if running < required:
271
- return [_err("CAT-V-ARC-015", f"archive requires agentbundle >= {min_ver}, running {CLI_VERSION}")]
298
+ return [_err(
299
+ "CAT-V-ARC-015",
300
+ f"archive requires agentbundle >= {min_ver}, running {CLI_VERSION}",
301
+ )]
272
302
  return []
273
303
 
274
304
 
@@ -335,7 +365,8 @@ def verify_archive(archive: Path, sha256_file: Path | None = None) -> VerifyResu
335
365
  try:
336
366
  tf.extractall(tmpdir, filter="data")
337
367
  except TypeError:
338
- # Python < 3.12: extract member-by-member; paths pre-validated by check_members()
368
+ # Python < 3.12: extract member-by-member;
369
+ # paths pre-validated by check_members()
339
370
  for _m in tf.getmembers():
340
371
  _dest = tmpdir / _m.name
341
372
  _dest.parent.mkdir(parents=True, exist_ok=True)
@@ -101,18 +101,18 @@ def build_catalogue(
101
101
 
102
102
  # Temporarily override module-level constants when config is present.
103
103
  # Restored in finally to avoid cross-test pollution.
104
- saved_branch = _build_main._DIST_BRANCH
105
- saved_desc = _build_main._MARKETPLACE_DESCRIPTION
104
+ saved_branch = _build_main._DIST_BRANCH # type: ignore[attr-defined]
105
+ saved_desc = _build_main._MARKETPLACE_DESCRIPTION # type: ignore[attr-defined]
106
106
  try:
107
107
  if config and config.build.claude_plugin_branch:
108
- _build_main._DIST_BRANCH = config.build.claude_plugin_branch
108
+ _build_main._DIST_BRANCH = config.build.claude_plugin_branch # type: ignore[attr-defined]
109
109
  if config and config.build.marketplace_description:
110
- _build_main._MARKETPLACE_DESCRIPTION = config.build.marketplace_description
110
+ _build_main._MARKETPLACE_DESCRIPTION = config.build.marketplace_description # type: ignore[attr-defined]
111
111
 
112
112
  rc = cmd_build(args)
113
113
  finally:
114
- _build_main._DIST_BRANCH = saved_branch
115
- _build_main._MARKETPLACE_DESCRIPTION = saved_desc
114
+ _build_main._DIST_BRANCH = saved_branch # type: ignore[attr-defined]
115
+ _build_main._MARKETPLACE_DESCRIPTION = saved_desc # type: ignore[attr-defined]
116
116
 
117
117
  return BuildResult(
118
118
  ok=(rc == 0),
@@ -16,13 +16,22 @@ from __future__ import annotations
16
16
  import json
17
17
  import re
18
18
  import tomllib
19
- from dataclasses import dataclass
19
+ from dataclasses import dataclass, field
20
20
  from pathlib import Path
21
21
  from urllib.parse import urlsplit
22
22
 
23
23
  # Bundled recipe IDs that are always valid without a path on disk.
24
24
  _BUNDLED_RECIPES: frozenset[str] = frozenset({"default"})
25
25
 
26
+ # Pack slug pattern (must match agentbundle.config._PACK_NAME_RE).
27
+ _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
28
+
29
+ # Reserved slugs that collide with well-known user-scope paths.
30
+ _RESERVED_SLUGS: frozenset[str] = frozenset({"bin", "state", "credentials", "state.toml"})
31
+
32
+ # Default user-scope pack directory.
33
+ _DEFAULT_USER_DIR = "~/.agentbundle"
34
+
26
35
  # Safe identifier pattern for catalogue names.
27
36
  _SAFE_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_\-]*$")
28
37
 
@@ -95,6 +104,9 @@ class CatalogueConfig:
95
104
  build: CatalogueBuild
96
105
  package: CataloguePackage
97
106
  distribution: DistributionConfig
107
+ # RFC-0074: per-pack config defaults and user-scope directory.
108
+ user_dir: str = _DEFAULT_USER_DIR
109
+ pack_defaults: dict[str, dict[str, str]] = field(default_factory=dict)
98
110
 
99
111
 
100
112
  def _load_schema() -> dict:
@@ -224,10 +236,10 @@ def _validate_path(root: Path, p: str, field_name: str) -> None:
224
236
  resolved_root = root.resolve()
225
237
  try:
226
238
  resolved_path = (root / p).resolve()
227
- except OSError:
239
+ except OSError as exc:
228
240
  raise CatalogueConfigError(
229
241
  f"catalogue.toml: {field_name!r} path cannot be resolved: {p!r}"
230
- )
242
+ ) from exc
231
243
  if not resolved_path.is_relative_to(resolved_root):
232
244
  raise CatalogueConfigError(
233
245
  f"catalogue.toml: {field_name!r} path escapes catalogue root "
@@ -446,6 +458,52 @@ def load_catalogue_config(root: Path) -> CatalogueConfig | None:
446
458
  )
447
459
  distribution = DistributionConfig(agentbundle=agentbundle_dist)
448
460
 
461
+ # --- RFC-0074: user-dir and pack-defaults ---
462
+
463
+ user_dir_raw = cat.get("user-dir", _DEFAULT_USER_DIR)
464
+ if not isinstance(user_dir_raw, str):
465
+ raise CatalogueConfigError(
466
+ f"catalogue.toml: catalogue.user-dir must be a string, "
467
+ f"got {type(user_dir_raw).__name__}"
468
+ )
469
+ # Must start with ~/ (home-relative). Absolute paths outside $HOME are rejected.
470
+ if not user_dir_raw.startswith("~/"):
471
+ raise CatalogueConfigError(
472
+ f"catalogue.toml: catalogue.user-dir must start with ~/ (home-relative); "
473
+ f"got {user_dir_raw!r}. Absolute paths outside $HOME are not allowed."
474
+ )
475
+ # Reject path traversal sequences (e.g. ~/../../etc).
476
+ from pathlib import PurePosixPath
477
+
478
+ if ".." in PurePosixPath(user_dir_raw[2:]).parts:
479
+ raise CatalogueConfigError(
480
+ f"catalogue.toml: catalogue.user-dir must not contain '..' path components; "
481
+ f"got {user_dir_raw!r}"
482
+ )
483
+
484
+ raw_pack_defaults = raw.get("pack-defaults", {}) or {}
485
+ if not isinstance(raw_pack_defaults, dict):
486
+ raise CatalogueConfigError(
487
+ "catalogue.toml: pack-defaults must be a table"
488
+ )
489
+ pack_defaults: dict[str, dict[str, str]] = {}
490
+ for slug, kv in raw_pack_defaults.items():
491
+ if not _SLUG_RE.match(slug):
492
+ raise CatalogueConfigError(
493
+ f"catalogue.toml: pack-defaults key {slug!r} must match "
494
+ f"^[a-z0-9][a-z0-9-]*$"
495
+ )
496
+ if slug in _RESERVED_SLUGS:
497
+ raise CatalogueConfigError(
498
+ f"catalogue.toml: pack-defaults key {slug!r} is reserved "
499
+ f"({sorted(_RESERVED_SLUGS)})"
500
+ )
501
+ if not isinstance(kv, dict):
502
+ raise CatalogueConfigError(
503
+ f"catalogue.toml: pack-defaults.{slug} must be a table"
504
+ )
505
+ pack_defaults[slug] = {str(k): str(v) for k, v in kv.items()}
506
+
449
507
  return CatalogueConfig(
450
508
  schema=schema_val,
451
509
  name=name,
@@ -456,4 +514,6 @@ def load_catalogue_config(root: Path) -> CatalogueConfig | None:
456
514
  build=build,
457
515
  package=package,
458
516
  distribution=distribution,
517
+ user_dir=user_dir_raw,
518
+ pack_defaults=pack_defaults,
459
519
  )
@@ -11,8 +11,12 @@ from __future__ import annotations
11
11
 
12
12
  from pathlib import Path
13
13
 
14
- from agentbundle.catalogue_tooling.config import CatalogueConfig, CatalogueConfigError, load_catalogue_config
15
- from agentbundle.catalogue_tooling.results import Diagnostic, SyncDefaultsResult, Severity
14
+ from agentbundle.catalogue_tooling.config import (
15
+ CatalogueConfig,
16
+ CatalogueConfigError,
17
+ load_catalogue_config,
18
+ )
19
+ from agentbundle.catalogue_tooling.results import Diagnostic, Severity, SyncDefaultsResult
16
20
 
17
21
  _GENERATED_HEADER = (
18
22
  "# Generated by agentbundle catalogue sync-defaults"
@@ -93,6 +97,16 @@ def compile_defaults(config: CatalogueConfig) -> str:
93
97
  parts.append("\n")
94
98
  parts.append(_emit_section("defaults", defaults_rows))
95
99
 
100
+ # [pack-defaults.<pack>] — sorted by pack name, then by key within each section.
101
+ for pack_name in sorted(config.pack_defaults):
102
+ kv = config.pack_defaults[pack_name]
103
+ rows: list[tuple[str, str]] = sorted(
104
+ (k, _emit_str(v)) for k, v in kv.items()
105
+ )
106
+ if rows:
107
+ parts.append("\n")
108
+ parts.append(_emit_section(f"pack-defaults.{pack_name}", rows))
109
+
96
110
  return "".join(parts)
97
111
 
98
112
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import enum
6
6
 
7
7
 
8
- class DiagnosticCode(str, enum.Enum):
8
+ class DiagnosticCode(enum.StrEnum):
9
9
  UNKNOWN = "UNKNOWN"
10
10
 
11
11
  # Lint codes — CAT-L001 through CAT-L027
@@ -38,5 +38,5 @@ class DiagnosticCode(str, enum.Enum):
38
38
  CAT_L027 = "CAT-L027" # Multiline metadata form not supported
39
39
  CAT_L028 = "CAT-L028" # Install profile invariant violation (scope, deps, order)
40
40
  CAT_L029 = "CAT-L029" # Catalogue seeds lint failure (blocklist, placeholder, patterns.jsonl)
41
- CAT_L030 = "CAT-L030" # First-value contract violation (Level A/B fields, writes-to-repo, tutorial)
42
- CAT_L031 = "CAT-L031" # Credentialed-skill convention violation (D1/D2/D2b/D3/broker-specific)
41
+ CAT_L030 = "CAT-L030" # First-value contract violation (Level A/B fields, writes-to-repo, tutorial) # noqa: E501
42
+ CAT_L031 = "CAT-L031" # Credentialed-skill convention violation (D1/D2/D2b/D3/broker-specific) # noqa: E501