AXe Skills HubSearch /

AXe Operator Skills Hub

The public, read-only catalogue at operator.axetechnologies.ca. It answers two

audiences over the same data: JSON for agents, HTML for people. Anything true of

one has to be true of the other — a fix that reaches only one side is not a fix.

Surfaces

PathAudienceWhat it is
/docs/skillspeoplethe catalogue index: counters, registries, categories
/docs/skills/search?q=peoplesearch results, server-rendered
/docs/skills/c/<category>peopleone category
/v1/skillsagentsthe flat list
/v1/skills/search?q=agentssearch, returns a bare JSON array
/v1/tools/find?q=agentssearch with a result envelope
/v1/categoriesagentsthe taxonomy
/v1/auditagentsprovenance for a tenant
/healthzanyoneliveness
/docspeoplethis documentation

The origin is read-only. POST, PUT and DELETE all answer 405.

Searching

Search is one URL. A ?q= that reaches the index is redirected into

/docs/skills/search rather than being dropped, so a search can never be

silently ignored while still answering 200.

Stopwords, and why an empty result explains itself

The ranker drops common filler words. That created a failure worth knowing

about: ?q=agent once returned zero results, because agent is a stopword in a

catalogue where almost everything is an agent skill — the query was stripped to

nothing *before* it was searched, and a discarded query looked exactly like an

empty catalogue.

Now, when every word in a query is a stopword, the query is matched literally

instead of being dropped, and both surfaces say so:

Known gap: /v1/skills/search returns a bare JSON array, so it has nowhere

to put that signal. Changing its shape would break its published contract; the

non-breaking route is the existing ?envelope=1 mechanism. Use /v1/tools/find

if you need the reason a result set is empty.

?fragment=1

/docs/skills/search?q=…&fragment=1 returns the results block alone — no

document, no navigation, no sidebar. It exists for the page's own live search.

Building the 197-row category sidebar costs about as much as running the search

itself, and live search discards it, so the fragment path short-circuits before

that work happens rather than doing it and throwing it away.

The fragment and the full page render from one function, so there is no second

implementation of a result that could drift from the first. Ranking, empty

states and the stopword notice are identical on both.

Provenance

Every row carries the registry it came from, and every registry is shown — not

the largest few. A chip row that silently stops is indistinguishable from a

catalogue that holds nothing else, and the small registries are the ones

somebody is most likely to be hunting for.

RegistryTier
ClawHubcommunity
skills.shcommunity
LobeHubcommunity
browse.shcommunity
NVIDIAcommunity
gstackcommunity
OpenAIcommunity
HuggingFacecommunity
Anthropiccommunity
claude-marketplacecommunity
built-infirst-party
optionalfirst-party

About the registry marks

Each registry is shown with a monogram tile in that vendor's own accent colour.

These are our own marks, not the vendors' logos, and are deliberately so:

wrong, and a wrong logo is worse than an honest monogram.

parties on every page view, and would break the day any of them moves a path.

Every mark is drawn in this repository and served from this origin. No page on

this hub loads an asset from anywhere else. Names are the vendors' own and are

used to say where a row came from; the trademarks belong to their owners.

Caching

The stylesheet is served from /static/axeskills-<hash>.css with

Cache-Control: public, max-age=31536000, immutable. The hash is taken from the

contents, so the filename changes whenever the CSS does — which is what makes an

immutable cache safe. Only the current hash is served; a superseded name 404s

rather than returning styling that no longer matches the markup.

Running it

The hub has no build step. A committed change is not live until the service

restarts:

launchctl kickstart -k gui/$(id -u)/com.axe.axeskills-public

The launchd job runs /usr/bin/python3, which on the build host is not the

same interpreter as python3. A green test run proves nothing about whether the

service can start; tests/test_imports_on_production_interpreter.py is the

guard that does.

What changed recently

/v1/skills — one row per name

The listing used to return every version row in the database, roughly 111k rows

for a catalogue of 83k named skills. That made pagination misleading and counts

wrong. It now returns one row per skill name — the latest published version —

matching what the HTML catalogue shows. The full version history for a name is

still available and intentionally so at /v1/skills/<name>/versions.

/v1/categories — full catalogue reconciliation

The categories endpoint used to query the skill_categories table directly

(18,624 classified rows) and omit the 56,874 skills that resolve to "other".

An agent using it to decide where to search could not see that more than

two-thirds of the catalogue sits in the unclassified bucket, making the map

actively misleading.

It now uses the same effective-category resolution the HTML catalogue uses:

ingested category wins unless it is "other", in which case the classifier's

derived category applies. The totals now reconcile exactly to the live

catalogue size. The "other" bucket is visible and queryable.

Per-skill permalink pages

Every skill has a stable URL at /docs/skills/s/<name>. Names that contain

colons (the axehub: vendor prefix) are percent-encoded in the URL. A

?fragment=1 parameter strips the shell to the card alone, for embedding.

A missing name returns an HTML 404 to browsers and a JSON error to API callers.

Summary cache

The index and search pages aggregate categories, totals and source distribution

on every request via a TEMP TABLE scan of 111k rows (~220ms per call). That

work is now cached per tenant and invalidated by the database file's mtime.

The mtime signal advances the moment the ingest job commits its first write to

the DB (journal_mode=delete, the SQLite default). The cache is never stale for

longer than one in-flight request that raced the ingest. Hot requests measure

~4ms; the per-skill search query is now the dominant cost.

If the database is ever switched to WAL mode this cache will break silently

— WAL commits land in the -wal sidecar and the main file's mtime may not

advance. The guard test tests/test_summary_cache_journal_mode.py asserts

the assumption in CI.