Helps Claude make deterministic, auditable choices about which agent and skills to use for a given task — replacing prose-scanning agent/skill selection with a typed scoring kernel.
Project description
claude-wayfinder
A typed, auditable dispatch matcher for Claude Code — post-cognitive routing with a deterministic-first scoring kernel.
What this is — and why it matters
A conventional LLM router enforces routing policy through prose instructions read at every decision point. In practice, this means the routing decision is made by the same model that's about to do the work — using the same signal that drove the original request. It drifts silently, makes the same decision differently across turns, and leaves no structured artifact you can inspect or replay.
claude-wayfinder replaces that loop for the mechanical cases — the ones that don't need judgment. It scores agents and skills against a structured task description composed by the router agent, not the raw user prompt, and returns one of seven typed decisions with confidence, rationale, and alternatives.
Three properties that matter:
- Auditable. Every dispatch decision is a structured artifact. Given the same context and catalog, the matcher returns the same answer. You can replay any past decision.
- Post-cognitive. The matcher fires after the router agent has read the conversation and extracted intent, file paths, and tools. Raw prompts are signal-poor; the router's interpretation is richer — same model, more signal.
- Auto-generated catalog. Built from skill sidecars and agent frontmatter at session start. No hand-curated rule config to drift out of sync.
The decision contract is a seven-member typed enum: delegate / self_handle / self_handle_unaided / advisory / ask_user / needs_more_detail / mixed_content.
Advanced reading: for the design rationale, see docs/design.md. For the algorithm specification, see docs/schema.md.
Install (Claude Code users)
Requires Python >= 3.11. The setup skill discovers Python automatically; it does not need to be on your $PATH.
Inside Claude Code, run these two commands:
/plugin marketplace add glitchwerks/claude-wayfinder
/plugin install claude-wayfinder@glitchwerks
Troubleshooting
claude-wayfinder requires setup banner on session start
The plugin uses a venv-based architecture introduced in v0.4 (#99). On first install you will see a SessionStart banner:
⚠ claude-wayfinder requires setup. Run /setup-wayfinder to materialize the Python venv.
Run /setup-wayfinder once. The skill will:
- Discover a Python >= 3.11 on your machine.
- Create a venv at
~/.claude/plugins/data/claude-wayfinder-glitchwerks/venv/. - Install
claude-wayfinderfrom PyPI. - Write a setup-state flag so subsequent sessions know setup is complete.
The same skill runs again after plugin updates — a STALE banner will prompt you.
"No Python >= 3.11 found"
The skill will ask you for an absolute path. Provide one such as /usr/local/bin/python3.12 or C:\Python313\python.exe. The path is persisted in the setup-state flag for re-runs.
Setup completed but dispatch still does not fire
Open a new session. Hooks read the setup-state flag at session start; an in-progress session does not pick up the flag retroactively.
How to use it
There are two distinct paths depending on your goal.
What is a dispatch context?
Before using either path, it helps to understand what the matcher actually reads. When the router agent invokes /dispatch, it passes a dispatch context — a small JSON object with up to five fields:
| Field | Type | What it carries |
|---|---|---|
task_description |
string | The task sentence. Tokenized into keywords for matching. Required. |
file_paths |
array of strings | File or directory paths mentioned or implied by the current turn. |
agent_mentions |
array of strings | Agent names the user explicitly named (e.g. "code-writer"). |
tool_mentions |
array of strings | Tool names the user explicitly named (e.g. "Bash", "Grep"). |
command_prefix |
string or null | The slash command the user typed, if any (e.g. "/refactor"). |
The ≥ 2 dimensions rule. The matcher counts how many of these fields are populated — each non-empty field is one "input dimension". If fewer than 2 dimensions are populated, the matcher returns needs_more_detail without attempting to score any catalog entries. This is not an error; it means the context is too sparse to route reliably. A task_description with at least one keyword counts as one dimension; each of file_paths, agent_mentions, tool_mentions, and a non-null command_prefix each add one dimension when non-empty. In addition, when file_paths is non-empty the matcher internally derives an extensions dimension (file-suffix set from the provided paths) that counts as a separate populated dimension — so a context with only task_description and file_paths yields three dimensions (keywords + paths + extensions), not two. See docs/schema.md §2 for details.
Good context (2 dimensions — task_description + file_paths):
{
"task_description": "implement OAuth login flow",
"file_paths": ["src/auth/oauth.py"],
"agent_mentions": [],
"tool_mentions": [],
"command_prefix": null
}
Too sparse (1 dimension — task_description only):
{
"task_description": "help",
"file_paths": [],
"agent_mentions": [],
"tool_mentions": [],
"command_prefix": null
}
The first example scores against the catalog and produces a routing decision. The second returns needs_more_detail — the task sentence carries no useful keyword signal and no other dimensions provide context. When you see needs_more_detail, recompose the context: name the verb, the target files, and any explicit agent or tool mentions, then retry.
For the full field-level reference and the decision-composition rules, see docs/schema.md §2.
Try the demo without integrating
/dispatch ships with bundled demo fixtures (a small pre-built catalog of sample agents and skills) so you can see all seven decision branches in action without touching your own agents or building a catalog. Pass --demo to activate this mode — the matcher runs against the bundled fixtures, not your live session traffic. It does not intercept your session or route your tasks automatically. After running it, you decide whether to wire the matcher into your own router agent.
When invoked with --demo, the skill runs the matcher against the bundled demo catalog and returns all seven decision branches with inputs, decisions, confidence scores, and rationale. A single decision block looks like this:
# illustrative — agent names and rationale text will differ in your catalog
[1/7] Branch: delegate
input : 'implement the authentication module'
file_paths : ['src/auth.py']
decision : delegate
confidence : 0.9000
agent : code-writer
rationale : matched keywords: implement.
skills : ['python']
How you know it's working: the decision field contains one of the seven typed strings; confidence is a float between 0 and 1; rationale names the specific triggers that fired. An ask_user result is valid in the contract but reserved in v0.1 — the matcher will not produce it against real input.
Difference between /dispatch --demo and python -m claude_wayfinder demo
/dispatch --demo (in Claude Code) |
python -m claude_wayfinder demo (CLI) |
|
|---|---|---|
| Catalog | Bundled demo fixtures | Bundled demo fixtures |
| Invocation | Skill triggered by router agent | Direct CLI invocation |
| Use case | Evaluate the matcher inside your Claude Code session | Evaluate the matcher without installing Claude Code |
| Output | Same seven-decision output | Same seven-decision output |
Both run the same matcher against the same bundled fixtures. The CLI path is the faster evaluation route if you are deciding whether to install the plugin.
Integrate into your router
Once you have seen demo mode and want the matcher routing your real tasks, you need to build a real catalog (a dispatch-catalog.json generated from your own agent and skill frontmatter). By default, /dispatch resolves the catalog from the canonical path (~/.claude/state/dispatch-catalog.json, or $CLAUDE_HOME/state/dispatch-catalog.json when $CLAUDE_HOME is set) — real routing is the default behavior. If no catalog exists at that path, the skill emits [CATALOG ERROR] and exits non-zero. Set $DISPATCH_CATALOG_PATH to override the canonical default with a custom path.
Feature density — the number of populated input dimensions in a dispatch context — determines whether the matcher attempts scoring. Provide at least two dimensions (for example, a task_description plus file_paths) or the matcher returns needs_more_detail without scoring.
Minimum path from zero to a working real-catalog /dispatch:
1. Install the plugin — inside Claude Code:
/plugin marketplace add glitchwerks/claude-wayfinder
2. One-time setup — when the next session starts, the SessionStart hook will show a setup banner. Run /setup-wayfinder once to materialize the Python venv. See Troubleshooting for details.
Step 2.5 — Do you have catalog-ready skills/agents? The catalog builder scans your skill and agent files for a triggers: block. Without trigger frontmatter, catalog build completes but produces an empty or near-empty catalog and /dispatch will not route anything useful.
Each skill or agent you want the matcher to consider needs a triggers.yml sidecar declaring at least one of: keywords, path_globs, agent_names, tool_names, or command_prefixes. See docs/dispatch-authoring-guide.md for field definitions and worked examples of adding triggers to existing skills and agents.
If you do not have any trigger frontmatter yet, skip ahead and run /dispatch --demo — it runs against bundled fixtures so you can see all seven decision branches in action without a real catalog. Come back to this step once you have added triggers.
3. Build a catalog — run this console script once (and again whenever your skill or agent frontmatter changes):
claude-wayfinder catalog build \
--skills-dir ~/.claude/skills \
--agents-dir ~/.claude/agents \
--out ~/.claude/dispatch-catalog.json \
--log ~/.claude/dispatch-catalog-build.log
On success, this writes ~/.claude/dispatch-catalog.json — a JSON file listing every skill and agent entry the matcher will score.
4. (Optional) Set $DISPATCH_CATALOG_PATH — if your catalog lives somewhere other than the canonical default (~/.claude/state/dispatch-catalog.json), point to it explicitly in your shell profile:
export DISPATCH_CATALOG_PATH=~/.claude/dispatch-catalog.json
Without this env var, /dispatch resolves to the canonical default (~/.claude/state/dispatch-catalog.json). If that file exists, real routing proceeds. If it does not, the skill emits [CATALOG ERROR] — build the catalog first (step 3) or pass --demo to run against bundled fixtures instead.
5. Add Skill to your router agent's tools: frontmatter — /dispatch is invoked as a skill, so the router must have Skill in its tool list. See docs/integration.md for the full router-agent prompt snippet and catalog refresh patterns.
Try it (no Claude Code required)
The CLI demo evaluates the matcher against bundled fixtures without requiring a Claude Code install. It covers all seven decision branches.
python -m claude_wayfinder demo
Expected output (truncated — seven decision blocks):
# illustrative — agent names and rationale text will differ in your catalog
[1/7] Branch: delegate
input : 'implement the authentication module'
file_paths : ['src/auth.py']
decision : delegate
confidence : 0.9000
agent : code-writer
[2/7] Branch: self_handle
...
[6/7] Branch: ask_user
decision : ask_user
rationale : Reserved — not produced by the v0.1 matcher. ask_user is
part of the 7-decision contract and reserved for future
clarification flows.
[7/7] Branch: needs_more_detail
...
ask_user is a valid member of VALID_DECISIONS but is reserved in v0.1 — the matcher never produces it.
CLI subcommands
The full CLI surface is documented via python -m claude_wayfinder --help. Key subcommands:
demo— run the matcher against bundled demo fixtures; covers all seven decision branches.dispatch— run the matcher against a live catalog; reads dispatch context JSON from stdin.--batch— NDJSON batch mode: read one context object per line from stdin, write one decision per line to stdout. Each output line includes an"input_index"field (0-based). Blank lines are skipped; malformed lines produce an error record without aborting the batch. The catalog is loaded once per invocation. Example:printf '{"task_description":"implement auth","file_paths":["src/auth.py"]}\n{"task_description":"fix css bug"}\n' \ | python -m claude_wayfinder dispatch --batch
catalog build— scan skill sidecars and agent frontmatter and write adispatch-catalog.json.audit-catalog— catalog-wide static analysis (conflict pairs, structural checks, matcher-aware semantic rules). Seedocs/dispatch-authoring-guide.md.health— router health report and observability drill-downs. Key subcommands (seeskills/router-health/SKILL.mdfor the full playbook):health --report— print a full health summary covering dispatch invocation rate, bypass rate, advisory override rate, catalog availability, and catalog stability.health drill --metric <name> --window <period>— drill into a specific metric (e.g.bypass,advisory-override,recent-drift) to surface event distributions and top-offending sessions.health top --kind <agents|skills> --window <period> --limit <n>— list the most-dispatched agents or most-invoked skills over a time window.health catalog-status— report catalog entry counts (agents, skills, routable agents) and flag unexpected zeros.
Bundled skills
The plugin ships two skills usable inside Claude Code:
claude-wayfinder:dispatch— runs the matcher against your live catalog by default (canonical path:~/.claude/state/dispatch-catalog.json; override with$DISPATCH_CATALOG_PATH). Pass--demoto run against bundled fixtures instead. Emits[CATALOG ERROR]and exits non-zero if no catalog is found and--demois not passed. See the Try the demo section above.claude-wayfinder:dispatch-authoring— matcher-aware authoring and troubleshooting knowledge for the full dispatch authoring surface (trigger frontmatter, applicable_agents, applicable_skills, routable). Covers the seven-decision ladder, scoring math, weight ladder, path-glob footguns, conflict-pair detection, and the audit-catalog CLI pointer. Seedocs/dispatch-authoring-guide.md.
Dispatch overrides
Dispatch overrides are hard-coded routing decisions that bypass the scoring pipeline entirely: when an override rule's predicates match the dispatch context, the matcher returns the rule's pre-declared (decision, agent, skills, confidence, rationale) verbatim and skips all scoring. Use them for routes you want pinned unconditionally — a /deploy command that must never be delegated, or all Python files that should always reach code-writer — and not as a substitute for a well-tuned catalog. Overrides take precedence over scored decisions; they fire first, and a matched override short-circuits the rest of the pipeline.
Set the env var to point at your rule file:
export DISPATCH_OVERRIDES_PATH=/path/to/dispatch-overrides.json
A minimal two-rule file covering the two most common predicates (substitute your own agent names):
{
"version": 1,
"rules": [
{
"id": "deploy-command",
"decision": "self_handle_unaided",
"agent": null,
"skills": [],
"confidence": 1.0,
"rationale": "/deploy is always handled manually",
"predicates": { "command_prefix": "/deploy" }
},
{
"id": "py-files-to-code-writer",
"decision": "delegate",
"agent": "code-writer",
"skills": ["python"],
"confidence": 0.99,
"rationale": "All Python edits go to code-writer unconditionally",
"predicates": { "path_globs": ["**/*.py"] }
}
]
}
When $DISPATCH_OVERRIDES_PATH is unset, scored matching runs unchanged. On load failure, [OVERRIDES ERROR] is emitted to stderr and the matcher falls back to scoring. For the full predicate vocabulary, audit rules, telemetry schema, and design rationale, see docs/dispatch-overrides.md.
What's next
If you want to use the matcher for real routing in your own Claude Code setup, there are two paths:
- Integrate now via the contributor path. Clone the repo, build your own catalog from your agent and skill frontmatter, and call the library API from your router agent. The Contributing section covers the mechanics, and
python -m claude_wayfinder --helpdocuments the CLI surface. - Wait for the bundled runtime. Issue #6 tracks the zero-friction-install spike — a bundled router agent and catalog generator that would make daily-driver routing available without manual integration. That work is scoped to v0.2.
Library API
The public API is documented in docs/api.md. A minimal integration looks like:
from pathlib import Path
from claude_wayfinder import load_catalog, build_features, score, decide, ScoredEntry
catalog = load_catalog(Path("/path/to/dispatch-catalog.json"))
features = build_features({
"task_description": "implement the login page",
"file_paths": ["src/auth/login.py"],
})
agents = [ScoredEntry(e, score(e, features)) for e in catalog if e.kind == "agent" and e.routable]
skills = [ScoredEntry(e, score(e, features)) for e in catalog if e.kind == "skill"]
result = decide(agents, skills, features, catalog)
# result["decision"] is one of the seven decision strings
The __all__-guarded surface (load_catalog, build_features, score, decide, VALID_DECISIONS, and the supporting dataclasses) is stable for the v0.1 series: patch releases will not rename, remove, or alter any public signature.
Prior art
wwadley-lucas/claude-dispatch— pioneered hook-based pre-cognitive matching with zero-LLM-in-default-path principles. Operates at a different lifecycle point (raw user prompt, not router-composed task description).darco81/skills-radar— lazy skill loading via embedding retrieval (BM25 + dense). Adjacent problem space, different mechanism.- Anthropic Tool Search Tool — upstream pattern for the MCP-tools case.
Contributing
Requirements: Python >= 3.11, uv.
git clone https://github.com/glitchwerks/claude-wayfinder.git
cd claude-wayfinder
uv venv .venv
uv pip install -e ".[dev]"
Run the test suite:
python -m pytest
Run the linter:
python -m ruff check src/ tests/
Validate the plugin manifest:
claude plugin validate .claude-plugin/plugin.json
claude plugin validate is the canonical manifest check and runs as a CI gate on every push and PR (inside the Validate Plugin Manifest job). The project also ships tests/test_plugin_manifests.py, which covers field-level conventions (name, description, author, etc.) that the official validator does not enforce — both checks are complementary.
The @anthropic-ai/claude-code version pinned in .github/workflows/ci.yml is tracked by Renovate via renovate.json. Bump PRs are opened weekly and require manual review — schema changes in claude plugin validate are exactly what the CI gate exists to surface.
Run the demo (confirms the matcher works end-to-end against bundled fixtures):
python -m claude_wayfinder demo
Filing issues: Use GitHub Issues. Before opening a new issue, check that one does not already exist for the same problem.
Workflow: Create a branch per issue, open a PR that references the issue number in its body (Closes #N). For non-trivial work, set up a git worktree per branch (see the CLAUDE.md contributor notes for the worktree convention used in this repo).
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_wayfinder-1.0.0.tar.gz.
File metadata
- Download URL: claude_wayfinder-1.0.0.tar.gz
- Upload date:
- Size: 500.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f937ff19adc726430772647973a75e172da7ce5e730a47b3bb592e557cf96989
|
|
| MD5 |
b4e466b30ea112488ba59026a0971c8d
|
|
| BLAKE2b-256 |
2ff4e908e40c10dd16e637b5fb4bca3bf27c0baffddce83cccb56d0487391ee3
|
Provenance
The following attestation bundles were made for claude_wayfinder-1.0.0.tar.gz:
Publisher:
release.yml on glitchwerks/claude-wayfinder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_wayfinder-1.0.0.tar.gz -
Subject digest:
f937ff19adc726430772647973a75e172da7ce5e730a47b3bb592e557cf96989 - Sigstore transparency entry: 1656840804
- Sigstore integration time:
-
Permalink:
glitchwerks/claude-wayfinder@8f2278b1c8549dbe6f3de9709780636c8ccecfa7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/glitchwerks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8f2278b1c8549dbe6f3de9709780636c8ccecfa7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file claude_wayfinder-1.0.0-py3-none-any.whl.
File metadata
- Download URL: claude_wayfinder-1.0.0-py3-none-any.whl
- Upload date:
- Size: 126.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c5cc5683e5542cd8c2ee8a5bf121c88bbca10ffde567b8906b1334f4e043a95
|
|
| MD5 |
c3dbf2b876f0dc4cdf59bf663d18c840
|
|
| BLAKE2b-256 |
92074e83c5e570d4dc5409b06e4f62526175ec7398cea7179b49495135c95f50
|
Provenance
The following attestation bundles were made for claude_wayfinder-1.0.0-py3-none-any.whl:
Publisher:
release.yml on glitchwerks/claude-wayfinder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_wayfinder-1.0.0-py3-none-any.whl -
Subject digest:
3c5cc5683e5542cd8c2ee8a5bf121c88bbca10ffde567b8906b1334f4e043a95 - Sigstore transparency entry: 1656840905
- Sigstore integration time:
-
Permalink:
glitchwerks/claude-wayfinder@8f2278b1c8549dbe6f3de9709780636c8ccecfa7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/glitchwerks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8f2278b1c8549dbe6f3de9709780636c8ccecfa7 -
Trigger Event:
push
-
Statement type: