Agent-almanac audit-discovery-symlinks
git clone https://github.com/pjt222/agent-almanac
T=$(mktemp -d) && git clone --depth=1 https://github.com/pjt222/agent-almanac "$T" && mkdir -p ~/.claude/skills && cp -r "$T/i18n/caveman-ultra/skills/audit-discovery-symlinks" ~/.claude/skills/pjt222-agent-almanac-audit-discovery-symlinks-ebe17e && rm -rf "$T"
i18n/caveman-ultra/skills/audit-discovery-symlinks/SKILL.mdaudit-discovery-symlinks
Use When
- After add new skills/agents/teams → almanac
- After repo rename/move → abs symlinks may break
- Slash cmds/agents not found in Claude Code
- Periodic health check → drift registries vs discovery
- Onboard new project → should discover shared almanac
Do NOT use → creating initial symlink hub from scratch. See symlink-architecture guide for first-time setup.
In
| Param | Type | Required | Desc |
|---|---|---|---|
| string | No | Abs path to agent-almanac root. Auto-detect from symlink targets or cwd if omitted |
| enum | No | , , or (default: ) |
| enum | No | (default: audit only), (fix all safe), (prompt each) |
Do
Step 1: Identify Almanac Path
Locate agent-almanac root dir.
# Auto-detect from current project's .claude/agents symlink ALMANAC_PATH=$(readlink -f .claude/agents 2>/dev/null | sed 's|/agents$||') # Fallback: check if cwd is the almanac if [ -z "$ALMANAC_PATH" ] || [ ! -f "$ALMANAC_PATH/skills/_registry.yml" ]; then if [ -f "skills/_registry.yml" ]; then ALMANAC_PATH=$(pwd) fi fi # Fallback: check global agents symlink if [ -z "$ALMANAC_PATH" ] || [ ! -f "$ALMANAC_PATH/skills/_registry.yml" ]; then ALMANAC_PATH=$(readlink -f ~/.claude/agents 2>/dev/null | sed 's|/agents$||') fi echo "Almanac path: $ALMANAC_PATH"
→
ALMANAC_PATH points to dir w/ skills/_registry.yml, agents/_registry.yml, teams/_registry.yml.
If err: Auto-detect fails → ask user for
almanac_path. Root = dir w/ skills/, agents/, teams/ + registries.
Step 2: Inventory Registries
Extract canonical lists from registries.
# Count registered skills (entries with "- id:" under domain sections) REGISTERED_SKILLS=$(grep '^ \{6\}- id:' "$ALMANAC_PATH/skills/_registry.yml" | awk '{print $3}' | sort) REGISTERED_SKILL_COUNT=$(echo "$REGISTERED_SKILLS" | wc -l) # Count registered agents REGISTERED_AGENTS=$(grep '^ \{2\}- id:' "$ALMANAC_PATH/agents/_registry.yml" | awk '{print $3}' | sort) REGISTERED_AGENT_COUNT=$(echo "$REGISTERED_AGENTS" | wc -l) # Count registered teams REGISTERED_TEAMS=$(grep '^ \{2\}- id:' "$ALMANAC_PATH/teams/_registry.yml" | awk '{print $3}' | sort) REGISTERED_TEAM_COUNT=$(echo "$REGISTERED_TEAMS" | wc -l) echo "Registered: $REGISTERED_SKILL_COUNT skills, $REGISTERED_AGENT_COUNT agents, $REGISTERED_TEAM_COUNT teams"
→ Counts match
total_skills, total_agents, total_teams in registry headers.
If err: Counts diverge → registry out of sync. Note discrepancy → continue using actual
- id: entries as source of truth.
Step 3: Audit Project-Level Symlinks
Check
.claude/skills/*, .claude/agents, .claude/teams in cwd.
PROJECT_CLAUDE=".claude" # --- Skills --- # Items on disk (excluding _template) PROJECT_SKILLS=$(ls "$PROJECT_CLAUDE/skills/" 2>/dev/null | grep -v '^_template$' | sort) PROJECT_SKILL_COUNT=$(echo "$PROJECT_SKILLS" | grep -c .) # Missing: in registry but not in project .claude/skills/ MISSING_PROJECT_SKILLS=$(comm -23 <(echo "$REGISTERED_SKILLS") <(echo "$PROJECT_SKILLS")) # Broken: symlink exists but target doesn't resolve BROKEN_PROJECT_SKILLS=$(find "$PROJECT_CLAUDE/skills/" -maxdepth 1 -type l ! -exec test -e {} \; -printf '%f\n' 2>/dev/null | sort) # Extraneous: in project but not in registry (and not external) EXTRA_PROJECT_SKILLS=$(comm -13 <(echo "$REGISTERED_SKILLS") <(echo "$PROJECT_SKILLS")) # --- Agents --- if [ -L "$PROJECT_CLAUDE/agents" ] || [ -d "$PROJECT_CLAUDE/agents" ]; then PROJECT_AGENT_STATUS="OK" test -d "$PROJECT_CLAUDE/agents" || PROJECT_AGENT_STATUS="BROKEN" PROJECT_AGENT_COUNT=$(ls "$PROJECT_CLAUDE/agents/"*.md 2>/dev/null | wc -l) else PROJECT_AGENT_STATUS="MISSING" PROJECT_AGENT_COUNT=0 fi # --- Teams --- # Teams are NOT symlinked. TeamCreate uses ~/.claude/teams/ for runtime state. # A .claude/teams symlink is a misconfiguration — warn if found. if [ -L "$PROJECT_CLAUDE/teams" ]; then PROJECT_TEAM_STATUS="MISCONFIGURED" PROJECT_TEAM_COUNT=0 # Stale symlink — should be removed to avoid collision with TeamCreate else PROJECT_TEAM_STATUS="OK" PROJECT_TEAM_COUNT=0 fi
→ Zero missing, zero broken. Extraneous classified + explained.
If err: No
.claude/ → no discovery setup. Note → skip to global audit.
Step 4: Audit Global Symlinks
Check
~/.claude/skills/* + ~/.claude/agents. Verify ~/.claude/teams is NOT a symlink (absent or real dir for TeamCreate runtime).
GLOBAL_CLAUDE="$HOME/.claude" # --- Skills --- GLOBAL_SKILLS_ALL=$(ls "$GLOBAL_CLAUDE/skills/" 2>/dev/null | sort) # Classify each entry: almanac vs external ALMANAC_GLOBAL_SKILLS="" EXTERNAL_GLOBAL_SKILLS="" for item in $GLOBAL_SKILLS_ALL; do target=$(readlink -f "$GLOBAL_CLAUDE/skills/$item" 2>/dev/null) if [ -z "$target" ]; then # Real directory (not a symlink) — external EXTERNAL_GLOBAL_SKILLS="$EXTERNAL_GLOBAL_SKILLS $item" elif echo "$target" | grep -q "^$ALMANAC_PATH"; then ALMANAC_GLOBAL_SKILLS="$ALMANAC_GLOBAL_SKILLS $item" else EXTERNAL_GLOBAL_SKILLS="$EXTERNAL_GLOBAL_SKILLS $item" fi done # Filter: _template is always extraneous for almanac content ALMANAC_GLOBAL_SKILLS=$(echo "$ALMANAC_GLOBAL_SKILLS" | tr ' ' '\n' | grep -v '^_template$' | grep -v '^$' | sort) # Missing: in registry but not in global almanac skills MISSING_GLOBAL_SKILLS=$(comm -23 <(echo "$REGISTERED_SKILLS") <(echo "$ALMANAC_GLOBAL_SKILLS")) # Broken: symlink exists but target doesn't resolve BROKEN_GLOBAL_SKILLS=$(find "$GLOBAL_CLAUDE/skills/" -maxdepth 1 -type l ! -exec test -e {} \; -printf '%f\n' 2>/dev/null | sort) # Stale almanac entries: in global almanac set but not in registry STALE_GLOBAL_SKILLS=$(comm -13 <(echo "$REGISTERED_SKILLS") <(echo "$ALMANAC_GLOBAL_SKILLS")) # --- Agents --- if [ -L "$GLOBAL_CLAUDE/agents" ] || [ -d "$GLOBAL_CLAUDE/agents" ]; then GLOBAL_AGENT_STATUS="OK" test -d "$GLOBAL_CLAUDE/agents" || GLOBAL_AGENT_STATUS="BROKEN" GLOBAL_AGENT_COUNT=$(ls "$GLOBAL_CLAUDE/agents/"*.md 2>/dev/null | wc -l) else GLOBAL_AGENT_STATUS="MISSING" GLOBAL_AGENT_COUNT=0 fi # --- Teams --- # Teams are NOT symlinked. TeamCreate uses ~/.claude/teams/ for runtime state. # A ~/.claude/teams symlink is a misconfiguration — warn if found. if [ -L "$GLOBAL_CLAUDE/teams" ]; then GLOBAL_TEAM_STATUS="MISCONFIGURED" GLOBAL_TEAM_COUNT=0 # Stale symlink — should be removed to avoid collision with TeamCreate else GLOBAL_TEAM_STATUS="OK" GLOBAL_TEAM_COUNT=0 fi
→ Zero missing almanac skills, zero broken. External listed, not flagged err.
If err: No
~/.claude/ → global hub not set up. See symlink-architecture guide for initial setup.
Step 5: Generate Audit Report
Summary table both layers.
# Discovery Symlink Audit Report **Date**: YYYY-MM-DD **Almanac**: <almanac_path> **Scope**: both | project | global ## Summary | Content | Registered | Project | Global (almanac) | Global (external) | |---------|------------|---------|-------------------|-------------------| | Skills | N | N | N | N | | Agents | N | STATUS | STATUS | — | | Teams | N | STATUS | STATUS | — | ## Issues ### Missing (registered but no symlink) - Project skills: [list or "none"] - Global skills: [list or "none"] ### Broken (symlink exists, target gone) - Project: [list or "none"] - Global: [list or "none"] ### Extraneous - Stale almanac (in discovery but not registry): [list or "none"] - _template in discovery path: [yes/no] - External content (non-almanac): [list — informational only]
→ Clear actionable report. Zero issues = clean.
If err: Report gen fails → raw counts + lists to console as fallback.
Step 6: Repair (Optional)
If
fix_mode = auto/interactive → fix found issues.
6a. Create missing project symlinks:
for skill in $MISSING_PROJECT_SKILLS; do ln -s "../../skills/$skill" "$PROJECT_CLAUDE/skills/$skill" done
6b. Create missing global symlinks:
for skill in $MISSING_GLOBAL_SKILLS; do ln -s "$ALMANAC_PATH/skills/$skill" "$GLOBAL_CLAUDE/skills/$skill" done
6c. Remove broken symlinks:
# Project for broken in $BROKEN_PROJECT_SKILLS; do rm "$PROJECT_CLAUDE/skills/$broken" done # Global for broken in $BROKEN_GLOBAL_SKILLS; do rm "$GLOBAL_CLAUDE/skills/$broken" done
6d. Remove stale almanac entries:
# Only remove items that target the almanac path but aren't in the registry for stale in $STALE_GLOBAL_SKILLS; do rm "$GLOBAL_CLAUDE/skills/$stale" done # Remove _template if present rm -f "$GLOBAL_CLAUDE/skills/_template" rm -f "$PROJECT_CLAUDE/skills/_template"
6e. Fix missing directory symlinks (agents/teams):
# Project agents if [ "$PROJECT_AGENT_STATUS" = "MISSING" ]; then ln -s ../agents "$PROJECT_CLAUDE/agents" fi # Project teams if [ "$PROJECT_TEAM_STATUS" = "MISSING" ]; then ln -s ../teams "$PROJECT_CLAUDE/teams" fi # Global agents if [ "$GLOBAL_AGENT_STATUS" = "MISSING" ]; then ln -s "$ALMANAC_PATH/agents" "$GLOBAL_CLAUDE/agents" fi # Global teams if [ "$GLOBAL_TEAM_STATUS" = "MISSING" ]; then ln -sf "$ALMANAC_PATH/teams" "$GLOBAL_CLAUDE/teams" fi
Important: Never remove items classified as external. Belong to other projects (peon-ping) → preserve.
→ All missing created, broken removed, stale cleaned. External untouched.
If err:
ln -s fails due to existing file/dir at target (empty dir instead of symlink) → remove blocker first w/ rmdir (empty) or flag for manual review (non-empty).
Step 7: Verify
Re-run Steps 3-4 checks → confirm repairs.
echo "=== Post-repair verification ===" echo "Project skills: $(ls "$PROJECT_CLAUDE/skills/" 2>/dev/null | grep -v '^_template$' | wc -l)" echo "Global skills (almanac): $(echo "$ALMANAC_GLOBAL_SKILLS" | wc -w)" echo "Broken project: $(find "$PROJECT_CLAUDE/skills/" -maxdepth 1 -type l ! -exec test -e {} \; -print 2>/dev/null | wc -l)" echo "Broken global: $(find "$GLOBAL_CLAUDE/skills/" -maxdepth 1 -type l ! -exec test -e {} \; -print 2>/dev/null | wc -l)" echo "Project agents: $PROJECT_AGENT_STATUS ($PROJECT_AGENT_COUNT .md files)" echo "Global agents: $GLOBAL_AGENT_STATUS ($GLOBAL_AGENT_COUNT .md files)" echo "Project teams: $PROJECT_TEAM_STATUS ($PROJECT_TEAM_COUNT .md files)" echo "Global teams: $GLOBAL_TEAM_STATUS ($GLOBAL_TEAM_COUNT .md files)"
→ Zero missing, zero broken. Counts match registered totals (almanac). External listed separately.
If err: Issues remain post-repair → report specific failures. Common: perms on
~/.claude/, NTFS path length on /mnt/, non-empty dir blocking symlink.
Check
- Almanac path identified + contains all 3 registries
- Registry counts match
headers (or discrepancy noted)total_* - Project-level skills/agents/teams audited
- Global-level skills/agents/teams audited
- External content identified + excluded from issue counts
-
entries flagged extraneous_template - Audit report generated w/ counts + lists
-
→ safe repairs applied, external untouchedfix_mode=auto - Post-repair verification → zero missing, zero broken
Traps
-
External vs missing almanac:
may contain skills from other projects (peon-ping). Check symlink target is under almanac path before classifying stale/extraneous.~/.claude/skills/ -
Remove external: Never delete items not targeting almanac. Belong to other projects, intentional.
-
Symlink
dirs: Scaffolding, not consumable. Should never appear in_template
or.claude/skills/
. Bulk sync scripts must skip..claude/agents/ -
Stale
symlink: Symlink pointing to team defs = misconfig. TeamCreate uses.claude/teams
for runtime (config.json, inboxes). If this path symlinks almanac~/.claude/teams/
→ runtime artifacts written to git-tracked repo. Remove anyteams/
symlink found..claude/teams -
Relative vs absolute paths: Project-level skill symlinks = relative (
). Global = absolute (../../skills/<name>
). Mixing → breakage on moves./path/to/almanac/skills/<name> -
Registry header vs actual count:
in header may be stale if entries added w/o count update. Trust actualtotal_skills
entries, not header.- id:
→
- repair-broken-references — general broken link/ref repair
- tidy-project-structure — project dir org
- create-skill — inc. symlink creation for new skills (Step 13)
- create-agent — inc. discovery verification (Step 10)
- create-team — team creation w/ registry integration