skm-cli 0.2.1__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.
- {skm_cli-0.2.1 → skm_cli-0.3.0}/.gitignore +1 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/AGENTS.md +18 -6
- {skm_cli-0.2.1 → skm_cli-0.3.0}/PKG-INFO +24 -3
- {skm_cli-0.2.1 → skm_cli-0.3.0}/README.md +22 -1
- skm_cli-0.3.0/kb/known-issues.md +15 -0
- skm_cli-0.3.0/kb/notes/2026-10-23-github-pr-requirements-research.md +169 -0
- skm_cli-0.3.0/kb/plans/2026-09-04-sparse-checkout-clone.md +101 -0
- skm_cli-0.3.0/kb/sessions/2026-09-04-sparse-checkout-clone.md +53 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/pyproject.toml +1 -1
- skm_cli-0.3.0/skills.example.yaml +76 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/install.py +69 -26
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/update.py +15 -11
- skm_cli-0.3.0/src/skm/detect.py +106 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/git.py +49 -3
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/linker.py +6 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/types.py +26 -4
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_cli_e2e.py +224 -5
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_config.py +2 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_detect.py +97 -1
- skm_cli-0.3.0/tests/test_git.py +219 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_install.py +67 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_linker.py +49 -0
- skm_cli-0.3.0/tests/test_types.py +83 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/uv.lock +1 -1
- skm_cli-0.2.1/CLAUDE.md +0 -1
- skm_cli-0.2.1/skills.example.yaml +0 -38
- skm_cli-0.2.1/src/skm/detect.py +0 -50
- skm_cli-0.2.1/tests/test_git.py +0 -54
- skm_cli-0.2.1/tests/test_types.py +0 -39
- {skm_cli-0.2.1 → skm_cli-0.3.0}/draft.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/images/skm-install.png +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/plans/2026-03-09-skm-skill-manager.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-09-refactor-config-structure.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-10-add-local-path-support.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-10-extend-skm-install-direct-source.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-10-handle-existing-skill-dir-override.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-10-implement-skm-view-command.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/kb/sessions/2026-03-10-install-idempotent-sync.md +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/link-skill +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/__init__.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/cli.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/clonefile.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/__init__.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/check_updates.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/list_cmd.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/remove.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/commands/view.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/config.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/lock.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/py.typed +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/tui.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/src/skm/utils.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_clonefile.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_install_from_source.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_list_cmd.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_local_path.py +0 -0
- {skm_cli-0.2.1 → skm_cli-0.3.0}/tests/test_lock.py +0 -0
|
@@ -16,8 +16,8 @@ src/skm/
|
|
|
16
16
|
├── types.py # Pydantic data models + constants
|
|
17
17
|
├── config.py # Load skills.yaml → SkmConfig
|
|
18
18
|
├── lock.py # Read/write skills-lock.yaml
|
|
19
|
-
├── detect.py # Walk cloned repos for SKILL.md files
|
|
20
|
-
├── git.py #
|
|
19
|
+
├── detect.py # Walk cloned repos for SKILL.md files; select_skill_dirs() applies the same rules to a git tree listing
|
|
20
|
+
├── git.py # Sparse clone (no-checkout + ls-tree + sparse-checkout), pull, fetch, commit SHA helpers (unified run_cmd error handling)
|
|
21
21
|
├── utils.py # Utility functions (compact_path)
|
|
22
22
|
├── linker.py # Symlink skills to agent dirs, resolve includes/excludes
|
|
23
23
|
└── commands/
|
|
@@ -29,8 +29,8 @@ tests/
|
|
|
29
29
|
├── test_types.py # Pydantic model validation
|
|
30
30
|
├── test_config.py # Config loading, error handling
|
|
31
31
|
├── test_lock.py # Lock file I/O
|
|
32
|
-
├── test_detect.py # Skill detection logic (local + network tests against real repos)
|
|
33
|
-
├── test_git.py # Git operations (clone,
|
|
32
|
+
├── test_detect.py # Skill detection logic (local + network tests against real repos); select_skill_dirs() path-list rules
|
|
33
|
+
├── test_git.py # Git operations (sparse clone command sequence, real local-repo sparse checkout, pull re-sparsing, clone failure handling)
|
|
34
34
|
├── test_linker.py # Symlink creation, agent filtering
|
|
35
35
|
├── test_install.py # Install command unit tests
|
|
36
36
|
└── test_cli_e2e.py # End-to-end CLI tests for all commands
|
|
@@ -45,7 +45,11 @@ tests/
|
|
|
45
45
|
|
|
46
46
|
## Architecture
|
|
47
47
|
|
|
48
|
-
Config-driven: parse `skills.yaml` → clone repos to store → detect skills by walking for `SKILL.md` → symlink to agent dirs → write lock file.
|
|
48
|
+
Config-driven: parse `skills.yaml` → sparse-clone repos to store → detect skills by walking for `SKILL.md` → symlink to agent dirs → write lock file.
|
|
49
|
+
|
|
50
|
+
## Sparse Clone
|
|
51
|
+
|
|
52
|
+
`clone_or_pull()` in `git.py` never checks out a whole repo. It clones with `--filter=blob:none --no-checkout`, lists HEAD's tree via `git ls-tree -r -z HEAD` (tree objects are always fetched, so this is offline), feeds the path list to `select_skill_dirs()` in `detect.py`, and runs `git sparse-checkout set --cone <dirs>` before `git checkout`. After every `git pull` the sparse set is recomputed so skills added upstream get materialized. A root-level `SKILL.md` (singleton) or a repo with no skills disables sparse checkout and checks out everything. `select_skill_dirs()` must stay in sync with `detect_skills()`; both share the rules listed under Skill Detection.
|
|
49
53
|
|
|
50
54
|
Each command function (`run_install`, `run_list`, etc.) accepts explicit paths and agent dicts as parameters, making them testable with `tmp_path` fixtures without touching real filesystem locations.
|
|
51
55
|
|
|
@@ -59,7 +63,7 @@ Paths stored in `skills-lock.yaml` (e.g. `linked_to`) use `compact_path()` from
|
|
|
59
63
|
|
|
60
64
|
## CLI Commands
|
|
61
65
|
|
|
62
|
-
- `skm install` —
|
|
66
|
+
- `skm install` — Sparse-clone repos (idempotent: skips pull if already cloned), detect skills, create symlinks, remove stale links, update lock. Treats `skills.yaml` as declarative state: removes links for skills dropped from config or agents changed by includes/excludes. Only removes links tracked in the lock file — manually created files in agent dirs are never touched.
|
|
63
67
|
- `skm list` — Show installed skills and their linked paths from lock file
|
|
64
68
|
- `skm check-updates` — Fetch remotes, compare against locked commits, show changelog
|
|
65
69
|
- `skm update <skill_name>` — Pull latest for a skill's repo, re-link, update lock
|
|
@@ -81,6 +85,9 @@ packages:
|
|
|
81
85
|
agents: # optional: further filter agents for this package
|
|
82
86
|
excludes:
|
|
83
87
|
- standard
|
|
88
|
+
- local_path: ~/Code/devblocks
|
|
89
|
+
skills_excludes: # optional: install all skills EXCEPT these (mutually exclusive with skills)
|
|
90
|
+
- team-squad
|
|
84
91
|
- repo: https://github.com/blader/humanizer # installs all detected skills to default agents
|
|
85
92
|
```
|
|
86
93
|
|
|
@@ -94,6 +101,8 @@ A skill is a directory containing a `SKILL.md` file with YAML frontmatter includ
|
|
|
94
101
|
3. Otherwise → walk all subdirectories from repo root
|
|
95
102
|
4. Stop descending once `SKILL.md` is found (no nested skill-in-skill)
|
|
96
103
|
|
|
104
|
+
During traversal, `.git` and directories whose names start with `_` are skipped.
|
|
105
|
+
|
|
97
106
|
## Known Agents
|
|
98
107
|
|
|
99
108
|
Defined in `src/skm/types.py` as `KNOWN_AGENTS`:
|
|
@@ -101,6 +110,9 @@ Defined in `src/skm/types.py` as `KNOWN_AGENTS`:
|
|
|
101
110
|
- `claude` → `~/.claude/skills`
|
|
102
111
|
- `codex` → `~/.codex/skills`
|
|
103
112
|
- `openclaw` → `~/.openclaw/skills`
|
|
113
|
+
- `pi` → `~/.pi/agent/skills`
|
|
114
|
+
|
|
115
|
+
All agents get symlinks except `openclaw`, which `AGENT_OPTIONS` sets to `use_hardlink` because OpenClaw skips symlinks that resolve outside its skill roots. `standard` used hardlinks until 2026-10-23, since OpenClaw also reads `~/.agents/skills`. Codex (0.164) and Hermes (0.22) were tested and load symlinked skills there. In symlink mode `link_skill()` swaps a leftover materialized copy for a symlink when `_is_managed_materialized_dir()` says nothing in it differs from the source.
|
|
104
116
|
|
|
105
117
|
## Testing
|
|
106
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: skm-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Skill Manager - manage agent skills from GitHub repos
|
|
5
5
|
Author-email: Reorx <novoreorx@gmail.com>
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -96,6 +96,8 @@ agents:
|
|
|
96
96
|
|
|
97
97
|
packages:
|
|
98
98
|
- repo: https://github.com/vercel-labs/agent-skills
|
|
99
|
+
clone_strategy: shallow # optional: use git clone --filter=blob:none --depth 1
|
|
100
|
+
skills_dir: skills/.curated # optional: search only this repo-relative directory
|
|
99
101
|
skills: # optional: install only these skills (omit = all)
|
|
100
102
|
- vercel-react-best-practices
|
|
101
103
|
agents: # optional: further filter agents for this package
|
|
@@ -107,10 +109,23 @@ packages:
|
|
|
107
109
|
- local_path: ~/Code/my-custom-skills # use a local directory as package source
|
|
108
110
|
skills:
|
|
109
111
|
- my-skill
|
|
112
|
+
|
|
113
|
+
- local_path: ~/Code/devblocks
|
|
114
|
+
skills_excludes: # optional: install all skills EXCEPT these (mutually exclusive with skills)
|
|
115
|
+
- team-squad
|
|
116
|
+
- team-council
|
|
110
117
|
```
|
|
111
118
|
|
|
112
119
|
Each package must specify exactly one of `repo` or `local_path`. Local path packages use the directory directly (no cloning) and are skipped by `check-updates` and `update`.
|
|
113
120
|
|
|
121
|
+
Repo packages are cloned with a sparse checkout: skm runs `git clone --filter=blob:none --no-checkout`, lists the tree with `git ls-tree`, and checks out only the directories that contain a `SKILL.md` (using the same detection rules as below, honoring `skills_dir`). A large monorepo that ships a couple of skills therefore costs a few hundred KB on disk instead of the whole working tree. The store stays a normal git repo, so `check-updates` and `update` work unchanged; the sparse set is recomputed after every pull so skills added upstream are picked up automatically. Repos whose root is itself a skill are checked out in full.
|
|
122
|
+
|
|
123
|
+
`clone_strategy: shallow` is optional for repo packages. When set, the first clone additionally passes `--depth 1`; omitted or other values keep the full history (blobs are still fetched lazily). Existing cached repos are reused and pulled, so changing this option does not re-clone an already-present repo. Caches created before sparse checkout was introduced shrink on their next `skm update`.
|
|
124
|
+
|
|
125
|
+
`skills_dir` is optional and must be a relative path inside the package. When set, skm searches only that directory for `SKILL.md` files instead of using the default detection order.
|
|
126
|
+
|
|
127
|
+
`skills` and `skills_excludes` are mutually exclusive. `skills` installs only the listed skills; `skills_excludes` installs all detected skills except the listed ones. Omitting both installs all detected skills.
|
|
128
|
+
|
|
114
129
|
## Install Sync Behavior
|
|
115
130
|
|
|
116
131
|
`skm install` treats `skills.yaml` as a declarative state file. Each run syncs the agent directories to match the config:
|
|
@@ -130,6 +145,8 @@ A skill is a directory containing a `SKILL.md` file with YAML frontmatter (`name
|
|
|
130
145
|
3. Otherwise — walk all subdirectories from repo root
|
|
131
146
|
4. Stop descending once `SKILL.md` is found (no nested skills)
|
|
132
147
|
|
|
148
|
+
If a package sets `skills_dir`, this default order is bypassed and only that directory is scanned. If `skills_dir` itself contains `SKILL.md`, it is treated as a singleton skill.
|
|
149
|
+
|
|
133
150
|
## Known Agents
|
|
134
151
|
|
|
135
152
|
Skills are symlinked into these directories by default:
|
|
@@ -147,12 +164,16 @@ When skm links a skill into an agent directory, it picks a strategy based on the
|
|
|
147
164
|
|
|
148
165
|
### 1. Symlink (default)
|
|
149
166
|
|
|
150
|
-
A symbolic link from `<agent_dir>/skills/<skill_name>` → `<store>/<skill_name>`. This is the default for all agents
|
|
167
|
+
A symbolic link from `<agent_dir>/skills/<skill_name>` → `<store>/<skill_name>`. This is the default for all agents except `openclaw`. Changes in the store are immediately visible.
|
|
168
|
+
|
|
169
|
+
If the agent dir holds a copy that an older skm version materialized (every file identical to the source or a hardlink of it), `skm install` replaces it with a symlink without asking. A copy whose content differs from the source still prompts before it is overwritten.
|
|
151
170
|
|
|
152
171
|
### 2. Hardlink
|
|
153
172
|
|
|
154
173
|
When `use_hardlink: true` is set for an agent in `AGENT_OPTIONS`, skm creates hardlinks instead. Each file in the skill directory gets its own hardlink pointing to the same inode as the source. This only works when source and target are on the **same filesystem/device**.
|
|
155
174
|
|
|
175
|
+
Only `openclaw` uses this, because OpenClaw skips skill symlinks whose real path is outside its skill root. Hardlinks break when an editor saves a file by replacing it, and new or dot-prefixed files are not carried over, so the copy needs another `skm install` after the source changes.
|
|
176
|
+
|
|
156
177
|
### 3. Reflink (copy-on-write)
|
|
157
178
|
|
|
158
179
|
When hardlinks can't be used (source and target on **different devices**), skm attempts a reflink/COW clone. This creates an independent copy that shares physical disk blocks with the source until either side is modified — fast and space-efficient.
|
|
@@ -85,6 +85,8 @@ agents:
|
|
|
85
85
|
|
|
86
86
|
packages:
|
|
87
87
|
- repo: https://github.com/vercel-labs/agent-skills
|
|
88
|
+
clone_strategy: shallow # optional: use git clone --filter=blob:none --depth 1
|
|
89
|
+
skills_dir: skills/.curated # optional: search only this repo-relative directory
|
|
88
90
|
skills: # optional: install only these skills (omit = all)
|
|
89
91
|
- vercel-react-best-practices
|
|
90
92
|
agents: # optional: further filter agents for this package
|
|
@@ -96,10 +98,23 @@ packages:
|
|
|
96
98
|
- local_path: ~/Code/my-custom-skills # use a local directory as package source
|
|
97
99
|
skills:
|
|
98
100
|
- my-skill
|
|
101
|
+
|
|
102
|
+
- local_path: ~/Code/devblocks
|
|
103
|
+
skills_excludes: # optional: install all skills EXCEPT these (mutually exclusive with skills)
|
|
104
|
+
- team-squad
|
|
105
|
+
- team-council
|
|
99
106
|
```
|
|
100
107
|
|
|
101
108
|
Each package must specify exactly one of `repo` or `local_path`. Local path packages use the directory directly (no cloning) and are skipped by `check-updates` and `update`.
|
|
102
109
|
|
|
110
|
+
Repo packages are cloned with a sparse checkout: skm runs `git clone --filter=blob:none --no-checkout`, lists the tree with `git ls-tree`, and checks out only the directories that contain a `SKILL.md` (using the same detection rules as below, honoring `skills_dir`). A large monorepo that ships a couple of skills therefore costs a few hundred KB on disk instead of the whole working tree. The store stays a normal git repo, so `check-updates` and `update` work unchanged; the sparse set is recomputed after every pull so skills added upstream are picked up automatically. Repos whose root is itself a skill are checked out in full.
|
|
111
|
+
|
|
112
|
+
`clone_strategy: shallow` is optional for repo packages. When set, the first clone additionally passes `--depth 1`; omitted or other values keep the full history (blobs are still fetched lazily). Existing cached repos are reused and pulled, so changing this option does not re-clone an already-present repo. Caches created before sparse checkout was introduced shrink on their next `skm update`.
|
|
113
|
+
|
|
114
|
+
`skills_dir` is optional and must be a relative path inside the package. When set, skm searches only that directory for `SKILL.md` files instead of using the default detection order.
|
|
115
|
+
|
|
116
|
+
`skills` and `skills_excludes` are mutually exclusive. `skills` installs only the listed skills; `skills_excludes` installs all detected skills except the listed ones. Omitting both installs all detected skills.
|
|
117
|
+
|
|
103
118
|
## Install Sync Behavior
|
|
104
119
|
|
|
105
120
|
`skm install` treats `skills.yaml` as a declarative state file. Each run syncs the agent directories to match the config:
|
|
@@ -119,6 +134,8 @@ A skill is a directory containing a `SKILL.md` file with YAML frontmatter (`name
|
|
|
119
134
|
3. Otherwise — walk all subdirectories from repo root
|
|
120
135
|
4. Stop descending once `SKILL.md` is found (no nested skills)
|
|
121
136
|
|
|
137
|
+
If a package sets `skills_dir`, this default order is bypassed and only that directory is scanned. If `skills_dir` itself contains `SKILL.md`, it is treated as a singleton skill.
|
|
138
|
+
|
|
122
139
|
## Known Agents
|
|
123
140
|
|
|
124
141
|
Skills are symlinked into these directories by default:
|
|
@@ -136,12 +153,16 @@ When skm links a skill into an agent directory, it picks a strategy based on the
|
|
|
136
153
|
|
|
137
154
|
### 1. Symlink (default)
|
|
138
155
|
|
|
139
|
-
A symbolic link from `<agent_dir>/skills/<skill_name>` → `<store>/<skill_name>`. This is the default for all agents
|
|
156
|
+
A symbolic link from `<agent_dir>/skills/<skill_name>` → `<store>/<skill_name>`. This is the default for all agents except `openclaw`. Changes in the store are immediately visible.
|
|
157
|
+
|
|
158
|
+
If the agent dir holds a copy that an older skm version materialized (every file identical to the source or a hardlink of it), `skm install` replaces it with a symlink without asking. A copy whose content differs from the source still prompts before it is overwritten.
|
|
140
159
|
|
|
141
160
|
### 2. Hardlink
|
|
142
161
|
|
|
143
162
|
When `use_hardlink: true` is set for an agent in `AGENT_OPTIONS`, skm creates hardlinks instead. Each file in the skill directory gets its own hardlink pointing to the same inode as the source. This only works when source and target are on the **same filesystem/device**.
|
|
144
163
|
|
|
164
|
+
Only `openclaw` uses this, because OpenClaw skips skill symlinks whose real path is outside its skill root. Hardlinks break when an editor saves a file by replacing it, and new or dot-prefixed files are not carried over, so the copy needs another `skm install` after the source changes.
|
|
165
|
+
|
|
145
166
|
### 3. Reflink (copy-on-write)
|
|
146
167
|
|
|
147
168
|
When hardlinks can't be used (source and target on **different devices**), skm attempts a reflink/COW clone. This creates an independent copy that shares physical disk blocks with the source until either side is modified — fast and space-efficient.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Known Issues
|
|
2
|
+
|
|
3
|
+
> 当前影响使用或稳定性、仍待处理的问题;解决后删除,处理结果写入 KB。
|
|
4
|
+
|
|
5
|
+
## 列表:目录名称前缀相同时可能显示错误 agent
|
|
6
|
+
|
|
7
|
+
- 记录:2026-10-23,见 [PR 需求调研](notes/2026-10-23-github-pr-requirements-research.md)。
|
|
8
|
+
- `src/skm/commands/list_cmd.py` 的 `_agent_name_from_link` 使用字符串 `startswith` 判断归属。例如 lock 路径 `~/.agents/skills-extra/demo` 会匹配内置 `standard` 的 `~/.agents/skills`,导致列表显示错误 agent;自定义 agent 目录与另一个目录共享前缀时可触发。
|
|
9
|
+
- 已通过当前代码静态确认,尚未执行独立复现测试。开放的 [PR #7](https://github.com/reorx/skm/pull/7) 提供父目录精确比较及回归测试,尚未合入。处理时应先补充失败测试,再修复判断。
|
|
10
|
+
|
|
11
|
+
## openclaw:源文件被替换式保存后硬链副本变旧,install 卡在覆盖确认
|
|
12
|
+
|
|
13
|
+
- 记录:2026-10-23,standard 改为软链时确认 openclaw 仍走硬链(`AGENT_OPTIONS`)。
|
|
14
|
+
- 编辑器、agent 的 Edit 工具和 `sed -i` 保存时会换掉 inode,`~/.openclaw/skills` 里的副本还是旧内容,源里新增的文件和点开头的文件也不会带过去。之后 `skm install` 判定副本"不是受管副本",逐个询问是否覆盖;非终端环境下直接报错退出。只影响启用了 openclaw 的用户,本机目前没启用。
|
|
15
|
+
- 处理方向:OpenClaw 新版支持 `skills.load.allowSymlinkTargets`,配置 skm store 和 local_path 目录后也许可以改回软链,需要实测;或者把 lock 里记录过的副本直接当作受管副本刷新,前提是先确认副本里没有只存在于副本的改动。
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
created: 2026-10-23
|
|
3
|
+
tags:
|
|
4
|
+
- github
|
|
5
|
+
- requirements
|
|
6
|
+
- skill-manager
|
|
7
|
+
- agent-config
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# GitHub PR 需求意图与项目价值调研
|
|
11
|
+
|
|
12
|
+
## 调研范围与结论
|
|
13
|
+
|
|
14
|
+
截至 2026-10-23,`reorx/skm` 共检索到 5 个 PR:#1、#2、#3、#6 已合并,#7 仍开放。本文覆盖全部 PR,每个 PR 单独整理需求,并区分作者原始诉求、代码实际行为和本次建议。
|
|
15
|
+
|
|
16
|
+
依据为 GitHub PR 正文、diff、讨论、关联 issue,以及当前代码。远端 master 为 `4a8372b80968d4d2877089abe6e94cf1f03adb94`;本地调研基线为 `514a92fd571dae967cba5f1d8763ec51819f381d`,只比远端多删除 `CLAUDE.md` 软链的提交,业务代码一致。#7 的 head 为 `3a4f024cb50160fc7e6129ea19cc2895f370b360`。
|
|
17
|
+
|
|
18
|
+
| PR | 独立需求 | 状态 | 对项目的意义 | 建议 |
|
|
19
|
+
|---|---|---|---|---|
|
|
20
|
+
| [#1](https://github.com/reorx/skm/pull/1) | 跨平台正确读取技能文件,并避免安装仓库内部元数据 | 2026-03-15 合并 | 高:修复 Windows 安装、查看失败 | 保留;明确隐藏文件过滤边界 |
|
|
21
|
+
| [#2](https://github.com/reorx/skm/pull/2) | 安装位置跟随用户的 agent 配置目录 | 2026-03-15 合并 | 高:避免安装成功但 agent 找不到技能 | 保留,纳入统一路径解析 |
|
|
22
|
+
| [#3](https://github.com/reorx/skm/pull/3) | 列表正确识别缩写路径对应的 agent | 2026-03-17 合并 | 高:补齐 #2 的展示一致性 | 保留;继续修复路径前缀误判 |
|
|
23
|
+
| [#6](https://github.com/reorx/skm/pull/6) | 非软链安装兼容不同文件系统,并支持更新实体内容 | 2026-03-19 合并 | 高:解决真实 Btrfs 子卷安装障碍 | 保留;区分兼容性、更新和隔离语义 |
|
|
24
|
+
| [#7](https://github.com/reorx/skm/pull/7) | 在单一配置中统一 agent 定义,开放路径与安装模式覆盖 | 开放 | 高:用户可自行适配环境,无需修改源码 | 支持核心需求;先明确附带的更新语义变化 |
|
|
25
|
+
|
|
26
|
+
这些需求均围绕 SKM 的核心任务:让同一批技能在不同 agent 和本机环境中可靠安装、更新、被识别。#7 的产品价值主要来自用户可以覆盖配置;单纯将常量移到另一个文件,收益有限。
|
|
27
|
+
|
|
28
|
+
## PR #1:跨平台技能读取与实体安装过滤
|
|
29
|
+
|
|
30
|
+
### 背景与意图
|
|
31
|
+
|
|
32
|
+
作者报告 Windows 上默认文本编码造成 `UnicodeDecodeError`,以及硬链接 `.git/objects/pack` 时出现权限错误。用户需要正常安装、查看含非 ASCII 内容的技能,而不必调整系统 locale 或仓库内部权限。[原始报告与实现](https://github.com/reorx/skm/pull/1/files)
|
|
33
|
+
|
|
34
|
+
### 独立需求描述
|
|
35
|
+
|
|
36
|
+
技能管理器应以 UTF-8 读取 `SKILL.md`,使技能发现与内置分页查看行为不依赖操作系统默认编码。创建实体安装目录时,应排除不属于技能运行内容的仓库元数据,避免为安装技能复制 Git 内部状态。
|
|
37
|
+
|
|
38
|
+
验收应覆盖:中文或特殊符号出现在技能正文时,发现和查看均成功;源目录含 `.git` 时,实体安装不包含该目录;常规脚本和资源仍完整安装。
|
|
39
|
+
|
|
40
|
+
### 实现范围与价值判断
|
|
41
|
+
|
|
42
|
+
PR 修改 `detect.py` 与 `commands/view.py` 的 `read_text`,显式指定 UTF-8;同时在硬链接遍历中跳过所有点号开头条目和软链接。改动很小,却解决核心流程阻断,值得保留。
|
|
43
|
+
|
|
44
|
+
需要区分“排除 `.git`”与“排除所有隐藏资源”:后者是实际实现的更宽泛规则。如果某个技能依赖隐藏文件或符号链接资源,实体安装可能不完整。该规则现在仍在 `linker.py` 中;本次没有发现具体受影响技能,因此作为边界记录,不认定为已发生故障。该 PR 也没有提供完整 Windows 平台支持的证明,不能据此宣称全部命令已通过 Windows 验收。
|
|
45
|
+
|
|
46
|
+
## PR #2:安装路径跟随 agent 配置目录
|
|
47
|
+
|
|
48
|
+
### 背景与意图
|
|
49
|
+
|
|
50
|
+
原先 agent 目录写死在源码中。用户通过环境变量改变 agent 配置根目录后,SKM 仍将技能安装到旧位置,造成两套目录不一致。[PR 正文](https://github.com/reorx/skm/pull/2)
|
|
51
|
+
|
|
52
|
+
### 独立需求描述
|
|
53
|
+
|
|
54
|
+
用户已经为 agent 指定配置根目录时,SKM 应将技能安装到该根目录下的 `skills` 子目录;未设置或设置为空时使用默认目录。这个解析结果应贯穿安装、更新和列表展示。
|
|
55
|
+
|
|
56
|
+
此 PR 的具体映射是 `CLAUDE_CONFIG_DIR` 对应 Claude,`CODEX_HOME` 对应 Codex。这里描述的是 PR 和 SKM 当前实现,不额外推断这些产品所有版本的配置约定。
|
|
57
|
+
|
|
58
|
+
验收应覆盖:默认目录不变;设置覆盖值后安装进入新目录;`~` 正确展开;lock 仍使用既有路径压缩规则;后续列表能识别安装目标。
|
|
59
|
+
|
|
60
|
+
### 实现范围与价值判断
|
|
61
|
+
|
|
62
|
+
`types.py` 增加 `_get_known_agents()`,应用两个环境变量覆盖。需求直接避免“装好了却不能用”,价值高,且无需用户维护两套重复配置。
|
|
63
|
+
|
|
64
|
+
当前 `KNOWN_AGENTS` 在模块导入时计算,普通 CLI 进程通常足够,但同一进程内改变环境后不会自动重新解析。#7 改为命令执行过程中的统一解析,更容易保证一致性。后续若同时支持配置文件覆盖,需要明确优先级,而非在各命令中分别处理。
|
|
65
|
+
|
|
66
|
+
## PR #3:列表中的 agent 归属正确且稳定
|
|
67
|
+
|
|
68
|
+
### 背景与意图
|
|
69
|
+
|
|
70
|
+
这是 #2 带出的配套问题:环境变量解析得到绝对目录,lock 使用 `~` 压缩路径;两种表示直接比较时不匹配。作者说“list 不显示安装的技能”,但从实现看,具体修复的是技能条目中的 agent 归属显示,技能条目本身仍从 lock 读取。[变更与测试](https://github.com/reorx/skm/pull/3/files)
|
|
71
|
+
|
|
72
|
+
### 独立需求描述
|
|
73
|
+
|
|
74
|
+
同一安装路径无论使用绝对形式还是 `~` 形式,列表均应显示正确的 agent 名称。路径展示的隐私压缩不应影响路径身份判断。
|
|
75
|
+
|
|
76
|
+
验收应覆盖:默认目录、环境变量覆盖目录、lock 中的缩写路径都能识别;不同目录即使名称前缀相同,也不能认作同一 agent。
|
|
77
|
+
|
|
78
|
+
### 实现范围与价值判断
|
|
79
|
+
|
|
80
|
+
PR 在比较前也展开 lock 路径,并增加安装与列表测试。它保障“安装事实”和“用户看到的状态”一致,值得保留。
|
|
81
|
+
|
|
82
|
+
但当前比较仍使用字符串 `startswith`:例如 `~/.agents/skills-extra/demo` 会被 `~/.agents/skills` 匹配,误显示为 `standard`。这是代码可直接确认的边界缺陷;#7 已提出使用安装路径父目录精确比较,并有对应测试。建议将此修复视为独立的正确性需求,优先于大型配置重构。
|
|
83
|
+
|
|
84
|
+
## PR #6:跨文件系统的实体安装与刷新
|
|
85
|
+
|
|
86
|
+
### 背景与意图
|
|
87
|
+
|
|
88
|
+
[Issue #4](https://github.com/reorx/skm/issues/4) 给出实际使用场景:store 所在的 `~/.local/share` 与 home 位于不同 Btrfs 子卷,硬链接失败。作者同时提出两个方向:增加 reflink 回退,或允许用户覆盖安装策略。#6 处理前者,#7 延续后者。
|
|
89
|
+
|
|
90
|
+
### 独立需求描述
|
|
91
|
+
|
|
92
|
+
对于需要真实目录的 agent,用户应能在硬链接不可用的存储布局中安装技能。系统应自动选取可用方式,必要时普通复制,不能因为一种优化不适用就阻断安装。更新后,agent 使用的实体内容应反映新版本;无法完成的真实 I/O 故障必须暴露。
|
|
93
|
+
|
|
94
|
+
验收应覆盖:同设备硬链接;跨设备时尝试 reflink;不支持 reflink 时复制;权限、磁盘故障不得误作“不支持”吞掉;更新后检查目标文件内容及删除行为,而不只检查 lock commit。
|
|
95
|
+
|
|
96
|
+
### 实现范围与价值判断
|
|
97
|
+
|
|
98
|
+
PR 引入 `clonefile.py`:Linux 使用 FICLONE,macOS 使用 `clonefile(2)`;`linker.py` 根据设备和能力选择硬链接、reflink 或复制;`update` 传入 `force=True` 以允许重建旧实体目录。[PR 实现](https://github.com/reorx/skm/pull/6/files);[维护者补充 macOS 支持的说明](https://github.com/reorx/skm/pull/6#issuecomment-4087075634)。
|
|
99
|
+
|
|
100
|
+
需求价值高,但应准确说明保证:跨设备并不保证 reflink 可用,普通复制才是最终兼容回退;硬链接与 reflink 也不是相同隔离模型,硬链接原地编辑会影响共享 inode 的另一端,不能描述为独立副本。
|
|
101
|
+
|
|
102
|
+
刷新也有两个层次:当前 `_materialize_tree` 跳过已存在文件;但外层 `link_skill` 在发现目标与源不一致、且 `force=True` 时会删除目录重建。因此不能仅凭内层函数断言“当前所有 update 都不会更新内容”。#7 的刷新改动应结合外层分支与端到端内容断言评估。
|
|
103
|
+
|
|
104
|
+
## PR #7:统一 agent 配置与用户覆盖
|
|
105
|
+
|
|
106
|
+
### 背景与讨论演变
|
|
107
|
+
|
|
108
|
+
[Issue #5](https://github.com/reorx/skm/issues/5) 最初希望把 `KNOWN_AGENTS`、环境变量映射、`AGENT_OPTIONS` 合并到静态配置。维护者随后明确建议复用 `skills.yaml` 的 `agents`,增加 `agents.override`,避免让用户管理第二份配置文件。[维护者意见](https://github.com/reorx/skm/pull/7#issuecomment-4087118375)
|
|
109
|
+
|
|
110
|
+
最新实现已经采用该意见:内置定义保留为 Python 中的 `DEFAULT_AGENT_SPECS`,用户覆盖写在 `skills.yaml`。因此旧标题“move agent specs to bundled config file”已不准确;也不能因为 issue #5 已关闭,就判断需求已经合入。
|
|
111
|
+
|
|
112
|
+
### 独立需求描述
|
|
113
|
+
|
|
114
|
+
用户应能在现有配置文件中为内置 agent 指定技能目录与安装方式,无需修改 SKM 源码。安装、更新、列表必须使用同一套解析结果,同时保持现有配置兼容。
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
agents:
|
|
118
|
+
default:
|
|
119
|
+
- claude
|
|
120
|
+
- standard
|
|
121
|
+
override:
|
|
122
|
+
claude:
|
|
123
|
+
path: ~/.agents/skills
|
|
124
|
+
install_mode: materialize
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
这里 `path` 是完整技能目录,环境变量值则是配置根目录,二者含义不能混淆。`materialize` 表示生成实体目录,具体采用硬链接、reflink 还是复制由系统决定。
|
|
128
|
+
|
|
129
|
+
最新实现的路径优先级为:`--agents-dir` > `agents.override.<name>.path` > agent 环境变量 > 内置路径。只覆盖 `install_mode` 不会取消环境变量路径。`agents.default` 限定部署范围,包级 includes/excludes 再过滤该范围。
|
|
130
|
+
|
|
131
|
+
验收应覆盖以下行为:
|
|
132
|
+
|
|
133
|
+
- 旧配置继续使用现有默认目录与模式;未知 agent 或非法模式明确报错。
|
|
134
|
+
- 路径覆盖、模式覆盖可以单独设置;优先级在安装、更新和列表中一致。
|
|
135
|
+
- 直接安装的交互选项只包含默认启用的 agent。
|
|
136
|
+
- `list` 能识别自定义安装位置;配置损坏时仍可带警告读取 lock。
|
|
137
|
+
- 切换模式、迁移路径后,lock 与实际安装保持一致,不误删未管理内容。
|
|
138
|
+
|
|
139
|
+
### 相对当前主分支,真正剩下什么
|
|
140
|
+
|
|
141
|
+
GitHub 返回的 #7 diff 相对较旧基线包含 28 个文件。重新 fetch 后以当前 `origin/master...PR head` 比较,实际剩余 17 个文件、473 行新增、203 行删除。下表将累计 diff 中的内容与当前待合入内容分开。
|
|
142
|
+
|
|
143
|
+
| 内容 | 当前主分支状态 | 评价 |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `AgentSpec`、统一解析、`agents.override` | 尚无 | 核心新增,价值高 |
|
|
146
|
+
| Pi 的 `PI_CODING_AGENT_DIR` 映射 | 尚无;Pi 默认目录已有 | 延续 #2 的一致性需求 |
|
|
147
|
+
| 精确匹配列表路径、交互选项限制、配置损坏时列表降级 | 尚无这些改动 | 有价值的正确性与可用性补充 |
|
|
148
|
+
| 实体刷新替换文件、普通文件冲突处理、旧文件保留警告 | 尚无这些改动 | 应独立审查更新语义 |
|
|
149
|
+
| `skills_excludes`、`skills_dir`、`clone_strategy: shallow` | 已有 | 不应再次列为待开发 |
|
|
150
|
+
| 稀疏检出与 `_` 目录过滤 | 已有 | 已实现的仓库访问与发现能力 |
|
|
151
|
+
|
|
152
|
+
### 价值与取舍
|
|
153
|
+
|
|
154
|
+
建议接受统一配置的产品方向。它直接回应 #4 中“让我选择安装方式”的诉求,也能让不需要实体目录的用户自行选择软链接,避免为某一 agent 的兼容问题要求所有用户接受同样策略。最新实现仅支持覆盖内置 agent,不支持注册任意新 agent;不宜将其宣传为通用插件注册机制。
|
|
155
|
+
|
|
156
|
+
合入前最需要确认的是**实体更新遇到目标多余文件时如何处理**。当前主分支的测试要求上游删除的文件从实体安装中消失;#7 将该测试改成“旧文件保留,输出警告”。当目录其余内容仍被判断为受管理副本时,会走保留分支;不匹配且 force 重建时仍可能删除,因此并非所有情况下都保证保留。
|
|
157
|
+
|
|
158
|
+
保留有助于避免误删用户添加的文件,但也可能让过期说明或脚本继续影响 agent;这改变了声明式同步的预期,不能当作内部重构附带接受。建议单独决定同步契约,并补充“只删除”“修改加删除”“用户添加文件”三类验收。更完整的后续方案是保存受管理文件清单或内容指纹,让删除判断基于所有权;这是本次建议,不是 PR 已提供的能力。
|
|
159
|
+
|
|
160
|
+
维护者曾在 [早期 review](https://github.com/reorx/skm/pull/7#issuecomment-4242977222) 指出刷新跳过现有文件;最新 head 已改为替换现有文件,并强化更新后内容断言,不能把旧 review 原样当作未修复结论。作者的[最新回复](https://github.com/reorx/skm/pull/7#issuecomment-5548017604)报告 140 个非网络测试通过;这是作者报告,本次未执行 PR 分支测试,也未进行跨平台实机验收。
|
|
161
|
+
|
|
162
|
+
## 建议的决策顺序
|
|
163
|
+
|
|
164
|
+
1. 保留 #1、#2、#3、#6 的已合并能力,无需重新立项。
|
|
165
|
+
2. 优先处理列表路径精确匹配等独立正确性问题,避免它们一直被大型重构阻塞。
|
|
166
|
+
3. 同意 #7 的统一 agent 配置、路径和模式覆盖;将标题与说明更新为当前实际范围。
|
|
167
|
+
4. 单独决定实体目录的删除与本地修改策略,再评估刷新部分是否合入。需求值得做,不代表当前整包实现已完成验收。
|
|
168
|
+
|
|
169
|
+
本次仅做需求调研和代码比对,没有修改业务代码、合并 PR 或向 GitHub 发送评论。仓库没有 `kb/next-up.md`,无到期事项需要提醒。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Sparse Checkout Clone (Plan F)
|
|
2
|
+
|
|
3
|
+
**Goal:** Stop materializing whole repositories in the store when only a handful of skill directories are needed. Clone without checkout, discover skill directories from the git tree, then sparse-checkout only those directories.
|
|
4
|
+
|
|
5
|
+
**Background:** Benchmark on `herdrdev/herdr` (2851 files, 5 skills):
|
|
6
|
+
|
|
7
|
+
| Strategy | Time | Disk |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `--filter=blob:none` (current default) | 4.5s | 48M |
|
|
10
|
+
| `--filter=blob:none --depth 1` (current `shallow`) | 4.9s | 46M |
|
|
11
|
+
| `--no-checkout` + `ls-tree` discovery + sparse-checkout | 4.0s | 636K |
|
|
12
|
+
|
|
13
|
+
`--filter=blob:none` alone barely helps: checking out HEAD still fetches every blob of the current tree. The win comes from never checking out directories that contain no skill.
|
|
14
|
+
|
|
15
|
+
**Architecture:** Partial clone already fetches all tree objects, so `git ls-tree -r HEAD` works offline right after a `--no-checkout` clone. Reuse the existing detection rules (root singleton, `skills/` dir, `skills_dir`, skip `_*`, stop descending at the first `SKILL.md`) on that path list to pick the sparse set. The store stays a real git repo, so `check-updates`, `update`, `git fetch` and `git log` keep working unchanged. Re-apply the sparse set after every pull so skills added upstream get materialized.
|
|
16
|
+
|
|
17
|
+
**Tech Stack:** git ≥ 2.37 (cone-mode `sparse-checkout`), Python 3.12, pytest.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Task 1: Path-list skill discovery (TDD)
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Modify: `src/skm/detect.py`
|
|
25
|
+
- Modify: `tests/test_detect.py`
|
|
26
|
+
|
|
27
|
+
**Step 1: Write failing tests** for `select_skill_dirs(paths, skills_dir) -> list[str] | None`:
|
|
28
|
+
|
|
29
|
+
- root `SKILL.md` → `None` (full checkout)
|
|
30
|
+
- `skills/a/SKILL.md`, `skills/b/SKILL.md`, `other/x.py` → `['skills/a', 'skills/b']`
|
|
31
|
+
- no `skills/` dir → walk from root: `foo/SKILL.md`, `bar/baz/SKILL.md` → `['bar/baz', 'foo']`
|
|
32
|
+
- directory starting with `_` is skipped at every level
|
|
33
|
+
- nested skill inside a skill is not listed (stop descending)
|
|
34
|
+
- `skills_dir='pkg/curated'` → only dirs under it; `skills_dir` that is itself a skill → `[skills_dir]`
|
|
35
|
+
- `skills_dir` with no `SKILL.md` below → `[]`
|
|
36
|
+
- no `SKILL.md` anywhere → `[]`
|
|
37
|
+
|
|
38
|
+
**Step 2: Implement** `select_skill_dirs` in `detect.py`, sharing the same rules as `detect_skills` / `_walk_for_skills`. Paths are repo-relative POSIX strings of blobs.
|
|
39
|
+
|
|
40
|
+
**Step 3:** `uv run pytest tests/test_detect.py -v` passes.
|
|
41
|
+
|
|
42
|
+
## Task 2: Sparse clone in git.py (TDD)
|
|
43
|
+
|
|
44
|
+
**Files:**
|
|
45
|
+
- Modify: `src/skm/git.py`
|
|
46
|
+
- Modify: `tests/test_git.py`
|
|
47
|
+
|
|
48
|
+
**Step 1: Write failing tests**
|
|
49
|
+
|
|
50
|
+
- Update the two `fake_run_cmd` command-sequence tests: clone now emits
|
|
51
|
+
`git clone --filter=blob:none [--depth 1] --no-checkout <url> <dest>`, then `git ls-tree -r -z HEAD`, then `git sparse-checkout set --cone <dirs>` (or `git sparse-checkout disable` when no dirs / root singleton), then `git checkout`. The fake returns ls-tree output listing `skills/a/SKILL.md` so the sparse path is exercised.
|
|
52
|
+
- Real local-repo test: source repo with `skills/a/SKILL.md`, `skills/b/SKILL.md`, `big/blob.bin`, `README.md`. After `clone_or_pull`: `skills/a` and `skills/b` exist, `big/` does not, `git sparse-checkout list` contains both dirs.
|
|
53
|
+
- Real local-repo test: add `skills/c/SKILL.md` upstream, call `clone_or_pull` again → `skills/c` now exists in dest.
|
|
54
|
+
- Real local-repo test: singleton repo (root `SKILL.md` plus `docs/x.md`) → full checkout, `docs/x.md` exists.
|
|
55
|
+
- Real local-repo test: `skills_dir='pkg'` → only `pkg/*` skill dirs checked out.
|
|
56
|
+
|
|
57
|
+
**Step 2: Implement**
|
|
58
|
+
|
|
59
|
+
- `list_tree_files(repo_path) -> list[str]`: parse `git ls-tree -r -z HEAD`, return paths of blobs, skipping symlinks (mode `120000`) and submodules (type `commit`).
|
|
60
|
+
- `apply_sparse_checkout(repo_path, skills_dir) -> list[str] | None`: `dirs = select_skill_dirs(list_tree_files(...), skills_dir)`; if `dirs` is `None` or empty → `git sparse-checkout disable`; else `git sparse-checkout set --cone <dirs>`. Returns `dirs`.
|
|
61
|
+
- `clone_or_pull(repo_url, dest, clone_strategy=None, skills_dir=None)`:
|
|
62
|
+
- existing repo: `git pull --ff-only` then `apply_sparse_checkout`.
|
|
63
|
+
- new repo: clone with `--no-checkout`, `apply_sparse_checkout`, then `git checkout`.
|
|
64
|
+
|
|
65
|
+
**Step 3:** `uv run pytest tests/test_git.py -v` passes.
|
|
66
|
+
|
|
67
|
+
## Task 3: Thread `skills_dir` through commands
|
|
68
|
+
|
|
69
|
+
**Files:**
|
|
70
|
+
- Modify: `src/skm/commands/install.py` (two `clone_or_pull` call sites)
|
|
71
|
+
- Modify: `src/skm/commands/update.py` (one call site)
|
|
72
|
+
- Modify: `tests/test_cli_e2e.py`
|
|
73
|
+
|
|
74
|
+
**Step 1: Write failing e2e tests**
|
|
75
|
+
|
|
76
|
+
- `install` on a repo containing `skills/a` and an unrelated `big/` dir: store repo has `skills/a`, lacks `big/`, skill is linked, lock recorded.
|
|
77
|
+
- After upstream adds `skills/b`, `update --all` materializes `skills/b` and links it.
|
|
78
|
+
|
|
79
|
+
**Step 2:** Pass `skills_dir=repo_config.skills_dir` at every `clone_or_pull` call.
|
|
80
|
+
|
|
81
|
+
**Step 3:** `uv run pytest -v` (full suite) passes.
|
|
82
|
+
|
|
83
|
+
## Task 4: Docs
|
|
84
|
+
|
|
85
|
+
**Files:**
|
|
86
|
+
- Modify: `README.md` (`clone_strategy` paragraph → describe sparse clone; note that existing caches shrink on next `skm update`)
|
|
87
|
+
- Modify: `AGENTS.md` (architecture line, `git.py`/`detect.py` descriptions, install command description)
|
|
88
|
+
|
|
89
|
+
## Task 5: Commit
|
|
90
|
+
|
|
91
|
+
Run the full suite once more, then commit with a message describing the sparse checkout clone.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Decisions and caveats
|
|
96
|
+
|
|
97
|
+
- **Existing caches are not rewritten by `install`.** `install` skips the pull for already-cloned repos, so legacy full clones only shrink on the next `update` (which pulls and re-applies the sparse set). Documented in README.
|
|
98
|
+
- **Cone mode keeps top-level files of ancestor directories** (e.g. `README.md`, `skills/README.md`). This is small and keeps `detect_skills` behaviour identical: it only walks directories that exist.
|
|
99
|
+
- **Skills referencing files outside their own directory** (`../../docs/x.md`) will break under sparse checkout. The `skills` CLI has the same limitation because it copies only the skill directory. `clone_strategy` is left in place; a future `full` value could opt out if this ever matters.
|
|
100
|
+
- **`skills`/`skills_excludes` filters are not used to narrow the sparse set.** Skill names live in frontmatter, which needs blob content; all detected skill dirs are small anyway and the lock/stale logic expects full detection.
|
|
101
|
+
- **No `--reclone` flag.** Out of scope; `rm -rf` of the store dir followed by `skm install` achieves the same.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
created: 2026-09-04
|
|
3
|
+
tags:
|
|
4
|
+
- sparse-checkout
|
|
5
|
+
- git
|
|
6
|
+
- clone
|
|
7
|
+
- detect
|
|
8
|
+
- vercel-skills
|
|
9
|
+
- research
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# 调研 vercel-labs/skills 的下载策略,并为 skm 实现 sparse-checkout 克隆
|
|
13
|
+
|
|
14
|
+
## 概要
|
|
15
|
+
|
|
16
|
+
起因是担心 skm 从大型 monorepo 安装技能时需要把整个仓库克隆下来,代价太高。先调研了 `vercel-labs/skills`(`npx skills add`)的实现:对普通仓库它同样是整仓 `git clone --depth 1` 到临时目录再复制技能目录;只有白名单 owner(vercel、vercel-labs、heygen-com、zapier/connectors)走一条不经 git 的 blob 快速路径(GitHub Trees API 列树、raw.githubusercontent.com 读 frontmatter、skills.sh 下载预打包快照),另外支持直接下载 raw SKILL.md 或 tar/zip 链接。
|
|
17
|
+
|
|
18
|
+
在 `herdrdev/herdr`(2851 个文件、5 个技能)上实测发现 skm 原有的 `--filter=blob:none` 对减小体积几乎无效:checkout HEAD 时仍要拉取当前树的全部 blob,工作区照样 46MB。真正有效的是不 checkout 无关目录。于是实现了方案 F:`--filter=blob:none --no-checkout` 克隆后,用 `git ls-tree -r -z HEAD` 离线列树,按 skm 既有的技能检测规则选出技能目录,`git sparse-checkout set --cone` 后再 `git checkout`;每次 pull 后重新计算 sparse 集合。端到端验证 store 从 48MB 降到 2.9MB(加 `clone_strategy: shallow` 可再降到约 600KB),146 个测试全部通过,已提交为 `0d44a2f`。
|
|
19
|
+
|
|
20
|
+
## 修改的文件
|
|
21
|
+
|
|
22
|
+
- `kb/plans/2026-09-04-sparse-checkout-clone.md` — 新建。含基准数据、五个任务的拆分、设计取舍与已知限制。
|
|
23
|
+
- `src/skm/detect.py` — 新增 `select_skill_dirs(paths, skills_dir)` 与 `_select_under()`,对路径列表套用与 `detect_skills` 相同的规则(根单例、`skills/` 优先、`skills_dir`、跳过 `_*` 与 `.git`、遇到 SKILL.md 停止下钻)。根单例返回 `None` 表示全量 checkout。
|
|
24
|
+
- `src/skm/git.py` — `clone_or_pull` 新增 `skills_dir` 参数,克隆改为 `--no-checkout`;新增 `list_tree_files()`(解析 `ls-tree -z`,跳过符号链接与子模块)和 `apply_sparse_checkout()`(无技能或根单例时 `sparse-checkout disable`)。已有仓库在 `git pull --ff-only` 后也重新应用 sparse 集合。
|
|
25
|
+
- `src/skm/commands/install.py`、`src/skm/commands/update.py` — 三处 `clone_or_pull` 调用传入 `skills_dir=repo_config.skills_dir`。
|
|
26
|
+
- `tests/test_detect.py` — 新增 9 个 `select_skill_dirs` 用例。
|
|
27
|
+
- `tests/test_git.py` — 重写两个命令序列用例(含根单例走 `disable` 的用例),新增 5 个真实本地仓库用例:只 checkout 技能目录、pull 后出现新技能、根单例全量、`skills_dir` 生效、旧全量克隆在 pull 后收缩。
|
|
28
|
+
- `tests/test_cli_e2e.py` — 新增 `TestSparseCheckout` 三个端到端用例;`fake_clone_or_pull` 签名补上 `skills_dir`。
|
|
29
|
+
- `tests/test_install.py` — `fake_clone_or_pull` 签名补上 `skills_dir`。
|
|
30
|
+
- `README.md` — 重写 clone 策略段落,说明 sparse 行为、`shallow` 的新含义、旧缓存在下次 `update` 收缩。
|
|
31
|
+
- `AGENTS.md` — 架构行、`detect.py`/`git.py` 描述、`skm install` 描述更新,新增 "Sparse Clone" 小节。
|
|
32
|
+
|
|
33
|
+
## 注意事项
|
|
34
|
+
|
|
35
|
+
- **`--filter=blob:none` 不减小工作区。** 它只延迟历史版本的 blob,checkout HEAD 仍需全部当前 blob。想省流量必须配合 sparse-checkout,这是本次最重要的认知纠正。
|
|
36
|
+
- **partial clone 下 `git ls-tree` 是离线的。** blob:none 过滤的只是 blob,tree 对象总会被拉取,因此 `--no-checkout` 后立即 `ls-tree -r HEAD` 不产生网络请求,可以据此决定 sparse 集合。
|
|
37
|
+
- **`select_skill_dirs()` 与 `detect_skills()` 必须保持同步。** 两者是同一套规则的两种实现(路径列表 vs 文件系统),改动检测规则时两处都要改,且 `test_detect.py` 两组用例都要更新。
|
|
38
|
+
- **cone 模式会保留祖先目录的顶层文件**(如根目录的 README、`skills/README.md`),体积很小,且保证 `detect_skills` 只会遍历真实存在的目录,行为与之前一致。
|
|
39
|
+
- **测试字符串里 `"\0100644"` 会被 Python 当作八进制转义 `\010`。** 构造 `ls-tree -z` 的假输出时要用 `\x00`。
|
|
40
|
+
- **skills CLI 的白名单快速路径依赖 skills.sh 中心化缓存**,实测该服务对非白名单仓库也返回 200,但对 skm 来说引入第三方服务不值得;tarball 方案虽最快但会丢掉 git 仓库,`check-updates`/`update` 的增量逻辑要全部重写,也不采用。
|
|
41
|
+
- **skills CLI 值得借鉴的另一点:** 它用 GitHub Trees API 做更新检查,按技能目录的 tree SHA 而非整仓 commit 判断是否有更新,上游改无关代码时不会误报。
|
|
42
|
+
|
|
43
|
+
## 遗留问题
|
|
44
|
+
|
|
45
|
+
- 本机 `~/.local/share/skm/skills/` 下的旧缓存不会被 `install` 收缩,需要跑一次 `skm update --all`。
|
|
46
|
+
- 技能 SKILL.md 若引用自身目录之外的文件(如 `../../docs/x.md`),sparse 后会断链。skills CLI 有同样限制。若将来遇到,可给 `clone_strategy` 加 `full` 值退出 sparse。
|
|
47
|
+
- `herdrdev/herdr` 在 `.agents/skills/` 下还有 3 个技能,但 skm 检测规则在存在 `skills/` 目录时只看 `skills/`,因此未发现。skills CLI 会同时扫描 `.agents/skills/`、`.claude/skills/` 等一组约定目录,可作为后续改进。
|
|
48
|
+
- `check-updates` 仍按整仓 commit 判断更新,可考虑借鉴按技能目录 tree SHA 比较的做法(`git rev-parse HEAD:<dir>` 即可,无需 GitHub API)。
|
|
49
|
+
- 调研用的 `tmp/vercel-skills/` 克隆仍留在工作目录(未纳入 git),不需要时可删除。
|
|
50
|
+
|
|
51
|
+
## 相关文档
|
|
52
|
+
|
|
53
|
+
- [Sparse Checkout Clone (Plan F)](../plans/2026-09-04-sparse-checkout-clone.md) — 本次 session 新建并按此计划实现
|