Dispatch Skill
The /dispatch skill is a mode-aware wrapper around the
claude-wayfinder deterministic 7-decision matcher.
Modes
Real-catalog mode (default)
The skill reads a dispatch context JSON from stdin, runs the matcher against your live catalog, and returns the decision JSON verbatim. This is the default — no flag required. Catalog path resolution:
--catalog-path <path>CLI flag.$DISPATCH_CATALOG_PATHenv var.- Canonical default —
$CLAUDE_HOME/state/dispatch-catalog.jsonor~/.claude/state/dispatch-catalog.json(see "Canonical catalog path" below).
Demo mode (--demo flag)
Pass --demo to run the matcher against bundled demo fixtures instead
of the live catalog. Returns decision output for all 7 routing branches
so you can evaluate the matcher before integrating it into your router.
--demo wins over --catalog-path and $DISPATCH_CATALOG_PATH — if
both are present, demo mode runs and the catalog inputs are ignored.
Hard-error guarantee: Without --demo, if no catalog can be
resolved (no flag, no env var, no file at the canonical path), or if
the resolved file is missing/unreadable/malformed, the skill emits a
[CATALOG ERROR] banner on stderr and exits non-zero. It does not
silently fall back to demo mode — a broken or missing catalog is
surfaced immediately so the consumer knows routing is degraded. The
banner names the canonical default path and the repair hint inline.
Dispatch context JSON (real-catalog mode)
The consumer's router agent must compose a JSON object and pass it on
stdin. task_description is the only required field:
{
"task_description": "...",
"file_paths": ["..."],
"agent_mentions": ["..."],
"tool_mentions": ["..."],
"command_prefix": "...",
"session_id": "...",
"domain": "code",
"posture": "build",
"confidence": "high",
"area_span": 1
}
All fields except task_description are optional; omit or pass null
for fields that are not applicable. The four two-axis labels (domain,
posture, confidence, area_span) are documented under Two-axis
labels below.
session_id (optional string, added in fix #294, auto-populated in
#296) — the Claude Code session identifier for the calling session.
When present, this value is written verbatim into the matcher_decision
log entry, enabling per-session attribution in the dispatch log.
Auto-population (issue #296): when session_id is absent from the
input JSON and the CLAUDE_SESSION_ID env var is not set, the matcher
automatically walks its ancestor process chain looking for a PID-keyed
state file written by the session-start-record-session hook. The hook
fires at SessionStart, captures the CC process's PID and start time
(psutil.Process(ppid).create_time()), and writes
~/.claude/state/wayfinder-sessions/<ppid>-<create_time_int>.txt. The
matcher finds its own CC ancestor's file and reads the session_id from
it. The result is cached for the matcher process's lifetime.
Concurrent-session safety: each CC session gets a unique file keyed by both PID and integer create_time. Two concurrent CC sessions write two separate files; the matcher's process-tree walk reaches only its own CC ancestor's PID, so there is no cross-contamination. Do not simplify this to a single shared file — a shared file is broken under concurrent sessions (each SessionStart overwrites the prior session's ID). The per-file-per-session design is load-bearing.
Two-axis labels (Matcher v3)
Four optional caller-supplied labels the matcher consumes for two-axis (domain × posture) routing. The matcher runs no encoder — it routes on exactly what the caller labels — so these are the only signals it cannot derive lexically. They describe the task, not a target agent: the matcher resolves domain/posture to an agent internally (via its own routing policy), so the caller never names an agent here.
Caller: classify each task into these four labels and include them in the
context JSON you compose — apply the rubric below and obey the confidence
fail-safe (never label high on a guess). Label only what the task is.
All four are optional and additive — the matcher ignores absent or unknown
fields, so emitting them never changes current behavior. While the rollout flag
is off they feed shadow-mode telemetry only (the v3 route is computed and logged
beside the live lexical decision); high-confidence labels begin steering live
routes only after the flag flip.
| Field | Type | Default when absent |
|---|---|---|
domain |
enum / null |
is_any (no domain gate) |
posture |
enum / null |
no posture route |
confidence |
high | medium | low / null |
low (fail-safe) |
area_span |
integer ≥ 1 | 1 |
domain — what kind of artifact/area the task touches
| Value | Task is about |
|---|---|
code |
source code: features, fixes, refactors, tests (.py / .ts / …) |
docs_prose |
documentation, READMEs, ADRs, plans, specs, changelogs |
project_meta |
harness self-edits — agents/**, skills/**/SKILL.md, CLAUDE.md, AGENTS.md, GEMINI.md, project governance |
infra_deploy |
infrastructure, deployment, IaC (bicep/terraform), CI/CD, topology |
is_any / null |
cross-cutting, or no single domain dominates (no domain gate) |
posture — what action the task performs
| Value | Intent |
|---|---|
build |
create / implement / add new behavior |
diagnose |
find the root cause of a failure / bug |
assess |
review / evaluate existing code quality |
critique |
adversarial critique (architecture or idea soundness) |
verify |
conformance / consistency check vs. a stated source of truth |
plan |
scope / design / requirements |
research |
prior-art discovery before planning |
operate |
read-only operations (e.g. status / read queries) |
confidence — fail-safe; never label high on a guess
How sure the caller is of the domain + posture pair. The fail-safe governs
delegate posture-routes: the matcher hard-routes to a preferred agent only
on high; medium / low / absent fall through to the lexical scorer. A wrong
high is the one label that can mis-steer a live delegate, so when unsure, omit
confidence (⇒ treated as low) rather than inventing a level.
Exception: a harness self-edit (domain: project_meta, posture: build)
abstains to the router (self_handle) regardless of confidence — that
abstention is not a delegate, so the high-gate does not apply.
area_span — default 1
Number of distinct layers/areas the task genuinely spans (e.g. code + infra +
data = 3). Defaults to 1. Emit ≥ 2 only for genuinely multi-layer work — a
single-file bug is 1; an outage spanning service + database + config is ≥ 2.
Values are coerced to int; anything missing, non-numeric, or < 1 becomes 1.
Output schema (both modes)
Real-catalog mode returns the matcher's decision JSON verbatim on stdout:
{
"decision": "delegate",
"agent": "Explore",
"skills": ["python"],
"confidence": 0.92,
"rationale": "matched keywords: implement.",
"alternatives": [{"agent": "Plan", "score": 0.4}]
}
Demo mode produces human-readable text instead of JSON (one block per decision branch). The output schema is identical across the 7 branches shown in demo mode. Consumer routers should use real-catalog mode for machine-readable output.
Consumer router requirements
The consumer's router agent must include Skill in its tools: frontmatter
for /dispatch to be invocable. Example of correct frontmatter:
tools: Glob, Grep, Read, Edit, Write, Bash, Skill, ToolSearch
See docs/integration.md for the end-to-end wiring guide.
Prerequisites
claude-wayfinder must be installed in a Python ≥ 3.11 environment. After
running /setup-wayfinder, the plugin venv lives at
${CLAUDE_PLUGIN_DATA}/venv/ — that is the canonical interpreter to use.
Use the plugin venv's interpreter explicitly. Do not rely on bare
python resolving to the right environment via $PATH — on Windows in
particular, a global Python (e.g. C:\Python313\python.exe) often takes
precedence over the venv and does NOT have claude-wayfinder installed,
producing No module named claude_wayfinder at runtime.
| Platform | Path |
|---|---|
| POSIX | ${CLAUDE_PLUGIN_DATA}/venv/bin/python |
| Windows | ${CLAUDE_PLUGIN_DATA}/venv/Scripts/python.exe |
The skill's invocations below show the explicit path. Bare python is
fine as a shorthand only when the plugin venv is activated in the
calling shell, or its bin/Scripts dir is first on $PATH.
# Confirm the package is available
"${CLAUDE_PLUGIN_DATA}/venv/Scripts/python.exe" -m claude_wayfinder dispatch --help # Windows
"${CLAUDE_PLUGIN_DATA}/venv/bin/python" -m claude_wayfinder dispatch --help # POSIX
If claude-wayfinder is not installed yet, run /setup-wayfinder — that
skill materializes the venv at the canonical location and pins the
matching plugin version into it.
Canonical catalog path
The live catalog is at ~/.claude/state/dispatch-catalog.json (or
$CLAUDE_HOME/state/dispatch-catalog.json when $CLAUDE_HOME is set).
This is the default real-catalog mode resolves to when neither
--catalog-path nor $DISPATCH_CATALOG_PATH is supplied. Override only
for test fixtures or unusual deployments. The bundled hooks
(refresh-catalog-on-stale.js, check-catalog-health.js) use the same
default.
Running
PY="${CLAUDE_PLUGIN_DATA}/venv/Scripts/python.exe" # Windows
# PY="${CLAUDE_PLUGIN_DATA}/venv/bin/python" # POSIX
# DISPATCH_SHADOW gates the v3 shadow-route compute (see "Two-axis labels"
# above). The computed route remains telemetry-only unless its domain is
# also enabled by DISPATCH_HARD_ROUTING_DOMAINS. Sourced from the userConfig
# `shadow_enabled` toggle in plugin.json so users can opt out without
# touching source.
#
# Only an exact case-insensitive match of "0", "false", or "no" disables
# shadow compute. Every other value fails open to enabled — this
# includes "off", "disabled", an empty string, and any malformed or
# whitespace-padded value (e.g. " false "). Absent (unset) also fails
# open to enabled.
export DISPATCH_SHADOW="${user_config.shadow_enabled}"
# DISPATCH_HARD_ROUTING_DOMAINS is the surgical serving gate: a comma-
# separated set of domain tokens such as "is_any" or "is_any,code".
# Unset, empty, or whitespace-only values fail closed to OFF. Tokens are
# stripped, lowercased, and deduplicated; unrecognized tokens are dropped
# with a stderr warning rather than failing dispatch.
export DISPATCH_HARD_ROUTING_DOMAINS="is_any"
# The two gates have opposite safe defaults: absent DISPATCH_SHADOW means
# Compose compute is ON, while absent DISPATCH_HARD_ROUTING_DOMAINS means
# hard routing is OFF. DISPATCH_HARD_ROUTING_DOMAINS scopes cutover per
# domain; DISPATCH_SHADOW=0 is the coarse, all-domain kill switch because it
# skips Compose entirely and therefore always serves the lexical decision.
# Real-catalog mode — default; resolves to the canonical catalog
echo '{"task_description": "implement auth module", "file_paths": ["src/auth.py"], "agent_mentions": [], "tool_mentions": [], "command_prefix": null}' \
| "$PY" -m claude_wayfinder dispatch
# Demo mode — opt in with --demo
"$PY" -m claude_wayfinder dispatch --demo
# Explicit catalog override (e.g. test fixture)
export DISPATCH_CATALOG_PATH=/path/to/test-catalog.json # POSIX
# $env:DISPATCH_CATALOG_PATH = "C:\path\to\test-catalog.json" # PowerShell
echo '{...}' | "$PY" -m claude_wayfinder dispatch
Stale-catalog warning
When $DISPATCH_SKILLS_DIR and/or $DISPATCH_AGENTS_DIR are set and any
source file within them has a modification time newer than the catalog
file, the skill emits a warning to stderr:
[DISPATCH WARNING] Catalog mtime is older than source files: ...
Consider running `claude-wayfinder catalog build` to refresh.
Proceeding with stale catalog.
Execution proceeds with the stale catalog — staleness is a
degraded-quality signal, not an error. Run
"$PY" -m claude_wayfinder catalog build (using the same explicit
interpreter path resolved in the Running section above) to refresh.
The 7 decision branches
| Branch | When it fires |
|---|---|
needs_more_detail |
Feature density < 2; provide more context to route accurately. |
delegate |
One agent scores ≥ 0.85 with a gap ≥ 0.2 above the next. |
self_handle |
At least one skill scores ≥ 0.5; no dominant agent. |
mixed_content |
Gap < 0.2; ≥ 2 agents clamped at 1.0 on path-disjoint lanes. Output includes lanes[] (agent, score, matched_paths, skills per lane) and unassigned_paths[]. |
advisory |
Best agent ≥ 0.5. Covers both tie (gap < 0.2, rationale includes gap=) and marginal (gap ≥ 0.2 but score < 0.85) cases. Top agent named; alternatives populated. |
ask_user |
Reserved — not produced by the v0.1 matcher. |
self_handle_unaided |
Nothing scores above threshold; proceed without delegation. |