| name | plans-library |
| description | Builds and opens a filterable HTML gallery of all plans in the plans directory. Scans HTML plans, parses metadata, and writes index.html. Use when asked to browse plans or view plan history. |
| allowed-tools | Bash, Read, Write, ToolSearch, ExitPlanMode |
plans-library
Scan every HTML plan in the plans directory, parse each plan's metadata, populate the gallery template, write index.html, and open it in the browser.
Exit plan mode
ExitPlanMode is a deferred tool. Only call it if currently in plan mode — skip this step entirely when not in plan mode. When calling: use ToolSearch with select:ExitPlanMode first, then call ExitPlanMode silently.
Step 1 — Resolve the plans directory
Read plansDirectory following Claude Code's settings precedence — project-local .claude/settings.local.json, then project .claude/settings.json, then global ~/.claude/settings.json; the first that sets it wins. Fall back to ${PWD}/docs/plans if none do.
PLANS_DIR=$(python3 - <<'EOF'
import json, os, sys
candidates = (
os.path.join(os.getcwd(), '.claude', 'settings.local.json'),
os.path.join(os.getcwd(), '.claude', 'settings.json'),
os.path.join(os.path.expanduser('~'), '.claude', 'settings.json'),
)
for path in candidates:
try:
v = json.load(open(path)).get('plansDirectory', '').strip()
if v:
print(v); sys.exit(0)
except Exception:
pass
print(os.path.join(os.getcwd(), 'docs', 'plans'))
EOF
)
If the directory does not exist, or contains no .html files (other than index.html) at the top level, tell the user:
"No HTML plans found in <PLANS_DIR>. Run /plan-agent:implementation-plan to create your first plan."
STOP. (Saved artifacts have their own gallery at docs/artifacts/, built by build-artifacts-index.sh — they are not part of the plans library.)
Step 2 — Locate plugin templates directory
The plugin may be installed as a versioned cached copy (e.g. …/plan-agent/0.11.0/templates) or loaded directly (e.g. …/plan-agent/templates). Both patterns must be tried:
TEMPLATES_DIR=$( { \
find ~/.claude/plugins -path "*/plan-agent/*/templates" -type d 2>/dev/null | sort -rV; \
find ~/.claude/plugins -path "*/plan-agent/templates" -type d 2>/dev/null; \
find "$PWD" -path "*/plan-agent/templates" -type d 2>/dev/null; \
} | head -1 )
If TEMPLATES_DIR is empty, output:
"Templates not found. Install the plugin or load it with --plugin-dir." and STOP.
Step 3 — Scan plan files
Collect all .html files in PLANS_DIR, excluding index.html. The -maxdepth 1 flag prevents recursion into archive/, artifacts/, or any other subdirectory — saved artifacts have their own gallery (docs/artifacts/) and never appear here. Capture the result in PLAN_FILES.
PLAN_FILES=$(find "$PLANS_DIR" -maxdepth 1 -name "*.html" ! -name "index.html" 2>/dev/null | sort)
Do not sort by filesystem modification time (ls -t). A git clone/checkout resets every file's mtime to checkout time, so mtime order is meaningless. The gallery's newest-first order is established in Step 4 from each plan's plan-created metadata, not from the filesystem.
Step 4 — Build {{GALLERY_ENTRIES}}
Iterate over each file $f from $PLAN_FILES (one path per line). For each, parse its metadata using Python 3. Output is JSON to safely handle titles that contain | or other special characters:
while IFS= read -r f; do
python3 - "$f" <<'EOF'
import re, sys, os, json
f = sys.argv[1]
try:
content = open(f, encoding='utf-8', errors='replace').read()
except Exception:
sys.exit(0)
def meta(name, fallback=''):
m = re.search(r'<meta\s+name="' + name + r'"\s+content="([^"]*)"', content)
return m.group(1).strip() if m else fallback
def get_title():
m = re.search(r'<title>(?:Plan:\s*)?([^<]+)</title>', content, re.I)
return m.group(1).strip() if m else os.path.basename(f)
print(json.dumps({
'status': meta('plan-status', 'todo'),
'type': meta('plan-type', 'untyped'),
'effort': meta('plan-effort', '').lower(),
'created': meta('plan-created', ''),
'title': get_title(),
}))
EOF
done <<< "$PLAN_FILES"
Parse the JSON output with json.loads() into a list of entries. Sort the list newest-first by created descending (compare the YYYY-MM-DD strings; entries with an empty created sort last, then break ties by title ascending). Then, from each entry in sorted order, generate one <a> block:
<a class="gallery-card" href="{HREF}"
data-status="{STATUS}" data-type="{TYPE}" data-effort="{EFFORT}" data-title="{TITLE_LOWER}">
<div class="card-badges">
<span class="status-chip status-{STATUS}">{STATUS_DISPLAY}</span>
<span class="type-chip type-{TYPE}">{TYPE}</span>
<span class="effort-chip effort-{EFFORT}">{EFFORT}</span>
</div>
<div class="card-title">{TITLE}</div>
<div class="card-meta">
<span class="card-date">{CREATED}</span>
<span class="card-file">{BASENAME}</span>
</div>
</a>
Where:
{BASENAME} = filename without path (e.g. add-dark-mode-toggle.html), used directly as the link target
{STATUS} = plan-status value, lowercased (e.g. todo, in-progress, completed)
{STATUS_DISPLAY} = {STATUS} with hyphens replaced by spaces (e.g. in progress)
{TYPE} = plan-type value, lowercased (e.g. feature, fix)
{EFFORT} = plan-effort value, lowercased (low | medium | high). When empty (no plan-effort tag), set data-effort="" and omit the entire <span class="effort-chip …"> element — a no-effort plan shows no badge and passes every effort filter
{TITLE_LOWER} = title lowercased (used by search filter)
{TITLE} = title text (strip a leading "Plan: " prefix if present)
{CREATED} = plan-created value (e.g. 2026-05-30); omit the <span class="card-date"> if empty
HTML-escape all values before inserting: replace & → &, < → <, > → >, " → ", ' → '.
Step 5 — Populate and write the gallery
- Get the current timestamp:
GENERATED_AT=$(date '+%Y-%m-%d %H:%M')
-
Read the gallery template from $TEMPLATES_DIR/plans-gallery.html.
-
Substitute in the template:
{{GALLERY_TITLE}} → Plans (the shared template also serves the Artifacts gallery, which substitutes Artifacts)
{{GALLERY_ENTRIES}} → concatenated <a> blocks from Step 4
{{PLAN_COUNT}} → total number of plan cards rendered
{{GENERATED_AT}} → value of $GENERATED_AT
-
Write the result to $PLANS_DIR/index.html.
Step 6 — Open in browser
GALLERY_PATH=$(realpath "$PLANS_DIR/index.html" 2>/dev/null || echo "$PLANS_DIR/index.html")
open "$GALLERY_PATH" 2>/dev/null || xdg-open "$GALLERY_PATH" 2>/dev/null || true
Tell the user:
"Plans library generated at <PLANS_DIR>/index.html with {count} plans — opened in your browser. Click any card to open it."
Step 7 — Stop
STOP. Do not run git commands or invoke other skills after delivering the gallery.