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.
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.pre-commit-config.yaml +3 -3
- {mdcompose-0.2.0 → mdcompose-0.3.0}/CHANGELOG.md +12 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/PKG-INFO +7 -4
- {mdcompose-0.2.0 → mdcompose-0.3.0}/README.md +6 -3
- {mdcompose-0.2.0 → mdcompose-0.3.0}/TODO.md +23 -22
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/concepts.md +2 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/core-contract.md +31 -12
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/cli.py +2 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/init_cmd.py +13 -3
- mdcompose-0.3.0/mdcompose/commands/skill_cmd.py +346 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/init_ops.py +109 -34
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/manifest.py +51 -11
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/skill_composition.py +18 -0
- mdcompose-0.3.0/mdcompose/core/skill_library_ops.py +253 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/skills.py +110 -19
- {mdcompose-0.2.0 → mdcompose-0.3.0}/pyproject.toml +1 -1
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_init.py +66 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_init_ops.py +285 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_manifest.py +140 -0
- mdcompose-0.3.0/tests/test_skill_commands.py +645 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_skill_composition.py +14 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_skills.py +114 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/uv.lock +1 -1
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.gitattributes +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/FUNDING.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/bump.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/ci.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/codeql.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/dependency-review.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/mutation.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/python-eol.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/release.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/scorecard.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/workflows/supply-chain.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.github/zizmor.yml +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/.gitignore +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/CONTRIBUTING.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/LICENSE +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/SECURITY.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/benchmarks.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/commit-and-release-tooling.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/hardening-review.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/optional-tooling.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/publishing.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/scorecard.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/docs/versioning-explained.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/__init__.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/__init__.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/config_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/convert_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/eject_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/import_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/snippet_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/commands/target_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/__init__.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/composition.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/config.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/convert_ops.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/eject.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/exit_codes.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/files.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/import_ops.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/library_ops.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/managed_block.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/output.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/platform.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/report.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/sections.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/snippets.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/core/targets.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/prompts.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/mdcompose/version.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/renovate.json +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/scripts/check_ascii.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/scripts/check_licenses.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/benchmarks/conftest.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/benchmarks/test_core_ops.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/conftest.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/mdcompose.lock.schema1.json +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.bom.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.crlf.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/plain.lf.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/fixtures/prose.cp1252.md +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_cli.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_config.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_config_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_convert.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_doctor_drift.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_e2e_import_convert.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_eject.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_files.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_fuzz.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_import_cmd.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_import_ops.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_layering.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_managed_block.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_onedrive.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_output.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_platform.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_prompts.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_report.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_sections.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_snippet_commands.py +0 -0
- {mdcompose-0.2.0 → mdcompose-0.3.0}/tests/test_snippets.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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 (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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 (
|
|
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,
|
|
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),
|
|
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
|
|
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
|
|
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
|
|
961
|
-
A directory the user has put another file into is left in place; only the
|
|
962
|
-
An empty
|
|
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
|
|
967
|
-
|
|
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(
|
|
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(
|