mdcompose 0.2.0__tar.gz → 0.3.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 (110) hide show
  1. {mdcompose-0.2.0 → mdcompose-0.3.0}/.pre-commit-config.yaml +3 -3
  2. {mdcompose-0.2.0 → mdcompose-0.3.0}/CHANGELOG.md +12 -0
  3. {mdcompose-0.2.0 → mdcompose-0.3.0}/PKG-INFO +7 -4
  4. {mdcompose-0.2.0 → mdcompose-0.3.0}/README.md +6 -3
  5. {mdcompose-0.2.0 → mdcompose-0.3.0}/TODO.md +23 -22
  6. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/concepts.md +2 -0
  7. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/core-contract.md +31 -12
  8. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/cli.py +2 -0
  9. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/init_cmd.py +13 -3
  10. mdcompose-0.3.0/mdcompose/commands/skill_cmd.py +346 -0
  11. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/init_ops.py +109 -34
  12. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/manifest.py +51 -11
  13. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/skill_composition.py +18 -0
  14. mdcompose-0.3.0/mdcompose/core/skill_library_ops.py +253 -0
  15. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/skills.py +110 -19
  16. {mdcompose-0.2.0 → mdcompose-0.3.0}/pyproject.toml +1 -1
  17. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_init.py +66 -0
  18. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_init_ops.py +285 -0
  19. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_manifest.py +140 -0
  20. mdcompose-0.3.0/tests/test_skill_commands.py +645 -0
  21. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_skill_composition.py +14 -0
  22. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_skills.py +114 -0
  23. {mdcompose-0.2.0 → mdcompose-0.3.0}/uv.lock +1 -1
  24. {mdcompose-0.2.0 → mdcompose-0.3.0}/.gitattributes +0 -0
  25. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/FUNDING.yml +0 -0
  26. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  27. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  28. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/bump.yml +0 -0
  31. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/ci.yml +0 -0
  32. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/codeql.yml +0 -0
  33. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/dependency-review.yml +0 -0
  34. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/mutation.yml +0 -0
  35. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/python-eol.yml +0 -0
  36. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/release.yml +0 -0
  37. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/scorecard.yml +0 -0
  38. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/supply-chain.yml +0 -0
  39. {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/zizmor.yml +0 -0
  40. {mdcompose-0.2.0 → mdcompose-0.3.0}/.gitignore +0 -0
  41. {mdcompose-0.2.0 → mdcompose-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  42. {mdcompose-0.2.0 → mdcompose-0.3.0}/CONTRIBUTING.md +0 -0
  43. {mdcompose-0.2.0 → mdcompose-0.3.0}/LICENSE +0 -0
  44. {mdcompose-0.2.0 → mdcompose-0.3.0}/SECURITY.md +0 -0
  45. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/benchmarks.md +0 -0
  46. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/commit-and-release-tooling.md +0 -0
  47. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/hardening-review.md +0 -0
  48. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/optional-tooling.md +0 -0
  49. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/publishing.md +0 -0
  50. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/scorecard.md +0 -0
  51. {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/versioning-explained.md +0 -0
  52. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/__init__.py +0 -0
  53. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/__init__.py +0 -0
  54. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/config_cmd.py +0 -0
  55. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/convert_cmd.py +0 -0
  56. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/eject_cmd.py +0 -0
  57. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/import_cmd.py +0 -0
  58. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/snippet_cmd.py +0 -0
  59. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/target_cmd.py +0 -0
  60. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/__init__.py +0 -0
  61. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/composition.py +0 -0
  62. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/config.py +0 -0
  63. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/convert_ops.py +0 -0
  64. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/eject.py +0 -0
  65. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/exit_codes.py +0 -0
  66. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/files.py +0 -0
  67. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/import_ops.py +0 -0
  68. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/library_ops.py +0 -0
  69. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/managed_block.py +0 -0
  70. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/output.py +0 -0
  71. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/platform.py +0 -0
  72. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/report.py +0 -0
  73. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/sections.py +0 -0
  74. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/snippets.py +0 -0
  75. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/targets.py +0 -0
  76. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/prompts.py +0 -0
  77. {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/version.py +0 -0
  78. {mdcompose-0.2.0 → mdcompose-0.3.0}/renovate.json +0 -0
  79. {mdcompose-0.2.0 → mdcompose-0.3.0}/scripts/check_ascii.py +0 -0
  80. {mdcompose-0.2.0 → mdcompose-0.3.0}/scripts/check_licenses.py +0 -0
  81. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/benchmarks/conftest.py +0 -0
  82. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/benchmarks/test_core_ops.py +0 -0
  83. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/conftest.py +0 -0
  84. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/mdcompose.lock.schema1.json +0 -0
  85. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.bom.md +0 -0
  86. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.crlf.md +0 -0
  87. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.lf.md +0 -0
  88. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/prose.cp1252.md +0 -0
  89. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_cli.py +0 -0
  90. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_config.py +0 -0
  91. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_config_cmd.py +0 -0
  92. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_convert.py +0 -0
  93. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_doctor_drift.py +0 -0
  94. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_e2e_import_convert.py +0 -0
  95. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_eject.py +0 -0
  96. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_files.py +0 -0
  97. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_fuzz.py +0 -0
  98. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_import_cmd.py +0 -0
  99. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_import_ops.py +0 -0
  100. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_layering.py +0 -0
  101. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_managed_block.py +0 -0
  102. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_onedrive.py +0 -0
  103. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_output.py +0 -0
  104. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_platform.py +0 -0
  105. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_prompts.py +0 -0
  106. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_report.py +0 -0
  107. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_sections.py +0 -0
  108. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_snippet_commands.py +0 -0
  109. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_snippets.py +0 -0
  110. {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_targets.py +0 -0
@@ -37,13 +37,13 @@ repos:
37
37
  - id: check-merge-conflict
38
38
 
39
39
  - repo: https://github.com/astral-sh/ruff-pre-commit
40
- rev: v0.16.6
40
+ rev: v0.16.7
41
41
  hooks:
42
42
  - id: ruff-check
43
43
  args: [--fix]
44
44
 
45
45
  - repo: https://github.com/gitleaks/gitleaks
46
- rev: v8.30.1
46
+ rev: v8.30.0
47
47
  hooks:
48
48
  - id: gitleaks
49
49
 
@@ -63,7 +63,7 @@ repos:
63
63
  - id: check-github-workflows
64
64
 
65
65
  - repo: https://github.com/commitizen-tools/commitizen
66
- rev: v4.18.0
66
+ rev: v4.18.1
67
67
  hooks:
68
68
  - id: commitizen
69
69
  stages: [commit-msg]
@@ -3,6 +3,18 @@
3
3
  All notable changes to mdcompose are recorded here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## v0.3.0 (2026-09-18)
7
+
8
+ ### Added
9
+
10
+ - support script-bearing skills as directory entries (#17)
11
+
12
+ ## v0.2.1 (2026-09-14)
13
+
14
+ ### Added
15
+
16
+ - add mdcompose skill command group
17
+
6
18
  ## v0.2.0 (2026-09-14)
7
19
 
8
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mdcompose
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Keep CLAUDE.md and AGENTS.md in sync and composed from a personal snippet library, without hand-editing every project
5
5
  Project-URL: Homepage, https://github.com/Rovetown/mdcompose
6
6
  Project-URL: Repository, https://github.com/Rovetown/mdcompose
@@ -58,8 +58,7 @@ mdcompose keeps those as small snippets in one place and builds each project's `
58
58
  - **A committed lockfile.** `mdcompose.lock` embeds each snippet's content, so a clone or fork reproduces the same files with an empty library.
59
59
  - **Correct paths on Windows, WSL, and Linux**, including a warning when a managed file sits inside a OneDrive-synced folder.
60
60
  - **No install required.** `uvx mdcompose doctor` or `pipx run mdcompose doctor` run the real thing in a throwaway environment -- try it, or use it in a one-off script, without adding anything to your machine.
61
- - **Skills cost tokens too, and that's next (planned).** Every skill an agent does not need loaded for a project is context spent before it has done any work.
62
- A skill file -- frontmatter, a body, sometimes a script -- is already snippet-shaped, so curating which skills a project loads, the same way AGENTS.md and CLAUDE.md are curated today, is the natural next composition target: real token savings, not just tidiness.
61
+ - **Skills cost tokens too, so they're composed the same way.** A second, independent library curates which Claude Code Skills a project loads into `.claude/skills/`, so an agent stops paying context for skills a given project never needed.
63
62
  See [docs/concepts.md](https://github.com/Rovetown/mdcompose/blob/main/docs/concepts.md).
64
63
 
65
64
  It makes no network requests and collects no telemetry.
@@ -146,6 +145,10 @@ mdcompose snippet list the snippets in your library
146
145
  mdcompose snippet edit <id> open one in your editor
147
146
  mdcompose snippet remove <id> delete one
148
147
  mdcompose snippet adopt save a project's embedded snippets to your library
148
+ mdcompose skill list the skills in your skill library
149
+ mdcompose skill edit <id> open one in your editor
150
+ mdcompose skill remove <id> delete one
151
+ mdcompose skill adopt save a project's embedded skills to your skill library
149
152
  mdcompose import <file> pull sections out of an existing CLAUDE.md or AGENTS.md
150
153
  mdcompose convert <src> <dst> move content between this project's AGENTS.md and CLAUDE.md
151
154
  mdcompose eject stop managing a directory: remove markers, delete the lock
@@ -217,7 +220,7 @@ A few projects cover the same idea of a personal snippet library composed per pr
217
220
  What mdcompose does that they do not: import-versus-copy tied to Claude Code's real `@import`; drift detection by hashing one owned block; a committed lock with embedded content so a fork works without the author's library; and real Windows, WSL, and mounted-drive path correctness.
218
221
  It deliberately does no format translation: a target receives Markdown, not Cursor rules or Copilot instructions.
219
222
 
220
- **None of them go past agent instructions -- mdcompose plans to.** A skill file is markdown plus optional scripts, same as a snippet; composing a curated skill selection per project the way AGENTS.md and CLAUDE.md are composed today means an agent stops paying context for skills a given project never needed loaded in the first place.
223
+ **None of them go past agent instructions -- mdcompose does.** A skill file is markdown plus frontmatter, same as a snippet; a curated skill selection is composed per project into `.claude/skills/` the same way AGENTS.md and CLAUDE.md are, so an agent stops paying context for skills a given project never needed loaded in the first place.
221
224
 
222
225
  See [docs/concepts.md](https://github.com/Rovetown/mdcompose/blob/main/docs/concepts.md) for the fuller case for each point.
223
226
 
@@ -28,8 +28,7 @@ mdcompose keeps those as small snippets in one place and builds each project's `
28
28
  - **A committed lockfile.** `mdcompose.lock` embeds each snippet's content, so a clone or fork reproduces the same files with an empty library.
29
29
  - **Correct paths on Windows, WSL, and Linux**, including a warning when a managed file sits inside a OneDrive-synced folder.
30
30
  - **No install required.** `uvx mdcompose doctor` or `pipx run mdcompose doctor` run the real thing in a throwaway environment -- try it, or use it in a one-off script, without adding anything to your machine.
31
- - **Skills cost tokens too, and that's next (planned).** Every skill an agent does not need loaded for a project is context spent before it has done any work.
32
- A skill file -- frontmatter, a body, sometimes a script -- is already snippet-shaped, so curating which skills a project loads, the same way AGENTS.md and CLAUDE.md are curated today, is the natural next composition target: real token savings, not just tidiness.
31
+ - **Skills cost tokens too, so they're composed the same way.** A second, independent library curates which Claude Code Skills a project loads into `.claude/skills/`, so an agent stops paying context for skills a given project never needed.
33
32
  See [docs/concepts.md](https://github.com/Rovetown/mdcompose/blob/main/docs/concepts.md).
34
33
 
35
34
  It makes no network requests and collects no telemetry.
@@ -116,6 +115,10 @@ mdcompose snippet list the snippets in your library
116
115
  mdcompose snippet edit <id> open one in your editor
117
116
  mdcompose snippet remove <id> delete one
118
117
  mdcompose snippet adopt save a project's embedded snippets to your library
118
+ mdcompose skill list the skills in your skill library
119
+ mdcompose skill edit <id> open one in your editor
120
+ mdcompose skill remove <id> delete one
121
+ mdcompose skill adopt save a project's embedded skills to your skill library
119
122
  mdcompose import <file> pull sections out of an existing CLAUDE.md or AGENTS.md
120
123
  mdcompose convert <src> <dst> move content between this project's AGENTS.md and CLAUDE.md
121
124
  mdcompose eject stop managing a directory: remove markers, delete the lock
@@ -187,7 +190,7 @@ A few projects cover the same idea of a personal snippet library composed per pr
187
190
  What mdcompose does that they do not: import-versus-copy tied to Claude Code's real `@import`; drift detection by hashing one owned block; a committed lock with embedded content so a fork works without the author's library; and real Windows, WSL, and mounted-drive path correctness.
188
191
  It deliberately does no format translation: a target receives Markdown, not Cursor rules or Copilot instructions.
189
192
 
190
- **None of them go past agent instructions -- mdcompose plans to.** A skill file is markdown plus optional scripts, same as a snippet; composing a curated skill selection per project the way AGENTS.md and CLAUDE.md are composed today means an agent stops paying context for skills a given project never needed loaded in the first place.
193
+ **None of them go past agent instructions -- mdcompose does.** A skill file is markdown plus frontmatter, same as a snippet; a curated skill selection is composed per project into `.claude/skills/` the same way AGENTS.md and CLAUDE.md are, so an agent stops paying context for skills a given project never needed loaded in the first place.
191
194
 
192
195
  See [docs/concepts.md](https://github.com/Rovetown/mdcompose/blob/main/docs/concepts.md) for the fuller case for each point.
193
196
 
@@ -353,28 +353,29 @@ and parser fuzzing. `platform.py` and the interactive questionary pickers in
353
353
  `init` and `import` are the accepted remaining coverage gap; their flag paths
354
354
  are fully covered.
355
355
 
356
- ### Agent skill composition (Claude Code Skills and equivalents)
357
-
358
- Not scoped. Raised during the README rewrite as a marketing point worth
359
- having a real backing plan for, not yet an OpenSpec change.
360
-
361
- - [ ] Explore composing a project's agent-skill files the same way AGENTS.md
362
- and CLAUDE.md are composed today: a personal library of reusable skills,
363
- picked per project instead of an agent loading every skill unconditionally.
364
- A skill (a `SKILL.md`-shaped file: frontmatter plus a body, occasionally a
365
- script alongside it) is already close enough to a snippet in shape that the
366
- existing managed-block-and-manifest machinery may extend to it directly
367
- rather than needing a second mechanism.
368
- - [ ] The actual argument for doing this, not just the organizational one:
369
- every skill an agent does not need loaded for a given project is context it
370
- never has to spend, so a curated per-project skill selection is a
371
- token-efficiency win. That is the pitch in the README and
372
- [`docs/concepts.md`](docs/concepts.md); it has to survive contact with a real design before it
373
- becomes more than a pitch.
374
- - [ ] Revisit once v1 (AGENTS.md/CLAUDE.md composition) is stable. A second
375
- composed-file family is exactly the kind of expansion that has to prove it
376
- reuses the existing core rather than forking it, the standing bar for
377
- everything in this Roadmap section.
356
+ ### Agent skill composition (shipped)
357
+
358
+ Composing a project's skill library into `.claude/skills/<id>/SKILL.md` the
359
+ same way AGENTS.md/CLAUDE.md are composed is done (OpenSpec change
360
+ `agent-skill-composition`), and so is managing the skill library itself:
361
+ `skill list`, `skill edit`, `skill remove`, `skill adopt` (OpenSpec change
362
+ `skill-commands`), mirroring `mdcompose snippet` command-for-command. Both
363
+ reused the managed-block-and-manifest core rather than forking it, per the
364
+ standing bar for this Roadmap section.
365
+
366
+ A skill bundled with an accompanying script is also done (OpenSpec change
367
+ `skill-script-bundle`). The skill library entry is now either the original
368
+ single `.md` file or a directory (`<id>/SKILL.md` plus accompanying files at
369
+ other relative paths), sharing one id namespace; `mdcompose.lock` schema
370
+ version 3 embeds a directory-shaped skill's accompanying files under the
371
+ entry's `files` field; composition writes and drift-tracks each accompanying
372
+ file by a whole-file hash, since a script has no managed-block-safe comment
373
+ syntax; `skill list/edit/remove/adopt` all handle a directory-shaped entry,
374
+ with `--file` on `skill edit` targeting one accompanying file. See the
375
+ change's design.md for the full decision log (file-or-directory-never-both,
376
+ UTF-8-text-only accompanying files, no binary support).
377
+
378
+ Nothing open in this area.
378
379
 
379
380
  ### Third-party integrations
380
381
 
@@ -61,6 +61,8 @@ The frontmatter is regenerated from the library on every run, even when only the
61
61
  Deselecting a skill deletes its file outright, and its now-empty `.claude/skills/<id>/` directory with it, rather than leaving an empty block the way AGENTS.md would: an empty `SKILL.md` means nothing to a skill-loading agent.
62
62
  An empty or unconfigured skill library never blocks `init`, and the reverse holds too -- an empty snippet library does not stop skills from composing.
63
63
 
64
+ The skill library is managed the same way the snippet library is, not only by hand-editing files: `skill list`, `skill edit`, `skill remove`, and `skill adopt` mirror `snippet list`/`snippet edit`/`snippet remove`/`snippet adopt` command-for-command, including `skill adopt` pulling the skills embedded in a cloned project's manifest into your local skill library, with the same explicit, never-automatic collision handling.
65
+
64
66
  ## Drift detection: normalization and hashing
65
67
 
66
68
  A managed block's hash has to mean the same thing on Windows, under WSL, and on Linux, or a committed lock file could never be trusted across a team.
@@ -421,7 +421,7 @@ An implementation does not add the manifest to any ignore file, and does not tre
421
421
 
422
422
  | Field | Meaning |
423
423
  | ----- | ------- |
424
- | schema version | the format this file was written in (2 as of skill composition) |
424
+ | schema version | the format this file was written in (3 as of the skill script bundle) |
425
425
  | generated by | the version string of the mdcompose that wrote it |
426
426
  | generated at | when it was written |
427
427
  | detected stack | the signal files found when it was composed |
@@ -429,12 +429,16 @@ An implementation does not add the manifest to any ignore file, and does not tre
429
429
  | skills | the composed skills, in order, with content embedded (see 22) |
430
430
  | files | one entry per managed file, keyed by which file it is |
431
431
 
432
- `files` is keyed `agents_md` and `claude_md` for the composed pair, and by a skill's own id for each composed skill file (see 22.2).
433
- Each file entry carries a project-relative path, a mode of `import` or `copy` (a skill entry always carries `copy`, since a skill is always materialized), the recorded hash of that file's managed block, and which block id that hash refers to.
432
+ `files` is keyed `agents_md` and `claude_md` for the composed pair, by a skill's own id for that skill's composed `SKILL.md`, and by `<skill id>:<relative path>` for each of a directory-shaped skill's composed accompanying files (see 22.2, 23.4).
433
+ Each file entry carries a project-relative path, a mode of `import` or `copy` (a skill entry always carries `copy`, since a skill is always materialized), and a recorded hash.
434
+ For an `agents_md`, `claude_md`, or `SKILL.md` entry that hash covers the entry's managed block, alongside which block id it refers to; for an accompanying-file entry it covers the whole file instead, and the entry carries no block id at all (see 10).
434
435
  An import-mode entry also records what it imports.
435
436
 
437
+ Each skill entry may carry a `files` field: a mapping from an accompanying file's path, relative to the skill's own directory, to its embedded content. It is present only for a directory-shaped skill and is empty or absent for the common, file-shaped one, so a schema-2 manifest (which never wrote it) still reads unchanged.
438
+
436
439
  A schema-1 manifest carries no `skills` list and no per-entry block id.
437
440
  It still reads: no skills are recorded, and `agents_md`/`claude_md` fall back to their well-known block ids.
441
+ A schema-2 manifest carries no `files` field on a skill entry; it still reads, with every skill resolving as file-shaped.
438
442
 
439
443
  Unrecognized fields are preserved on read.
440
444
  A schema version higher than the implementation understands is refused, naming the file and both versions, rather than read with the wrong field meanings.
@@ -474,6 +478,9 @@ Restoring a deleted file is not the same operation as reconciling an edited bloc
474
478
  Because the hash covers only the block, an edit outside the block is clean.
475
479
  And because content is normalized before hashing, a fresh clone is clean regardless of the line ending convention the checkout used.
476
480
 
481
+ A file entry with no block id at all, which is what a skill's accompanying file always records (see 9.2), is compared by a hash of its whole normalized content instead of a block's: clean or drifted only, since there is no block to remove or markers to malform.
482
+ A directory recorded where a file is expected reads as missing, the same as a genuinely absent path.
483
+
477
484
  Detection reads only.
478
485
  Neither the managed files nor the manifest is modified by determining their state.
479
486
 
@@ -915,9 +922,13 @@ The registrations themselves are kept: a later global write restores the project
915
922
 
916
923
  ## 22. The skill library
917
924
 
918
- A second library, entirely independent of the snippet library: a flat directory of markdown files, one skill per file, YAML frontmatter then body, filename minus `.md` as the id.
925
+ A second library, entirely independent of the snippet library: a flat directory of skills, one entry per skill.
919
926
  Configured and defaulted the same way as the snippet library (a `skills` sibling directory, or an explicit `skill_library_path`), and just as readable and editable without mdcompose installed.
920
927
 
928
+ A skill entry is one of two shapes. Usually it is a single markdown file, YAML frontmatter then body, filename minus `.md` as the id. It may instead be a directory, named by the id, holding `SKILL.md` (parsed the same way as the file shape) plus zero or more accompanying files (a script, a config) at other relative paths inside that directory. A directory with no `SKILL.md` is not a skill and is left alone, the same as any other unrecognized entry. Both shapes share one id namespace: a bare `<id>.md` file and an `<id>/` directory both present for the same id is refused rather than one silently winning.
929
+
930
+ An accompanying file's identity within its skill is its path relative to the skill's directory. It is read and written as UTF-8 text under the same size limit, byte order mark handling, and line-ending preservation as every other file mdcompose handles; a file past the limit, or not valid UTF-8, is refused by name. No binary asset support.
931
+
921
932
  ### 22.1 Frontmatter
922
933
 
923
934
  A skill's frontmatter supports `title`, `description`, `tags`, `stack_signals`, and `category`.
@@ -926,7 +937,7 @@ An unrecognized field is ignored; a wrong-typed known field is refused, naming t
926
937
 
927
938
  ### 22.2 mdcompose writes only skill files into the skill library
928
939
 
929
- Nothing but a skill `.md` file is written into the skill library directory: no index, no cache, no bookkeeping.
940
+ Nothing but a skill's own files is written into the skill library directory: no index, no cache, no bookkeeping. For a directory-shaped skill that means `SKILL.md` plus its accompanying files, at their recorded relative paths, inside the skill's own directory and nowhere else; a file the user placed in that directory by hand is left untouched and never composed.
930
941
  A skill id of exactly `agents_md` or `claude_md` is refused, since either would collide with the manifest's two well-known `files` keys.
931
942
 
932
943
  ### 22.3 The skill library starts empty and is independently optional
@@ -935,9 +946,15 @@ No skill is shipped, generated, or seeded.
935
946
  An empty skill library, or one that was never configured, never blocks `init`: AGENTS.md and CLAUDE.md compose exactly as they would without this feature.
936
947
  The reverse holds too: an empty snippet library does not stop skills from composing.
937
948
 
949
+ ### 22.4 The skill commands
950
+
951
+ `skill list`, `skill edit`, `skill remove`, and `skill adopt` mirror the snippet commands (section 13) exactly, over the skill library instead: same filtering, same editor and non-interactive editing path, same confirm-before-delete, same adopt-from-manifest collision handling with the same keep/overwrite choice and the same normalization-aware identical-content check.
952
+ Adoption reads the manifest's `skills` list (see 9.2, 9.3) the same way `snippet adopt` reads `snippets`, and is exactly as explicit: no other operation ever writes into the skill library, and a manifest with an empty `skills` list is reported as embedding nothing rather than treated as an error or as "no manifest."
953
+
938
954
  ## 23. Skill composition
939
955
 
940
956
  Each skill selected in a target directory is composed into its own file at `.claude/skills/<id>/SKILL.md`, never blended into a shared block the way snippet bodies are.
957
+ A directory-shaped skill additionally composes every accompanying file at the same relative path under `.claude/skills/<id>/` that it occupies in the skill library.
941
958
 
942
959
  ### 23.1 Managed block and frontmatter
943
960
 
@@ -957,18 +974,20 @@ Re-running `init` reopens the skill picker with the manifest's recorded selectio
957
974
 
958
975
  ### 23.3 Deselecting deletes
959
976
 
960
- Unlike a deselected snippet, which leaves its managed block empty, a deselected skill has its `SKILL.md` file deleted outright, and its `.claude/skills/<id>/` directory removed if that leaves it empty.
961
- A directory the user has put another file into is left in place; only the `SKILL.md` file is removed.
962
- An empty file has no meaning to a skill-loading agent, where an empty block still leaves a valid, if empty, AGENTS.md.
977
+ Unlike a deselected snippet, which leaves its managed block empty, a deselected skill has every file mdcompose composed for it deleted outright: `SKILL.md`, and, for a directory-shaped skill, every accompanying file the manifest recorded. Its `.claude/skills/<id>/` directory, and any now-empty subdirectory an accompanying file's relative path created, are removed afterward if that leaves them empty.
978
+ A directory (or subdirectory) the user has put another file into is left in place; only the files mdcompose composed are removed.
979
+ An empty `SKILL.md` has no meaning to a skill-loading agent, where an empty block still leaves a valid, if empty, AGENTS.md.
980
+
981
+ A skill can also stay selected while one of its accompanying files is dropped from the library between runs: that file alone is deleted, by the same rule, without touching `SKILL.md` or the skill's other files.
963
982
 
964
983
  ### 23.4 Manifest and drift
965
984
 
966
- A composed skill gets a `files` entry keyed by its own id (see 9.2) and an entry in the manifest's `skills` list, embedding its full content the same way a snippet's is embedded.
967
- Drift detection is the same shared operation used for every managed file; it needs nothing skill-specific, since a `files` entry already names its own block id.
985
+ A composed skill gets a `files` entry keyed by its own id for `SKILL.md` (see 9.2), one further `files` entry per accompanying file keyed `<skill id>:<relative path>`, and an entry in the manifest's `skills` list embedding `SKILL.md`'s content the same way a snippet's is embedded, plus its accompanying files' content under the entry's own `files` field.
986
+ `SKILL.md`'s drift is the same shared managed-block operation used for every managed file; an accompanying file's drift is the same operation's whole-file-hash path (see 10), since its `files` entry carries no block id.
968
987
 
969
- Composing a skill from a manifest this machine did not write (a fresh clone) follows the same confirmation flow as AGENTS.md and CLAUDE.md: the content is shown before anything is written, and a flag supplies the confirmation non-interactively.
988
+ Composing a skill from a manifest this machine did not write (a fresh clone) follows the same confirmation flow as AGENTS.md and CLAUDE.md: the content is shown before anything is written, and a flag supplies the confirmation non-interactively. For a directory-shaped skill this covers every accompanying file as well as `SKILL.md`.
970
989
 
971
990
  ## Not yet specified
972
991
 
973
992
  Nothing.
974
- Every planned behavior for v1 is implemented and described above: `doctor`, `init` and `init --global`, `snippet list`, `snippet edit`, `snippet remove`, `snippet adopt`, `import`, `convert`, `config show`, `config set`, `config unset`, `config edit`, `eject` and `eject --global`, `target add`, `target list`, `target remove`, and skill composition as part of `init` (sections 22-23).
993
+ Every planned behavior for v1 is implemented and described above: `doctor`, `init` and `init --global`, `snippet list`, `snippet edit`, `snippet remove`, `snippet adopt`, `skill list`, `skill edit`, `skill remove`, `skill adopt`, `import`, `convert`, `config show`, `config set`, `config unset`, `config edit`, `eject` and `eject --global`, `target add`, `target list`, `target remove`, and skill composition as part of `init` (sections 22-23).
@@ -24,6 +24,7 @@ from mdcompose.commands import (
24
24
  eject_cmd,
25
25
  import_cmd,
26
26
  init_cmd,
27
+ skill_cmd,
27
28
  snippet_cmd,
28
29
  target_cmd,
29
30
  )
@@ -84,6 +85,7 @@ def main(
84
85
 
85
86
 
86
87
  app.add_typer(snippet_cmd.app, name="snippet")
88
+ app.add_typer(skill_cmd.app, name="skill")
87
89
  app.add_typer(config_cmd.app, name="config")
88
90
  app.add_typer(target_cmd.app, name="target")
89
91
  init_cmd.register(app)
@@ -482,14 +482,24 @@ def _show_drift(output: OutputContext, target: init_ops.FileTarget | None) -> No
482
482
  """Show what is there against what would replace it."""
483
483
  if target is None:
484
484
  return
485
- result = managed_block.read_blocks(target.path)
486
- block = result.find(target.block_id)
487
485
  output.info(" currently:")
488
- output.content_indented("" if block is None else block.content, empty=" (empty)")
486
+ output.content_indented(_current_content(target), empty=" (empty)")
489
487
  output.info(" would become:")
490
488
  output.content_indented(target.content, empty=" (empty)")
491
489
 
492
490
 
491
+ def _current_content(target: init_ops.FileTarget) -> str:
492
+ """What is on disk now, at the granularity drift for this target compares.
493
+
494
+ An accompanying file has no managed block: the whole file is what is
495
+ being compared, so the whole file is what is shown.
496
+ """
497
+ if target.block_id is None:
498
+ return files.read_text(target.path)
499
+ block = managed_block.read_blocks(target.path).find(target.block_id)
500
+ return "" if block is None else block.content
501
+
502
+
493
503
  def _validated_drift_choice(candidate: str) -> init_ops.DriftChoice:
494
504
  if candidate not in init_ops.DRIFT_CHOICES:
495
505
  raise AttentionError(