# Health

> Holistic project audit across endpoints, versions, docs, security, quality, CI, and library dimensions. Use when checking overall project health or running a periodic cross-cutting audit. Trigger on "project health", "audit the project", "health check", "is this project in good shape".

- Skill: `gabrielmoreira/health-3` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/health-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/health-3/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/gabrielmoreira/health-3

---


# /health — Project Health Audit

Orchestrates a cross-cutting audit across all project dimensions. Designed to run periodically via
the native `/loop` bundled skill.

**Architecture — hybrid orchestrator:**

- **Native checks** (no dedicated sub-skill exists for these): `endpoints`, `versions`, `docs`
- **Pure delegation** (sub-skill owns the logic): `security` → `/security-verify scan`, `quality` → `/quality-check`, `library` → `/library`, `releases` → `/release verify tags`, `ci` → `/ci-setup audit`, `website` → `/website-health`

## Quick Start

```
/health                    # auto-detect project, show summary
/health full               # comprehensive audit across all dimensions
/health endpoints          # check routes vs docs
/health versions           # version string consistency
/health docs               # CLAUDE.md + README staleness + license check
/health roadmap            # bidirectional sync: ROADMAP.md ↔ GitHub Issues ↔ CHANGELOG
/health license            # check LICENSE file; advisor mode if missing
/health security           # delegate to /security-verify scan
/health quality            # delegate to /quality-check + tests + coverage
/health techdebt           # delegate to /techdebt (duplicates, dead code, TODOs, oversized functions)
/health deps               # delegate to /deps scan (outdated deps, CVEs, decision coverage)
/health hygiene            # filesystem lint: gitignore gaps, orphans, structure drift, root clutter
/health library            # delegate to /library scan + sync + drift + new
/health releases           # delegate to /release verify tags
/health ci                 # delegate to /ci-setup audit (Forgejo workflow drift, conditional)
/health agents             # review unreviewed agent-created issues (ai-suggested label)
/health agents triage      # interactive triage: dismiss, promote, or skip each signal
/health docker             # delegate to /docker-audit (if Dockerfile* or compose.yaml detected)
/health website            # delegate to /website-health (if website project detected)
/health branches           # stale branch audit — local, GitHub, Forgejo
/health issues             # open GitHub BACKLOG issues for findings
```

## Loop Integration

```
/loop 1h /health full      # hourly comprehensive audit
/loop 1d /health security  # daily security check
/loop 1d /health library   # daily library drift check
```

Uses the native Claude Code `/loop` bundled skill (CronCreate under the hood).
Tasks are session-scoped and auto-expire after 3 days.

---

## Subcommand: `/health` (summary)

Auto-detect project type and print a one-line status per dimension.

**Steps:**

1. Detect project type: check for `pyproject.toml` (Python), `package.json` (Node), `Cargo.toml` (Rust), MCP server markers (`server.py`, `mcp.json`)
2. Run each dimension sequentially (each sub-check runs to completion; delegated checks like security/library may take 30-300s)
3. Print consolidated summary with overall gate

**Output:**

```
## Health Summary — <project-name> — <date>

Endpoints  [PASS]  14 routes, all documented
Versions   [WARN]  pyproject.toml (0.3.1) ≠ README badge (0.3.0)
Docs       [PASS]  CLAUDE.md current, README current
Security   [PASS]  No critical/high findings
Quality    [PASS]  0 lint errors, 45/45 tests, 87% coverage
Tech Debt  [PASS]  0 critical findings
Hygiene    [WARN]  .coverage not in .gitignore; 2 unexpected top-level dirs
Releases   [PASS]  3 tags, all match CHANGELOG + GH/FJ releases + artifacts (pypi: 3/3)
Agents     [WARN]  12 unreviewed signals across 3 repos — run /health agents triage
Branches   [WARN]  3 local merged, 1 remote stale on Forgejo (if git repo)
Docker     [WARN]  Non-root USER missing in Dockerfile (if Docker files detected)
Website    [PASS]  7/7 dimensions pass (if website project detected)

Overall: WARN — 3 FIX-NOW (apply immediately) · 2 TRACK (open issues)
```

---

## Subcommand: `/health endpoints`

Compare implemented routes vs what is documented in README or API docs.

**Detection patterns:**

| Stack           | Pattern                                                            |
| --------------- | ------------------------------------------------------------------ |
| FastAPI         | `@router.(get\|post\|put\|delete\|patch)`, `@app.(get\|post\|...)` |
| Click CLI       | `@click.command`, `@cli.command`, `@<group>.command`               |
| MCP             | `@mcp.tool`, `@server.tool`, `@server.list_tools`                  |
| Express/Next.js | `app.(get\|post\|...)`, `router.(get\|...)`, `pages/api/**` glob   |

**Steps:**

1. Grep codebase for route/command decorators → build implemented set
2. Parse README "API Reference", "Commands", or "Endpoints" section → build documented set
3. Diff: implemented ∩ documented = OK; implemented \ documented = undocumented; documented \ implemented = dead docs

**Gate:**

- `PASS`: all implemented routes documented
- `WARN`: undocumented routes exist (non-security-critical)
- `FAIL`: undocumented route with auth/data-mutation pattern (POST/PUT/DELETE without docs)

