Skip to content

vt-t-gc

Periodic garbage collection for drift detection. Scans for spec consistency, orphaned files, stale references, and CLAUDE.md validation. Delegates to doc-sync/repo-health/repo-audit for their domains.

Plugin: vt-toolkit
Category: Other
Command: /vt-t-gc


Garbage Collection — Drift Detection

Detect and report entropy that accumulates across sessions: spec/config inconsistencies, orphaned files, stale cross-references, and CLAUDE.md drift. Designed to run at session start, via /loop, or on-demand.

When to Use

  • At the start of a work session (quick health check)
  • Periodically via /loop 30m /vt-t-gc
  • Before /vt-d-5-finalize (ensure no drift before merge)
  • After bulk renames or component removals

Invocation

/vt-t-gc                          # Full scan (all 4 categories)
/vt-t-gc --category specs         # Spec consistency only
/vt-t-gc --category orphans       # Orphaned files only
/vt-t-gc --category refs          # Stale cross-references only
/vt-t-gc --category claude-md     # CLAUDE.md validation only
/vt-t-gc --fix                    # Auto-fix correctable issues
/vt-t-gc --full                   # Force full scan (ignore incremental state)

Execution Instructions

Calling the status vocabulary. Every status-vocab.sh call in this skill is written in the cross-plugin form "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/status-vocab.sh". The ../vt-base/ hop is deliberate: the CLI lives in the base plugin, while $CLAUDE_PLUGIN_ROOT names this skill's own plugin. If $CLAUDE_PLUGIN_ROOT is unset or that file does not exist, run the copy relative to this skill file's directory instead, ../../../vt-base/bin/status-vocab.sh. If neither exists, stop the step and report both paths you tried; never classify from memory or from a list written here. Do not shorten the call to a path under the toolkit's plugins/ directory: that resolves only when the working directory is the toolkit itself, so the step could not read the vocabulary in a product repo. Never paste a PATH or a VALUE read out of a file into a command: a status and a file name are both repo content, and a $(…) in one runs when it reaches a shell (review pass 4, SEC-20). Single quotes are NOT the remedy — that advice shipped here and was measured wrong (review pass 6, SEC6-3): they stop $(…), but a directory committed as 999-x'$(touch PWNED)' CLOSES the quote and executes. Let the shell expand the glob and bind the loop variable, and paste only a number, which cannot carry a quote. classify-file and accepts-file take the file and read the value themselves.

An exit 2 is not an unknown literal. exit 3 says the vocabulary was read and this value is not in it — warn, and never default it. exit 2 says the vocabulary could not be read AT ALL, or that the artifact was refused before being scanned: a missing or unparseable data file, a key this reader will not identify, a duplicate declaration of the same field, or a file over the size bound (review pass 7, SEC7-4). Report it and stop; do not carry on as though the answer were empty and do not treat the value as acceptable. Stated once here rather than in each step, because it is the contract of every call this file makes.

Step 1: Parse Arguments

Extract flags from invocation: - --category {name} — run only the specified category (specs, orphans, refs, claude-md) - --fix — attempt auto-correction for fixable issues - --full — force full scan, ignore .gc-state.yaml timestamp

If no flags: run all 4 categories with incremental scanning.

Step 2: Load Incremental State

Read .gc-state.yaml from the project root (same level as .design-state.yaml).

If file exists and --full not passed: - Read last_run timestamp - Display: "Incremental scan (changes since {last_run})" - In subsequent category checks, prioritize recently modified files

If file does not exist (EC-1: first run): - Display: "First run — establishing baseline with full scan" - Run full scan across all categories

If --full flag: - Display: "Full scan requested" - Ignore .gc-state.yaml timestamp

Step 3: Run Category Checks

Run the requested categories. For each, collect findings with severity levels.

CRITICAL severity rule (NFR-2): Only assign CRITICAL when a referenced path has been verified non-existent via Glob or ls. Never infer critical findings from naming patterns alone.


Category 1: Spec Consistency

