--- name: wf-spec-housekeeping description: Repair the numbering of a specs/ directory: give name-only specs a stable NNN id, retire terminal specs into .archive/ keeping their number, fix the cross-references the moves break, and optionally rebuild the index in number order. Moves files and commits. Operates on one directory at a time, so it works whether specs sit flat under specs/ or inside a track. argument-hint: [--number-only | --index-only] [target-dir] allowed-tools: Read, Grep, Glob, Edit, Write, Agent, Bash(ls *), Bash(git mv *), Bash(git add *), Bash(git restore *), Bash(git commit *), Bash(git status *), Bash(git log *), Bash(git push *), Bash(grep *), Bash(sed *), Bash(awk *), Bash(perl *), Bash(cat *), Bash(wc *), Bash(sort *), Bash(uniq *), Bash(cut *), Bash(basename *) --- # Specs housekeeping Bring one specs directory back to a single invariant: **every spec in it is `NNN-name.md` in that directory's own number space; active specs stay in place, terminal specs retire under `.archive/` but keep their number so `depends_on:` frontmatter still resolves.** Then rebuild the index for that directory in number order. The scope is a *directory*, not a repo. Numbering is a per-directory reading order that composes with track grouping: `specs/local/003-live-serve.md` is both grouped and numbered, and `specs/local/003-…` and `specs/cloud/003-…` are separate number spaces that do not collide. Whatever moves here, the dependency graph is unaffected — `depends_on` paths are repo-root-relative and form one DAG across the whole tree, so keeping numbers stable across an archive move is what keeps that graph resolving. `$ARGUMENTS` may contain a mode flag and/or a target directory: - default (no flag): do both the numbering pass and the README rebuild. - `--number-only`: number + archive + fix references; skip the README rebuild. - `--index-only`: rebuild the README index only; assume numbering is already clean. - a path: operate on that directory — either another repo's `specs/`, or a single track inside one (`specs/local/`). This skill mutates files, renames with history, and commits. Work on `main` only if that is the repo's convention (check `git log`); otherwise branch first. ## Step 0: Fix the scope Decide which single directory to work on. A path in `$ARGUMENTS` names it directly; otherwise use `specs/` itself. Glob that directory's own `*.md` (not recursively — sub-directories are separate scopes, and child specs under a `/` folder belong to their parent, not to this number space). Then read the intent already in that directory: - Some or all specs already carry an `NNN-` prefix → it has adopted numbering; proceed to make it consistent. - No spec carries one → adopting numbering is a new convention, not a repair. Say so and confirm with the user before renaming anything. A directory full of track sub-directories and no specs of its own has nothing to number. Say which sub-directories are candidates and let the user pick one, rather than recursing on your own. If the repository, or a sibling repository you can read, already has a `specs/README.md`, use it as the reference shape for the index. Otherwise use this shape: a status-legend line plus one `| # | Spec | Status |` table ordered by number, active rows linking to the root and archived rows into `.archive/`. ## Step 1: Inventory Extract `(number, location, filename, status, title)` for every spec. Numbers come from the `NNN` filename prefix; name-only files have none yet. ```bash cd /specs meta() { local loc="$1"; shift; for f in "$@"; do n=$(basename "$f" | grep -oE '^[0-9]+') t=$(grep -m1 '^title:' "$f" | sed 's/^title: *//; s/^"//; s/"$//') s=$(grep -m1 '^status:' "$f" | sed 's/^status: *//; s/ *$//') printf '%s\t%s\t%s\t%s\t%s\n' "${n:-NONE}" "$loc" "$(basename "$f")" "${s:-none}" "$t" done; } meta root $(ls [0-9]*.md 2>/dev/null) # numbered active meta root $(ls *.md 2>/dev/null | grep -vE '^[0-9]|^README') # NAME-ONLY: the drift meta archive $(ls .archive/[0-9]*.md 2>/dev/null) ``` The name-only list is the work. For each, read its frontmatter `status`. ## Step 2: Classify — terminal vs live Status vocab varies by repo; map onto two buckets: - **Terminal** (archive it): `complete`, `shipped`, `implemented`, `superseded`, `abandoned`, `archived`, `stale`, `deferred`. Also treat a `draft`/`drafted` spec that carries an **Outcome** section (shipped-but-status-stale) as terminal, and flip its status to `complete`, the lifecycle status for shipped work, when you move it. - **Live** (keep at the root, just give it a number if it lacks one): `vague`, `drafted`/`draft` with no Outcome, `validated`, `testing`, `in-progress`. When a name-only spec's status is ambiguous, read the body tail for an `## Outcome` / "Done"/"shipped" signal before deciding. If still unclear, ask. ## Step 3: Assign numbers Numbers are **stable IDs**; never renumber an existing spec. Find the highest number in use across **both** the root and `.archive/`, and append from `max+1` in created-date order (read each `created:` field). Created-order is a good default because it keeps a superseding spec after the specs it supersedes; contiguous same-family numbering is an equally fine alternative. ## Step 4: Cross-reference grep BEFORE renaming (the step that blocks) A name-only spec is often a **cross-repo companion** — the same bare filename exists in sibling repos, and its own frontmatter `depends_on`/`affects` may point across `../`. Renaming can strand references. Grep the whole workspace for every basename first: ```bash cd # the dir that holds all sibling repos, e.g. ../ for b in ...; do echo "=== $b.md ===" grep -rIn --exclude-dir=node_modules --exclude-dir=.git "$b\.md" . done ``` Triage the hits: - **Same-repo hard links** — this repo's own frontmatter (`supersedes:`, `superseded_by:`, `depends_on:`), README rows, and internal doc links. **You must fix every one of these** in the rename commit. - **Sibling-repo index/prose listings** (another repo's README pointing at `thisrepo/specs/name.md`). These are usually already stale ecosystem-wide and are that repo's housekeeping, not yours. Do **not** edit sibling repos; note them for the user instead. (If the ecosystem convention is that every repo numbers its own copy, your rename matches it — say so.) ## Step 5: Move with `git mv` and fix same-repo references ```bash git mv name.md .archive/NNN-name.md # history follows; one per file ``` Then, in the same working set, fix every same-repo reference found in Step 4: - `superseded_by:` / `supersedes:` / `depends_on:` frontmatter paths that pointed at the old location (use the repo's path convention — usually repo-root-relative `specs/.archive/NNN-name.md`). - README table rows and internal `docs/**` markdown links. - Flip any shipped-but-`draft` status to `complete`. Leave live-but-newly-numbered specs at the root: `git mv name.md NNN-name.md`. ## Step 6: Verify the moves (do this before committing) ```bash # a) every same-repo depends_on / supersede target still resolves for f in specs/[0-9]*.md specs/.archive/[0-9]*.md; do awk '/^(depends_on|supersedes):/{c=1;next} /^[a-z_]+:/{c=0} c&&/^ *- /{gsub(/^ *- */,"");gsub(/"/,"");print} /^superseded_by:/{v=$2;gsub(/"/,"",v);print v}' "$f" \ | while read p; do case "$p" in specs/*) [ -f "$p" ] || echo "BROKEN $f -> $p";; esac; done done ``` Any `BROKEN` line must be fixed before you commit. Optionally run `/wf-spec-validate` for the full document-model check. ## Step 7: Commit the numbering pass (atomic) One commit for renames + cross-reference fixes, README excluded: ```bash git add -A && git restore --staged specs/README.md git commit -m "specs: number the N name-only specs into .archive/ (NNN-MMM)" ``` Follow the repo's commit conventions (check `git log`): small scoped diffs, no Co-Authored-By trailer unless the repo uses one, no em dashes in the message if the repo avoids them. ## Step 8: Rebuild README as a clean index (skip if `--number-only`) Generate the table **mechanically** from the inventory; do not hand-type ~100 rows or invent descriptions (use each spec's `title` as the topic). ```bash cd /specs { meta root $(ls [0-9]*.md); meta archive $(ls .archive/[0-9]*.md); } \ | sort -t$'\t' -k1,1n \ | awk -F'\t' '{ n=$1; loc=$2; file=$3; st=$4; title=$5 link=(loc=="root")?file:".archive/" file if(loc=="root"){ d=(st=="drafted"||st=="draft")?"drafted": (st=="in-progress")?"in progress": (st=="implemented")?"✅ implemented": (st=="complete")?"✅ complete": (st=="superseded")?"superseded": st } else { d=(st=="abandoned")?"📦 archived (abandoned)": (st=="superseded")?"📦 archived (superseded)": (st=="archived")?"📦 archived":"📦 archived (shipped)" } printf "| [%s](%s) | %s | %s |\n", n, link, title, d }' > /tmp/spec-index-table.md ``` Assemble the README from three parts (write head + foot with the Write tool, then `cat head table foot > README.md`) so you never paste the long table by hand: - **Head** — the repo's existing intro + a folder-layout paragraph (drop any now-false "unnumbered drafts sit at the root" sentence) + a status legend matching the badges above + the `## Specs` heading and table header (`| # | Spec | Status |`). - **Table** — the generated `/tmp/spec-index-table.md`. - **Foot** — the **institutional-memory** sections a flat table cannot encode. Preserve, do not delete: in-progress state, deferral triggers ("un-defer when…"), pending external/tunable decisions, locked decisions, and closed-scope ("do not re-litigate") lists. Fold any older per-track or per-tier status tables into the one index, which now carries their rows, and remove links to index files that no longer exist. Non-numbered archive files (`*-README.md`, spikes, reconciled drafts) stay unlisted in the table; mention them once in the folder-layout paragraph. ## Step 9: Verify the README, then commit ```bash # every markdown link in the README resolves (skip http and ../ cross-repo) grep -oE '\]\(([^)]+\.md)\)' specs/README.md | sed -E 's/\]\(//; s/\)$//' | sort -u \ | while read p; do case "$p" in http*|../*) ;; *) [ -f "specs/$p" ] || echo "MISSING $p";; esac; done ``` Fix any `MISSING`, then commit the README as the second atomic diff: ```bash git add specs/README.md git commit -m "specs: rebuild README as a single NNN-ordered index" ``` Push if the repo pushes to main directly (check convention). ## Report Tell the user: how many name-only specs were numbered and their new `NNN-name.md` targets; which were kept live at the root vs archived; every same-repo reference fixed; any **sibling-repo** listings left stale on purpose (with paths, so they can fix those repos); and whether the README was rebuilt. ## Gotchas - **The `.archive` grep undercounts the max number.** Active specs at the root usually hold the highest numbers. Compute `max` over root **and** `.archive/`. - **Cross-repo companions look local but aren't.** A spec covering one workstream that spans several repositories often carries the same bare filename in each of them. Grep `../` before renaming; expect sibling READMEs to reference the old name and leave them alone. - **A `draft` status can be a lie.** A spec with an `## Outcome` section shipped; treat it as terminal and correct the status on the way to `.archive/`. - **Never renumber.** Gaps in the sequence are fine and expected (folded/reverted specs). Only ever append at `max+1`. - **Don't delete institutional memory.** "Clean index" means one ordered table *plus* the decision-record prose, not a table that erased why things were deferred or ruled out.