---

## Subcommand: `/health versions`

Cross-check all version strings across manifest files and README.

**Version file detection** (reuses `/release changelog` Step 6 patterns):

| File          | Path                                                              |
| ------------- | ----------------------------------------------------------------- |
| Python        | `pyproject.toml` → `[project] version` or `[tool.poetry] version` |
| Node          | `package.json` → `.version`                                       |
| Rust          | `Cargo.toml` → `[package] version`                                |
| Legacy Python | `setup.cfg` → `version =`                                         |
| README badge  | `shields.io/badge/version-X.Y.Z` or similar                       |

**Steps:**

1. Find all version files via glob
2. Extract version string from each
3. Compare — flag any that differ
4. Output version matrix table

**Gate:**

- `PASS`: all versions match
- `WARN`: README badge differs (cosmetic)
- `FAIL`: any two production manifests disagree (pyproject.toml, package.json, Cargo.toml, setup.cfg — README badge excluded from FAIL since it's cosmetic)
  Note: normalize ranges (`^1.2` → `1.2.x`) before comparing; VCS/path deps are flagged as WARN with a note to pin

---

## Subcommand: `/health docs`

Check required documentation exists at the root, is in the right location, and is not stale.
Also audits user-facing docs via `/docs audit` (dual mode: Starlight for apps, markdown for libs/services).

**Delegates to:**

- `/docs audit` — user-facing docs (dual mode auto-detected from repo):
  - **App mode** (`app-*`): `site/` present? Starlight pages stale? links rotti? language parity IT↔EN?
- `/documentation-updater` — checklist completeness (only when all required files exist)
- `/registry stale 30` — stale `act`-status skills (>30 days); `plan`/`do`/`check` with null dates are excluded (not yet in validation phase)

### Step 0 — Existence check

Required root-level docs and their init path when missing:

| File            | Severity if missing | Init suggestion                               |
| --------------- | ------------------- | --------------------------------------------- |
| `CLAUDE.md`     | `FAIL`              | `/claude-md-management:claude-md-improver`    |
| `README.md`     | `FAIL`              | `/documentation-updater`                      |
| `LICENSE`       | `WARN`              | `/health license` (advisor mode)              |
| `ROADMAP.md`    | `WARN`              | `/documentation-updater` (architecture docs)  |
| `TECH-STACK.md` | `WARN`              | `/documentation-updater` (configuration docs) |

For each missing file: report the finding, state the severity, and offer the init command.

**LICENSE note:** If `LICENSE` is missing, note it as WARN and print: "Run `/health license` to get an interactive advisor that will recommend the right license based on your project type."

### Step 1 — Location check

Search for required docs in non-root locations and flag misplaced copies:

```bash
for f in CLAUDE.md README.md ROADMAP.md TECH-STACK.md; do
  # Check root
  root_present=$([ -f "$f" ] && echo "yes" || echo "no")
  # Find any copies elsewhere (exclude .git, node_modules, .venv)
  elsewhere=$(find . -name "$f" \
    -not -path "./.git/*" \
    -not -path "./node_modules/*" \
    -not -path "./.venv/*" \
    -not -path "./.archive/*" \
    -not -maxdepth 1 2>/dev/null)
  # If found in subdir but not at root → WARN: propose move
  # If found in subdir AND at root → INFO: note the duplicate
done
```

Report as `WARN` with the path and suggest: "Move to project root and update any links."

### Step 2 — Staleness check

Only runs for files confirmed present at root. Uses Unix timestamps to avoid sign-flip bug when file is absent.

```bash
# Last CLAUDE.md/README change vs last code change — use Unix timestamps for arithmetic
DOC_TS=$(git log -1 --format="%ct" -- CLAUDE.md README.md ROADMAP.md TECH-STACK.md 2>/dev/null || echo 0)
CODE_TS=$(git log -1 --format="%ct" -- src/ app/ lib/ 2>/dev/null || echo 0)
DIFF_DAYS=$(( (CODE_TS - DOC_TS) / 86400 ))
# Positive DIFF_DAYS = code newer than docs (stale)
# Only evaluate if DOC_TS > 0 (files exist in git history)
```

**Gate:**

- `PASS`: all required files present at root AND `DIFF_DAYS` < 7
- `WARN`: `ROADMAP.md` or `TECH-STACK.md` missing OR any misplaced doc OR `DIFF_DAYS` ≥ 14
- `FAIL`: `CLAUDE.md` or `README.md` missing OR `DIFF_DAYS` ≥ 28 OR registry has >5 stale `act`-status skills (exclude `plan`/`do`/`check` with null `last_manual_test` — expected pre-validation)

### Step 2.5 — Instruction file structure check

Runs after staleness check. Checks CLAUDE.md, AGENTS.md, or both (whichever are present).

**a) Line count**

```bash
for f in CLAUDE.md AGENTS.md; do
  [ -f "$f" ] && wc -l < "$f" || echo 0
done
```

- `PASS`: ≤150 lines each
- `WARN`: 151–250 lines → "Consider extracting coding patterns to `.claude/rules/`"
- `FAIL`: >250 lines → "Instruction file bloated — split into root + rules or per-package files"

Line count FAIL escalates the overall Docs gate to FAIL.

**b) Monorepo without per-package instruction files**

```bash
# Detect monorepo
is_monorepo=$([ -f pnpm-workspace.yaml ] || [ -f turbo.json ] || [ -f lerna.json ] && echo yes || echo no)
# Count packages with their own CLAUDE.md
pkg_count=$(find apps packages libs -maxdepth 2 -name "CLAUDE.md" -o -name "AGENTS.md" 2>/dev/null | wc -l)
# Count total packages
total_pkgs=$(find apps packages libs -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
```

- `WARN` if monorepo detected AND `pkg_count < total_pkgs / 2`
- Message: "Monorepo detected — consider per-package CLAUDE.md for packages missing one"

Monorepo check is WARN-only (advisory).

**c) Dual-file alignment**

If both `CLAUDE.md` and `AGENTS.md` exist:

```bash
# Extract ## section headers from both files
claude_headers=$(grep "^## " CLAUDE.md 2>/dev/null | sort)
agents_headers=$(grep "^## " AGENTS.md 2>/dev/null | sort)
overlap=$(comm -12 <(echo "$claude_headers") <(echo "$agents_headers") | wc -l)
total=$(echo "$claude_headers" "$agents_headers" | sort -u | wc -l)
```

- `WARN` if overlap/total > 0.30 (>30% section headers shared)
- `INFO` always: "Both CLAUDE.md and AGENTS.md present — CLAUDE.md = architecture/context, AGENTS.md = build/style"

### Step 3 — User-facing docs audit (dual mode)

Delegates to `/docs audit` which auto-detects mode from repo:

**App mode** (`app-*` repos):

- Checks `site/` exists with `astro.config.mjs`
- Reads `site/docs-registry.yaml` → counts pages by status
- Verifies staleness: code newer than docs by >30 days?
- Checks language parity: IT vs EN page count
- Checks for broken internal links


- Checks `docs/user-guide/` exists
- Reads `docs/docs-registry.yaml` if present
- Verifies staleness: code changes vs `docs/user-guide/` last update
- Reports missing essential docs (getting-started, api-reference for services)

**Gate (combined with Steps 0–2.5):**

- `PASS`: Steps 0–2.5 pass AND `/docs audit` returns PASS
- `WARN`: Steps 0–2.5 pass but `/docs audit` returns WARN (stale pages, missing EN, etc.)
- `FAIL`: Steps 0–2.5 fail OR `/docs audit` returns FAIL (required pages missing, site/ absent for app)


---

## Subcommand: `/health security`

Delegates to `/security-verify scan` (fast SAST) and `/security-verify audit` (adversarial logic review).

```
Step 1: Invoke /security-verify scan    → pattern-based SAST (seconds)
Step 2: Invoke /security-verify audit   → adversarial logic review (minutes, spawns subagent)
```

Output of each step is shown. Gate:

- `PASS`: scan clean AND audit finds no HIGH/CRITICAL
- `WARN`: scan clean but audit finds MEDIUM findings
- `FAIL`: scan finds critical/high OR audit finds HIGH/CRITICAL

When invoked from `/health full`, both steps run. When invoked from `/loop 1d /health security`, both steps run. `scan` alone is the pre-commit gate (not this subcommand).

---

## Subcommand: `/health library`

Delegates entirely to `/library`. Runs four sub-checks in sequence:

```
Invoke: /library sync   → version matrix (current project vs local source)
Invoke: /library drift  → duplication findings
Invoke: /library scan   → extraction candidates
Invoke: /library new    → repeated pattern proposals (≥3x)
```

Summarizes findings into single `Library [PASS|WARN|FAIL]` line for the health report.


---

## Subcommand: `/health releases`

Verify release integrity — tag↔changelog↔GitHub-release consistency.

Always derive owner and repo from the Forgejo push remote:

```bash
FJ_OWNER=$(echo "$FORGEJO_REMOTE" | sed 's|.*:\(.*\)/.*\.git|\1|')
FJ_REPO=$(basename "$FORGEJO_REMOTE" .git)
```


**Delegates to:** `/release verify tags`

```
Invoke: /release verify tags
```

Output is passed through verbatim. Gate inherits from `/release verify`:

- `PASS`: all tags ↔ sections match, GH/FJ releases present, all expected artifacts published
- `WARN`: missing GH or Forgejo release for a tag, OR expected artifact MISSING (publish workflow exists but package absent)
- `FAIL`: tag without section OR section without tag

---

## Subcommand: `/health ci`



**Delegates to:** `/ci-setup audit`

```
Invoke: /ci-setup audit
```

Detection check:

```bash
```

Output is collapsed into a single gate line for the consolidated report:

```
CI  [PASS]  4 workflows match template · secrets: REGISTRY_TOKEN ✓ PYPI_TOKEN ✓
CI  [WARN]  missing release.yml · REGISTRY_TOKEN absent
CI  [FAIL]  legacy unified pattern detected · REGISTRY_TOKEN missing
```

Gate thresholds:

- `PASS`: all expected workflows present, all match templates, REGISTRY_TOKEN provisioned
- `WARN`: minor drift (e.g., missing `release.yml` but quality workflows present) OR REGISTRY_TOKEN missing
- `FAIL`: legacy patterns (`old-unified-pattern`, `legacy-setup-deps`), missing security workflow, OR REGISTRY_TOKEN missing

