kit/plugins/plan-agent/skills/plans-library/SKILL.md
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.
npx skillsauth add shawn-sandy/agentics plans-libraryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
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
# Claude settings precedence: project-local → project → user-global
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-planto 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.)
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.
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). Agit clone/checkoutresets 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'splan-createdmetadata, not from the filesystem.
{{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 emptyHTML-escape all values before inserting: replace & → &, < → <, > → >, " → ", ' → '.
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_ATWrite the result to $PLANS_DIR/index.html.
Confirm the written index is complete and renders one card per plan that Step 4 actually parsed.
Set SOURCE_COUNT to the number of entries Step 4 emitted — not the raw line count of $PLAN_FILES. Step 4 skips any plan it cannot read, so comparing against the raw file list reports a mismatch for a file that was deliberately skipped. If Step 4 skipped anything, name those files when reporting.
SOURCE_COUNT=<number of entries parsed in Step 4>
python3 - "$PLANS_DIR/index.html" "$SOURCE_COUNT" <<'EOF'
import sys
from html.parser import HTMLParser
path, expected = sys.argv[1], int(sys.argv[2])
html = open(path, encoding='utf-8').read()
class Counter(HTMLParser):
cards = 0
def handle_starttag(self, tag, attrs):
if tag == 'a' and 'gallery-card' in dict(attrs).get('class', '').split():
self.cards += 1
c = Counter()
c.feed(html)
if not html.rstrip().endswith('</html>'):
sys.exit(f"TRUNCATED: {path} does not end with </html>")
print(f"OK: {c.cards} cards from {expected} plans" if c.cards == expected
else f"MISMATCH: index has {c.cards} cards but {expected} plans were parsed")
sys.exit(0 if c.cards == expected else 1)
EOF
If the command exits non-zero, report the failure to the user — naming the index path, the card count, the parsed count, and any plans Step 4 skipped — and STOP instead of opening the gallery.
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.htmlwith {count} plans — opened in your browser. Click any card to open it."
STOP. Do not run git commands or invoke other skills after delivering the gallery.
development
Checks whether the branch's PR is ready and merges it when green. Runs the readiness gate, lint, and an approval prompt. Use when the user asks "merge?" or if a PR is ready to merge.
development
Implements a plan file that already exists. Walks its steps, ticks the spec, re-renders, and runs the completion gates. Use when asked to implement an existing plan.
development
Audits and optimizes CLAUDE.md project memory files. Checks adherence to Claude Code best practices and produces actionable fixes. Use when the user asks to audit, optimize, or diagnose a CLAUDE.md.
development
Converts an HTML artifact or Markdown file into a draft post for a static site. Scopes CSS to keep interactive blocks alive and escapes prose for MDX. Use when asked to turn an artifact into a post.