NOTE: This is the canonical provider-independent contract. Both provider wrappers must load it in full.
- Resolve supporting assets and relative script paths against
.claude/skills/tier-offload/; the canonical prose lives here while implementation assets remain with the compatibility launcher. - A named skill call means the host's skill-dispatch primitive. A fresh-context task invocation or workflow means an isolated host action with the requested capability tier. Provider wrappers map these roles to their native APIs.
- Model tier names in inherited procedures describe capability roles. Resolve them through
config/model-tier-map.json; an unavailable required capability returnsrequired_tool_missingrather than weakening a gate. - Never expose hidden chain-of-thought. Preserve only decisions, evidence, commands, structured artifacts, and operator-facing rationale required by this contract.
Reads every SKILL.md under your .claude/skills/, classifies each skill's LLM-bearing roles with a fixed taxonomy, then applies the Switchboard routing rule to decide which (and only which) sub-tasks are safe to run on a cheap local model instead of the orchestrator's primary provider. It emits two artifacts: a human-readable inventory (markdown, grouped by routing verdict) and a switchboard config (enabled_call_sites map) that the switchboard local_judge client loads directly.
This is the generic, contributable scanner half of Switchboard offload. It is discovery + config only — it identifies and configures the local-safe slices; the actual wiring of each skill to call local_judge stays a guided, per-user edit you make afterward. It never edits a skill.
- You've adopted the
switchboardlocal-offload core and want to know which of your own skills have a sub-task safe to route to the local model. - You want to regenerate the offload inventory after adding or changing skills (the safe surface drifts as skills evolve).
- You want a config file the
local_judgeclient can load, without hand-authoring theenabled_call_sitesmap and risking a shape mismatch.
The offload's one governing rule (Switchboard Decision 9 / the 3-tier judge split): authorship, planning, orchestration, and any final/gating judgment stay on the orchestrator's primary provider; only a fan-out array of cheap judges/graders goes local; mechanically-checkable work goes to a script (no LLM). The local model is never on a correctness gate — it advises, the strong-tier provider gate decides. Default strong gate: Claude (primary provider); override via router configuration.
Classification follows <repo>/_shared/skill-role-taxonomy.md: its §1 taxonomy (the seven tags — ORCH / AUTHOR / PLAN / JUDGES / GATE / MECH / SOLO — plus the tag boundaries), its §2 fan-out classification protocol, and its §3 output shape. That reference is direction-neutral; this skill layers the down-direction (offload) rules on top: the four corrections below, the hard gate-precondition invariant, and the LOCAL / PRIMARY / SCRIPT verdict columns added in Phase 2.
- Authorship fan-outs are NOT offloadable.
user-brainstorm/user-learn/repo-init/repo-updatefan out, but each task writes content (AUTHOR), so it stays on the orchestrator's primary provider. A fan-out is only local-safe if every arm is judging, not producing. - Only the Style reviewer lens is cheap. In a multi-lens reviewer (review-gauntlet / review-deep / build-step), the Correctness and Bugs lenses are deep-reasoning drift-catchers (
code-quality.md) and stay on the orchestrator's primary provider. Only the Style lens is the cheap local slice. (review-deep's Style lens already runs on the haiku tier — smallest lift.) - A checklist "fan-out" that is really one primary-provider pass is NOT a drop-in.
plan-review/plan-wrapenumerate "sections" but run them as a single primary-provider pass today, not a real parallel array. Tag SOLO, route to the orchestrator's primary provider; it's refactorable-to-local later, not offloadable now. - Tool-using judge arms are never local-safe. A JUDGES array whose arms require live tool use (WebFetch/browser,
gh, substrate commands) cannot route tolocal_judgeregardless of shape — the local endpoint is text-in/text-out. Verdict PRIMARY with a tier note (sonnet, low effort) naming the blocking tool. Canonical example: deep-research's 3-vote source-verification array — perfect JUDGES shape, blocked by its WebSearch/WebFetch requirement.
And the hard invariant: offloading a JUDGES array is only safe if the strong-tier provider GATE consolidates its findings. If a skill's reviewers gate directly today, routing them local without first inserting a strong-tier final judge makes the weak model the gate — forbidden. Flag this in the inventory as a precondition (gate-precondition: insert-strong-tier-final-judge), never as already-safe.
build-step-style is a standing hard false — never emit true. A "build-step now consolidates on its Step 7 primary-provider aggregate" observation does NOT re-license it: Step 7 is the merge-decision gate, and Switchboard Decision 3 keeps the local model out of a merge context entirely (even as an advisory Style lens); Style is also the lowest-token lens, so there is no offset to weigh. This exact "precondition satisfied → enable" read already emitted a defective true into offload-scan-out/offload-config.json (found + reverted 2026-07-21). The skill's own eval enforces false (test_sample_config_loads.py::test_enabled_call_sites_gate_as_expected); emit build-step-style: false regardless of the Step 7 check.
Parse args. Resolve --skills-dir:
- If given, resolve to an absolute path.
- Default: walk up from cwd to the innermost ancestor containing a
.claude/skills/directory; use that.
Enumerate the skill set per the shared reference's §2 steps 1–2 (Glob discovery, skip non-skill entries, ls the matched filenames before quoting any into a sub-agent prompt).
Resolve --out-dir (default ./offload-scan-out). Create it if absent (PowerShell: New-Item -ItemType Directory -Force <path>).
Print the discovery line before analysis: the resolved skills dir, the count of skills found, and the out dir.
Run the shared reference's §2 fan-out classification protocol exactly (read-only fresh-context task invocations using the sonnet tier (resolve via model-tier-map.json) per the reference §2, 6–8 skills per batch, all of a batch's tasks dispatched in one message, the §1 taxonomy verbatim in every task's prompt, read-only/classify-only discipline), with these down-direction additions:
- Each task's prompt must include — besides the reference's §1 taxonomy verbatim, per the reference's own protocol — the four corrections verbatim (above). Every task must apply the SAME rules, or the inventory drifts per-batch.
- Each task extends the reference's §3 output shape with the down-direction verdict columns, returning per skill:
Skill | Roles (tags) | Verdict | Local slice (if LOCAL) | Note, where Verdict is:- LOCAL — has a genuine JUDGES array (parallel cheap scoring), every arm judging not producing. Name the specific slice (e.g. "structural/rubric grader", "Style lens").
- PRIMARY — only ORCH / AUTHOR / PLAN / GATE / SOLO roles (or a JUDGES array that fails a correction).
- SCRIPT — only MECH roles; nothing to offload to an LLM.
- For each LOCAL verdict, require: the slice name, and whether a
gate-preconditionapplies (does the skill's array gate directly today, needing a strong-tier final judge inserted first?).
Collect every task's table before Phase 3.
Merge all agent tables into one inventory, grouped exactly like the Switchboard Appendix:
- The local surface — the table of LOCAL skills only:
Skill | Local slice (small model) | Everything else → primary provider | Note. This is the load-bearing output. - Primary-provider routed — bulleted list of PRIMARY-verdict skills (authorship / planning / single-pass reasoning / orchestration / final-gate).
- No LLM (scriptable / mechanical / doc) — bulleted list of SCRIPT-verdict skills.
Write the inventory to <out-dir>/inventory.md. Lead it with a one-paragraph summary: N skills scanned, K local-safe slices found, and the date.
For each LOCAL slice, mint a task_class name and add it to the enabled_call_sites map. The task_class is the call-site identifier the wired skill will pass to local_judge(task_class=...). It MUST satisfy switchboard's name-safety rule — match ^[A-Za-z0-9._\-]+$ exactly (letters, digits, ., _, - only; no spaces, no slashes, no : or other metacharacters). Use a stable <skill>-<slice> slug, e.g. skill-iterate-grader, review-gauntlet-style, context-slim-classifier.
Map each task_class to:
true— offload allowed, use switchboard's default model (the common case; the production deployment uses one model for all slices per switchboard D2).- a model-name string — only if you deliberately pin a different model for that slice (must also match
^[A-Za-z0-9._\-]+$). Default totrue.
Do NOT include a slice that has an unmet gate-precondition as plain true — instead emit it as false (configured but disabled) and note in the inventory that it activates only after the strong-tier final judge is inserted. This keeps an unsafe slice from silently becoming a live gate.
Write the config as JSON to <out-dir>/offload-config.json with exactly this shape (this is the integration contract — it must load into switchboard.config.SwitchboardConfig):
{
"less_token_mode": true,
"enabled_call_sites": {
"skill-iterate-grader": true,
"skill-evolve-grader": true,
"review-gauntlet-style": true,
"review-deep-style": true,
"goblin-suggest-judge": true,
"context-slim-classifier": true,
"build-step-style": false
}
}Shape rules (match switchboard/config.py exactly):
- Top-level keys are a subset of
SwitchboardConfig's fields:less_token_mode(bool),enabled_call_sites(object), optionallyeffort(object), and optionallybase_url/model/cold_timeout_s/warm_timeout_s/max_tokens. Emit onlyless_token_mode+enabled_call_sitesunless the user pinned endpoint values — let the rest default. enabled_call_sitesis an object mapping each task_class string to a bool or a model-name string. No nested objects, no arrays, no nulls.effort(optional) is an object mapping a task_class string to a primary-provider reasoning-effort tier — one oflow/medium/high/xhigh/max. HONEST SCOPE: this is a hint for primary-provider task dispatch (a host reasoning-effort override when the slice's judges run on the orchestrator's primary provider);local_judgedoes NOT consume it. switchboard validates + round-trips it but acts on nothing — emiteffortonly to record a per-slice recommendation (e.g.{"review-deep-style": "low"}), never as a live local knob.- Every task_class key AND every model-name value must match
^[A-Za-z0-9._\-]+$. - A slice that gates directly today (unmet
gate-precondition) is emitted asfalse.
Print a summary (do not truncate):
tier-offload — <skills-dir>
Scanned: N skills
Local-safe: K slices (M live / G gated-pending-final-judge)
Primary-routed: A skills
No-LLM: S skills
Artifacts:
inventory.md — <out-dir>/inventory.md
offload-config.json — <out-dir>/offload-config.json (loads into SwitchboardConfig.enabled_call_sites)
Next (per-user, NOT done by this skill):
Install the config so the switchboard client loads it: copy offload-config.json to
~/.switchboard/config.json (the home default, $HOME\.switchboard\config.json on Windows)
OR set $env:SWITCHBOARD_CONFIG to its path, then verify with `python -m switchboard config`
(offload_active:true means it is live). See switchboard/README.md "Turning offload on/off"
for the full enable/disable flow + resolution order.
For each live slice, wire the skill's sub-task to call local_judge(task_class="<key>").
For each gated slice, first insert a strong-tier final judge that consolidates, then flip the
config entry from false → true.
End with the exact standalone line:
tier-offload wrote the inventory + config — wiring each slice to local_judge stays a guided per-user edit (it was NOT auto-applied).
- Discovery + config only. Never auto-wire. This skill never edits a
SKILL.mdto calllocal_judge. It writes the inventory and the config; the operator wires each slice. - Use read-only fresh-context task invocations for classification. They cannot edit or write — the safe substrate for reading someone's skills. Use the sonnet tier (resolve via model-tier-map.json).
- Apply the four corrections. Authorship fan-outs → the orchestrator's primary provider; only the Style reviewer lens is cheap; checklist single-passes are SOLO not LOCAL; tool-using judge arms are never local.
- Never emit a directly-gating array as live (
true). Emitfalse+ agate-preconditionnote; it activates only after a strong-tier final judge is inserted (Switchboard Decision 3 — the local model is never the gate). - The config must load. Every task_class key and model-name value must match
^[A-Za-z0-9._\-]+$. No spaces/slashes/colons. Values are bool or model-name string only. A mismatch here is the bug classtest_sample_config_loads.pyexists to catch — if unsure, validate againstswitchboard/config.pybefore writing. - Autonomous — no mid-run (y/n) prompts. Run bare = scan + report + write inventory + config.
--dry-runprints the report and writes nothing. - Do not commit. Leave artifacts in the out dir for the operator.