/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:
- Detect project type: check for
pyproject.toml(Python),package.json(Node),Cargo.toml(Rust), MCP server markers (server.py,mcp.json) - Run each dimension sequentially (each sub-check runs to completion; delegated checks like security/library may take 30-300s)
- 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:
- Grep codebase for route/command decorators → build implemented set
- Parse README "API Reference", "Commands", or "Endpoints" section → build documented set
- Diff: implemented ∩ documented = OK; implemented \ documented = undocumented; documented \ implemented = dead docs
Gate:
PASS: all implemented routes documentedWARN: 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:
- Find all version files via glob
- Extract version string from each
- Compare — flag any that differ
- Output version matrix table
Gate:
PASS: all versions matchWARN: 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?
- App mode (
/documentation-updater— checklist completeness (only when all required files exist)/registry stale 30— staleact-status skills (>30 days);plan/do/checkwith 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:
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.
# 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 ANDDIFF_DAYS< 7WARN:ROADMAP.mdorTECH-STACK.mdmissing OR any misplaced doc ORDIFF_DAYS≥ 14FAIL:CLAUDE.mdorREADME.mdmissing ORDIFF_DAYS≥ 28 OR registry has >5 staleact-status skills (excludeplan/do/checkwith nulllast_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
for f in CLAUDE.md AGENTS.md; do
[ -f "$f" ] && wc -l < "$f" || echo 0
done
PASS: ≤150 lines eachWARN: 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
# 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)
WARNif monorepo detected ANDpkg_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:
# 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)
WARNif overlap/total > 0.30 (>30% section headers shared)INFOalways: "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 withastro.config.mjsReads
site/docs-registry.yaml→ counts pages by statusVerifies 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/existsReads
docs/docs-registry.yamlif presentVerifies staleness: code changes vs
docs/user-guide/last updateReports missing essential docs (getting-started, api-reference for services)
Gate (combined with Steps 0–2.5):
PASS: Steps 0–2.5 pass AND/docs auditreturns PASSWARN: Steps 0–2.5 pass but/docs auditreturns WARN (stale pages, missing EN, etc.)FAIL: Steps 0–2.5 fail OR/docs auditreturns 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/CRITICALWARN: scan clean but audit finds MEDIUM findingsFAIL: 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:
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 publishedWARN: 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:
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 provisionedWARN: minor drift (e.g., missingrelease.ymlbut quality workflows present) OR REGISTRY_TOKEN missingFAIL: 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:
- Detect project type (Python/Node/Rust) from manifest files
- Run lint + format check (
ruff/eslint/clippy) - Run fast test suite (
-m "not integration and not e2e") — full suite is CI's domain - Measure coverage (informational only locally; threshold enforced on CI)
Gate:
PASS: 0 lint errors, all fast tests passWARN: 1-5 lint errors OR fast tests partially failingFAIL: 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 functionsFAIL: >5 duplicate blocks OR >10 TODOs OR >3 oversized functions OR dead code in critical pathsSKIP: 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 dateWARN: 0 critical · unassessed HIGH/MEDIUM exist · OR ≥1 decision due within 7 daysFAIL: Any CRITICAL (CVE/EOL) · OR any overdue deferred decisionSKIP: 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:
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):
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:
git ls-files | grep -E '\.(bak|tmp|orig|swp|swo)$|~$'
B2 — Zero-byte tracked files (exclude .gitkeep, __init__.py, py.typed):
git ls-files | while read f; do [ -f "$f" ] && [ ! -s "$f" ] && echo "$f"; done
B3 — OS artifacts in tracked files:
git ls-files | grep -E '\.DS_Store$|Thumbs\.db$|desktop\.ini$'
Check C — Directory structure audit
C1 — Parse expected structure from README tree block:
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:
ls -d */ 2>/dev/null | sed 's|/$||'
EXPECTED + PRESENT→ OKEXPECTED + MISSING→ WARNPRESENT + 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:
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:
git ls-files --full-name | grep -cv '/'
- ≤15: PASS | 16-25: INFO | >25: WARN
E2 — Nested .git directories (accidental submodules):
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):
[ -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
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 findingsWARN: tracked OS artifacts, missing gitignore for stack, >25 root files, >10MB file, binary without LFS, OR any stash entryFAIL: 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:
- 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]) - Propose
gh issue createcommand with--milestone BACKLOG,--label enhancement - 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:
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:
Discover repos: for each directory in
~/dev/*/plus~/.claude, run:git -C <dir> remote get-url origin 2>/dev/nullExtract
owner/reposlugs via regex. Deduplicate.For each repo with a GitHub remote, run:
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)}'Group by repo, then by kind (
bug,tech-debt,enhancement).Compute age of oldest issue per repo.
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 reviewIf WARN/FAIL: suggest
/health agents triage.
Gate:
PASS: 0 openai-suggestedissuesWARN: 1–15 open issues across all reposFAIL: >15 open issues OR any issue older than 14 daysSKIP: 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:
Run the same repo discovery and issue query as
/health agents.For each repo with open issues, print the issue list grouped by kind.
For each issue (or allow batch selection), present 3 actions:
- Dismiss — close as not actionable:
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):
gh issue edit N -R owner/repo \ --remove-label ai-suggested \ --milestone <chosen-milestone> - Skip — leave as-is for next triage session.
- Dismiss — close as not actionable:
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
# 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
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:
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.
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):
# 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 anywhereWARN: ≥1 local merged branch OR ≥1 remote stale >30 daysFAIL: ≥1 branch stale >60 days on any remoteSKIP: 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-healthoverall gate (PASS/WARN/FAIL)
Subcommand: /health docker
Conditional check — delegates to /docker-audit. Skipped if no Docker files are detected.
Detection:
# 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 findingsWARN: any HIGH finding (non-root user, no HEALTHCHECK,:latesttag, 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#Nissue reference - Extract: (item text, version/section, issue ref if present)
# Extract headings and items from ROADMAP.md
grep -n "^#\|^- \|^* " ROADMAP.md
Step 2 — Fetch GitHub Issues and Milestones
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:
- Check if it contains an explicit
#Nreference → verify that issue exists - If no explicit ref: fuzzy-match title against open issues (≥60% word overlap)
- Classify as:
LINKED— has verified issue referenceFUZZY_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.Yor## SPRINT-N→ use that milestone title - Section
## Plannedor## In Progress(no version) → useBACKLOG - Section
## Unreleased→ useBACKLOG
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):
- For each orphaned item, run the
gh issue createcommand and capture the returned issue URL/number:
…(truncated)