Run `/ci-setup fix` to apply any fixes surfaced by this check.

---

## Subcommand: `/health quality`

Periodic code quality snapshot — lint, tests, coverage.

**Delegates to:**

- `/quality-check` — linting, formatting, type checking
- Test runner (auto-detected: `pytest` / `npm test` / `cargo test`)
- Coverage tool (auto-detected: `pytest --cov` / `npm test --coverage`)

**Steps:**

1. Detect project type (Python/Node/Rust) from manifest files
2. Run lint + format check (`ruff` / `eslint` / `clippy`)
3. Run fast test suite (`-m "not integration and not e2e"`) — full suite is CI's domain
4. Measure coverage (informational only locally; threshold enforced on CI)

**Gate:**

- `PASS`: 0 lint errors, all fast tests pass
- `WARN`: 1-5 lint errors OR fast tests partially failing
- `FAIL`: fast tests failing OR > 5 lint errors

**Note:** This is a periodic health snapshot. For commit-adjacent validation, use `/pre-commit` which also includes security scanning and changelog updates. Coverage >= 80% is a CI gate, not a local health gate.

---

## Subcommand: `/health techdebt`

Delegates entirely to `/techdebt`. Scans for accumulated technical debt across the codebase.

```
Invoke: /techdebt
```

**Checks delegated:**

- Duplicated code blocks (≥10 identical lines across files)
- Dead code (unused functions, unreachable branches, commented-out blocks)
- TODOs / FIXMEs / HACKs left in code
- Oversized functions (>50 lines) and files (>500 lines per code-quality.md)

Output is passed through verbatim. Gate derived from finding counts:

- `PASS`: 0 critical findings (no duplicates >20 lines, no dead code, ≤3 TODOs)
- `WARN`: 1-5 duplicate blocks OR 1-10 TODOs OR 1-3 oversized functions
- `FAIL`: >5 duplicate blocks OR >10 TODOs OR >3 oversized functions OR dead code in critical paths
- `SKIP`: No source files detected (config-only or docs-only repo)

**Note:** Tech debt TRACK findings open with `--milestone TECH-DEBT` (not BACKLOG) via `/health issues`.

---

## Subcommand: `/health deps`

Delegates entirely to `/deps scan`. Audits dependency freshness, security posture, and
decision coverage — how many outdated deps have a recorded update/defer/skip rationale.

```
Invoke: /deps scan
```

**What it checks:**

- Outdated packages (Python, Node.js, Rust, Go) — classified by semver bump severity
- CVEs and EOL packages via `pip audit` / `npm audit` / `cargo audit`
- Active decisions in Atrium — marks assessed deps, flags overdue deferred decisions

Output gate derived from `/deps scan` results:

- `PASS`: 0 critical (CVE/EOL) · 0 unassessed HIGH · all deferred decisions within review date
- `WARN`: 0 critical · unassessed HIGH/MEDIUM exist · OR ≥1 decision due within 7 days
- `FAIL`: Any CRITICAL (CVE/EOL) · OR any overdue deferred decision
- `SKIP`: No manifest files found (config-only or docs-only repo)

**Atrium offline:** Gate still works (local scan only). Decision coverage shows as `n/a`.

**Note:** When FAIL on a TRACK finding (CVE, overdue decision), open issue with `--milestone TECH-DEBT`. Patch/minor updates are FIX-NOW — bump and test inline.
Run `/deps decide` after the audit to record decisions for unassessed findings.

---

## Subcommand: `/health hygiene`

Filesystem lint — catches structural rot, gitignore gaps, orphaned artifacts, and directory drift.
This is a **native check** (no delegation — uses Glob, Grep, Bash directly).

### Check A — Gitignore hygiene

**A1 — Untracked files matching known noise patterns per detected stack:**

```bash
git ls-files --others --exclude-standard
```

Cross-reference against known noise patterns:

