Scan a directory or workspace for SKILL.md files across all agents and repos, capture supporting files (references, scripts, linked docs), dedupe vendored copies, enrich each Tessl tile with registry signals, and emit a canonical JSON inventory validated by JSON Schema. Then run four analytical phases in parallel against the inventory — staleness + git provenance (history, broken refs, contributors), quality (Tessl `skill review`), duplicates (similarity + LLM judgement), registry-search (per-standalone-skill registry suggestions, HTTP only) — and render a self-contained interactive HTML report with a top-of-report health overview, top-issues panel, recently-changed list, and per-tessl.json manifests view.
84
90%
Does it follow best practices?
Impact
97%
1.44xAverage score across 2 eval scenarios
Advisory
Suggest reviewing before use
Orchestrate the full skill-insights pipeline: discovery → 3 parallel analytical phases → HTML render.
One scan = one report. Whatever the user asks for — "scan this repo", "scan the workspace", "scan A and B and C" — produce ONE consolidated discovery.json + ONE report.html covering all of it. Do NOT run the pipeline once per repo and produce three separate reports — discovery's --repo flag is designed exactly so you can scan multiple repos in a single run.
discover-skills → discovery.json
↓
┌────────┬────┴────┬────────────┐
↓ ↓ ↓ ↓
staleness quality duplicates registry-search
(script) (script+LLM)(script+LLM) (script+HTTP)
↓ ↓ ↓ ↓
└────────┴────┬────┴────────────┘
↓
render reportEach analytical phase reads discovery.json independently and writes its own output. They never share state. The render step reads all five (discovery, staleness, quality, duplicates, registry-search) and produces a unified HTML report.
This step is critical and easy to get wrong. Map the user's request to ONE of three scan modes:
| User said... | Mode | What you do |
|---|---|---|
| "scan this repo" / "run on the current repo" / no repos named | single repo | SCAN_ROOT=$(pwd) (or the named repo path); no --repo flags |
| "scan all my repos" / "scan the workspace" / "scan ~/repos" | workspace auto | SCAN_ROOT=<the parent dir>; no --repo flags. Discovery picks up every immediate git child. |
| "scan repo A and repo B" / "scan lightdash and monorepo and m" / any list of named repos | explicit selection | SCAN_ROOT=<their common parent>; pass --repo PATH once per named repo to discovery. ONE pipeline run, ONE report covering all of them. |
Do NOT dispatch separate orchestrator instances per repo. The whole point of the explicit-selection mode is that a single scan handles multiple repos and produces ONE consolidated report. If the user names three repos, you call discovery once with three --repo flags, then run the analytical phases ONCE against the combined discovery.json.
If you're unsure whether the user wanted one report or three, ask. Default assumption: one consolidated report.
SCAN_ROOT — the directory to scan. Defaults to $(pwd). For explicit-selection mode, use the common parent of the named repos (e.g. ~/repos/).REPOS — optional list of repo paths for explicit-selection mode. Each becomes a --repo PATH flag passed to discovery.OUTPUT_DIR — where to write outputs. Defaults to $SCAN_ROOT/.skill-insights/. The whole pipeline writes here.SCAN_ROOT="${SCAN_ROOT:-$(pwd)}"
OUTPUT_DIR="$SCAN_ROOT/.skill-insights"
mkdir -p "$OUTPUT_DIR"
SCAN_ID="scan-$(date +%Y%m%d-%H%M%S)"
if [ -d "$HOME/.tessl/tiles/tessleng/skill-insights/references" ]; then
TILE_ROOT="$HOME/.tessl/tiles/tessleng/skill-insights"
elif [ -d "$(pwd)/.tessl/tiles/tessleng/skill-insights/references" ]; then
TILE_ROOT="$(pwd)/.tessl/tiles/tessleng/skill-insights"
else
echo "ERROR: skill-insights tile not found." >&2; exit 1
fiInvoke discover-skills (see its SKILL.md). It writes $OUTPUT_DIR/discovery.json.
Pass --repo PATH repeatedly when the user named a specific set of repos:
# Single repo (or workspace auto-discovery)
python3 "$TILE_ROOT/skills/discover-skills/scripts/discover_skills.py" \
--scan-root "$SCAN_ROOT" \
--output "$OUTPUT_DIR/discovery.json"
# Explicit selection: scan repos A, B, C in one combined run
python3 "$TILE_ROOT/skills/discover-skills/scripts/discover_skills.py" \
--scan-root "$SCAN_ROOT" \
--repo "$SCAN_ROOT/repo-a" \
--repo "$SCAN_ROOT/repo-b" \
--repo "$SCAN_ROOT/repo-c" \
--output "$OUTPUT_DIR/discovery.json"Wait for it to complete. Validate:
[ -f "$OUTPUT_DIR/discovery.json" ] && jq -e '.schema_version == "1.3"' "$OUTPUT_DIR/discovery.json" > /dev/nullIf discovery fails, stop. The downstream phases all depend on discovery.json.
Once discovery.json exists, dispatch the four phases as parallel subagents. Each phase reads discovery.json independently and writes its own output JSON. Quality and duplicates have absorbed their LLM work into single Python scripts (quality shells out to tessl skill review; duplicates still uses prep + per-pair subagents — see below) — so from the orchestrator's perspective, all four phases look uniform.
In a single assistant message, invoke the Task tool four times:
Skill: analyze-skill-staleness. Reads discovery.json, runs a single git log per skill, derives age signals + per-skill git_provenance (created_by, last_modified_by, contributors, recent commits) from one stream, writes staleness.json (schema 1.1). Fully deterministic — no LLM dispatch.
Pass to the subagent:
$OUTPUT_DIR/discovery.json$OUTPUT_DIR/staleness.jsonSkill: analyze-skill-quality. Reads discovery.json, then runs tessl skill review --json per skill in parallel batches (concurrency 8). Tile-level quality is pulled directly from discovery.tiles[].registry.scores.quality for tiles already scored on the registry. The orchestrating subagent runs ONE script (analyze_quality.py) which emits quality.json directly — no per-skill prompt files, no per-skill verdict dispatch, no finalize step.
Pass to the subagent:
$OUTPUT_DIR/discovery.json$OUTPUT_DIR/quality.json--skip-published-skills to avoid tessl skill review calls for skills whose owning tile already has a registry.scores.quality (uses the tile-level score as a passthrough; trades per-skill detail for speed)Skill: detect-skill-duplicates. Reads discovery.json, runs prep script to compute candidate pairs (default cap: 10 highest-similarity pairs) and write a duplicates-prompts/ directory (one prompt file per pair + an index.json), dispatches all sub-subagents in a single parallel batch (each writing its verdict to duplicates-verdicts/<idx>.json), then runs the finalize script.
Pass to the subagent:
$OUTPUT_DIR/discovery.json$OUTPUT_DIR/duplicates-prompts/$OUTPUT_DIR/duplicates-verdicts/$OUTPUT_DIR/duplicates.jsonSkill: registry-search. Reads discovery.json, filters to skills with source_type: "standalone", queries /experimental/search (hybrid mode) once for the top-1 skill match and once for the top-1 tile match per candidate, picks the higher-aggregate-score hit as best_match, and writes registry-search.json (schema 1.2). Stdlib + HTTP only — no LLM, no agent judgement.
Pass to the subagent:
$OUTPUT_DIR/discovery.json$OUTPUT_DIR/registry-search.json--registry-base-url, --concurrency (defaults: https://api.tessl.io, 8). The endpoint is queried anonymously — no Tessl auth required.You MUST wait for every subagent to return before proceeding to render. Each one writes its output to disk; the render step reads from disk.
If any phase fails:
tessl skill review is unavailable, the user isn't authenticated, or the registry is unreachable. The phase records failed_skills[] in metadata; quality.json still gets written with whatever succeeded. Proceed.failed_pairs[] in metadata. Duplicates.json still gets written. Proceed.registry-search.json with per-record search_errors[] and a top-level warnings[] notice; the renderer treats it as "skipped". Proceed.After all return, verify:
ls -la "$OUTPUT_DIR"/{discovery,staleness,quality,duplicates,registry-search}.json 2>/dev/null
jq -e '.schema_version == "1.3"' "$OUTPUT_DIR"/discovery.json > /dev/null
jq -e '.schema_version == "1.1"' "$OUTPUT_DIR"/staleness.json > /dev/null
jq -e '.schema_version == "2.0"' "$OUTPUT_DIR"/quality.json > /dev/null
jq -e '.schema_version == "1.0"' "$OUTPUT_DIR"/duplicates.json > /dev/null
jq -e '.schema_version == "1.2"' "$OUTPUT_DIR"/registry-search.json > /dev/nullEach phase script also self-validates its output against the corresponding JSON Schema in references/schemas/ before writing (when jsonschema is installed). A schema-mismatched output exits with code 2 before reaching disk, so by the time you read these files they're guaranteed to conform to the schema versions above. Missing-file is acceptable for the render step (the report degrades gracefully); malformed JSON or a wrong schema_version is not.
Read the HTML report template. It now has multiple injection points:
<!--@DISCOVERY_DATA@--> — the full discovery.json contents<!--@STALENESS_DATA@--> — the full staleness.json contents (or null if missing)<!--@QUALITY_DATA@--> — the full quality.json contents (or null if missing)<!--@DUPLICATES_DATA@--> — the full duplicates.json contents (or null if missing)<!--@REGISTRY_SEARCH_DATA@--> — the full registry-search.json contents (or null if missing)Substitute each placeholder. Write to $OUTPUT_DIR/report.html. Prefer the bundled renderer so template placeholders and phase filenames stay in one tested place:
python3 "$TILE_ROOT/skills/run-skill-insights/scripts/render_report.py" \
--template "$TILE_ROOT/references/report-template.html" \
--output-dir "$OUTPUT_DIR" \
--report "$OUTPUT_DIR/report.html"Equivalent implementation sketch:
TEMPLATE="$TILE_ROOT/references/report-template.html"
REPORT="$OUTPUT_DIR/report.html"
python3 <<'PY'
import json, os
def load_or_null(p):
if not os.path.exists(p): return "null"
return open(p).read().replace('</', '<\\/')
template = open(os.environ['TEMPLATE']).read()
out = template
out = out.replace('<!--@DISCOVERY_DATA@-->', load_or_null(os.environ['OUTPUT_DIR'] + '/discovery.json'))
out = out.replace('<!--@STALENESS_DATA@-->', load_or_null(os.environ['OUTPUT_DIR'] + '/staleness.json'))
out = out.replace('<!--@QUALITY_DATA@-->', load_or_null(os.environ['OUTPUT_DIR'] + '/quality.json'))
out = out.replace('<!--@DUPLICATES_DATA@-->', load_or_null(os.environ['OUTPUT_DIR'] + '/duplicates.json'))
out = out.replace('<!--@REGISTRY_SEARCH_DATA@-->', load_or_null(os.environ['OUTPUT_DIR'] + '/registry-search.json'))
open(os.environ['REPORT'], 'w').write(out)
PY
# Verify no placeholders remain
! grep -q '<!--@.*_DATA@-->' "$REPORT" || echo "ERROR: placeholder not substituted"Skill Insights scan complete.
Scan root: <scan_root>
Repos: <N>
Skills: <N> logical (<M> paths)
Tiles: <N> total — published_to_registry=<N>, authored_only=<N>
by_tier: published=<N>, authored=<N>, github=<N>
Security: <N> tiles flagged MEDIUM/HIGH/CRITICAL
Updates: <N> tiles outdated vs registry
Staleness: median <D>d, <stale> stale, <broken> with broken refs
Quality: avg <S>/100, <good>/<accept>/<needs_work>/<poor>
(<R> from registry, <S> via tessl skill review, <P> passthrough)
Duplicates: <C> clusters covering <N> skills (potential reduction: <R>)
Registry: <K> skill matches, <T> tile matches across <N> non-published skills
Context budget: <FL> tokens front-loaded, <ODmin>-<ODmax> on-demand range
JSON: <OUTPUT_DIR>/{discovery,staleness,quality,duplicates,registry-search}.json
Report: <OUTPUT_DIR>/report.html
Open the report:
open <OUTPUT_DIR>/report.htmlRead the four JSON files to populate the summary; gracefully omit any phase that didn't produce output.
End. The user has a JSON inventory + per-skill staleness scores + per-skill quality scores + duplicate clusters + per-skill registry-search suggestions + a unified HTML report.
Behaviour analysis (load-bearing skills from local agent conversation logs), security (Snyk integration), missing-skills (workflow-gap analysis from logs). Each would slot in as another parallel branch alongside the current three. The render step's template would gain a new injection point per phase.
A sibling skill, posthog-skill-query, partially addresses what an "activation health" phase would do — it pulls org-wide skill / loaded-skill / MCP usage from PostHog's cli:agent-signals:* event stream. It's deliberately standalone and not invoked by this orchestrator; it writes its own org_usage.json + org_usage.html for downstream cross-reference. Run it independently when you want that view.