| 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)
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
- 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).
- Read
project_root:
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.
mkdir -p <project_root>/knowledge-base/cases <project_root>/knowledge-base/navigation
- Ensure
knowledge-base/ is in the project .gitignore (not committed):
sh ../../scripts/run.sh ../../scripts/knowledge.py ensure-gitignore --project-root <project_root>
- Completion check (F1, dangling-pointer guard):
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):
sh ../../scripts/run.sh ../../scripts/knowledge.py memory-rebuild --kb <project_root>/knowledge-base
Step 5 — Dual-track landing (optional Obsidian copy)
AskUserQuestion「复制一份到 Obsidian 库?」:
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.