Skip to main content
ClaudeWave
Skill161 repo starsupdated 7d ago

filetree

The filetree skill defines cross-cutting rules for file tree initialization and updates, including standards for writing file summaries, selecting a canonical language across the manifest, and preserving unchanged summaries during updates to avoid redundant token use.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/nekocode/filetree-skill /tmp/filetree && cp -r /tmp/filetree/skills/filetree ~/.claude/skills/filetree
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# 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:

0. `config.language` from the `todo` output (set when `.filetree.json` pins `language`). When present it is authoritative — skip the rest of the chain.
1. Else the dominant natural language of `CLAUDE.md` / `AGENTS.md` (the agent contract — most authoritative).
2. Else `README` (any localized variant).
3. Else (`/filetree:update` only) the dominant language of existing manifest entries.
4. Else English.

Then it passes that one language verbatim into EVERY sub-agent prompt ("Write all summaries in <language>"). Sub-agents never re-detect; they run in parallel and would diverge if left to choose.

---

## UNCHANGED bias (for /filetree:update ONLY)

> **Scope.** This entire section applies to `/filetree:update` only. During `/filetree:init` the manifest starts empty, so there is no old summary to keep — `UNCHANGED` has nothing to refresh and `apply` will 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_summary` in 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 diff` already 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:

```json
{ "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