| Stack       | Patterns that should be in .gitignore                                            |
| ----------- | -------------------------------------------------------------------------------- |
| All         | `.DS_Store`, `Thumbs.db`, `*.swp`, `*~`, `.env`, `.env.local`, `.env.*.local`    |
| Python      | `__pycache__/`, `*.pyc`, `.venv/`, `*.egg-info/`, `dist/`, `build/`, `.coverage` |
| Node        | `node_modules/`, `.next/`, `.nuxt/`, `.output/`, `dist/`                         |
| Rust        | `target/`                                                                        |
| IDE         | `.idea/`, `.vscode/` (except shared settings), `*.code-workspace`                |
| Testing     | `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, `coverage/`, `htmlcov/`        |
| Claude Code | `projects/`, `plans/`, `tasks/`, `debug/`, `history.jsonl`, `memory-index.db*`   |

Report each match with the suggested `.gitignore` line to add.

**A2 — Tracked files that match .gitignore patterns (`git rm --cached` candidates):**

```bash
git ls-files -i --exclude-standard 2>/dev/null
```

Each result is a FAIL finding with the fix command: `git rm --cached <file>`.

**A3 — Missing gitignore entries for detected stack:**

Detect stack from manifest files (`pyproject.toml` → Python, `package.json` → Node, `Cargo.toml` → Rust).
Compare required patterns (table above) against actual `.gitignore` content. Report missing as WARN.

### Check B — Orphaned and stale files

**B1 — Backup/temp files tracked:**

```bash
git ls-files | grep -E '\.(bak|tmp|orig|swp|swo)$|~$'
```

**B2 — Zero-byte tracked files** (exclude `.gitkeep`, `__init__.py`, `py.typed`):

```bash
git ls-files | while read f; do [ -f "$f" ] && [ ! -s "$f" ] && echo "$f"; done
```

**B3 — OS artifacts in tracked files:**

```bash
git ls-files | grep -E '\.DS_Store$|Thumbs\.db$|desktop\.ini$'
```

### Check C — Directory structure audit

**C1 — Parse expected structure from README tree block:**

```bash
grep -n "^├──\|^└──\|^│" README.md 2>/dev/null
```

If a tree block exists: extract directory names → build expected set.
If no tree block: skip C2 (no reference to compare against).

**C2 — Compare actual vs expected top-level dirs:**

```bash
ls -d */ 2>/dev/null | sed 's|/$||'
```

- `EXPECTED + PRESENT` → OK
- `EXPECTED + MISSING` → WARN
- `PRESENT + NOT EXPECTED` → INFO (undocumented, not necessarily wrong)

**C3 — Misplaced files:**

| Signal                                  | Check                            |
| --------------------------------------- | -------------------------------- |
| `*.py` at root when `src/` exists       | Python file may belong in `src/` |
| `*.test.*` at root when `tests/` exists | Test file may belong in `tests/` |

Root-level config files (`setup.py`, `conftest.py`, `Makefile`, `pyproject.toml`) are excluded.

### Check D — Large and binary files

**D1 — Large tracked files:**

```bash
git ls-files | while read f; do
  [ -f "$f" ] && size=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null)
  [ "$size" -gt 1048576 ] && echo "$f ($((size / 1048576))MB)"
done
```

- > 1MB: INFO | >10MB: WARN | >50MB: FAIL

**D2 — Binary files without LFS:**

Check tracked binaries (MIME type `application/`, `image/`, `audio/`, `video/`) against `.gitattributes`.
If `.gitattributes` has no LFS rule for the file type: WARN (small binaries in `docs/` <100KB excluded).

### Check E — Root-level clutter

**E1 — Root file count:**

```bash
git ls-files --full-name | grep -cv '/'
```

- ≤15: PASS | 16-25: INFO | >25: WARN

**E2 — Nested .git directories** (accidental submodules):

```bash
find . -name ".git" -type d -not -path "./.git" 2>/dev/null
```

If found: WARN — "nested .git detected; verify with `git submodule status`."

**E3 — Orphaned lock files** (lock without corresponding manifest):

```bash
[ -f "package-lock.json" ] && [ ! -f "package.json" ] && echo "package-lock.json"
[ -f "poetry.lock" ] && [ ! -f "pyproject.toml" ] && echo "poetry.lock"
[ -f "uv.lock" ] && [ ! -f "pyproject.toml" ] && echo "uv.lock"
[ -f "Cargo.lock" ] && [ ! -f "Cargo.toml" ] && echo "Cargo.lock"
```

### Check F — Stash audit

```bash
git stash list
```

If the stash is empty: PASS (no output).

For each stash entry, capture:

- **Index** (`stash@{N}`)
- **Branch** it was stashed from (from the `WIP on <branch>` part)
- **Message** (commit description)
- **Age** — compute from reflog: `git log -g --format="%ci" stash@{N} | head -1`

**Age classification:**

| Age       | Severity |
| --------- | -------- |
| <7 days   | INFO     |
| 7–30 days | WARN     |
| >30 days  | WARN     |

**Output (when stash non-empty):**

```
**Stash:**
- ⚠ stash@{0}: "WIP on main: fix auth" — 12 days old
- ⚠ stash@{1}: "WIP on feature/x: draft" — 45 days old
```

Any stash entry triggers at minimum WARN — a non-empty stash means forgotten work. List the `git stash show stash@{N}` command next to each entry so the user can see what's in it.

### Gate

- `PASS`: 0 WARN/FAIL findings
- `WARN`: tracked OS artifacts, missing gitignore for stack, >25 root files, >10MB file, binary without LFS, OR any stash entry
- `FAIL`: tracked file matching .gitignore, >50MB file, orphaned lock file, nested .git (non-submodule)
- `SKIP`: Not a git repository

### Output format

```
### Hygiene [PASS|WARN|FAIL]

**Gitignore:**
- ✓ .gitignore covers detected stack (Python)
- ⚠ Missing entry: `.coverage` (Python testing artifact)
- ✗ Tracked file matches .gitignore: `.DS_Store` → run: git rm --cached .DS_Store

**Orphaned files:**
- ✓ No backup/temp files tracked
- ✓ No OS artifacts tracked

**Structure:**
- ✓ 6/6 expected directories present (per README tree)
- ℹ Unexpected top-level: `commands/`, `shared/` (not in README tree — may be intentional)

**Large files:**
- ✓ No files >1MB tracked

