Use when distilling a finished specode spec into atomic, locate-oriented markdown knowledge points — writes cases/ + navigation/ + MEMORY.md into the project's own <project_root>/knowledge-base/ (md-only, no auto-injection), optionally copied to an Obsidian vault. Trigger ONLY /specode:distill <slug>, never auto-run by the main specode flow.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add qxbyte/specode --skill distill --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Distill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/qxbyte-distill)More formats (shields.io, HTML) on the badges page.
---
name: distill
description: Use when distilling a finished specode spec into atomic, locate-oriented markdown knowledge points — writes cases/ + navigation/ + MEMORY.md into the project's own <project_root>/knowledge-base/ (md-only, no auto-injection), optionally copied to an Obsidian vault. Trigger ONLY /specode:distill <slug>, never auto-run by the main specode flow.
---
# distill — project knowledge-base sedimenter (atomic dual-track)
> **Host-tool convention** 🔧: `AskUserQuestion` in this skill means a structured multiple-choice question — named for Claude-family hosts (Claude Code / CodeBuddy) where it's most reliable. On a host without it, use the nearest structured-question tool, or fall back to a plain-text question. The behavior (offer choices, wait for the user) is what matters, not the tool name.
## Top-level invariant 🔒
**The KB is "for locating, not for facts".** Every knowledge point distill writes is a **location pointer** — file path + call chain + reusable navigation experience, **not a factual conclusion**. The retrieval side uses it only to quickly **locate** real code; real code is always the only source of truth. Never advance a new requirement on KB content alone, or treat a historical conclusion as the truth about current code. This invariant is the reason the whole design exists; where any downstream behavior conflicts with it, this rule wins (see design doc §0).
The primary product is the **project directory** `<project_root>/knowledge-base/` (what the retrieval side reads), **not** Obsidian. Obsidian is only an optional human-readable copy.
---
## Positioning & history
distill is a **manual, on-demand** sedimenter: it turns a finished (or in-progress) spec + the **current agent context** into a set of **atomic knowledge points** (one requirement fans out into N standalone docs), files them into the spec's owning project `knowledge-base/`, and maintains a lightweight index `MEMORY.md`.
**md-only** (produces only markdown, no silent injection). Current shape = a project-`knowledge-base/`-primary atomic dual-track (case + navigation) location sedimenter + optional Obsidian copy. (The v1-v3 auto-injection / yml shape was fully removed in 4.0.0 / 5.0.1; for the old behavior checkout `backup/specode-v3.4.0-task-swarm-v0.9.2`. History detail is in the CHANGELOG.)
---
## Trigger
```
/specode:distill <slug> [--target-dir <abs-path>]
```
| Arg | Default | Meaning |
|---|---|---|
| `<slug>` | (required) | spec slug under `<specsRoot>/` |
| `--target-dir` | (none) | **optional** Obsidian copy directory (absolute path; written directly, no concatenation). If omitted, Step 5 asks via `AskUserQuestion` |
The primary product **always** lands in `<project_root>/knowledge-base/` and cannot be overridden by an argument. `project_root` is read via `resolve_root.py read-project-root --spec <specsRoot>/<slug>` (from that spec's `requirements.md` frontmatter `project_root`), **never re-derived from cwd**.
**Manual only**: there is no auto-trigger. After acceptance the main specode flow only **offers** the entry point (「是否进入 distill 沉淀本次经验?」), never forcing or auto-running it. Run this command when you want to update the project's location library.
> **Recommended: run distill after execution + acceptance are complete.** Distilling "landed + verified" knowledge points has the highest value; distilling an unfinished spec risks pointers into **planned-but-unbuilt code** (e.g. a util file the tasks plan mentions but execution never created). Step 1 has a completion check that warns about this.
---
## Inputs
| Source | What it provides |
|---|---|
| `<specsRoot>/<slug>/` | spec dir (**read-only**): `requirements.md` / `design.md` / `tasks.md` / `implementation-log.md` and other structured facts |
| `<specsRoot>/<slug>/requirements.md` YAML frontmatter | `project_root` (read via `read-project-root`, used to locate the primary product directory) |
| **current agent context** | **human navigation experience** beyond the spec itself: front/back-end call chains, "this page button → which file", "which config maps to which backend entry", etc. — the model distills these from the current context (the primary source of navigation-type points) |
---
## Output structure
```
<project_root>/knowledge-base/ # primary product (not committed to the repo)
├── MEMORY.md # generated by knowledge.py memory-rebuild from each doc's frontmatter
├── cases/<topic-kebab>.md # one case-type atomic point per file
└── navigation/<topic-kebab>.md # one navigation-type atomic point per file
<obsidian-target-dir>/ # optional copy: cp the docs over + rerun memory-rebuild there
├── MEMORY.md
├── cases/...
└── navigation/...
```
- Files are named by **topic** (kebab), **not by slug**; `slug` degrades to the docs' frontmatter `来源` provenance field. Each doc's frontmatter / body structure follows `references/doc-template.md` (two templates: `case` / `navigation`, frontmatter keys are fixed).
- `MEMORY.md` is **never hand-edited** — always rebuilt in full by `knowledge.py memory-rebuild` from each doc's frontmatter (frontmatter is the single source of truth).
---
## Flow (5 steps)
> Every `resolve_root.py` / `knowledge.py` call **must** go through the `run.sh` wrapper. The scripts live in the plugin's `scripts/` directory — relative to this skill that is `../../scripts/`; use this skill's base directory to make the path absolute (no env-var resolution, no cache `find`). Call them as `../../scripts/run.sh ../../scripts/<name>.py <verb>`.
### Step 1 — Resolve slug + project_root, make dirs, ensure .gitignore
1. Get `<specsRoot>` via `resolve_root.py get-root` (**exit 4** → specsRoot configured but **unreachable**, e.g. external drive not mounted → surface the cause and re-prompt for a path rather than reporting the spec missing) and confirm `<specsRoot>/<slug>/requirements.md` exists (else error out).
2. Read `project_root`:
```bash
sh ../../scripts/run.sh ../../scripts/resolve_root.py read-project-root --spec <specsRoot>/<slug>
```
exit 0 → stdout is `project_root`; non-zero (missing frontmatter / invalid path) → error and tell the user the spec needs its `project_root` field first.
3. `mkdir -p <project_root>/knowledge-base/cases <project_root>/knowledge-base/navigation`
4. Ensure `knowledge-base/` is in the project `.gitignore` (not committed):
```bash
sh ../../scripts/run.sh ../../scripts/knowledge.py ensure-gitignore --project-root <project_root>
```
5. **Completion check (F1, dangling-pointer guard)**:
```bash
sh ../../scripts/run.sh ../../scripts/resolve_root.py plan-unchecked --spec <specsRoot>/<slug>
```
- exit 0 → the spec's plan Tasks are all checked (executed), proceed normally.
- exit 2 (unchecked `- [ ]` Tasks remain) or exit 3 (no tasks.md and design.md has no checkboxes — no plan yet) → the spec is **not fully executed**, and distilled points may reference **unbuilt code**. First `AskUserQuestion` to warn and let the user pick 「继续沉淀 / 中止」; if they abort, end without writing any doc.
(5.x legacy specs keep the plan in design.md; `plan-unchecked` detects that automatically, no branching needed.)
### Step 2 — Read the whole spec + review agent context (don't consume old KB)
`Read` every `.md` under `<specsRoot>/<slug>/` (requirements / design / tasks / implementation-log, etc.), and review the "find a file / call chain / config mapping" navigation experience you repeatedly used in this session's agent context, saved for Step 3.
> **Don't consume old KB as fact**: do **not** read any old KB (`knowledge-base/` / `MEMORY.md`) as factual input. distill only **produces** pointers, never consumes historical KB as truth. (Exception: before writing a navigation point in Step 4 you may `Read` `MEMORY.md` **only for dedup comparison** — that's index maintenance, not feeding historical KB as fact into new content, and doesn't conflict with the invariant.)
### Step 3 — `AskUserQuestion` to propose the atomic breakdown (not skippable)
Per `references/breakdown-heuristics.md`, break the spec + context into a set of **atomic knowledge points**, one doc per point, in one of two categories:
- `case` — what this requirement "changed + in which front/back-end files + call chain + gotchas" (one point per independent change; don't cram a whole requirement into one doc).
- `navigation` — **project-level navigation experience** beyond this requirement (page button → file, config → backend entry, the location routine for a class of pages), reusable across requirements.
Give each candidate `标题 / 类型 / 来源(slug) / tags / 描述`; `tags` are taken from three kinds of concrete nouns — **page name / field name / functional domain** (no controlled vocabulary). `AskUserQuestion` lets the user confirm / add / drop / rename / recategorize; once locked, go to Step 4.
### Step 4 — Write case/navigation docs + rebuild MEMORY
Write each confirmed knowledge point per `references/doc-template.md` into `<project_root>/knowledge-base/{cases,navigation}/<topic-kebab>.md` (the host agent authors the markdown directly, no external writer CLI).
- Same-name file already exists → `Read` it, then ask the user `overwrite / skip / merge`.
- **navigation cross-spec dedup merge (must check before writing, F7)**: **before writing each navigation point, first `Read` `<project_root>/knowledge-base/MEMORY.md`** and compare by `tags` + `标题` for an existing same-topic navigation point — if found, **merge/update the existing doc** (don't create a duplicate file), else create new. Skipping the check and creating new produces duplicate points, and `memory-rebuild` **does not dedup** at the index layer (it would index both). (case is one-change-one-doc per requirement and usually doesn't involve dedup.)
Once all are written, rebuild the index (rebuilt in full from frontmatter, never hand-edit MEMORY.md):
```bash
sh ../../scripts/run.sh ../../scripts/knowledge.py memory-rebuild --kb <project_root>/knowledge-base
```
### Step 5 — Dual-track landing (optional Obsidian copy)
`AskUserQuestion`「复制一份到 Obsidian 库?」:
- **No** → end.
- **Yes** → have the user **enter an absolute path** (state clearly: this is the exact write directory, distill does **no** concatenation). If the path is under `/Volumes/`, first verify the mount (`ls "/Volumes/<name>"` succeeds, else refuse and warn). Then:
```bash
sh ../../scripts/run.sh ../../scripts/knowledge.py copy-to --kb <project_root>/knowledge-base --dest <obsidian-target-dir>
```
`copy-to` does it **in one step**: copies `cases/` + `navigation/` to the copy directory and rebuilds that directory's own `MEMORY.md` (dest absolute path written directly, no concatenation). The Obsidian side doesn't regenerate content, only copies + rebuilds the index. (The old "manual `cp` twice + remember to `memory-rebuild`" multi-step is replaced by this verb, eliminating the missed rebuild.)
> The dual-track logic belongs to distill alone: it doesn't affect the main flow, nor the retrieval side — retrieval always reads only the project directory's `knowledge-base/`, never the Obsidian copy.
---
## Red lines
| Red line | Note |
|---|---|
| Primary write domain = `<project_root>/knowledge-base/` | distill's primary product always lands in the project dir's `cases/` + `navigation/` + `MEMORY.md` |
| Spec dir read-only | never modify any file under `<specsRoot>/<slug>/` |
| `project_root` single source | read via `resolve_root.py read-project-root --spec ...`, **not re-derived from cwd** |
| Obsidian is an optional copy, written directly | `--target-dir` / the user-entered absolute path is used as the write directory as-is; distill does no path concatenation |
| `/Volumes/` mount check | when the copy dir is under `/Volumes/`, verify the mount first, refuse if not mounted |
| `knowledge-base/` not committed | local-private location asset; on landing, `ensure-gitignore` guarantees `.gitignore` contains `knowledge-base/` |
| Md-only | produces only markdown (no yml, no silent injection) |
| Don't consume old KB / no injection | distill doesn't read old KB as fact; its output feeds no later spec's `requirements.md`, and not task-swarm |
| MEMORY not hand-edited | always rebuilt in full by `knowledge.py memory-rebuild` from each doc's frontmatter |
| Read-before-overwrite | target md already exists → `Read` it, then ask the user overwrite / skip / merge |
---
## References
- `references/breakdown-heuristics.md` — distilling atomic knowledge points → the case / navigation two-category mapping.
- `references/doc-template.md` — the frontmatter + body templates for the case / navigation doc types.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!