{% extends "base.html" %} {% import "_empty.html" as empty %} {% block title %}Skills{% endblock %} {% block topbar_main %}

Skills

What your agents load, and where it comes from {% endblock %} {% block topbar_actions %}
{# Only shown when there is a choice to make. One agent and this is a control that can only ever return what is already on the screen. #} {% if agents | length > 1 %} {% elif agent %}{% endif %} {% if shadowed %}{% endif %}
{% if shadowed %}Hide shadowed{% else %}Show shadowed{% endif %} Proposals {% endblock %} {% block content %}
{% if said %}
{{ said }}
{% endif %} {% if report.summary.defects %}
{{ report.summary.defects }} defect{{ '' if report.summary.defects == 1 else 's' }} found. Nothing is broken on its own, but an agent may be loading a copy you did not mean it to. Read them under Health.
{% endif %}
{{ report.summary.effective }}
In effect
{{ report.summary.shadowed }}
Shadowed
{% if agents | length > 1 %} {% for name in agents %}
{{ report.summary.by_agent[name] }}
{{ name }}
{% endfor %} {% else %}
{{ report.summary.by_scope.plugin }}
From plugins
{% endif %}
{{ report.summary.defects }}
Defects
{# The table is the page; the rest is what you go looking for. Anchors, so with no JavaScript this is a row of jump links and every panel is still on the page — the script below only hides the ones you are not reading. #}
{% if rows %}
{% if scope %}{{ scope }} skills{% else %}Skills{% endif %} {{ rows | length }} of {{ report.summary.packages }}
SkillAgentScopeFrom DescriptionFiles
{% for row in rows %}
{{ row.name }} {% if not row.effective %}shadowed{% endif %} {{ row.directory }} {{ row.agent }} {{ row.scope }} {{ row.plugin or row.scope }} {{ row.description or '—' }} {{ row.file_count }} files
{% endfor %} {% include "_pager.html" %}
{% else %} {% call empty.state('search', 'Nothing found') %}

No skill package matched. A skill is a directory holding a SKILL.md, under .claude/skills in a project, in your home directory, or inside an installed plugin.

Flanner reads those directories and never writes to them. Nothing in a package is executed by a scan.

{% endcall %} {% endif %}
{# What to do about the numbers, before the numbers. Every item is a join the data always supported: a skill being used while a copy of it sits shadowed was readable only by holding two tables side by side. The coverage caveat comes first because it qualifies the rest. #} {% if attention %}
Worth your attention {{ attention | length }}
{% for item in attention %}
{{ item.headline }}

{{ item.detail }}

{% if item.skills %}

{% for name in item.skills[:12] %}{{ name }}{% if not loop.last %}·{% endif %}{% endfor %} {%- if item.skills | length > 12 %}·and {{ item.skills | length - 12 }} more{% endif %}

{% endif %}
{% endfor %}
{% endif %} {% set scope_row = (observe.scopes | selectattr('observing') | list | first) or (observe.scopes | list | first) %}
Watching skill use {% if scope_row and scope_row.observing %}on{% else %}off{% endif %}

Records that a named skill was invoked, when, and by which local session. Not your prompts, not the agent's replies. Nothing leaves this machine. Only explicit invocations are visible: Claude Code shows every skill's description to the model without reporting it, so reads are unknown rather than zero.

{% if scope_row and scope_row.observing %}
Keeping {{ scope_row.retention_days }} days · {{ scope_row.observations }} recorded {%- if scope_row.dropped %} · {{ scope_row.dropped }} dropped{% endif %} {% else %}
Off. Nothing is being recorded. {% endif %}
{% if usage.coverage.days %} {% set covered = usage.coverage.days | select | list | length %}

Watched on {{ covered }} of {{ usage.coverage.days | length }} days, oldest on the left.

{% endif %}
{# Searched, sorted and paged in the browser, like every other list. The rows are one report built for the page, so there is no server query to page, and the Skills tab above already owns the page's `page` address. #}
Used in the last {{ usage.window_days }} days {% if usage.coverage.watching %}{{ usage.rows | length }} skill(s) {% else %}nothing was watching{% endif %} {% if usage.rows %} {% endif %}
{% if usage.rows %} {% set busiest = usage.rows | map(attribute='invocations') | max %}
SkillUsesOver the window Last usedModels
{% for row in usage.rows %}
{# The caveat belongs to the name, not to the count. What is in doubt is which package these uses were of, and the number of them is not the uncertain part. #} {{ row.skill }} {%- if row.attributed == 0 %}no package by this name {%- elif row.attributed < row.invocations %}{{ row.invocations - row.attributed }} use(s) unversioned{% endif %} {{ row.invocations }} {# Shape, not size: the sparkline scales to its own peak, so a skill used twice still shows whether that was one day or two. #} {# `preserveAspectRatio="none"` is what lets the series fill whatever width the cell has, and it scales the stroke with the geometry: wider than tall, a flat run rendered fat while a spike thinned to a hairline. `non-scaling-stroke` keeps the line one width at any size, which is the whole reason a sparkline is legible small. #} {{ row.last_used_at[:16].replace('T', ' ') }} {% for k, v in row.by_model.items() %}{{ k }} ({{ v }}){% if not loop.last %}, {% endif %}{% endfor %}
{% endfor %}
{% else %} {% if usage.coverage.watching %} {% call empty.inside('clear', 'Nothing was invoked in this window') %}

Something was watching throughout, so this is a real zero rather than a gap in the record.

{% endcall %} {% else %} {% call empty.inside('search', 'Nothing has been watching') %}

A zero here means nothing rather than “unused”. Turn watching on and this fills in as skills are invoked.

{% endcall %} {% endif %} {% endif %} {# The coverage, spelled out. A closed window still counts as coverage — correctly, something was watching for part of the period — but a reader seeing "nothing was invoked" while the switch reads off needs telling which of those two facts they are looking at. #}

{% if usage.coverage.watching %} Watched during {{ usage.coverage.windows }} period{{ '' if usage.coverage.windows == 1 else 's' }} of these {{ usage.window_days }} days{% if not (scope_row and scope_row.observing) %}, and not watching now{% endif %}. {% else %} Nothing watched any part of these {{ usage.window_days }} days. {% endif %} {% if usage.coverage.dropped %} {{ usage.coverage.dropped }} event(s) arrived and could not be stored, so the counts are short by at least that. {% endif %}

Versions flanner is holding {{ stored | length }} stored · {{ installs | length }} install(s)
{% if installs %}
InstalledWhereState
{% for row in installs %}
{{ row.installed_at[:16].replace('T', ' ') }} {{ row.name }} {{ row.manifest_hash[:23] }}… {% if row.intact %}intact {% elif row.status == 'installed' %}edited since {% else %}{{ row.status }}{% endif %} {% if row.replaced_hash and row.status == 'installed' %}
{% endif %}
{% endfor %} {% else %} {% call empty.inside('skill', 'Nothing installed by flanner') %}

Skills stay where their owner put them. flanner skills adopt <name> keeps a copy flanner can put back.

{% endcall %} {% endif %}
From your team {{ transfers | length }} arrived · {{ channels | selectattr('subscribed') | list | length }} followed
{% if transfers %}
SkillStateFromHow
{% for row in transfers %}
{{ row.skill }}{% if row.detail and row.state == 'rejected' %} {{ row.detail }}{% endif %} {% if row.state == 'installed' %}installed {% elif row.state == 'verified' %}verified, not installed {% elif row.state == 'rejected' %}rejected {% else %}{{ row.state }}{% endif %} {{ row.from_device[:12] or '-' }} {% if row.pinned %}a one-time copy{% else %}channel {{ row.channel }}{% endif %} {% if row.state == 'verified' %}
{% endif %}
{% endfor %} {% else %} {% call empty.inside('team', 'Nothing has arrived') %}

A package a teammate sends shows up here and waits: receiving one is not installing it.

{% endcall %} {% endif %} {% if channels %}
{% for row in channels %}
{{ row.name }} {% if row.subscribed %}following{% else %}not following{% endif %}
{% endfor %}

Following tells you about a new version. It never installs one: that would hand whoever publishes it control of what your agent reads.

{% endif %}
{% if report.findings %}
What is wrong {{ report.summary.defects }} defect{{ '' if report.summary.defects == 1 else 's' }}, {{ report.summary.advice }} advisory
FindingSkillWhat to do
{% for f in report.findings %}
{{ f.code }} {{ f.skill }}{{ f.detail }} {{ f.remedy }}
{% endfor %}
{% endif %} {# Closed by default: it is a couple of dozen directories somebody consults when a skill is missing, not something to scroll past every visit. The summary carries the count and how many were not there, so the answer it usually gives is readable without opening it. Not `card-prose`: the body is a list of file paths, and a reading measure cut them in half and wrapped nineteen of them. The notes at the foot are sentences, so those keep it. #} {% set missing = report.coverage.roots | rejectattr('exists') | list | length %}
Where this was read from {{ report.coverage.roots | length }} director{{ 'y' if report.coverage.roots | length == 1 else 'ies' }}{% if missing %}, {{ missing }} not there{% endif %} {% for name in report.agents %}

{{ name }}

    {% for r in report.coverage.roots if r.agent == name %}
  • {{ r.path }} {{ r.scope }}{% if r.revision %} · {{ r.revision }}{% endif %} {% if not r.exists %}not there{% endif %} {% if not r.active %}not the installed revision{% endif %}
  • {% endfor %}
{% endfor %}
{% for note in report.coverage.notes %}

{{ note }}

{% endfor %}
{% endblock %}