**Root cleanliness:**
- ✓ 12 root-level files (clean)
- ✓ No nested .git directories
- ✓ No orphaned lock files

**Stash:**
- ✓ Stash is empty
```

**Note:** Hygiene TRACK findings (orphaned lock files, nested .git) open with `--milestone TECH-DEBT`. FIX-NOW hygiene items (.gitignore gaps, tracked artifacts) are fixed inline — no issue needed.

---

## Subcommand: `/health issues`

Open GitHub issues **only for TRACK findings** — findings that cannot be resolved immediately
(structural work, investigation required, multi-session effort, or delegated sub-skills).

**Never open issues for FIX-NOW findings.** Those must be fixed in-place first.

**For each TRACK finding:**

1. Format as `[BACKLOG] [P2] #NEW <finding summary>` per github-workflow.md convention
   (permanent milestones like BACKLOG, TECH-DEBT, BUG have no order suffix;
   sprint/version milestones use `|ORDER`, e.g. `[SPRINT-1|03]`)
2. Propose `gh issue create` command with `--milestone BACKLOG`, `--label enhancement`
3. Ask user to confirm **all at once** — one Y/n for the full batch, not per-finding

**Shell safety — titles with metacharacters:** Issue titles may contain `|` (from
`[MILESTONE|ORDER]` convention on sprint/version milestones). Always use **single quotes**
for `--title` values to prevent shell interpretation of `|`, `$`, backticks, and other
metacharacters. If the title itself contains a single quote, escape it as `'''`.

**Example:**

```bash
gh issue create \
  --title '[BACKLOG] [P2] Undocumented POST /upload route' \
  --body "Found by /health endpoints on 2026-03-28. Needs API docs update." \
  --label "enhancement" \
  --milestone "BACKLOG"
```

---

## Subcommand: `/health agents`

Native check — no delegation. Scans all repos with GitHub remotes for open issues labeled `ai-suggested` (auto-created by the session analyzer nightly job).

**Steps:**

1. Discover repos: for each directory in `~/dev/*/` plus `~/.claude`, run:

   ```bash
   git -C <dir> remote get-url origin 2>/dev/null
   ```

   Extract `owner/repo` slugs via regex. Deduplicate.

2. For each repo with a GitHub remote, run:

   ```bash
   gh issue list -R owner/repo --label ai-suggested --state open \
     --json number,title,labels,createdAt \
     --jq '.[] | {number, title, createdAt, kind: ([.labels[].name] | map(select(. == "bug" or . == "tech-debt" or . == "enhancement")) | first)}'
   ```

3. Group by repo, then by kind (`bug`, `tech-debt`, `enhancement`).

4. Compute age of oldest issue per repo.

5. Print summary table:

   ```
   ### Agents [PASS|WARN|FAIL]

   | Repo                   | Bugs | Tech-Debt | Enhancements | Total | Oldest |
   |------------------------|------|-----------|--------------|-------|--------|
   | claude-dotfiles        | 8    | 2         | 1            | 11    | 5d ago |

   Total: 15 unreviewed signals across 2 repos — run `/health agents triage` to review
   ```

6. If WARN/FAIL: suggest `/health agents triage`.

**Gate:**

- `PASS`: 0 open `ai-suggested` issues
- `WARN`: 1–15 open issues across all repos
- `FAIL`: >15 open issues OR any issue older than 14 days
- `SKIP`: No GitHub remote in current repo (still runs globally from project scan)

---

## Subcommand: `/health agents triage`

Interactive triage mode. Works across all repos, not just the current one.

**Steps:**

1. Run the same repo discovery and issue query as `/health agents`.
2. For each repo with open issues, print the issue list grouped by kind.
3. For each issue (or allow batch selection), present 3 actions:
   - **Dismiss** — close as not actionable:
     ```bash
     gh issue close N -R owner/repo --comment "Dismissed during /health agents triage — not actionable or already fixed."
     ```
   - **Promote** — make it a human-owned issue (ask for target milestone: BACKLOG, BUG, SPRINT-N):
     ```bash
     gh issue edit N -R owner/repo \
       --remove-label ai-suggested \
       --milestone <chosen-milestone>
     ```
   - **Skip** — leave as-is for next triage session.

4. After all repos are processed, print updated counts.

**Key invariant:** promoting removes the `ai-suggested` label → the issue becomes human-owned and exits the agents check. Dismissing closes it. Both actions reduce the count.

---

## Subcommand: `/health branches`

Stale branch audit across local, GitHub, and Forgejo remotes. Native check — no delegation.

**Steps:**

### Step 1 — Fetch and collect branch data

```bash
# Refresh remote refs (prune deleted remote-tracking branches)
git fetch --prune origin

# Local branches merged into main
git branch --merged main | grep -v '^\*\s*main$'

# Local branches NOT merged, sorted by last commit date
git for-each-ref --sort=committerdate refs/heads/ \
  --format='%(committerdate:iso8601) %(refname:short) %(objectname:short)'

# Remote tracking branches (all remotes)
git for-each-ref --sort=committerdate refs/remotes/ \
  --format='%(committerdate:iso8601) %(refname:short)'
```

### Step 2 — Forgejo remote branches via API


