--- name: filetree description: > Use when running /filetree:init or /filetree:update — the shared rules those commands load before generating or syncing FILETREE.md. Not invoked directly. license: MIT --- # 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 "). 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 → ` 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 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): ```bash echo '{"updates": []}' | python3 .../filetree.py apply ``` - **1 or more batches** → spawn one `claude-haiku-4-5` sub-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: `Read` SKILL.md, `Read` its assigned `batch_NN.json`, then write `/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: ```json {"updates": [{"path": "...", "summary": "..." | "UNCHANGED"}]} ``` Apply all parts in one call (the shell expands the glob, the script merges): ```bash python3 .../filetree.py apply /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: 1. READ `missing_from_manifest` — any indexable file still without an entry (a dropped sub-agent output, a forgotten file). This is the completion gate. 2. READ the fixable anomaly keys: `skipped_unchanged_new` (a wrong `UNCHANGED`), `skipped_missing_path` (a hallucinated path). 3. If 1 or 2 are non-empty → spawn one more `claude-haiku-4-5` sub-agent to summarize those files. Pass it the `split_dir`, the exact paths to fix, and an output filename that does NOT collide with the existing `part_NN.json` (use `part_fixup_NN.json`, NN incrementing each retry). It writes `/part_fixup_NN.json`; then re-run `apply /part_*.json` — the glob still catches the fixup part, and `apply` merges and dedups by path. Keep this fixup off the main session too — same as the batch work. Loop until both clear. 4. IGNORE `skipped_excluded` — real files the config keeps out, nothing to fix. Do NOT gate on `applied == received`: a legitimate `skipped_excluded` makes `applied < received` hold forever, which would loop here. 5. 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.