Check .design-state.yaml ↔ actual spec directories.

  1. Read .design-state.yaml specs_status section
  2. For each entry with specs_dir:
  3. Verify the directory exists (Glob: specs/{N}-*/)
  4. Verify spec.md exists inside it
  5. If directory missing → [CRITICAL] SPEC-{NNN} registered in .design-state.yaml but specs/{dir}/ does not exist
  6. Scan specs/*/ directories:
  7. For each directory: check if it has a matching .design-state.yaml entry
  8. If no entry → [WARNING] specs/{dir}/ exists but has no .design-state.yaml registration
  9. For each spec with state.yaml, check its status against the declared vocabulary: The shell expands the glob; the directory name is NEVER pasted into the command. A directory committed as 999-x'$(touch PWNED)' closes a single quote and executes — measured, review pass 6, SEC6-3.
for f in specs/*/state.yaml; do
  [ -e "$f" ] || continue
  printf '%s\t' "$f"
  "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/status-vocab.sh" classify-file "$f"
done

Exit 0 prints namespace field literal canonical class, exit 3 means the literal is declared nowhere, exit 4 means the file carries no status: at all. Pass the FILE, never the status read out of it: a status is repo content, and a $(…) in one ran when it was pasted into an argument (SEC-20) - On exit 3 → [WARNING] specs/{dir}/state.yaml has an undeclared status: {value} - ⚠ This used to be a list of six literals written here, and it had already gone stale: it never learned the end state A1 added, so a correctly archived spec was reported as invalid. 5. Check for stale in_progress specs (activated >30 days ago and not yet ended): - [INFO] SPEC-{NNN} has been in_progress since {date} (>30 days)

--fix actions: - Missing .design-state.yaml entry → create it with the single writer, which reads that metadata itself from the spec's frontmatter and state.yaml:

python3 "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/spec-status-write.py" --cwd "$(git rev-parse --show-toplevel)" \
  --create SPEC-NNN --dir <N>-kebab-title

If $CLAUDE_PLUGIN_ROOT is not set, locate the writer relative to this skill file at ../../../vt-base/bin/spec-status-write.py, resolving the skill file's real path first (deployed skills are symlinks into the toolkit checkout). Never call it by a repo-relative plugins/vt-base/… path (BUG-342): this skill runs in the project, not the toolkit, so that path exists only when the project is the toolkit. If the writer does not resolve, that is a toolkit defect — report the path tried and stop.

  • Invalid status → skip (ambiguous — needs human decision)

Category 2: Orphaned Files

Detect files not referenced by code or configuration.

  1. Scan for backup/temporary files:

    Glob: plugins/**/*.bak
    Glob: plugins/**/*.old
    Glob: plugins/**/*.orig
    Glob: plugins/*/hooks/.backups/*
    
    Each found → [INFO] Backup file: {path}

  2. Check manifest integrity (4 manifest files):

  3. Read plugins/*/.claude-plugin/skill-symlinks.manifest
  4. Read plugins/*/.claude-plugin/agent-symlinks.manifest
  5. Read plugins/*/.claude-plugin/command-symlinks.manifest
  6. Read plugins/*/.claude-plugin/hook-symlinks.manifest
  7. For each entry: verify the target directory/file exists
  8. If target missing → [WARNING] Manifest entry '{name}' points to non-existent '{target}'

  9. Reverse check: scan skill/agent/command directories for files not in any manifest:

  10. List plugins/*/skills/*/SKILL.md
  11. For each: check if it has a manifest entry
  12. If no entry → [WARNING] Skill '{name}' has no manifest entry (not deployed)

--fix actions: - Missing manifest entry → add to appropriate manifest file - Backup files → skip (user may want to keep them)


Category 3: CLAUDE.md Validation

Verify CLAUDE.md references are current.

  1. Read CLAUDE.md from project root
  2. Extract skill/command references matching /vt-<x>-* patterns (one letter per plugin: c d o t p b v f)
  3. For each reference: check if the skill exists in plugins/*/.claude-plugin/skill-symlinks.manifest
  4. If not found → [WARNING] CLAUDE.md references '/vt-<x>-{name}' which is not in the skill manifest
  5. Extract file/directory path references (patterns like plugins/..., docs/..., specs/...)
  6. For each path: verify it exists via Glob
  7. If not found → [INFO] CLAUDE.md references path '{path}' which does not exist

