From autopilot
Scans local conversation history to detect recurring procedures and corrections, then generates personal custom skills routed into your own skill directories.
How this skill is triggered — by the user, by Claude, or both
Slash command
/autopilot:distillThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
autopilot ships this **distiller** (the factory). The skills it produces are **yours** (the products)
autopilot ships this distiller (the factory). The skills it produces are yours (the products)
and land in your skill dirs, never in autopilot's repo. Mirrors how CC's run-skill-generator
writes a per-project skill; autopilot only ships the generator.
~/.claude/skills/autopilot-distill-skills/skills/<slug>/SKILL.md — a skills-directory
plugin pack (autopilot-distill-skills@skills-dir) that is also a private git repo = your fleet
sync unit, namespaced + separate from your hand-authored personal skills.<project>/.claude/skills/<slug>/SKILL.md — rides that project's own git.Routing is decided by the originating project (cwd) of each signal.
# routine run: only sessions new/changed since last distill (remembers a per-session cursor)
node ${CLAUDE_PLUGIN_ROOT}/scripts/distill-scan.js --real-only --new-only
# first ever run, or "show me everything again": drop --new-only for the full cumulative report
node ${CLAUDE_PLUGIN_ROOT}/scripts/distill-scan.js --real-only
Emits frequency atoms in two buckets: ritual candidates (de-noised procedural command
n-grams) and correction candidates (recurring user-friction contexts). Evidence (counts, source
project) is deterministic — never invented. --json for machine output; --top N to widen.
Cursor (--new-only / --incremental). Each session jsonl is scanned whole exactly once;
its per-session atom contribution is cached in ~/.autopilot/distill/scan-state.json keyed by
{size, mtime}. Unchanged (completed) sessions are reused — only new/grown ones are re-read.
Cumulative totals stay identical to a full scan (the ≥N× value gate is unaffected); the cursor
only changes which sessions are re-read and, with --new-only, filters the report to candidates
whose cumulative count rose this run — i.e. "what's newly worth distilling since last time". This
is what makes /distill cheap to re-run: it picks up where it left off instead of re-proposing what
you already triaged. (Deliberately NOT a raw byte-offset — that would split a session's command
sequence across runs and risk a half-written trailing line. See the script header.)
Name each genuinely recurring procedure; abstract to generic steps. Refuse to propose a procedure
that cannot be expressed without a specific literal (inherently-specific) — unless self-use scope,
where the user's own identifiers (their git email, their host alias) may stay. Classify each candidate's
scope (global vs which project) from its cwd.
Frequency mode scans history for what you KEEP redoing; episodic mode distills what you JUST finished while the memory is hot. Complementary by design — 頻率管跨週遺忘的長尾, 情節管熱記憶的深流程. Both structural blind spots of the ≥3× frequency threshold are episodic territory (2026-07-04 first full scan, empirical): a once-only project-scale methodology never crosses the frequency bar, and compound-command rituals are invisible to the tokenizer (scanner recall fixes are a separate BACKLOG item — not this mode's job).
Triggers: 「把剛做完的專案蒸餾成 skill」「趁熱把這套流程收下來」「這個專案的方法論值得留」, "distill this project/session" — or arriving from finish-flow L-5.6's evaluation question.
Steps 1E–2E replace Steps 1–2 ONLY. Everything from Step 3 on (identifier lint, human gate, normalize-slug, write + commit-on-approve, pack sync / consolidate) is the SAME pipeline — episodic products get zero special handling downstream.
Answer four questions about the just-finished project / long session:
Each candidate MUST cite its source event (which rework / which decision) — not a
frequency count. A candidate that cannot name a concrete source event is not proposed.
Routing follows the existing rules (global → pack; project-specific → that project's
.claude/skills).
For products meant to be shared or executed by weaker models, run one RED round (headless weak model on a real task → autopsy the artifacts only → enumerated patch). Methodology reference (external — lives in YOUR pack, never in autopilot): skill-red-testing.
Demotion drafting (quality-floor P4): from retro's escalation aggregate (or a direct tree.js escalations sweep), for each recurring escalation draft a CANDIDATE stub — a references/probe-playbook.md entry (4 mandatory fields per its schema) if the escalation was resolved by a novel probe, or a references/acceptance-patterns.md addition if it was an acceptance gap. Candidates are DRAFTS: they go through this skill's existing human-gated review before merging; never auto-append to the catalogs.
The gate stays, but the friction is collapsed: present the whole candidate list once and let the
user pick which to accept in a single AskUserQuestion (multiSelect: true) instead of one
yes/no per candidate. Approval is still explicit and per-candidate — nothing is written that the
user did not tick.
The lint runs first, per candidate, and gates the batch. Run the identifier lint + the user's
deny-list (~/.autopilot/distill/identifiers.deny, one real hostname/client name per line) on every
draft SKILL.md. The lint reliably catches structured tokens (email / IPv4 / /home/<user>/ / FQDN /
key-shapes); bare hostnames and client names are the gate's job.
This keeps the privacy backbone (no auto-write of anything the lint touched) while giving the "distill, then accept a batch" UX. For self-use scope, the user's own identifiers (their git email, their host alias) may stay — that exemption is theirs to grant per candidate, not a default.
On approval, write a well-formed SKILL.md (name + description so scripts/validate.sh passes;
body = the generic procedure). Parameterize identifiers; keep real values in ~/.ssh/config / local
config, not in the synced skill body.
Normalize the slug (pack scope) — the cross-machine convergence key. Before writing, run the slug
through the deterministic normalizer so two machines that name the same procedure land on the same
path (the precondition for consolidate in Step 5 to ever fire):
slug=$(${CLAUDE_PLUGIN_ROOT}/scripts/distill-consolidate.sh normalize-slug "<llm-chosen-slug>")
It lowercases, drops a tiny stopword set (fix/ensure/setup/…), and preserves token order (no
sort — readability kept), so fix-git-identity, git-identity-fix, ensure-git-identity all converge
to git-identity, while antonym pairs (add-user vs remove-user) stay distinct. Use the normalized
slug for the pack write path; set the frontmatter name: to match. (Project-scoped skills keep the
LLM's literal slug — a project has one repo, no fleet of writers to converge.)
cd ~/.claude/skills/autopilot-distill-skills
# write skills/<slug>/SKILL.md, then immediately:
git add skills/<slug>/ && git commit -m "distill: <slug>"
Now the approved skill is in git history → recoverable even under concurrency / machine loss.
First-ever pack: scaffold .claude-plugin/plugin.json + git init first. (Creating ~/.claude/skills/
the first time needs one CC restart; adding a skill to an already-loaded pack may need /reload-plugins.)The approved skill is already committed locally (Step 4). Sync = propagate that commit. After a batch of approvals, ask the user once (not per skill) "push these N distilled skills back to the shared private pack?" — a single yes/no.
Before pushing, check each pushed slug for cross-machine divergence — proactively, NOT by triggering a
merge conflict. For every <slug> in the batch:
${CLAUDE_PLUGIN_ROOT}/scripts/distill-consolidate.sh compare <slug> # JSON: identical|divergent|absent-theirs|absent-mine
identical / absent-theirs (no upstream divergence) → nothing to do; this slug just pushes.divergent (another machine already pushed a different SKILL.md for the same normalized slug) →
consolidate it now, in the clean working tree (no rebase/merge state is ever entered — this is the
whole point of comparing before committing the push):
mine = the working-tree skills/<slug>/SKILL.md; theirs =
git -C <pack> show @{u}:skills/<slug>/SKILL.md.name:/description:. If the two variants are not recognizably the
same procedure, STOP and hand to the user — do not merge unrelated content (the normalizer can,
rarely, over-collapse two distinct procedures; this is the backstop).AskUserQuestion: approve / edit / reject).skills/<slug>/SKILL.md with the canonical, git add,
git commit -m "consolidate: <slug>". On reject → leave yours; STOP/handoff that slug.Then push (normal, no merge commit — the canonical already contains theirs, so the rebase applies clean):
git -C ~/.claude/skills/autopilot-distill-skills pull --rebase # absorb other machines
git -C ~/.claude/skills/autopilot-distill-skills push # share the consolidated canonical
Other machines pick the canonical up on their next sync (both variants are now ancestors → no
re-conflict). Convergence is DAG-level; if a 3rd machine later adds yet another variant, the
canonical is re-merged then (content converges as variants stop arriving, not via a fixpoint guarantee).
Concurrent same-slug consolidate self-heals via git's push-reject (second push rejected → pull → re-merge).
Rollback (a bad canonical that already pushed): git -C <pack> revert <sha> && git push; other
machines absorb the revert next sync — but if a peer already re-consolidated on top, the revert is itself
a same-slug conflict → manual STOP. See references/sync-setup.md. Project
skills ride the project's own git; guard first-run by setting upstream first.
/distill does on a routine re-run)distill-scan.js --real-only --new-only → only candidates new since the last cursor.git commit into the pack (Step 4, commit-on-approve).compare each slug → consolidate any divergent one
(human-gated) → pull --rebase then push (this step).
The cursor advances automatically, so the next /distill resumes from new conversations only.Correctness note: the deterministic scripts (
normalize-slug/migrate/compare) are tested for git-plumbing correctness; the LLM merge quality is human-gated, not test-gated — the human gate (step 3 above) is the real backstop for whether a consolidation is correct.
A pack created before slug-normalization may hold non-normalized dirs (fix-git-identity etc.). Run once
per pack to rename them (dir and frontmatter name:) to their canonical slug so future cross-machine
compares line up:
${CLAUDE_PLUGIN_ROOT}/scripts/distill-consolidate.sh migrate # git mv staged — review + commit
If two existing dirs normalize to the same slug it STOPs (a real consolidation case — resolve by hand,
don't let migrate merge them). Tell the user to commit the rename + push so the fleet converges.
Don't make the user hand-copy git plumbing (the .gitignore negation is easy to get wrong — the
obvious .claude/ + !.claude/skills/ is silently broken). Drive it with the script:
scripts/distill-sync-setup.sh status (JSON: pack exists? has remote? + a
next-step hint on stderr).AskUserQuestion before proceeding:
distill-sync-setup.sh init-remote <url>
(machine #1) or enroll <url> (new machine). Both are idempotent.git -C <repo> check-ignore .claude/skills/<name>/SKILL.md
prints anything, the repo ignores it and it will never propagate. Run
scripts/distill-sync-setup.sh fix-gitignore <repo> (idempotent; emits the correct .claude/*
!.claude/skills/ form and verifies), then tell the user to commit the .gitignore change.Only ask when a decision is genuinely needed — if status shows a remote already configured, skip
the questions and just sync.
Durability — the pack MUST have a remote. A single on-disk copy is one
rm -rffrom total loss. The remote is backup, not just sync — set it up before relying on distilled skills (see sync-setup.md). Concurrency is loss-safe given commit-on-approve.
compare)Two machines distilling the same procedure now converge automatically: the slug normalizer (Step 4)
makes them collide on one path, and Step 5's proactive compare detects a divergent upstream
variant before committing the push, so the human-gated LLM merge runs in the clean working tree —
never inside a held rebase/merge transaction. The earlier per-host-staging design was rejected (it
regressed Claude Code skill loading and used a self-defeating content-hash key); see
plan 2026-06-04-distill-consolidate §v3 for the
design and the two dialectic rounds behind it.
| Script | Purpose |
|---|---|
scripts/distill-scan.js | Deterministic history scanner → frequency atoms (two buckets). --real-only, --json, --top N. Cursor: --incremental reuses cached per-session atoms (only re-reads new/changed jsonl; totals identical to full scan); --new-only reports only candidates risen since last run. State in ~/.autopilot/distill/scan-state.json. No LLM in the count path. |
scripts/distill-sync-setup.sh | Onboarding plumbing for pack sync: status / init-remote <url> / enroll <url> / fix-gitignore [repo]. Idempotent; emits the correct .claude/* + !.claude/skills/ negation (the obvious .claude/ form is silently broken). Drives Step 5 first-run setup. |
scripts/distill-consolidate.sh | Cross-machine consolidation plumbing (deterministic, no LLM): normalize-slug <raw> (machine-stable slug — lowercase + drop tiny stopword set + preserve order), migrate [pack] (one-time: rename existing dirs to normalized slugs and rewrite each frontmatter name: — a skill's identity is its name:, so both must converge; STOPs on collision), compare <slug> [pack] (proactive divergence check against @{u} → JSON identical/divergent/absent-theirs/absent-mine; no merge-conflict state). The human-gated LLM merge lives in Step 5, not the script. |
npx claudepluginhub cookys/autopilot --plugin autopilotDistills workflows from MemSearch memory into reusable skills. Manages skill candidate creation, review, and installation when the user asks to make, extract, or install a skill from past work.
Distill session insights into rules, skill improvements, recipes, and cross-repo promotions to marketplace plugins. Use when capturing learnings, codifying workflow into .claude/rules, or promoting a session-invented pattern into a specific plugin/skill as a PR.
Mines SpecStory coding histories from any agent into a persistent corpus, surfaces reproducible workflows as skill candidates, and interactively forges chosen ones into installed skills. Use when the user wants to turn past AI sessions into reusable skills.