| name | 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. |
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
<parent>/ 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.
cd <repo>/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)
meta root $(ls *.md 2>/dev/null | grep -vE '^[0-9]|^README')
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 implemented 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:
cd <workspace-root>
for b in <basename1> <basename2> ...; 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
git mv name.md .archive/NNN-name.md
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 implemented.
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)
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 the
spec:validate skill for the full document-model check.
Step 7: Commit the numbering pass (atomic)
One commit for renames + cross-reference fixes, README excluded:
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).
cd <repo>/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 old Shipped tracks /
Native backend / per-tier tables into the one index — they are now
redundant. Remove any pointer to a deleted
ARCHIVED.md.
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
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:
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 (learned the hard way)
- 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.