--fix actions: - Stale skill reference → suggest correct name if a manifest entry differs only by prefix, suffix, or a single segment change (e.g., vt-d-wf-plan → vt-d-wf-work) - Stale path reference → skip (ambiguous)


Category 4: Stale Cross-References

Detect broken references between skills and agents.

  1. Scan all SKILL.md files for references to other skills:
    Grep: /vt-[a-z]-[a-z0-9-]+ across plugins/*/skills/*/SKILL.md
    
  2. For each unique reference found: check if the target skill exists in the manifest
  3. If not found → [WARNING] {source-skill} references '/vt-<x>-{target}' which is not in manifest
  4. Scan agent files for skills: YAML references:
    Grep: skills: across plugins/*/agents/**/*.md
    
  5. For each skill reference in agent files: verify it exists
  6. If not found → [WARNING] Agent '{agent}' declares skill dependency '{skill}' which does not exist

--fix actions: - Stale skill reference → replace with correct name if unambiguous match found - Ambiguous match (multiple candidates) → [MANUAL] Cannot auto-fix: '{ref}' has multiple possible matches: {candidates}


Step 4: Generate Report

Display findings grouped by category with severity tags:

Garbage Collection Report
═══════════════════════════════════════════════════════════════

Category: Spec Consistency
─────────────────────────────────────────────────────────────
  [CRITICAL] specs/27-pd-capture-decisions/ registered but directory missing
  [WARNING]  specs/36-gsd-wave-execution/ missing state.yaml
  [INFO]     SPEC-033 in_progress since 2026-02-20 (>30 days)

Category: Orphaned Files
─────────────────────────────────────────────────────────────
  [WARNING]  Manifest entry 'vt-c-foo' points to non-existent 'foo/'
  [INFO]     3 backup files in hooks/.backups/

Category: CLAUDE.md Validation
─────────────────────────────────────────────────────────────
  All clear ✓

Category: Stale Cross-References
─────────────────────────────────────────────────────────────
  All clear ✓

Summary: 1 critical | 2 warning | 2 info
Scan type: incremental (since 2026-03-22)

Related checks (not covered by GC — run separately):
  /vt-t-doc-sync --report-only  — documentation ↔ toolkit state
  /vt-c-repo-health             — governance traffic light
  /vt-c-repo-audit              — deployment integrity

═══════════════════════════════════════════════════════════════

For no drift found (EC-2):

All clear — no drift detected. ✓

Step 5: Update State

Write or update .gc-state.yaml:

last_run: "{ISO-8601 timestamp}"
categories_run: [specs, orphans, refs, claude-md]
findings_count: {total findings}

Step 6: Fix Summary (--fix mode only)

If --fix was passed, display a fix summary after the report:

Auto-Fix Results
─────────────────────────────────────────────────────────────
  [FIXED]  Added .design-state.yaml entry for specs/27-pd-capture-decisions/
  [FIXED]  Added manifest entry for skill 'vt-d-adr'
  [SKIP]   Cannot auto-fix: stale reference in skills/foo has 2 possible matches
  [SKIP]   Backup files in hooks/.backups/ — manual review needed

Fixed: 2 | Skipped: 2

Relationship to Other Skills

Skill Relationship Overlap?
/vt-t-doc-sync Documentation ↔ toolkit state (counts, terms, versions) None — GC delegates
/vt-c-repo-health Quick governance traffic light None — GC delegates
/vt-c-repo-audit Deployment integrity (partitions, LFS, hooks) None — GC delegates
/vt-c-repo-init Initial governance setup None — different lifecycle
/vt-d-activate Reads spec state (GC validates what activate created) Complementary