Skill guides¶
3 verified entries in plugin / skill.
distill¶
plugin:skill:distill
Use when the user wants to inherit a finished, dead, or abandoned session — "distill session X / 继承那个 session 的经验 / 接手它的工作 / harvest or salvage a session". Given a SpexCode session id, harness thread id, or transcript path, read it without resuming, extract decisions, traps, and next actions, salvage unmerged work, retire only resources proven safe to remove, then rename the inheriting session to mark the handoff.
| Field | Value |
|---|---|
| Source path | .spec/spexcode/.plugins/skills/distill/spec.md |
| Source revision | 981942eb4ba0dc1a263842e2cd17fba0622308c6 |
| Content SHA-256 | 799f8637c8cb782036767c1e9272313ff7111e97ba321e8dd0318eaf6deb14f5 |
| Content role | prompt |
# distill
Inherit a finished session's **mind and desk** without waking it: mind is its transcript; desk is its
worktree and branch. **Never resume, reopen, send to, or otherwise re-prompt the old session.** Read files
and git state only until its work has landed.
## Resolve and digest
Accept a SpexCode session id, a Claude/Codex thread id, or a transcript `.jsonl` path.
- For a SpexCode id, find `~/.spexcode/projects/*/sessions/<id>/session.json` (a unique prefix is enough).
Keep its prompt, `worktree_path`, `branch`, `harness`, and `harness_session_id`; Claude uses the session
id as its transcript id, while Codex uses `harness_session_id`.
- Otherwise pass the harness id or transcript path directly. The digest header supplies its `cwd` and branch
when recorded; those locate the desk even when it belongs to another repository.
From the repository root run the plugin's co-located digest:
`node .spec/*/.plugins/skills/distill/digest.mjs <id-or-path>`
It searches the harness's normal local transcript stores and prints human prompts, agent text, concise tool
calls, errors, metadata, edited files, and the raw path. A miss fails loudly; never recover by waking the old
session. For output above about 100 KB, have a subagent return only the distillation below.
## Distill forward
Do not paste the transcript or retell changes git already records. Preserve what git does not:
- goal, actual stopping point, and whether anything landed;
- decisions and rejected alternatives, with reasons;
- failures, dead ends, and user corrections;
- unfinished work and next actions;
- edited files, relevant spec nodes, and the raw transcript path.
## Salvage, then retire
Use the record or digest to inspect the old repository's worktree, branch, merge base, commits, dirty files,
and untracked files. Cross-check the digest's edited-file list because some work may live outside that
worktree. Distinguish an already-merged branch from a branch equal to its merge base, which never committed.
Carry unmerged commits onto the current branch with `git cherry-pick <base>..<branch>`; if replay is
unsuitable, apply the branch diff. Apply dirty changes and copy needed untracked files, then commit them
with the source session named. Keep authorship and `Session:` trailers where possible.
Only after verifying the salvage is present (or the branch truly merged), retire a SpexCode session with
`spex session close <id>`, or remove a bare worktree and then its branch. If proof is incomplete, keep the
resources and report why.
`<id>` is the OLD session's id — the one you are inheriting, spelled out. It is never `.` and never your own
id: `.` means THIS session (the rename step below relies on that), and closing yourself deletes the worktree
you are running in, mid-turn, along with your branch and record. Retiring the source is the only close this
skill asks for; your own ending stays a declaration.
## Rename yourself
Last step: mark the inheritance on the board. If you run as a SpexCode session (inside a session
worktree), `spex session rename . "<name>"` — `.` selects this worktree's own session. Name what this
session now carries — the inherited goal going forward, not the old session's id and no `distill:`
prefix. Outside a SpexCode session there is nothing to rename; skip this step.
e2e-review¶
plugin:skill:e2e-review
Use after an e2e run that recorded whole-session video with a timeline sidecar, or when the human asks to review/annotate recordings — "审录屏 / 标注 / annotate / review the e2e videos". Splits the recordings into per-scenario clips, files each as video eval evidence, and points the human at the dashboard annotator.
| Field | Value |
|---|---|
| Source path | .spec/spexcode/.plugins/skills/e2e-review/spec.md |
| Source revision | 1055e5774acc0d15194e57e6660f99fee1d52b48 |
| Content SHA-256 | a35e88371b647bb775dc2e856a2e744fe94a2fc8f486772c214fdc7433d07b28 |
| Content role | prompt |
# e2e-review
Turn a whole-session e2e recording into reviewable, per-scenario **video evidence** — filed through
eval readings, reviewed in the dashboard. This skill imports a WORKFLOW (ported from gugu's `.agent/skills/e2e`,
its ancestor); the tooling it once shipped (a standalone HTML annotator, a Range-serving HTTP script, a
frame-PNG saver) is already product — the dashboard annotator, the blob route's Range support, the
issue/evidence seams — so the skill never starts a second UI.
## input contract
A directory of recordings, scanned recursively for pairs: one `.webm` beside one `*.timeline.json`
(Playwright POOL workers each produce such a pair; any emitter that writes the shape qualifies). The
emitter timeline is `{ events: [{ atMs, kind, label }] }`:
- `kind: "narrate"`, label `▶ <scenario> · <title>` — a scenario's start boundary (the next `▶` ends it).
- `kind: "frame"`, label `📷 <step>` — a named step inside the running scenario.
The same scenario appearing in several recordings (retry, another worker) resolves to the newest.
## the loop
1. **Split** — `node .spec/<root>/.plugins/e2e-review/split-recordings.mjs <recordings-dir> <out-dir>`
(`--ffmpeg <path>` if ffmpeg isn't on PATH). Each scenario becomes `<scenario>.mp4` (h264 faststart,
browser-seekable) plus `<scenario>.timeline.json` in SpexCode's step-timeline format (`{v:1, events:
[{tMs, step}]}`, clip-relative — validated LOUD at filing). No title cards, no burned-in captions:
the annotator renders scenario context live from the spec tree, so pixels stay evidence.
2. **File** — map each clip to the spec node whose behavior it exercises (`spex spec search <topic>`; the
node's `eval.md` names its scenarios) and file your verdict WITH the clip:
`spex eval add <node> --scenario <s> --pass|--fail --video <clip>.mp4 --timeline <clip>.timeline.json`.
3. **Hand to the human** — the dashboard Evals page (`#/evals`), video-first: each clip plays in
the annotator with its clickable step ruler; the human circles a region to file an issue on the
responsible node (clip + timeline ride as typed evidence), disputes a verdict with their own manual
reading, or discusses on the eval's comment thread. Say where to look; do not build or serve
anything else.
## judgment stays yours
The split is mechanical; the FILING is not. A clip's verdict is your reading of the recording against
the scenario's `expected` — watch before you file, and file `--fail` honestly when the loop broke.
Scenario names come from the `▶` markers, so the emitter's vocabulary should match the governed
node's `eval.md`; when they diverge, fix the emitter or the eval.md, never hand-rename clips.
taste¶
plugin:skill:taste
SpexCode's engineering taste — load when making a design/architecture decision, weighing whether a change is worth its complexity, or sanity-checking work against the project's principles (harness-agnostic zero-friction adoption, spend-complexity-only-to-buy-it-back, one unified mechanism over special-cases, self-launch-is-the-main-body).
| Field | Value |
|---|---|
| Source path | .spec/spexcode/.plugins/skills/taste/spec.md |
| Source revision | a2e5b0c52c04a694420a4408befc821cd2aff54d |
| Content SHA-256 | baafa73d8bd29e0d62ed33a83fc1a84ece162c2851411239227286bef4ff536a |
| Content role | prompt |
# taste
## raw source
SpexCode is **self-referential** — we build a spec-driven tool *with* the spec-driven tool, so the principles
that guide its development must themselves live in the spec tree, not only in a chat that can be compacted away.
These are the durable "tastes" (品味) the maintainer has stated while building it. Preserve the raw intent; let
the campaign distill + refine, and let the rules that should govern every agent graduate into `.plugins`
surface:system nodes (the way [[memory-hygiene]] already did).
The principles, in the maintainer's own framing:
1. **Harness-agnostic, zero-friction adoption.** The ideal path is `npm install spexcode` → `spex init` →
the user launches their own `claude`/`codex`, with NO further human operation, NO pollution of global
claude/codex, and NO overwrite of the user's existing CLAUDE.md / AGENTS.md content.
2. **Deterministic, simple, UNIFIED system — not tons of special cases.** Favor one mechanism over many
`if/else` branches.
3. **Spend complexity only to BUY it back.** A user need earns a code change ONLY when satisfying it
*reduces* system complexity, or you have a clever way to make it a complexity-reducer — never "one more
if-else." Don't add complexity trading for a need.
4. **The project folder holds only human-readable things.** Abstract internal runtime (manifests, hashes,
locks) is hidden into the global store; what stays in-tree must be prose a human would accept.
5. **Self-launch is the MAIN BODY.** A user on the most naive Claude Code / a directly-launched Codex — NO
dashboard, NO server — must still get the FULL experience (prompt, hooks, every mechanism) through
`spex init`/`spex spec lint` + materialize→auto-discovery. The dashboard is one consumer that REDUCES to that
path plus a minimal governed delta. Adapting to this "unmanaged" usage is the forcing function that makes
the architecture robust and unified — the point is NOT to add if-else per usage mode (that loses the forcing).
6. **Memory hygiene** ([[memory-hygiene]]): never write session/role-specific content or identity markers to
the project-keyed memory — by instruction, not programmatic control (no 画蛇添足).
7. **Use fresh-context agents** to brainstorm, confront, debate, and divide labor — they are less overfit,
carry a naive taste, and dare to challenge the architecture. They are also a **complexity probe**: if a
fresh agent can't understand the spec↔code relationship, grasp what the code is for, or round-trip
spec→code→spec, that misalignment IS the measurement. Don't drag a huge context and brute-force solo.
8. **Read the docs, read the source, run experiments.** Many mechanisms are only understood after
experimenting; the codex source reveals the least-convoluted, most-claude-unified implementation. Don't
reason from assumption.
9. **Milestone merges, not big-bang** — land work in versioned milestones so a rollback is cheap.
10. **One frontend design language; unify the icons; NO emoji** (e.g. retire the attachment 📎).
11. **Keep finding behavior-equivalent but simpler / more-unified approaches**, and sanity-check every change
against these existing principles.
12. **Self-reference**: sediment this guidance into the spec / `.plugins` / source so it is never lost.
13. **YATU** (You As The User): measure through the real product surface a user touches, not an internal helper.
14. **Capabilities enter the ecosystem through the pillars we already stand on** (git / agent harness / test
framework) — before adopting a new protocol or dependency, check whether an existing pillar already
delivers it indirectly. The canonical case: LSP-grade code intelligence reaches SpexCode *through the
harness* (agents navigate code; the LLM judge reads semantics) and symbol-level history *through git*
(`log -L`, hunk-header funcname drivers) — so SpexCode never speaks the LSP protocol itself. A capability
worth having usually has a pillar-native form; integrating it directly is how tools bloat.
15. **Each pillar gets exactly ONE adapter seam.** The maintainer's framing: "harness adapter, language
adapter, test framework adapter — 我们最终会拥有这三方面的适配". Harness adapter exists (`harness.ts` +
launchers + materialize); language adapter is the anchor/coverage extraction seam; test-framework adapter
is where eval evidence producers will plug in. Every seam shares one shape: an interface + an ordered
registry + per-instance DATA rows (a new harness/language/runner is a row, not a branch), loud degradation
when a tier is unavailable — and product semantics never learn which adapter sits on the other side.
16. **Don't invent what the pillar isn't prepared for.** The maintainer's framing: "不要擅自发明 git 没有准备
好的东西". Stated when rejecting an explicit node-identity field to make rename tracking provable: git
*detects* renames by content similarity rather than *recording* them, so an id field would erect a second
identity model beside git's object model — two truths to keep in sync, and no way to know which to believe
the first time they disagree. The consequence is accepted deliberately: we live inside git's rename
semantics, and therefore the acceptance bar is "agrees with git's rename events across the whole tree",
not "provably correct in some model git does not have". This is [[taste]] 14 applied to the seam itself —
a pillar's *limits* enter the ecosystem along with its capabilities.
17. **Where the application-level call is genuinely unclear, let implementation simplicity decide.** The
maintainer's framing: "对于这种应用层拿不准的地方,我们就从自己的实现角度考虑,哪种实现最简单就怎么来".
Not a tie-breaker to reach for early — it applies precisely when the product argument has been made in
good faith and stays balanced. Then the honest question is which option costs less mechanism, and the
answer is often that one of them is already the behaviour and costs zero lines.
18. **Stabilise first, then measure the shape of the cost — not just its size.** The maintainer's framing:
"先保证稳定,然后计算一下复杂度,是线性还是平方级,和 git 历史长度是否有关,还是说只和文件数目有关".
A wall-clock number tells you a run was slow; the scaling dimension tells you which lever exists. The
same 4-second lint means opposite things if it grows with history (nothing local can fix it) versus with
source bytes (memoisation applies) versus with node count (the walk is wrong). Measure the dimension
before designing the fix, and never buy a cache before knowing whether the work is repeated at all.
19. **Separate what is PERMANENT from what is CURRENT, and let the invalidation scope be derived, not
assigned.** Git history is append-only: what a commit changed, what trailers it carries, what a merge
itself authored are *permanent properties of that commit* — they can be accumulated once and never
recomputed. What those facts mean *today* — which node owns a path after renames, which window a
version opens — is a question about the current tree, and is cheap. The costly half is permanent; the
tip-relative half is cheap. A read that recomputes the permanent half from scratch pays O(history)
forever, so the project gets slower every day it is worked on — measured here as ~0.33 ms per commit
across two full-history walks. The same confusion at the cache layer produces a worse failure: when a
rebuild costs more than the interval of the watchdog that guards it, every finished rebuild is
invalidated before it can be served, and a safety net becomes a latency amplifier — the fix is to make
the rebuild cheap, never to lengthen the interval, which only moves the threshold. The corollary is
about *scope*: derived data spread across several projections (board, review snapshot, session eval
revision) has no single place declaring what depends on what, so an invalidation scope ends up
hand-assigned by whoever traced the call chain last — and the honest default becomes "invalidate
everything". Cheap correctness there comes from letting a projection *declare its inputs*, so the scope
follows from the declaration; until it does, narrowing a scope by inspection is a correctness change
disguised as a performance one, and must be proven field-by-field against a full recompute. Keep the
slow, obviously-correct full recompute in the repo as the specification, and hold every faster path to
byte-equality against it.
20. **What falls out at a seam is picked up, never designed for.** The maintainer's framing, invoking
Liang Wenfeng's spirit: on the road to AGI, the things you can get along the way you simply take. The
three layers ([[spexcode]]) are the main body and are judged by their own criteria; how natural or
accessible the L0/L1 seam happens to be — the "borderland" — is **secondary**. Small capabilities fall
out of it for free (a session with no board membership still has a readable mailbox; a read still works
with no backend running; a subscription may or may not be persisted because it carries no truth). Take
them: they cost nothing, and the fact that they fall out at all is evidence the seam sits where it
should. But a borderland convenience is never a criterion and never a reason to move the seam — its
real value is the quiet pressure it puts on the architecture to stay honest. The failure this guards
against is promoting the byproduct to the definition: restating the layering as a horizontal
read-versus-own cut classifies *operations* and silently destroys the *adoption ladder*, which is the
thing that makes each layer worth having on its own.
21. **Our real failure mode is misidentifying the data source, and it never announces itself.** The two
operating rules, in the maintainer's framing: "先读答案自己带的元数据,再 diff 它的正文" and "一个坏的提取器
会给出最令人安心的那种假答案". This is knowledge about how *we* go wrong, so it is governed like any other
contract. A reasoning error runs into a contradiction and gets caught; reading the wrong source returns a
clean, internally consistent, confidently wrong answer, and nothing in the loop objects. Measured in one
evening, four times, none caught by a human: `/api/graph`'s body read while ignoring the
`x-spexcode-graph: stale, refreshing` header it attaches to itself, turning one mid-rebuild sample into a
claimed permanent product hole; a comparison keyed on a top-level `overlays` field the board does not
have (its keys are `identity`/`issuesStamp`/`nodes`/`sessions`, overlays live under `nodes[].overlays`),
so both sides returned zero and the script reported "identical"; an isolated Vite server that ignored
`?api=` and served its default backend, so two eval readings measured the wrong process; and a
43-node/0.487-zoom reading taken as a whole-tree sample when it was already the drill-down's *folded*
output, which defined a feature for a phenomenon that does not exist. So: before diffing a body, read what
the answer says about itself — its freshness label, its own key set, which endpoint actually served it.
And prefer the product's own instrument to a measurement built for the occasion: a `PATROL-REPAIR` count
of zero answers the causal question ("was any watcher blind?") that a 1-second stopwatch poller can only
circle around, and four separate experiments that evening were made unnecessary by first asking whether
the product already had the answer. A cheap extractor is not the cheap half of the job; it is where the
expensive mistake lives.
22. **Prose is not a free edit: you can hold a tree's proof and then move the contract that proof was
against.** The maintainer's framing: "散文不是免费的编辑:你可以握着一棵树的证明,然后把那个证明所针对的
契约挪走。" Freshness has more than one axis, and the rule we already wrote down only guards one of them:
"commit the verified tree, then file" catches the *code* moving after the proof, and says nothing about the
*contract* moving after the proof. Measured, self-caught by the session that did it: it filed a passing
reading, then softened the wording of the same scenario in `eval.md`, and both readings went stale on the
scenario axis — in its own words, "I was holding the proof of that tree, and then I moved the contract that
proof was against." The reason this one gets its own entry rather than an amendment to the code-axis rule is
that the two feel nothing alike: editing code, you know you are moving the thing under test; editing wording
feels like polishing a document, and `scenarioHash` disagrees. Its sibling is the same shape wearing a
different costume — a *sampling* choice that silently decides the claim. One-build-per-fresh-process does not
merely hide an in-process memo, it **inverts the conclusion**: 507ms cold against 12–15ms warm is a factor of
forty, so "a cache buys ~0 here" and "this is the headline finding" were separated by nothing but how the
sample was taken; the corpus picked to prove it could not exhibit the defect at all (0 of 1144 distinct
reading `codeSha`s reachable from that HEAD, therefore zero selector queries reaching the engine). Hence the
method rule that came out of it: on a loaded box a **fixed wall-clock threshold is not a claim** — compare
load-matched pairs, and put the falsifiable half of the claim on something load-independent. The stopwatch is
the symptom; the count of git child processes is the assertion.
23. **An expected value that can be derived must be derived — hardcoding it once opens a second source of
truth.** Say the fact once; derive it everywhere else. Two independent instances landed in one evening,
arrived at from opposite directions, and both are *guards* rather than fixes: `docs-quickstart.test.ts`
reconstructs the expected harness list from the live `NATIVE_HARNESS_IDS` registry, so registering a
harness without updating the three surfaces that teach adoption (both READMEs' Quick start, `spex guide`'s
setup page) fails there — measured, on the author of this entry, within the hour of adding a new harness;
and `session-create-cli.test.ts` parses `cli.ts` with the TypeScript AST to derive three sets
(the `VALUE_FLAGS` literal, every `flag('x')` value read, every `rejectUnknownFlags` allowlist) and
assert their relationship with **no flag name written in the test at all**, so the next person who adds a
valued flag and forgets the distant table fails on the right assertion instead of receiving a confident
wrong sentence about a *different* flag. That second failure mode is the whole point: a restated list does
not fail loudly when it drifts, it fails *elsewhere*, wearing the costume of an unrelated bug. This is the
same bone as [[taste]] 16 — two truths will disagree eventually, and on that day nothing tells you which
one to believe — and it is what [[taste]] 3 looks like when complexity is genuinely bought back: an AST
test is more code than a string array, and it retires a class of defect rather than one instance of it.
The discipline it pairs with is 22's, but the two are not the same kind: 22 is *temporal* (do not move the
contract after the proof) and a human has to remember it, while this one is *structural* and enforces
itself — a derived expectation catches you without anyone remembering anything. Two cautions keep it
honest: derive from the authoritative home, because deriving from a copy silently promotes the copy; and
the rule is about *expectations*, not about outlawing constants — a fact with exactly one home is fine
where it lives, the defect is the second place that restates it.
## expanded spec
This node is the seed. The de-drift campaign distills these into a sharper checklist (the "20 tastes" + the
issue-selection criteria), audits the tree against them, and graduates the agent-governing ones into
`.plugins` surface:system so every launched agent inherits them. Until then, this node is the durable record —
read it when a design decision needs the project's own taste, and add to it (raw source first) when the
maintainer states a new one.