Skip to content

wommy/wom.womr-skill-reachability

v0.1.0MIT

Session-start warning for bare-name skill routes the Skill tool cannot load.

womr-skill-reachability

Warns at session start when prose routes to a skill by bare name that the Skill tool cannot load.

The problem

The harness scans <root>/*/SKILL.md at one level only. A skill nested at depth ≥ 2 is real, readable by absolute path, and un-invocable by bare name. Measured 2026-08-19 across the live roots: 929 SKILL.md directories, 107 loadable at depth 1, 822 invisible.

Nesting alone is not the defect — a naive "every nested SKILL.md needs a depth-1 entry" audit reports 1331 of 1535 dark and is useless. Per unix-philo, only a router's entry point must be depth-1; nested members are legitimate by design and are reached by absolute path. Mass-linking them would also load 1331 model-invoked descriptions into every turn. The defect is nesting plus a bare-name route: a name with no skill anywhere is prose, not a bug, and stays silent.

Live instances found 2026-08-18, all five confirmed by the harness listing the skill immediately after the symlink landed: artifact-cube-pipeline, systematic-debugging, hermes-local-durability, hermes-usercustomize-overlay, hermes-hotpatch-apply-revert. The last three are the Hermes runtime ownership boundary that AGENTS.md requires be loaded before any installed-runtime write — that always-loaded guard could not fire, because its skills were dark.

Why a plugin and not the script it replaces

~/.hermes/scripts/skill-reachability-audit.sh runs as step 6 of kanban-tick-guard.sh — it rides the kanban pump tick, which is the wrong cadence. A dark skill route matters when a session begins and the agent is about to route by name; it does not matter when a card moves. Worse, the guard it rides had not completed a lap since 05:39 on the day this was written, so the audit was effectively dead.

It is also cheaper. The shell version ran one find -L over three roots per routed name. This walks the roots once into an index and classifies every route against it — 0.3 s wall for 929 directories, then throttled off entirely.

Seam

on_session_start fires once per brand-new session from agent/conversation_loop.py:913 via hermes_cli.lifecycle.invoke_hook (kwargs: session_id, model, platform). It is not re-fired on continuation. The return value is ignored: this is a pure observer and never raises into the session-start path.

Decision logic lives in reachability.py — pure, no IO, no hermes imports, 22 tests. __init__.py is the only file that touches the filesystem or environment.

Guards

GuardWhy it is load-bearing
WOMR_SKILL_REACHABILITY_DISABLE kill switch, checked firsta disabled plugin costs a session start nothing
Interval throttle (default 900 s), checked before any IOsession start is latency-sensitive; the skill tree changes on the scale of days
Depth-bounded manual walk (default 4)cluster dirs are symlinks; the walk must follow them or under-report, and following without a bound can cycle
Visited-realpath set per roota self-referential cluster link cannot hang session start
Roots deduped by realpath~/.claude/skills is a symlink to ~/.agents/skills; without this one source looks like a DUPLICATE of itself
Reachable in any root countsthe documented cure symlinks one source into several roots
Empty index or empty routes ⇒ BLIND, never a passfinding nothing means the scan could not look; a detector that reports OK when blind launders a broken scan into a green light
NOT_A_SKILL is silenta bare word in prose is not a defect — this is what keeps the signal usable
Top-level except Exception → log + Nonean observer bug must never break session start
No writes, no subprocesses, no networkthe plugin only ever reports; the cure is printed for a human to run

Verdicts

VerdictMeaning
REACHABLEa depth-1 entry exists in some root
DARKthe skill is real but only below depth 1 — the route is a dead letter
DUPLICATEtwo distinct sources claim the same depth-1 name; which loads is scan-order dependent
NOT_A_SKILLno skill of that name anywhere — prose, reported silently as a count

Several depth-1 entries pointing at the same target are REACHABLE, not DUPLICATE: that is exactly the cured state the cure command produces.

Activation (operator-gated)

Installed disabled. The symlink only makes it discoverable:

ln -sfn /home/wom/.config/agents-wom/plugins/womr-skill-reachability \
        /home/wom/.hermes/plugins/womr-skill-reachability

Nothing loads until it is explicitly enabled:

hermes plugins enable womr-skill-reachability

Tuning

EnvDefaultEffect
WOMR_SKILL_REACHABILITY_DISABLEunsetany value but 0/false disables the hook entirely
WOMR_SKILL_ROOTS~/.claude/skills:~/.agents/skills:~/.config/agents-wom/skills:-separated roots to scan
WOMR_SKILL_ROUTE_SOURCE/home/wom/inbox/AGENTS.mdprose whose bare-name routes must resolve
WOMR_SKILL_REACHABILITY_INTERVAL_SECONDS900minimum seconds between scans, per process
WOMR_SKILL_REACHABILITY_MAX_DEPTH4walk depth bound

Test

python3 test/test_reachability.py     # 22 tests, pure python, no hermes runtime
hermes plugins doctor . --ci          # 1 hook registered

The suite was written and run RED first (ModuleNotFoundError: No module named 'reachability') to prove the seam was absent rather than the fixture malformed.