agent-skilltree 0.2.2__tar.gz → 0.4.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.
- {agent_skilltree-0.2.2/src/agent_skilltree.egg-info → agent_skilltree-0.4.0}/PKG-INFO +31 -2
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/README.md +30 -1
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/pyproject.toml +1 -1
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0/src/agent_skilltree.egg-info}/PKG-INFO +31 -2
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/agent_skilltree.egg-info/SOURCES.txt +4 -1
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/__init__.py +15 -4
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/cli.py +69 -6
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/cohere.py +57 -10
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/exchange.py +13 -7
- agent_skilltree-0.4.0/src/skilltree/forest.py +178 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/materialize.py +35 -9
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/model.py +76 -2
- agent_skilltree-0.4.0/src/skilltree/tome.py +86 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/validate.py +41 -3
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_cohere.py +1 -1
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_exchange.py +4 -2
- agent_skilltree-0.4.0/tests/test_portable.py +109 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_skilltree.py +4 -2
- agent_skilltree-0.4.0/tests/test_tome.py +185 -0
- agent_skilltree-0.2.2/src/skilltree/forest.py +0 -101
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/LICENSE +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/setup.cfg +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/agent_skilltree.egg-info/dependency_links.txt +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/agent_skilltree.egg-info/entry_points.txt +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/agent_skilltree.egg-info/requires.txt +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/agent_skilltree.egg-info/top_level.txt +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/federation.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/mapper.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/marketplace.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/registry.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/reports.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/src/skilltree/search.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_coords.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_emit_roundtrip.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_federation.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_folder_search.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_forest.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_glyph_search.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_mapper.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_marketplace.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_notifications.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_registry.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_reports.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_search.py +0 -0
- {agent_skilltree-0.2.2 → agent_skilltree-0.4.0}/tests/test_summaries.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-skilltree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A coordinate-addressed tree of agent skill dirs wired by Read-tool breadcrumbs, with programmatic validation.
|
|
5
5
|
Author: Isaac Wostrel-Rubin
|
|
6
6
|
License: MIT
|
|
@@ -82,8 +82,11 @@ One CLI for the whole skill-substrate toolkit — `skilltree <command> …`:
|
|
|
82
82
|
| `tree <root>` | show the coordinate tree on disk (every node by its `<coord>-<name>` address) |
|
|
83
83
|
| `map <folder> [--write]` | render a flat folder into **one** coordinate-addressed `CLAUDE.md` (Folder Map + addressable Index + branch summaries) — progressive disclosure over a flat pile |
|
|
84
84
|
| `search <folder> <query> [--scope <coord>]` | FTS5/BM25 over **any** folder; `--scope` restricts to a coordinate subtree when the folder carries skilltree coordinates (a plain folder = coordinate-free search) |
|
|
85
|
+
| `fold <framework_dir> --into <node> --tree <root>` | **bind** a framework (its skill dir) into a volume node's `## Frameworks` tome table — idempotent, one row per framework; the manifest is the source of truth and the node's SKILL.md is regenerated from it |
|
|
86
|
+
| `project <root> <target> [--policy flat\|progressive] [--link absolute\|relative] [--node N]` | **deploy** a tree into a skills dir as symlinks: `progressive` surfaces root + first-layer branches (the former `link_tree`); `flat` projects a volume's tome rows + holder so every held framework auto-discovers top-level (the former nomicon-atomize). Projecting is an install, so it localizes the tree's breadcrumbs first |
|
|
87
|
+
| `relink <root> [--localize]` | rewrite only the breadcrumb paths: **portable** (the default and the shipped form — relative to each node's folder, no machine's path in any file) or `--localize` (absolute for this machine — the install-time step) |
|
|
85
88
|
| `cohere <root>` | report drift between the on-disk tree and its coherent shape |
|
|
86
|
-
| `emit <root> [--root-forest]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled) |
|
|
89
|
+
| `emit <root> [--root-forest] [--localize]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled); breadcrumbs portable unless `--localize` |
|
|
87
90
|
| `unemit <root>` | reverse the last `emit`, byte-for-byte |
|
|
88
91
|
| `discover` · `validate` · `build` · `notify` · `watch` | read a tree · gate breadcrumb-resolvability · materialize from a manifest · write the notification rule · run the decoherence cron |
|
|
89
92
|
|
|
@@ -128,6 +131,32 @@ Two papers document the project:
|
|
|
128
131
|
|
|
129
132
|
## Changelog
|
|
130
133
|
|
|
134
|
+
### v0.4.0 — 2026-10-23
|
|
135
|
+
- **Trees ship portable.** Every breadcrumb names its child relative to the node's folder (the folder holding the SKILL.md's `.claude/`), and the manifest stores `skill_src` relative to itself, so a materialized tree carries no machine's path and validates wherever it is copied — with no step. `materialize`, `emit` and `exchange build` take `localize=True` (`--localize`) to write absolute paths for one machine; the new `relink` rewrites only the paths, either way; `project` localizes the tree it projects (a projected tree is read through a symlink, where only an absolute path resolves). `validate` accepts both forms. 80 tests (7 new, in `tests/test_portable.py`).
|
|
136
|
+
- **Developed in one source home and published here.** This repository and PyPI `agent-skilltree` are the published output of SkillTree's source home; changes arrive as releases, each with a version and an entry here.
|
|
137
|
+
- Frontmatter quotes YAML-unsafe scalars (#1), merged after 0.3.0 and not yet released.
|
|
138
|
+
- The figure script writes beside itself instead of to a fixed folder; a test comment names the author by role.
|
|
139
|
+
|
|
140
|
+
### v0.3.0 — 2026-07-09
|
|
141
|
+
- **The tome bind ops (SkillVolume/SkillTome).** A node can now HOLD frameworks: `TreeNode.frameworks`
|
|
142
|
+
rows live in the manifest and render as a generated `## Frameworks` pointer-table (`| Framework |
|
|
143
|
+
What it's for | Framework skill |`) on the node's SKILL.md — regenerated by both `materialize` and
|
|
144
|
+
the in-place re-cohere (`emit`), so a folded table can never be wiped or drift. New ops:
|
|
145
|
+
**`fold(framework_dir, into=..., tree_root=...)`** (bind a framework into a volume — idempotent, one
|
|
146
|
+
row per framework, surgical single-node rewrite) and **`project(tree, target, policy={progressive,flat},
|
|
147
|
+
link={absolute,relative}, node=...)`** — ONE deployment op unifying the top-level symlink surfacing
|
|
148
|
+
(`link_tree` is now a thin wrapper) with the flat auto-discoverable projection of a volume's held
|
|
149
|
+
frameworks (tome rows + holder, self-clobber-guarded, relative-link-capable). `validate` grew the
|
|
150
|
+
**tome-table round-trip**: manifest rows must appear in the table, every row's target must resolve,
|
|
151
|
+
undeclared table rows warn. CLI: `skilltree fold [--relative]`, `skilltree project`. **Repo-portable
|
|
152
|
+
tomes:** `fold(relative=True)` stores rows relative to the holder dir, so a committed tome validates
|
|
153
|
+
and projects from ANY checkout — the CI flow is `emit` (localize the breadcrumbs) then `validate`.
|
|
154
|
+
Fixed `--version` (the click probe needed the `agent-skilltree` distribution name). Suite →
|
|
155
|
+
**70 passing** (+11 tome tests).
|
|
156
|
+
- Vocabulary: a branch holding frameworks is a **SkillVolume**; the overall corpus is a **SkillTome**
|
|
157
|
+
("nomicon" is the legacy alias). The table heading stays `## Frameworks` — existing table parsers
|
|
158
|
+
keep working (verified against the reference atomizer end-to-end).
|
|
159
|
+
|
|
131
160
|
### v0.2.0 — 2026-06-18
|
|
132
161
|
- **Folded the `skillmap` / `foldersearch` / `skillsearch` seedlings into skilltree as subcommands — one product.** `skilltree map <folder> [--write]` renders a flat folder into one coordinate-addressed `CLAUDE.md`, using skilltree's **own** coordinate code (`assign_coords`/`skill_name` from `model.py`; the vendored copy is gone). `skilltree search <folder> <query> [--scope <coord>]` is **one** FTS5/BM25 engine over any folder — coordinate-free on a plain folder, coordinate-subtree-scoped when the folder carries skilltree coordinates (`foldersearch` + `skillsearch` unified; the `--scope` flag is the only difference). Added the `tree` view command. The `<coord>-<name>` frontmatter and the Read-breadcrumb format are preserved (backward compatible). Suite grew 56 → **62 tests**.
|
|
133
162
|
|
|
@@ -68,8 +68,11 @@ One CLI for the whole skill-substrate toolkit — `skilltree <command> …`:
|
|
|
68
68
|
| `tree <root>` | show the coordinate tree on disk (every node by its `<coord>-<name>` address) |
|
|
69
69
|
| `map <folder> [--write]` | render a flat folder into **one** coordinate-addressed `CLAUDE.md` (Folder Map + addressable Index + branch summaries) — progressive disclosure over a flat pile |
|
|
70
70
|
| `search <folder> <query> [--scope <coord>]` | FTS5/BM25 over **any** folder; `--scope` restricts to a coordinate subtree when the folder carries skilltree coordinates (a plain folder = coordinate-free search) |
|
|
71
|
+
| `fold <framework_dir> --into <node> --tree <root>` | **bind** a framework (its skill dir) into a volume node's `## Frameworks` tome table — idempotent, one row per framework; the manifest is the source of truth and the node's SKILL.md is regenerated from it |
|
|
72
|
+
| `project <root> <target> [--policy flat\|progressive] [--link absolute\|relative] [--node N]` | **deploy** a tree into a skills dir as symlinks: `progressive` surfaces root + first-layer branches (the former `link_tree`); `flat` projects a volume's tome rows + holder so every held framework auto-discovers top-level (the former nomicon-atomize). Projecting is an install, so it localizes the tree's breadcrumbs first |
|
|
73
|
+
| `relink <root> [--localize]` | rewrite only the breadcrumb paths: **portable** (the default and the shipped form — relative to each node's folder, no machine's path in any file) or `--localize` (absolute for this machine — the install-time step) |
|
|
71
74
|
| `cohere <root>` | report drift between the on-disk tree and its coherent shape |
|
|
72
|
-
| `emit <root> [--root-forest]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled) |
|
|
75
|
+
| `emit <root> [--root-forest] [--localize]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled); breadcrumbs portable unless `--localize` |
|
|
73
76
|
| `unemit <root>` | reverse the last `emit`, byte-for-byte |
|
|
74
77
|
| `discover` · `validate` · `build` · `notify` · `watch` | read a tree · gate breadcrumb-resolvability · materialize from a manifest · write the notification rule · run the decoherence cron |
|
|
75
78
|
|
|
@@ -114,6 +117,32 @@ Two papers document the project:
|
|
|
114
117
|
|
|
115
118
|
## Changelog
|
|
116
119
|
|
|
120
|
+
### v0.4.0 — 2026-10-23
|
|
121
|
+
- **Trees ship portable.** Every breadcrumb names its child relative to the node's folder (the folder holding the SKILL.md's `.claude/`), and the manifest stores `skill_src` relative to itself, so a materialized tree carries no machine's path and validates wherever it is copied — with no step. `materialize`, `emit` and `exchange build` take `localize=True` (`--localize`) to write absolute paths for one machine; the new `relink` rewrites only the paths, either way; `project` localizes the tree it projects (a projected tree is read through a symlink, where only an absolute path resolves). `validate` accepts both forms. 80 tests (7 new, in `tests/test_portable.py`).
|
|
122
|
+
- **Developed in one source home and published here.** This repository and PyPI `agent-skilltree` are the published output of SkillTree's source home; changes arrive as releases, each with a version and an entry here.
|
|
123
|
+
- Frontmatter quotes YAML-unsafe scalars (#1), merged after 0.3.0 and not yet released.
|
|
124
|
+
- The figure script writes beside itself instead of to a fixed folder; a test comment names the author by role.
|
|
125
|
+
|
|
126
|
+
### v0.3.0 — 2026-07-09
|
|
127
|
+
- **The tome bind ops (SkillVolume/SkillTome).** A node can now HOLD frameworks: `TreeNode.frameworks`
|
|
128
|
+
rows live in the manifest and render as a generated `## Frameworks` pointer-table (`| Framework |
|
|
129
|
+
What it's for | Framework skill |`) on the node's SKILL.md — regenerated by both `materialize` and
|
|
130
|
+
the in-place re-cohere (`emit`), so a folded table can never be wiped or drift. New ops:
|
|
131
|
+
**`fold(framework_dir, into=..., tree_root=...)`** (bind a framework into a volume — idempotent, one
|
|
132
|
+
row per framework, surgical single-node rewrite) and **`project(tree, target, policy={progressive,flat},
|
|
133
|
+
link={absolute,relative}, node=...)`** — ONE deployment op unifying the top-level symlink surfacing
|
|
134
|
+
(`link_tree` is now a thin wrapper) with the flat auto-discoverable projection of a volume's held
|
|
135
|
+
frameworks (tome rows + holder, self-clobber-guarded, relative-link-capable). `validate` grew the
|
|
136
|
+
**tome-table round-trip**: manifest rows must appear in the table, every row's target must resolve,
|
|
137
|
+
undeclared table rows warn. CLI: `skilltree fold [--relative]`, `skilltree project`. **Repo-portable
|
|
138
|
+
tomes:** `fold(relative=True)` stores rows relative to the holder dir, so a committed tome validates
|
|
139
|
+
and projects from ANY checkout — the CI flow is `emit` (localize the breadcrumbs) then `validate`.
|
|
140
|
+
Fixed `--version` (the click probe needed the `agent-skilltree` distribution name). Suite →
|
|
141
|
+
**70 passing** (+11 tome tests).
|
|
142
|
+
- Vocabulary: a branch holding frameworks is a **SkillVolume**; the overall corpus is a **SkillTome**
|
|
143
|
+
("nomicon" is the legacy alias). The table heading stays `## Frameworks` — existing table parsers
|
|
144
|
+
keep working (verified against the reference atomizer end-to-end).
|
|
145
|
+
|
|
117
146
|
### v0.2.0 — 2026-06-18
|
|
118
147
|
- **Folded the `skillmap` / `foldersearch` / `skillsearch` seedlings into skilltree as subcommands — one product.** `skilltree map <folder> [--write]` renders a flat folder into one coordinate-addressed `CLAUDE.md`, using skilltree's **own** coordinate code (`assign_coords`/`skill_name` from `model.py`; the vendored copy is gone). `skilltree search <folder> <query> [--scope <coord>]` is **one** FTS5/BM25 engine over any folder — coordinate-free on a plain folder, coordinate-subtree-scoped when the folder carries skilltree coordinates (`foldersearch` + `skillsearch` unified; the `--scope` flag is the only difference). Added the `tree` view command. The `<coord>-<name>` frontmatter and the Read-breadcrumb format are preserved (backward compatible). Suite grew 56 → **62 tests**.
|
|
119
148
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-skilltree"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "A coordinate-addressed tree of agent skill dirs wired by Read-tool breadcrumbs, with programmatic validation."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-skilltree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A coordinate-addressed tree of agent skill dirs wired by Read-tool breadcrumbs, with programmatic validation.
|
|
5
5
|
Author: Isaac Wostrel-Rubin
|
|
6
6
|
License: MIT
|
|
@@ -82,8 +82,11 @@ One CLI for the whole skill-substrate toolkit — `skilltree <command> …`:
|
|
|
82
82
|
| `tree <root>` | show the coordinate tree on disk (every node by its `<coord>-<name>` address) |
|
|
83
83
|
| `map <folder> [--write]` | render a flat folder into **one** coordinate-addressed `CLAUDE.md` (Folder Map + addressable Index + branch summaries) — progressive disclosure over a flat pile |
|
|
84
84
|
| `search <folder> <query> [--scope <coord>]` | FTS5/BM25 over **any** folder; `--scope` restricts to a coordinate subtree when the folder carries skilltree coordinates (a plain folder = coordinate-free search) |
|
|
85
|
+
| `fold <framework_dir> --into <node> --tree <root>` | **bind** a framework (its skill dir) into a volume node's `## Frameworks` tome table — idempotent, one row per framework; the manifest is the source of truth and the node's SKILL.md is regenerated from it |
|
|
86
|
+
| `project <root> <target> [--policy flat\|progressive] [--link absolute\|relative] [--node N]` | **deploy** a tree into a skills dir as symlinks: `progressive` surfaces root + first-layer branches (the former `link_tree`); `flat` projects a volume's tome rows + holder so every held framework auto-discovers top-level (the former nomicon-atomize). Projecting is an install, so it localizes the tree's breadcrumbs first |
|
|
87
|
+
| `relink <root> [--localize]` | rewrite only the breadcrumb paths: **portable** (the default and the shipped form — relative to each node's folder, no machine's path in any file) or `--localize` (absolute for this machine — the install-time step) |
|
|
85
88
|
| `cohere <root>` | report drift between the on-disk tree and its coherent shape |
|
|
86
|
-
| `emit <root> [--root-forest]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled) |
|
|
89
|
+
| `emit <root> [--root-forest] [--localize]` | re-cohere in place; `--root-forest` tree-ifies a bare forest (lossless, journaled); breadcrumbs portable unless `--localize` |
|
|
87
90
|
| `unemit <root>` | reverse the last `emit`, byte-for-byte |
|
|
88
91
|
| `discover` · `validate` · `build` · `notify` · `watch` | read a tree · gate breadcrumb-resolvability · materialize from a manifest · write the notification rule · run the decoherence cron |
|
|
89
92
|
|
|
@@ -128,6 +131,32 @@ Two papers document the project:
|
|
|
128
131
|
|
|
129
132
|
## Changelog
|
|
130
133
|
|
|
134
|
+
### v0.4.0 — 2026-10-23
|
|
135
|
+
- **Trees ship portable.** Every breadcrumb names its child relative to the node's folder (the folder holding the SKILL.md's `.claude/`), and the manifest stores `skill_src` relative to itself, so a materialized tree carries no machine's path and validates wherever it is copied — with no step. `materialize`, `emit` and `exchange build` take `localize=True` (`--localize`) to write absolute paths for one machine; the new `relink` rewrites only the paths, either way; `project` localizes the tree it projects (a projected tree is read through a symlink, where only an absolute path resolves). `validate` accepts both forms. 80 tests (7 new, in `tests/test_portable.py`).
|
|
136
|
+
- **Developed in one source home and published here.** This repository and PyPI `agent-skilltree` are the published output of SkillTree's source home; changes arrive as releases, each with a version and an entry here.
|
|
137
|
+
- Frontmatter quotes YAML-unsafe scalars (#1), merged after 0.3.0 and not yet released.
|
|
138
|
+
- The figure script writes beside itself instead of to a fixed folder; a test comment names the author by role.
|
|
139
|
+
|
|
140
|
+
### v0.3.0 — 2026-07-09
|
|
141
|
+
- **The tome bind ops (SkillVolume/SkillTome).** A node can now HOLD frameworks: `TreeNode.frameworks`
|
|
142
|
+
rows live in the manifest and render as a generated `## Frameworks` pointer-table (`| Framework |
|
|
143
|
+
What it's for | Framework skill |`) on the node's SKILL.md — regenerated by both `materialize` and
|
|
144
|
+
the in-place re-cohere (`emit`), so a folded table can never be wiped or drift. New ops:
|
|
145
|
+
**`fold(framework_dir, into=..., tree_root=...)`** (bind a framework into a volume — idempotent, one
|
|
146
|
+
row per framework, surgical single-node rewrite) and **`project(tree, target, policy={progressive,flat},
|
|
147
|
+
link={absolute,relative}, node=...)`** — ONE deployment op unifying the top-level symlink surfacing
|
|
148
|
+
(`link_tree` is now a thin wrapper) with the flat auto-discoverable projection of a volume's held
|
|
149
|
+
frameworks (tome rows + holder, self-clobber-guarded, relative-link-capable). `validate` grew the
|
|
150
|
+
**tome-table round-trip**: manifest rows must appear in the table, every row's target must resolve,
|
|
151
|
+
undeclared table rows warn. CLI: `skilltree fold [--relative]`, `skilltree project`. **Repo-portable
|
|
152
|
+
tomes:** `fold(relative=True)` stores rows relative to the holder dir, so a committed tome validates
|
|
153
|
+
and projects from ANY checkout — the CI flow is `emit` (localize the breadcrumbs) then `validate`.
|
|
154
|
+
Fixed `--version` (the click probe needed the `agent-skilltree` distribution name). Suite →
|
|
155
|
+
**70 passing** (+11 tome tests).
|
|
156
|
+
- Vocabulary: a branch holding frameworks is a **SkillVolume**; the overall corpus is a **SkillTome**
|
|
157
|
+
("nomicon" is the legacy alias). The table heading stays `## Frameworks` — existing table parsers
|
|
158
|
+
keep working (verified against the reference atomizer end-to-end).
|
|
159
|
+
|
|
131
160
|
### v0.2.0 — 2026-06-18
|
|
132
161
|
- **Folded the `skillmap` / `foldersearch` / `skillsearch` seedlings into skilltree as subcommands — one product.** `skilltree map <folder> [--write]` renders a flat folder into one coordinate-addressed `CLAUDE.md`, using skilltree's **own** coordinate code (`assign_coords`/`skill_name` from `model.py`; the vendored copy is gone). `skilltree search <folder> <query> [--scope <coord>]` is **one** FTS5/BM25 engine over any folder — coordinate-free on a plain folder, coordinate-subtree-scoped when the folder carries skilltree coordinates (`foldersearch` + `skillsearch` unified; the `--scope` flag is the only difference). Added the `tree` view command. The `<coord>-<name>` frontmatter and the Read-breadcrumb format are preserved (backward compatible). Suite grew 56 → **62 tests**.
|
|
133
162
|
|
|
@@ -20,6 +20,7 @@ src/skilltree/model.py
|
|
|
20
20
|
src/skilltree/registry.py
|
|
21
21
|
src/skilltree/reports.py
|
|
22
22
|
src/skilltree/search.py
|
|
23
|
+
src/skilltree/tome.py
|
|
23
24
|
src/skilltree/validate.py
|
|
24
25
|
tests/test_cohere.py
|
|
25
26
|
tests/test_coords.py
|
|
@@ -32,8 +33,10 @@ tests/test_glyph_search.py
|
|
|
32
33
|
tests/test_mapper.py
|
|
33
34
|
tests/test_marketplace.py
|
|
34
35
|
tests/test_notifications.py
|
|
36
|
+
tests/test_portable.py
|
|
35
37
|
tests/test_registry.py
|
|
36
38
|
tests/test_reports.py
|
|
37
39
|
tests/test_search.py
|
|
38
40
|
tests/test_skilltree.py
|
|
39
|
-
tests/test_summaries.py
|
|
41
|
+
tests/test_summaries.py
|
|
42
|
+
tests/test_tome.py
|
|
@@ -11,14 +11,23 @@ from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
|
11
11
|
try:
|
|
12
12
|
__version__ = _pkg_version("agent-skilltree")
|
|
13
13
|
except PackageNotFoundError: # running from a source checkout that isn't pip-installed
|
|
14
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.3.0"
|
|
15
15
|
|
|
16
16
|
from .exchange import Exchange, Member, load_exchange
|
|
17
17
|
from .exchange import build as build_exchange
|
|
18
18
|
from .exchange import is_valid as exchange_is_valid
|
|
19
19
|
from .exchange import validate as validate_exchange
|
|
20
20
|
from .materialize import materialize
|
|
21
|
-
from .model import
|
|
21
|
+
from .model import (
|
|
22
|
+
TOME_HEADING,
|
|
23
|
+
SkillTree,
|
|
24
|
+
TreeNode,
|
|
25
|
+
assign_coords,
|
|
26
|
+
compose_summary,
|
|
27
|
+
find_node,
|
|
28
|
+
skill_name,
|
|
29
|
+
tome_table,
|
|
30
|
+
)
|
|
22
31
|
from .registry import (
|
|
23
32
|
Entry,
|
|
24
33
|
load_registry,
|
|
@@ -27,7 +36,8 @@ from .registry import (
|
|
|
27
36
|
validate_registry,
|
|
28
37
|
)
|
|
29
38
|
from .registry import search as registry_search
|
|
30
|
-
from .forest import build_forest, link_tree, list_links, unlink
|
|
39
|
+
from .forest import build_forest, link_tree, list_links, project, unlink
|
|
40
|
+
from .tome import fold
|
|
31
41
|
from .reports import (
|
|
32
42
|
list_reports,
|
|
33
43
|
mark_problem,
|
|
@@ -58,7 +68,8 @@ __all__ = [
|
|
|
58
68
|
"promote", "registry_search",
|
|
59
69
|
"walk_federation", "flatten_federation", "validate_federation",
|
|
60
70
|
"register_child", "local_resolver",
|
|
61
|
-
"link_tree", "build_forest", "list_links", "unlink",
|
|
71
|
+
"link_tree", "build_forest", "list_links", "unlink", "project",
|
|
72
|
+
"fold", "tome_table", "find_node", "TOME_HEADING",
|
|
62
73
|
"assign_coords", "skill_name",
|
|
63
74
|
"build_index", "search", "search_tree", "search_folder", "DEFAULT_EXTS",
|
|
64
75
|
"build_map", "write_map",
|
|
@@ -12,7 +12,7 @@ from .validate import validate
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@click.group()
|
|
15
|
-
@click.version_option(message="skilltree %(version)s")
|
|
15
|
+
@click.version_option(package_name="agent-skilltree", message="skilltree %(version)s")
|
|
16
16
|
def main() -> None:
|
|
17
17
|
"""SkillTree — a nested tree of skill dirs wired by `cat`-breadcrumbs (validated)."""
|
|
18
18
|
|
|
@@ -55,11 +55,14 @@ def cohere_cmd(root: Path) -> None:
|
|
|
55
55
|
@click.argument("root", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
56
56
|
@click.option("--root-forest", is_flag=True, help="tree-ify a bare forest (synthesize + save a root)")
|
|
57
57
|
@click.option("--name", "forest_name", default=None, help="name for the synthesized root")
|
|
58
|
-
|
|
58
|
+
@click.option("--localize", is_flag=True,
|
|
59
|
+
help="write breadcrumbs absolute for THIS machine (install time); default is portable")
|
|
60
|
+
def emit_cmd(root: Path, root_forest: bool, forest_name: str | None, localize: bool) -> None:
|
|
59
61
|
"""Re-cohere IN PLACE: reassign coords + rewrite breadcrumbs/index from the
|
|
60
|
-
canonical shape (non-destructive). With --root-forest, root a bare forest.
|
|
62
|
+
canonical shape (non-destructive). With --root-forest, root a bare forest.
|
|
63
|
+
Breadcrumbs are portable (relative to each node's folder) unless --localize."""
|
|
61
64
|
from .cohere import emit
|
|
62
|
-
report = emit(root, root_forest=root_forest, forest_name=forest_name)
|
|
65
|
+
report = emit(root, root_forest=root_forest, forest_name=forest_name, localize=localize)
|
|
63
66
|
if not report.get("ok"):
|
|
64
67
|
click.echo(f"✗ {report.get('error')}")
|
|
65
68
|
raise SystemExit(1)
|
|
@@ -70,6 +73,21 @@ def emit_cmd(root: Path, root_forest: bool, forest_name: str | None) -> None:
|
|
|
70
73
|
click.echo(f" journal: {report['journal']} (reverse with `skilltree unemit`)")
|
|
71
74
|
|
|
72
75
|
|
|
76
|
+
@main.command(name="relink")
|
|
77
|
+
@click.argument("root", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
78
|
+
@click.option("--localize", is_flag=True, help="absolute paths for THIS machine (install time)")
|
|
79
|
+
def relink_cmd(root: Path, localize: bool) -> None:
|
|
80
|
+
"""Rewrite only the breadcrumb paths, in place: portable (the shipped form, relative to
|
|
81
|
+
each node's folder) or --localize (absolute, the install-time step). Nothing else moves."""
|
|
82
|
+
from .cohere import relink
|
|
83
|
+
report = relink(root, localize=localize)
|
|
84
|
+
if not report.get("ok"):
|
|
85
|
+
click.echo(f"✗ {report.get('error')}")
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
form = "localized (absolute)" if localize else "portable (relative)"
|
|
88
|
+
click.echo(f"✓ relinked {report['nodes']} node(s): {form}")
|
|
89
|
+
|
|
90
|
+
|
|
73
91
|
@main.command(name="unemit")
|
|
74
92
|
@click.argument("root", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
75
93
|
def unemit_cmd(root: Path) -> None:
|
|
@@ -216,10 +234,12 @@ def reports_cmd(reports_path, kind):
|
|
|
216
234
|
@main.command(name="build")
|
|
217
235
|
@click.argument("manifest", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
218
236
|
@click.argument("root", type=click.Path(path_type=Path))
|
|
219
|
-
|
|
237
|
+
@click.option("--localize", is_flag=True,
|
|
238
|
+
help="write breadcrumbs absolute for THIS machine; default is portable")
|
|
239
|
+
def build_cmd(manifest: Path, root: Path, localize: bool) -> None:
|
|
220
240
|
"""Materialize a SkillTree from a JSON MANIFEST into ROOT, then validate."""
|
|
221
241
|
tree = SkillTree.load(manifest)
|
|
222
|
-
materialize(tree, root)
|
|
242
|
+
materialize(tree, root, localize=localize)
|
|
223
243
|
issues = validate(root)
|
|
224
244
|
errors = [v for v in issues if v.severity == "error"]
|
|
225
245
|
click.echo(f"built {len(tree.nodes())} nodes at {root}")
|
|
@@ -242,6 +262,49 @@ def validate_cmd(root: Path) -> None:
|
|
|
242
262
|
click.echo("✓ valid — every breadcrumb resolves.")
|
|
243
263
|
|
|
244
264
|
|
|
265
|
+
@main.command(name="fold")
|
|
266
|
+
@click.argument("framework_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
267
|
+
@click.option("--into", required=True, help="the volume node to bind into (name or coord)")
|
|
268
|
+
@click.option("--tree", "tree_root", required=True,
|
|
269
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
270
|
+
help="the materialized tree root (holds skilltree.json)")
|
|
271
|
+
@click.option("--what-for", default=None, help="the row's purpose text (default: the framework's description)")
|
|
272
|
+
@click.option("--name", default=None, help="the row name (default: the framework's frontmatter name)")
|
|
273
|
+
@click.option("--relative", is_flag=True,
|
|
274
|
+
help="store the row path relative to the volume node (repo-portable: validates/projects from any checkout)")
|
|
275
|
+
def fold_cmd(framework_dir: Path, into: str, tree_root: Path, what_for: str | None,
|
|
276
|
+
name: str | None, relative: bool) -> None:
|
|
277
|
+
"""Fold a FRAMEWORK_DIR into a volume node's tome table (idempotent, one row
|
|
278
|
+
per framework). The manifest is updated and the node's SKILL.md regenerated."""
|
|
279
|
+
from .tome import fold
|
|
280
|
+
rep = fold(framework_dir, into=into, tree_root=tree_root, what_for=what_for, name=name,
|
|
281
|
+
relative=relative)
|
|
282
|
+
if not rep.get("ok"):
|
|
283
|
+
click.echo(f"✗ {rep.get('error')}")
|
|
284
|
+
raise SystemExit(1)
|
|
285
|
+
verb = "updated" if rep["updated"] else "added"
|
|
286
|
+
click.echo(f"✓ {verb} row {rep['row']['name']!r} on node {rep['node']!r} "
|
|
287
|
+
f"({rep['rows']} framework(s) held)")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@main.command(name="project")
|
|
291
|
+
@click.argument("tree_root", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
|
292
|
+
@click.argument("target", type=click.Path(path_type=Path))
|
|
293
|
+
@click.option("--policy", type=click.Choice(["progressive", "flat"]), default="progressive",
|
|
294
|
+
help="progressive = root + first-layer branches; flat = a volume's tome rows + holder")
|
|
295
|
+
@click.option("--link", type=click.Choice(["absolute", "relative"]), default="absolute")
|
|
296
|
+
@click.option("--node", default=None, help="(flat) the volume node to project (name or coord; default root)")
|
|
297
|
+
def project_cmd(tree_root: Path, target: Path, policy: str, link: str, node: str | None) -> None:
|
|
298
|
+
"""Project TREE_ROOT into TARGET (a skills dir) as symlinks — progressive
|
|
299
|
+
disclosure, or a volume's flat auto-discoverable deployment. Projecting is an install:
|
|
300
|
+
TREE_ROOT's breadcrumbs are localized (absolute) for this machine first."""
|
|
301
|
+
from .forest import project
|
|
302
|
+
links = project(tree_root, target, policy=policy, link=link, node=node)
|
|
303
|
+
click.echo(f"✓ projected {len(links)} link(s) ({policy}, {link}) into {target}")
|
|
304
|
+
for p in links:
|
|
305
|
+
click.echo(f" - {p.name} -> {p.readlink()}")
|
|
306
|
+
|
|
307
|
+
|
|
245
308
|
@main.group()
|
|
246
309
|
def exchange() -> None:
|
|
247
310
|
"""Many skill trees in one repo, under a master."""
|
|
@@ -24,14 +24,18 @@ import shutil
|
|
|
24
24
|
from dataclasses import dataclass, field
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
-
from .model import SkillTree, TreeNode, assign_coords, compose_summary,
|
|
27
|
+
from .model import (PORTABLE_NOTE, SkillTree, TreeNode, assign_coords, compose_summary, crumb_path,
|
|
28
|
+
skill_name, tome_table)
|
|
28
29
|
|
|
29
30
|
# dirs that are never tree nodes
|
|
30
31
|
_SKIP = {".claude", "kb", "__pycache__", ".git", "node_modules", ".aios.src"}
|
|
31
32
|
# a coord-prefixed skill identity: "0.1.2-some-name" → ("0.1.2", "some-name")
|
|
32
33
|
_COORD_RE = re.compile(r"^(\d+(?:\.\d+)*)-(.+)$")
|
|
33
|
-
# everything from the first generated section to EOF (materialize appends them last)
|
|
34
|
-
|
|
34
|
+
# everything from the first generated section to EOF (materialize appends them last).
|
|
35
|
+
# Generated sections, in emit order: Index summary → Descend (branches), then the
|
|
36
|
+
# `## Frameworks` tome table (any node with frameworks rows) — leftmost heading wins,
|
|
37
|
+
# so a leaf-with-table strips from Frameworks and a branch strips from Index summary.
|
|
38
|
+
_SECT_RE = re.compile(r"\n+## (?:Index summary|Frameworks)\b.*\Z", re.S)
|
|
35
39
|
_CRUMB_RE = re.compile(r"`([^`]+?)/SKILL\.md`") # the path-before-SKILL.md, verb-agnostic
|
|
36
40
|
_CRUMB = "- {name} ({kind}): Read `{path}`" # the verb is the Read TOOL, never `cat`
|
|
37
41
|
|
|
@@ -214,7 +218,7 @@ def cohere(root: str | Path) -> list[Finding]:
|
|
|
214
218
|
|
|
215
219
|
|
|
216
220
|
# ── emit: rewrite coords + breadcrumbs + index IN PLACE (non-destructive) ─────
|
|
217
|
-
def _rewrite_node_md(smd: Path, node: TreeNode, node_dir: Path) -> None:
|
|
221
|
+
def _rewrite_node_md(smd: Path, node: TreeNode, node_dir: Path, localize: bool = False) -> None:
|
|
218
222
|
"""Refresh ONLY the coord prefix + Index/Descend sections of an existing
|
|
219
223
|
SKILL.md, preserving the hand-written body above them. Creates the file (a
|
|
220
224
|
stub) if absent (e.g. a freshly synthesized forest root)."""
|
|
@@ -242,24 +246,64 @@ def _rewrite_node_md(smd: Path, node: TreeNode, node_dir: Path) -> None:
|
|
|
242
246
|
if node.children:
|
|
243
247
|
crumbs = [_CRUMB.format(
|
|
244
248
|
name=skill_name(c), kind=c.kind,
|
|
245
|
-
path=(node_dir / c.name / ".claude" / "skills" / skill_name(c) / "SKILL.md"
|
|
249
|
+
path=crumb_path(node_dir / c.name / ".claude" / "skills" / skill_name(c) / "SKILL.md",
|
|
250
|
+
node_dir, localize=localize))
|
|
246
251
|
for c in node.children]
|
|
247
252
|
tail = (f"\n\n## Index summary\n{compose_summary(node, full=True)}\n\n"
|
|
248
253
|
f"## Descend — the next layer ({len(node.children)})\n"
|
|
249
254
|
"Only this layer is loaded now. To descend, use the **Read tool** on a child "
|
|
250
255
|
"below — that injects the child's layer. A Bash `cat` reads the bytes but loads "
|
|
251
|
-
"nothing; you must use the Read tool
|
|
256
|
+
"nothing; you must use the Read tool."
|
|
257
|
+
+ ("" if localize else f" {PORTABLE_NOTE}") + "\n\n" + "\n".join(crumbs))
|
|
252
258
|
else:
|
|
253
259
|
tail = ""
|
|
260
|
+
if node.frameworks: # regenerate the tome table from the model rows
|
|
261
|
+
tail += f"\n\n{tome_table(node)}"
|
|
254
262
|
out = f"---\nname: {sname}\ndescription: {desc}\n---\n\n{base}{tail}\n"
|
|
255
263
|
smd.write_text(out, encoding="utf-8")
|
|
256
264
|
|
|
257
265
|
|
|
266
|
+
def relink(root: str | Path, *, localize: bool = False) -> dict:
|
|
267
|
+
"""Rewrite ONLY the breadcrumb paths of a materialized tree, in place: PORTABLE (relative
|
|
268
|
+
to each node's folder — the shipped form) or LOCALIZED (absolute for this machine — the
|
|
269
|
+
install-time step). Coords, names, bodies and tome tables are untouched."""
|
|
270
|
+
root = Path(root)
|
|
271
|
+
manifest = root / "skilltree.json"
|
|
272
|
+
if not manifest.is_file():
|
|
273
|
+
return {"ok": False, "error": f"no skilltree.json manifest at {root}"}
|
|
274
|
+
tree = SkillTree.load(manifest)
|
|
275
|
+
n = 0
|
|
276
|
+
|
|
277
|
+
def walk(node: TreeNode, node_dir: Path) -> None:
|
|
278
|
+
nonlocal n
|
|
279
|
+
smd = node_dir / ".claude" / "skills" / skill_name(node) / "SKILL.md"
|
|
280
|
+
if node.children and smd.is_file():
|
|
281
|
+
text = smd.read_text(encoding="utf-8")
|
|
282
|
+
for c in node.children:
|
|
283
|
+
child_md = node_dir / c.name / ".claude" / "skills" / skill_name(c) / "SKILL.md"
|
|
284
|
+
pat = re.compile(r"^(- " + re.escape(skill_name(c)) + r" \(" + re.escape(c.kind)
|
|
285
|
+
+ r"\): Read `)[^`]+(`)", re.M)
|
|
286
|
+
path = crumb_path(child_md, node_dir, localize=localize)
|
|
287
|
+
text = pat.sub(lambda m: m.group(1) + path + m.group(2), text)
|
|
288
|
+
text = text.replace("you must use the Read tool:", "you must use the Read tool.")
|
|
289
|
+
text = text.replace(" " + PORTABLE_NOTE, "")
|
|
290
|
+
if not localize:
|
|
291
|
+
text = text.replace("you must use the Read tool.",
|
|
292
|
+
"you must use the Read tool. " + PORTABLE_NOTE, 1)
|
|
293
|
+
smd.write_text(text, encoding="utf-8")
|
|
294
|
+
n += 1
|
|
295
|
+
for c in node.children:
|
|
296
|
+
walk(c, node_dir / c.name)
|
|
297
|
+
|
|
298
|
+
walk(tree.root, root)
|
|
299
|
+
return {"ok": True, "nodes": n, "localized": localize}
|
|
300
|
+
|
|
301
|
+
|
|
258
302
|
JOURNAL = ".emit-journal.json" # the reversibility receipt written by a tree-ifying emit
|
|
259
303
|
|
|
260
304
|
|
|
261
305
|
def emit(root: str | Path, *, root_forest: bool = False,
|
|
262
|
-
forest_name: str | None = None) -> dict:
|
|
306
|
+
forest_name: str | None = None, localize: bool = False) -> dict:
|
|
263
307
|
"""Re-cohere the tree IN PLACE: assign coords + rewrite every node's
|
|
264
308
|
breadcrumbs/index from the canonical shape, without destroying content.
|
|
265
309
|
|
|
@@ -267,7 +311,10 @@ def emit(root: str | Path, *, root_forest: bool = False,
|
|
|
267
311
|
a root, and **tree-ify it**: each flat leaf is MOVED WHOLESALE into its nested
|
|
268
312
|
node-dir (all its files preserved — *not* a body-only re-render), so it stops
|
|
269
313
|
auto-loading. Every move is JOURNALED to `.emit-journal.json` so `unemit` can
|
|
270
|
-
reverse it exactly (your cache-for-reversibility requirement).
|
|
314
|
+
reverse it exactly (your cache-for-reversibility requirement).
|
|
315
|
+
|
|
316
|
+
Breadcrumbs are written PORTABLE (relative to each node's folder) unless `localize=True`,
|
|
317
|
+
which writes them absolute for the place the tree is installed — the install-time step."""
|
|
271
318
|
root = Path(root)
|
|
272
319
|
manifest = root / "skilltree.json"
|
|
273
320
|
synthesized = False
|
|
@@ -306,10 +353,10 @@ def emit(root: str | Path, *, root_forest: bool = False,
|
|
|
306
353
|
journal.append({"op": "move", "from": str(old_src), "to": str(target_dir),
|
|
307
354
|
"skill_md": orig})
|
|
308
355
|
shutil.move(str(old_src), str(target_dir))
|
|
309
|
-
_rewrite_node_md(target_dir / "SKILL.md", node, node_dir)
|
|
356
|
+
_rewrite_node_md(target_dir / "SKILL.md", node, node_dir, localize)
|
|
310
357
|
else:
|
|
311
358
|
existed = (target_dir / "SKILL.md").exists()
|
|
312
|
-
_rewrite_node_md(target_dir / "SKILL.md", node, node_dir)
|
|
359
|
+
_rewrite_node_md(target_dir / "SKILL.md", node, node_dir, localize)
|
|
313
360
|
if synthesized and not existed: # a freshly-created node (the synth root)
|
|
314
361
|
journal.append({"op": "create", "path": str(target_dir)})
|
|
315
362
|
n += 1
|
|
@@ -63,30 +63,35 @@ def _front(name: str, description: str, body: str) -> str:
|
|
|
63
63
|
return f"---\nname: {name}\ndescription: {description}\n---\n\n{body.rstrip()}\n"
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def build(exchange: Exchange, repo_dir: str | Path) -> Path:
|
|
67
|
-
"""Materialize every member tree + the master. Returns the master SKILL.md path.
|
|
66
|
+
def build(exchange: Exchange, repo_dir: str | Path, *, localize: bool = False) -> Path:
|
|
67
|
+
"""Materialize every member tree + the master. Returns the master SKILL.md path.
|
|
68
|
+
Breadcrumbs are portable (relative to the repo) unless `localize=True`."""
|
|
68
69
|
repo = Path(repo_dir)
|
|
69
70
|
members: list[tuple[str, Path]] = [] # (member name, member root SKILL.md)
|
|
70
71
|
for m in exchange.trees:
|
|
71
72
|
tree = SkillTree.load((exchange.base / m.manifest).resolve())
|
|
72
73
|
tree_dir = repo / "trees" / m.name
|
|
73
|
-
materialize(tree, tree_dir)
|
|
74
|
+
materialize(tree, tree_dir, localize=localize)
|
|
74
75
|
members.append((m.name, node_skill_md(tree_dir, tree.root.name).resolve()))
|
|
75
76
|
|
|
76
77
|
master_md = node_skill_md(repo, exchange.master)
|
|
77
78
|
master_md.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
|
|
79
|
+
from .model import PORTABLE_NOTE, crumb_path
|
|
80
|
+
crumbs = [f"- {name} (tree): Read `{crumb_path(root_md, repo, localize=localize)}`"
|
|
81
|
+
for name, root_md in members]
|
|
79
82
|
body = (f"Master of the **{exchange.name}** exchange — {len(members)} skill tree(s).\n\n"
|
|
80
83
|
"## Trees — pick one and **Read** (the Read tool) into its root\n"
|
|
81
84
|
"Only this master is loaded. Read a tree's root below to load it (a Bash `cat` won't); "
|
|
82
|
-
"each tree then walks its own breadcrumbs
|
|
85
|
+
"each tree then walks its own breadcrumbs." + ("" if localize else f" {PORTABLE_NOTE}")
|
|
86
|
+
+ "\n\n"
|
|
83
87
|
+ "\n".join(crumbs))
|
|
84
88
|
master_md.write_text(_front(exchange.master, f"master of the {exchange.name} exchange", body),
|
|
85
89
|
encoding="utf-8")
|
|
86
90
|
|
|
87
91
|
(repo / "exchange.lock.json").write_text(json.dumps(
|
|
88
92
|
{"name": exchange.name, "master": exchange.master,
|
|
89
|
-
"trees": [{"name": n, "root":
|
|
93
|
+
"trees": [{"name": n, "root": crumb_path(r, repo, localize=localize)} for n, r in members]},
|
|
94
|
+
indent=2), encoding="utf-8")
|
|
90
95
|
return master_md
|
|
91
96
|
|
|
92
97
|
|
|
@@ -104,7 +109,8 @@ def validate(repo_dir: str | Path, exchange: Exchange) -> list[Violation]:
|
|
|
104
109
|
return out
|
|
105
110
|
if not _has_frontmatter(master_md):
|
|
106
111
|
out.append(Violation("error", exchange.master, "master SKILL.md lacks frontmatter"))
|
|
107
|
-
found = {Path(p).
|
|
112
|
+
found = {(Path(p) if Path(p).is_absolute() else repo / p).resolve()
|
|
113
|
+
for p in _CRUMB_RE.findall(master_md.read_text(encoding="utf-8"))}
|
|
108
114
|
expected = {
|
|
109
115
|
node_skill_md(repo / "trees" / m.name, SkillTree.load((exchange.base / m.manifest).resolve()).root.name).resolve()
|
|
110
116
|
for m in exchange.trees
|