```bash
fj branch list "$OWNER/$REPO"
# Raw output if needed: fj --json branch list "$OWNER/$REPO" | python3 -c "
import json, sys, datetime
now = datetime.datetime.now(datetime.timezone.utc)
for b in json.load(sys.stdin):
    name = b['name']
    if name in ('main', 'master'): continue
    ts = b['commit']['timestamp']
    dt = datetime.datetime.fromisoformat(ts.replace('Z','+00:00'))
    age_days = (now - dt).days
    print(f'{age_days:4d}d  {name}')
"
```

### Step 3 — GitHub remote branches via gh CLI

If `gh auth status` succeeds AND repo has a GitHub remote:

```bash
gh api repos/{owner}/{repo}/branches --paginate \
  --jq '.[] | select(.name | test("^(main|master)$") | not) |
        [.name, .commit.commit.committer.date] | @tsv'
```

Correlate with open PRs: branches with an open PR are **not stale** regardless of age.

```bash
gh pr list --state open --json headRefName --jq '.[].headRefName'
```

### Step 4 — Classify findings

| Category                      | Condition                                   | Severity                 |
| ----------------------------- | ------------------------------------------- | ------------------------ |
| Local merged                  | Branch already merged into main             | FIX-NOW (safe to delete) |
| Local stale                   | Unmerged, no commit in >30 days, no open PR | WARN                     |
| Local stale                   | Unmerged, no commit in >60 days, no open PR | FAIL                     |
| Remote stale (GitHub/Forgejo) | No commit in >30 days, no open PR           | WARN                     |
| Remote stale (GitHub/Forgejo) | No commit in >60 days, no open PR           | FAIL                     |

Branches with an associated open PR on either forge are **excluded** from stale classification.
Branch `main` and `master` are always excluded.

### Step 5 — Suggest cleanup commands

For each FIX-NOW finding, print the exact delete command (do NOT run it — user confirms):

```bash
# Local merged branch
git branch -d <branch-name>

# Remote branch on GitHub
git push origin --delete <branch-name>

# Remote branch on Forgejo (via API — no direct git push since Forgejo is push-only remote)
fj branch delete {owner}/{repo} {branch}
```

**Gate:**

- `PASS`: No stale branches anywhere
- `WARN`: ≥1 local merged branch OR ≥1 remote stale >30 days
- `FAIL`: ≥1 branch stale >60 days on any remote
- `SKIP`: Not a git repo, or bare repo with no branches

---

## Subcommand: `/health website`

Conditional dimension — only runs if the project is a website. Also user-invokable directly: `/health website`.

**Detection** (check in order, use first match):

| Stack           | Marker                                                                           | baseURL extraction                          |
| --------------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
| Astro Starlight | `astro.config.mjs` or `astro.config.ts` + `@astrojs/starlight` in `package.json` | `site:` field in astro config or `SITE` env |
| Astro           | `astro.config.mjs` or `astro.config.ts`                                          | `site:` field in astro config or `SITE` env |
| Hugo            | `hugo.toml` or `config.toml` with `baseURL`                                      | `baseURL` value                             |
| Netlify         | `netlify.toml`                                                                   | from underlying framework config            |
| Next.js         | `next.config.js` or `next.config.ts`                                             | `NEXT_PUBLIC_SITE_URL` env or localhost     |
| Gatsby          | `gatsby-config.js` with `siteMetadata.siteUrl`                                   | `siteUrl` value                             |
| Generic         | `package.json` with `homepage`                                                   | `homepage` value                            |

**Astro Starlight note:** When detected, pass `--stack astro-starlight` to `/website-health` so it can apply docs-site specific checks (sidebar nav, search index, versioned docs, translation completeness).

If no marker found: skip dimension (report `[SKIP] No website project detected`).

**Delegates to:** `/website-health <baseURL>` — invokes the quick summary mode (all 7 dimensions, L1 depth).

**Output normalization:** Extract the overall gate (PASS/WARN/FAIL) and one-line status from `/website-health` output. Collapse into the standard health report format:

```
Website  [PASS|WARN|FAIL]  <one-line summary from /website-health>
```

**Gate:**

- Inherits from `/website-health` overall gate (PASS/WARN/FAIL)

---

## Subcommand: `/health docker`

Conditional check — **delegates to `/docker-audit`**. Skipped if no Docker files are detected.

**Detection:**

```bash
# Check for any Docker-related files
find . -maxdepth 3 \
  \( -name "Dockerfile" -o -name "Dockerfile.*" -o \
     -name "compose.yaml" -o -name "compose.yml" -o \
     -name "docker-compose.yml" -o -name "docker-compose.yaml" \) \
  -not -path "./.git/*" \
  -not -path "./node_modules/*" \
  | head -10
```

If no files found: report `[SKIP] No Docker files detected` — stop here.

**Delegates to:** `/docker-audit [path]` — runs the full 10-category audit plus Compose checklist.

**Output normalization:** Extract the overall score and CRITICAL/HIGH/MEDIUM/LOW counts from `/docker-audit` output. Collapse into standard health format:

```
Docker  [PASS|WARN|FAIL]  <N> files · <score>/10 categories · <k> critical, <n> high findings
```

**Gate:**

