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
A Tessl tile that scans one or more repositories for SKILL.md files, enriches every Tessl tile it finds with registry signals (quality, security, uplift, eval coverage, version drift, context cost), then runs four analytical phases — staleness + git provenance, quality, duplicates, registry-search — and produces a self-contained interactive HTML report. All cross-phase JSON outputs are validated against JSON Schemas at the IO boundary.
A separate sibling skill, posthog-skill-query, is standalone (not part of the orchestrator) and pulls org-wide skill / MCP / session telemetry from PostHog into its own JSON + HTML artifact. It exists to feed a downstream cross-reference step alongside the per-repo phases.
The unique value is multi-repo aggregate visibility — Tessl's registry knows about one tile at a time; skill-insights spans the full skill estate across repos, including vanilla Claude / Cursor skills that aren't on the registry at all.
discover-skills → discovery.json (skills[] + tiles[] with full enrichment)
↓
┌────────┬────┴────┬────────────┐
↓ ↓ ↓ ↓
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 JSON. They never share state. The render step inlines all five JSONs into one self-contained HTML report.
posthog-skill-query runs outside this orchestrated pipeline. It writes a separate org_usage.json + org_usage.html and reads no other phase's output. See its section below.
From any directory:
/tessl__run-skill-insightsBy default scans $(pwd). The discovery script supports three repo-selection modes:
| Mode | Trigger | Behaviour |
|---|---|---|
| Single repo | scan-root is a git repo | Just that repo |
| Workspace auto | scan-root is a parent dir with git children | Every immediate .git-having child |
| Explicit selection | One or more --repo PATH flags | Exactly the listed repos; ignores other children of scan-root |
# Cherry-pick: scan only monorepo + lightdash from a workspace dir
discover_skills.py \
--scan-root ~/repos \
--repo ~/repos/monorepo \
--repo ~/repos/lightdashOutput lands in .skill-insights/:
.skill-insights/
├── discovery.json ← canonical inventory + per-tile-instance enrichment (schema 1.4)
├── staleness.json ← per-skill staleness scores + git provenance + estate summary (schema 1.1)
├── quality.json ← per-skill review scores + per-tile rollup (schema 2.0)
├── duplicates.json ← duplicate clusters + overlapping pairs (schema 1.0)
├── registry-search.json ← per-standalone-skill registry suggestions (schema 1.2)
└── report.html ← self-contained interactive reportThe duplicates phase creates intermediate duplicates-prompts/ and duplicates-verdicts/ directories during its subagent dispatch. The finalize step deletes them once the verdicts are rolled up; pass --keep-intermediates to preserve them.
Every skill is stamped with a tier reflecting how it lives in the repo:
| Tier | What it means |
|---|---|
published_tile | Owned by a Tessl tile installed from the registry (.tessl/tiles/...) |
authored_tile | Owned by a Tessl tile authored locally (source: "file:..." in tessl.json, or a tile.json under tiles/ with no declaration) |
github_tile | Owned by a Tessl tile installed from a GitHub source |
claude_plugin | Owned by a Claude plugin (.claude-plugin/plugin.json) |
non_tile | Loose SKILL.md not part of any tile/plugin (e.g. .claude/skills/foo/SKILL.md in a vanilla Claude repo) |
Tiles get the same tier (minus non_tile). Tile records are materialised instances, so a local authored source and an installed .tessl/ copy of the same tile name stay separate. A locally-authored tile (tier: authored_tile) can also be published_to_registry: true — that's a common pattern for Tessl-internal tiles authored AND published from the same monorepo. The two flags are orthogonal.
skills/discover-skills/scripts/discover_skills.py (Python stdlib only).
Per skill (always available):
SKILL.md files; symlink-following with per-chain cycle detectionreferences/, scripts/, markdown links, @imports, inline backtick pathsPer tile (when tessl CLI is available + authenticated):
GET /v1/tiles/{ws}/{name}/versions/{ver} for every tile that resolves on the registry — pulls aggregate / quality / impact / security / eval scores / uplift multiplier / moderation / archived status / fingerprinttessl outdated --json per scanned repo, mapped per tile to detect "newer version available"tessl tile lint per tile parsed into front-loaded + on-demand token totals (per-skill breakdown)Broken-reference detection (git-history backed):
A path-shaped reference (markdown link, @import, or inline backtick) is flagged as broken iff:
References to paths that were never tracked are silently ignored — no false positives from prose mentioning external code or package names. No extension allowlist; the repo's own git history is the oracle.
See references/schemas/discovery.schema.json for the full output contract. The phase script validates its output against this schema before writing.
skills/analyze-skill-staleness/scripts/analyze_staleness.py. Reads discovery.json, runs a single git log per skill (with path-priority fallback so vendored gitignored copies still find their tracked source). One stream gives us age, commit count, and per-skill provenance (created_by, last_modified_by, top contributors, recent commits) all derived in-process. Extracts broken refs from discovery warnings, computes a 0-100 staleness score per skill.
Score factors:
commit_count == 1)Buckets: fresh / warm / stale / ancient / unknown.
Provenance fields per skill (added in schema 1.1) — useful for "who created this skill / who last touched it / who else worked on it". null when no git history is available. See references/schemas/staleness.schema.json.
skills/analyze-skill-quality/scripts/analyze_quality.py. Single async script that shells out to tessl skill review --json per skill in parallel batches (concurrency 8 by default).
registry.scores.quality from discovery.tiles[] when available, falls back to mean of per-skill review scores otherwise--skip-published-skills flag: trade per-skill detail for speed by passing through registry tile-level scores instead of running review per skillCost: ~12s per skill cold, but LiteLLM caches at the proxy for 24h — repeat scans on unchanged content are sub-second per skill. For 72 skills cold: ~2 min wall-clock. See references/schemas/quality.schema.json.
This phase replaces an earlier custom 6-dimension rubric. We use Tessl's canonical assessment so the scores match what tessl skill publish gates on.
skills/detect-skill-duplicates/scripts/{prepare_duplicates,finalize_duplicates}.py.
Hybrid:
--allow-cross-repo to broaden). Writes one judgement prompt per pair to duplicates-prompts/<idx>.txt.--max-pairs 10, so 10 subagents at most). Each returns duplicate / overlapping / independent.duplicate verdicts → transitive clusters. Picks dominant_skill_id per cluster. overlapping verdicts go into a separate list. Cleans up prompts/verdicts dirs unless --keep-intermediates.See references/schemas/duplicates.schema.json (final output) and references/schemas/duplicate-verdict.schema.json (per-pair subagent verdict contract).
skills/registry-search/scripts/registry_search.py. Reads discovery.json, filters to skills with source_type: "standalone" (skills not yet declared in any tile or agent harness), and queries the Tessl registry's /experimental/search endpoint twice per candidate (once filtered to type=skill, once to type=tile, both searchMode=hybrid, page[size]=1). The higher-aggregate-score hit is recorded as best_match, tagged with kind: "skill" | "tile" so consumers read kind-specific fields without re-querying. Per-request HTTP errors are captured in search_errors[] without aborting the phase.
Anonymous — no Tessl auth required; /experimental/search returns public registry data. Stdlib + HTTP only, no LLM. Runtime is dominated by HTTP latency: ~5-10s for 30 standalone skills at concurrency 8.
The phase exists to flag where a published Tessl tile or skill could replace a locally-authored standalone one. Skills that already belong to a tile (tessl_tile_skill, claude_skill, cursor_skill, agents_skill, claude_plugin_skill) are skipped because the user has already chosen a source for them. The skipped count surfaces as metadata.skills_skipped_non_standalone.
See references/schemas/registry-search.schema.json.
skills/posthog-skill-query/scripts/{fetch_org_usage,render_org_usage}.py. Pulls organisation-wide skill / MCP / session telemetry from PostHog (the cli:agent-signals:* events emitted by the Tessl CLI) and writes org_usage.json plus an interactive HTML report. Standalone — does not read discovery.json, is not invoked by run-skill-insights, and does not feed into the main report.html.
Captures, per configured time window (default 7d / 30d / 90d):
cli:agent-signals:skill-activation events keyed by (skillTile, skillName), with provider split (claude-code vs cursor-ide).skillTile (third-party / private SKILL.md).installedSkills[] array on activation events. Tells you who has each skill available, independent of activation. The same (tile, name) can appear at both project and global scope.cli:agent-signals:mcp-tool-activation per tool (query_library_docs, search, install, etc.).totalMessages, totalSkillCalls, tesslSkillCalls, tesslMcpCalls, tesslToolCalls from cli:agent-signals:session-processed events.properties.gitRepo (repos[] plus tiles_by_repo[], skills_by_repo[], untiled_skills_by_repo[], mcp_tools_by_repo[], session_aggregates_by_repo). Capped at top N repos (default 200, --top-repos) by primary-window activations. Powers the report's chip filter, which lets the reader untick repos client-side without re-querying PostHog. Events with no gitRepo are excluded from these views; their volume remains visible via filter.events_per_window.*.events_no_gitrepo and they still feed the all-repos totals."Org" is defined by two filters that combine with OR — an event passes if either matches:
--filter-repos (default github.com/tesslio) — properties.gitRepo prefix allowlist.--filter-email-domains (default tessl.io) — person.properties.email @<domain> suffix allowlist.Pass "" to disable either half. Disabling both pulls every event in the project. The output's filter.events_per_window block reports per-window matched/excluded counts and a per-source split (matched-by-repo / matched-by-email) so coverage is visible.
The output is raw counts only — no shelf_warmer / silent / active buckets, no conversion ratios, no warnings list of judgement calls. Cross-reference and value judgements happen downstream. See references/schemas/org-usage.schema.json.
Run it standalone:
python3 skills/posthog-skill-query/scripts/fetch_org_usage.py \
--output /tmp/org_usage.json
python3 skills/posthog-skill-query/scripts/render_org_usage.py \
--input /tmp/org_usage.json \
--output /tmp/org_usage.htmlThe orchestrator inlines all five JSON files into references/report-template.html (placeholders <!--@DISCOVERY_DATA@-->, <!--@STALENESS_DATA@-->, <!--@QUALITY_DATA@-->, <!--@DUPLICATES_DATA@-->, <!--@REGISTRY_SEARCH_DATA@-->) and writes report.html. Self-contained — no external CSS/JS beyond the Atkinson Hyperlegible web font.
The report is laid out top-down:
git_provenance. Click to open drawer.tiles/, apps/, packages/, research/, .tessl/tiles/, …). The Source badge marks how the tile was discovered (tessl.json declaration vs. found by walking the SKILL.md tree). Click a row for the full tile drawer (registry signals, eval breakdown, declarations, per-skill context cost).tessl.json the scan found, grouped by repo. Each row shows path, total/resolved/unresolved dependency counts, and the tiles each manifest declares (clickable → tile drawer).tessl.jsons depend on it), Paths, Frontmatter, Body preview, Supporting files, Bundled directories, Quality (dual-judge dimension scores + suggestions), Staleness (factors + broken refs), Provenance (created_by, last_modified_by, contributors, recent commits), Duplicate cluster membership.Each analytical phase reads only discovery.json. They can be run in any order, or one at a time:
python3 skills/analyze-skill-staleness/scripts/analyze_staleness.py \
--discovery /path/to/.skill-insights/discovery.json
python3 skills/analyze-skill-quality/scripts/analyze_quality.py \
--discovery /path/to/.skill-insights/discovery.json --max-skills 5
python3 skills/detect-skill-duplicates/scripts/prepare_duplicates.py \
--discovery /path/to/.skill-insights/discovery.json
python3 skills/registry-search/scripts/registry_search.py \
--discovery /path/to/.skill-insights/discovery.jsonUseful for iterating on one phase without re-running the others.
Every phase output (and every cross-phase intermediate) is described by a JSON Schema in references/schemas/. Each phase script validates its inputs and outputs against the relevant schema at the IO boundary — bad shape aborts the run before downstream phases get to read it.
| File | Schema | Contract |
|---|---|---|
discovery.json | 1.4 | [discovery.schema.json](references/schemas/discovery.schema.json) |
staleness.json | 1.1 | [staleness.schema.json](references/schemas/staleness.schema.json) (1.1 adds git_provenance per skill: created_by, last_modified_by, contributors, recent_commits) |
quality.json | 2.0 | [quality.schema.json](references/schemas/quality.schema.json) |
duplicates.json | 1.0 | [duplicates.schema.json](references/schemas/duplicates.schema.json) |
registry-search.json | 1.2 | [registry-search.schema.json](references/schemas/registry-search.schema.json) |
duplicates-prompts/index.json | 1.0 | [duplicates-prompts-index.schema.json](references/schemas/duplicates-prompts-index.schema.json) (handoff between prep and judge subagents) |
duplicates-verdicts/<n>.json | — | [duplicate-verdict.schema.json](references/schemas/duplicate-verdict.schema.json) (one file per subagent verdict) |
org_usage.json | 1.4 | [org-usage.schema.json](references/schemas/org-usage.schema.json) (standalone sibling — produced by posthog-skill-query, not consumed by the main render step) |
Validation is best-effort: scripts try to import jsonschema (recommended: pip install jsonschema), and if it isn't installed they print a single warning and skip validation, preserving the stdlib-only fallback. With jsonschema available, output validation runs strictly (a malformed output aborts the script with exit 2 and a paginated error report). Per-pair duplicate verdicts validate non-strictly — a bad verdict shape from one subagent is recorded in metadata.failed_pairs[] and the rest of the run continues.
| Requirement | Why | What happens if missing |
|---|---|---|
| Python 3 | Run discovery + analytical scripts | Hard dep |
git on PATH | Broken-ref detection, mtime tracking | Falls back to filesystem walk; no broken-ref signal |
tessl CLI on PATH | Outdated check, tile-lint context cost, skill review (quality phase) | Those enrichment fields will be missing; fall back to local-only signals |
~/.tessl/api-credentials.json | Registry enrichment per tile | published_to_registry: null, no security/uplift signals from registry |
jsonschema Python package | IO contract validation between phases (recommended) | One stderr warning per run; pipeline runs without validation |
~/.tessl/posthog/personal-api-key or $POSTHOG_PERSONAL_API_KEY | posthog-skill-query only — pulls org-wide telemetry from PostHog project 57574 | The standalone PostHog phase exits with a clear error. The main pipeline still runs unaffected. |
The pipeline degrades gracefully — every step is best-effort and missing capabilities surface as null in the output rather than aborting the scan.
SKILL.md file. Nothing else..tessl/, .claude/skills/tessl__*) are not first-party — .tessl/ SKILL.md files are excluded from skills[] entirely (they surface via tiles[] with source: "tessl_json"), and .claude/skills/tessl__* symlinks are deduped against their canonical source.AGENTS.md, CLAUDE.md, .cursor/rules/*, MCP configs — Project Insights territory.~/.claude/skills/) — scope is the target directory.tessl eval run) — we read existing eval results from the registry, but don't trigger new ones.posthog-skill-query writes a sibling JSON; an explicit cross-reference step that joins them with discovery.json / staleness.json / quality.json / duplicates.json / registry-search.json is the obvious next layer but isn't built yet.org_usage.json against discovery.json + the per-repo phase outputs to produce per-skill verdicts (only_local, recommend_install, cross_team_skill, etc.) and a unified report covering both per-repo and org-wide signals. The data inputs all exist; this is purely the join + render layer.m/agent-signals/normalizers/claude-code.ts so it also synthesises Skill events from raw Read of SKILL.md and from older <command-name> slash-command flows. Today's PostHog data systematically under-counts those paths; closing the gap improves posthog-skill-query's coverage materially.tessl skill review --optimize — trigger improvement runs for the worst-scoring skills (currently we just review, not optimize).Repo-scoped phases slot in alongside the existing four (parallel after discovery, before render). Org-scoped phases (like posthog-skill-query) are siblings — they don't go through the orchestrator.