filetree
GitHub定义 /filetree:init 和 /filetree:update 命令共享的规则,包括摘要风格、语言统一策略及更新时的 UNCHANGED 优化机制。
Trigger Scenarios
Install
npx skills add nekocode/filetree-skill --skill filetree -g -y
SKILL.md
Frontmatter
{
"name": "filetree",
"license": "MIT",
"description": "Use when running \/filetree:init or \/filetree:update — the shared rules those commands load before generating or syncing FILETREE.md. Not invoked directly.\n"
}
Filetree Skill — Shared Rules
Cross-cutting rules used by /filetree:init and /filetree:update. The commands themselves contain step-by-step flows; this file holds rules that apply across modes so they're maintained in one place.
/filetree:lint is read-only script invocation and does not need these rules.
Summary style
One line, max 25 words, describes what the file is FOR (its role / purpose). Not what it implements internally.
- Good: "JWT auth middleware; parses token from request header and injects user_id into context"
- Bad: "Defines AuthMiddleware class with init and call methods"
- Bad: "Handles auth" (too vague)
Present tense. No marketing words. For the language to write summaries in, see "Summary language" below — never pick per-file.
Summary language
One run, ONE language. Every summary in the manifest — and the command's own narration — uses it. Without a single anchor, parallel sub-agents each guess and the manifest ends up mixing Chinese and English.
The command resolves the canonical language ONCE, up front, by this priority:
config.languagefrom thetodooutput (set when.filetree.jsonpinslanguage). When present it is authoritative — skip the rest of the chain.- Else the dominant natural language of
CLAUDE.md/AGENTS.md(the agent contract — most authoritative). - Else
README(any localized variant). - Else (
/filetree:updateonly) the dominant language of existing manifest entries. - Else English.
Then it passes that one language verbatim into EVERY sub-agent prompt ("Write all summaries in
UNCHANGED bias (for /filetree:update ONLY)
Scope. This entire section applies to
/filetree:updateonly. During/filetree:initthe manifest starts empty, so there is no old summary to keep —UNCHANGEDhas nothing to refresh andapplywill drop it. In init, every file gets a real summary. Do not apply this bias to init sub-tasks.
Why this matters. Hash changes trigger the LLM, but most code changes (typos, refactors, comments, small additions) don't change a file's purpose. Outputting "UNCHANGED" lets cmd_apply refresh just the hash and keep the existing summary — the manifest itself carries the memory of "I already reviewed this version". In a healthy update run, 80%+ of changed items should resolve to UNCHANGED. Writing a fresh 25-word summary when the old one still fits wastes ~100x more tokens than a 4-byte "UNCHANGED" reply.
Decision rule. You have: old summary, old hash, new hash, and the file's new content (prefer reading the git diff over the full file — diff is far denser per token and is all you need for purpose-level judgement). If the diff comes back EMPTY (the change was already committed, so working tree == HEAD), fall back to reading the file — the hash moved, so judging purpose from a blank diff would falsely yield UNCHANGED.
Output "UNCHANGED" if the old summary still describes the file's PURPOSE. Refactors, renames, bug fixes, test additions, formatting, comment changes, small additions — these almost always leave the purpose intact.
Output a new summary string only if:
- A major new feature has been added that meaningfully expands purpose
- A previously central concern has been removed
- The file has been substantially rewritten for a different goal
- The old summary is in the wrong language (not the run's canonical language — see "Summary language"). Rewrite it in the target language even if the purpose is unchanged; this is how a legacy mixed-language manifest converges — gradually, as each file's hash changes and re-enters the work plan. Language mismatch ALWAYS overrides the UNCHANGED bias.
When in doubt (and the language already matches), output UNCHANGED.
Rationalizations — every one resolves to UNCHANGED
The pressure to "be thorough" pushes toward rewriting. Each excuse below is a trap; the right answer is UNCHANGED.
| Excuse | Reality |
|---|---|
| "The diff is large, so I should rewrite" | Diff size ≠ purpose change. A 500-line refactor with the same role is UNCHANGED. |
| "Let me polish the old summary while I'm here" | Polishing burns ~100x the tokens of UNCHANGED and isn't an exception. Only purpose change or wrong language qualifies. |
| "It's slightly more accurate now" | "Slightly better wording" is not "purpose changed". UNCHANGED. |
| "I'm not sure the purpose changed" | Not sure = it didn't. UNCHANGED. |
| "New function added, must re-describe" | A helper added to the same role doesn't expand purpose. UNCHANGED. |
Red flags — STOP, you're about to waste tokens
- About to write a summary that says the same thing as
old_summaryin new words - Justifying a rewrite by how much the code changed rather than whether the role changed
- "Improving" or "tidying" a summary whose language already matches
- Reading the full file when the
git diffalready answers the purpose question
All of these mean: output "UNCHANGED".
Symlinks
Some added / changed items carry a symlink_target field. For those: do not Read the file — a Read follows the link to the target's content (wasteful, and fails on a broken link). Write exactly symlink → <target> using the supplied symlink_target; do not infer a role you can't see. The script already hashes symlinks correctly from the link string.
Processing the work plan (todo --split)
Always run todo --split (the script chunks the LLM work and writes it to files, so you never count, truncate, or hand-split). Output:
{ "stats": {...}, "removed": [...], "renamed": [...],
"manifest_exists": true,
"config": {"manifest_path": "FILETREE.md", "language": null},
"split_dir": "/tmp/filetree_XXXX",
"batches": [{"file": ".../batch_00.json", "count": 25}, ...] }
The config block reflects .filetree.json (the script is the only config parser — never re-read the file yourself). manifest_path is where the manifest lives (may be renamed / relocated); language pins the summary language (see "Summary language" priority 0). exclude / include filtering is already applied inside the script, so the work plan only lists files that belong in the manifest. manifest_exists is whether the manifest file is already on disk — /filetree:update uses it to detect a not-yet-initialized repo (a present-but-empty manifest also reads total_in_manifest: 0, so the boolean is the reliable signal).
Each batch_NN.json is a JSON array of todo items (added + changed). Drive it purely off batches:
- 0 batches → no LLM work; no part files exist to glob, so apply the empty payload via stdin (it still syncs removed/renamed from repo state):
echo '{"updates": []}' | python3 .../filetree.py apply - 1 or more batches → spawn one
claude-haiku-4-5sub-agent per batch (good enough, ~10x cheaper) — always, even for a single batch. The main session never Reads a file or writes a summary itself; that work stays out of its context. Each sub-agent:ReadSKILL.md,Readits assignedbatch_NN.json, then write<split_dir>/part_NN.json. Sub-agents run in parallel and never see each other's batch.
Part-file shape (no hand-merging, no hashes)
Each part_NN.json carries ONLY summaries — hash is computed from disk, removed/renamed are recomputed from repo state, so neither belongs here:
{"updates": [{"path": "...", "summary": "..." | "UNCHANGED"}]}
Apply all parts in one call (the shell expands the glob, the script merges):
python3 .../filetree.py apply <split_dir>/part_*.json
Coverage gate — evidence from apply, never a hand-rolled diff
Before claiming the manifest is synced, run this gate on apply's return:
- READ
missing_from_manifest— any indexable file still without an entry (a dropped sub-agent output, a forgotten file). This is the completion gate. - READ the fixable anomaly keys:
skipped_unchanged_new(a wrongUNCHANGED),skipped_missing_path(a hallucinated path). - If 1 or 2 are non-empty → spawn one more
claude-haiku-4-5sub-agent to summarize those files. Pass it thesplit_dir, the exact paths to fix, and an output filename that does NOT collide with the existingpart_NN.json(usepart_fixup_NN.json, NN incrementing each retry). It writes<split_dir>/part_fixup_NN.json; then re-runapply <split_dir>/part_*.json— the glob still catches the fixup part, andapplymerges and dedups by path. Keep this fixup off the main session too — same as the batch work. Loop until both clear. - IGNORE
skipped_excluded— real files the config keeps out, nothing to fix. Do NOT gate onapplied == received: a legitimateskipped_excludedmakesapplied < receivedhold forever, which would loop here. - ONLY THEN report — straight from
apply's return.
Never hand-roll a coverage diff (concatenating batch lists, comparing counts): it is redundant and error-prone. The script's keys are the only evidence.
Version History
- 08fee6c Current 2026-07-05 20:15