- `PASS`: 0 critical, 0 high findings
- `WARN`: any HIGH finding (non-root user, no HEALTHCHECK, `:latest` tag, no `.dockerignore`)
- `FAIL`: any CRITICAL finding (secrets in Dockerfile ENV/ARG/COPY, root user with privileged port exposure)
- `SKIP`: no Docker files found

---

## Subcommand: `/health roadmap`

Bidirectional sync between `ROADMAP.md`, GitHub Issues, and `CHANGELOG.md`.
Ensures roadmap items have corresponding issues and issues are reflected in the roadmap.

**Prerequisites:** GitHub CLI authenticated (`gh auth status`). Roadmap check is skipped (SKIP) if no GitHub remote is detected.

### Step 1 — Parse ROADMAP.md

If `ROADMAP.md` is absent: report `[SKIP]` with note "run /documentation-updater to create ROADMAP.md first."

Parse sections looking for:

- Version headings: `## v1.2.0`, `## Unreleased`, `## Planned`, `## In Progress`, `## Released`
- Feature lines: any `- `, `* ` bullet OR item with `#N` issue reference
- Extract: (item text, version/section, issue ref if present)

```bash
# Extract headings and items from ROADMAP.md
grep -n "^#\|^- \|^* " ROADMAP.md
```

### Step 2 — Fetch GitHub Issues and Milestones

```bash
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)

# Existing milestones (open + closed)
gh api "repos/$REPO/milestones?state=all&per_page=100" \
  --jq '.[] | {number: .number, title: .title, state: .state, open_issues: .open_issues, closed_issues: .closed_issues}'

# Open issues with milestones
gh api "repos/$REPO/issues?state=open&per_page=100" \
  --jq '.[] | select(.pull_request == null) | {number: .number, title: .title, milestone: .milestone.title, labels: [.labels[].name]}'

# Recently closed issues (last 90 days — may already be shipped)
gh api "repos/$REPO/issues?state=closed&per_page=50" \
  --jq '.[] | select(.pull_request == null) | {number: .number, title: .title, closed_at: .closed_at}'
```

Build a **milestone map**: `{ roadmap_section_title → github_milestone_number_or_null }` by matching ROADMAP.md section headings (`## v1.2.0`, `## Q2 2026`, etc.) against existing GitHub milestone titles (case-insensitive, strip `##` prefix and trim).

For each section heading in ROADMAP.md that is NOT in BACKLOG/BUG/TECH-DEBT/Unreleased/Released **and** matches a versioning/sprint pattern (`vX.Y.Z`, `SPRINT-N`, `QN YYYY`, or `SPRINT-N-feature`): note whether the milestone exists in GitHub or is `MISSING`.

Skip sections that do not match these patterns (e.g., `## Ideas for v2`, `## Parking Lot`, `## In Progress`) — these are informational headings, not actionable milestones.

### Step 3 — Map roadmap items → issues

For each roadmap item:

1. Check if it contains an explicit `#N` reference → verify that issue exists
2. If no explicit ref: fuzzy-match title against open issues (≥60% word overlap)
3. Classify as:
   - `LINKED` — has verified issue reference
   - `FUZZY_MATCH` — probable match found (show candidate)
   - `ORPHANED` — no issue found

### Step 4 — Find orphaned roadmap items → create milestones + issues + write back

#### Step 4a — Create missing milestones

For each ROADMAP.md section that has `MISSING` milestone in GitHub (from Step 2 milestone map), print the proposed creation:

```
⚠ ROADMAP section has no GitHub Milestone:
  Section: ## v1.3.0
  Action:  gh api repos/$REPO/milestones --method POST -f title="v1.3.0"
```

After listing all missing milestones, ask **once**: "Create all N milestones on GitHub? [Y/n]"

**If confirmed:** run each `gh api` call and update the milestone map with the newly created milestone numbers. Report:

```
✓ Created milestone "v1.3.0" (#5 on GitHub)
```

**Priority inference for issues:** determine milestone from the roadmap section the item belongs to:

- Section `## v1.X.Y` or `## SPRINT-N` → use that milestone title
- Section `## Planned` or `## In Progress` (no version) → use `BACKLOG`
- Section `## Unreleased` → use `BACKLOG`

#### Step 4b — Create issues for orphaned items

For each `ORPHANED` item in "Planned", "In Progress", or versioned sections, print the proposed command using the **correct milestone** (not hardcoded BACKLOG):

```
⚠ ROADMAP item has no GitHub Issue:
  Section: ## v1.3.0  →  milestone: v1.3.0
  Item:    "Add multi-tenant support"
  Action:  gh issue create \
             --title '[v1.3.0|05] [P2] Add multi-tenant support' \
             --body "From ROADMAP.md ## v1.3.0. No existing issue found." \
             --label "enhancement" \
             --milestone "v1.3.0"
```

Title prefix follows `[MILESTONE|ORDER]` convention from `github-workflow.md`.
**Shell safety:** Always use **single quotes** for `--title` values — titles with `|` (e.g.
`[v1.3.0|05]`) will break the shell if double-quoted or unquoted.

After listing all orphaned items, ask **once**: "Create all N issues and update ROADMAP.md? [Y/n]"

**If confirmed (Y or Enter):**

1. For each orphaned item, run the `gh issue create` command and capture the returned issue URL/number:

 

…(truncated)
