Skip to content
Conduit

skill

vzt-agent-protocol

Automatic model routing for Claude Code — Fable 5, Opus 4.8, Sonnet 5, Haiku 4.5. Right model, right task, zero manual switching. Part of the VZT Protocol ecosystem.

vonzelle-vzt/vzt-agent-protocolcommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/vonzelle-vzt/vzt-agent-protocol itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 22, 2026

VZT Agent Protocol

Automatic model routing for Claude Code — Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5. Right model, right task, zero manual switching.

Part of the VZT Tech Consulting Protocol ecosystem.

License: MIT Version Tiers


The problem

Running every prompt on your best model burns through weekly usage limits in days. Running everything on a cheap model caps quality. Manually flipping /model per task is friction nobody sustains.

The solution

The VZT Agent Protocol classifies every prompt and routes the work to the cheapest model tier that can do it well — automatically, on every prompt, with zero API cost for the routing itself:

TierModelOwns
4Fable 5.1No-prior-art architecture, impossible bugs, root-cause, security analysis
3+Opus 5.5 @ maxRoutine planning — architecture, specs, roadmaps, migration plans (the opus@max rung)
3Opus 5.5Large refactors, dense algorithms, performance surgery, load-bearing review
2Sonnet 5.5Standard implementation — the default (burns its own separate weekly bucket)
1Haiku 5.5Search, summaries, renames, formatting, commit messages — nearly free

Why this preserves your limits: Max plans meter a Sonnet-only weekly bucket separately from the all-models bucket that Fable/Opus consume. Routing execution to Sonnet and mechanical work to Haiku means your premium quota is spent only where premium reasoning actually changes the outcome.

Recommended setup — Opus first line: sit on Opus 5.5 (/model opus) so strong reasoning is always on tap, and let the protocol delegate routine builds down to Sonnet and mechanical work down to Haiku, reaching up to Fable only on the ≤10% of turns that are genuinely frontier-hard. Opus 5.5 is a step change on deep reasoning at 40% of Fable's price, so planning now runs on the opus@max rung rather than the frontier tier. See Chair Profiles for every chair's behavior.

Quick start

No clone needed — run it straight from GitHub with npx:

# install globally for every project
npx github:vonzelle-vzt/vzt-agent-protocol install --global

# or install into a single project's .claude/
npx github:vonzelle-vzt/vzt-agent-protocol install --target /path/to/your/project

# verify
npx github:vonzelle-vzt/vzt-agent-protocol doctor --global

Or clone first if you prefer:

git clone https://github.com/vonzelle-vzt/vzt-agent-protocol.git
cd vzt-agent-protocol
node cli/vzt-agent.js install --global   # or: install --target /path/to/project
node cli/vzt-agent.js doctor --global

Restart Claude Code. Pick the chair that matches how you work — the protocol adapts the routing doctrine to it either way:

  • Opus 5.5 chair (/model opus) — build and plan inline, delegate routine execution down to Sonnet and mechanical work to Haiku, escalate up to Fable only for no-prior-art architecture and impossible bugs. Best when you want strong first-line reasoning on tap and Sonnet as your workhorse below it.
  • Sonnet 5.5 chair (/model sonnet) — most work stays inline on the Sonnet-only bucket; escalate up to Opus/Fable only when a task earns it. Best for maximum quota efficiency.

How it works — four real routing layers

Every layer here uses a mechanism Claude Code actually enforces — not prompt-only suggestions:

1. Per-prompt classifier hook (UserPromptSubmit)

A deterministic, <50ms, zero-API-cost classifier scores every prompt against the routing matrix (25+ signal patterns + length heuristics) and injects a [VZT-ROUTE] directive: which tier, which agent, whether to handle inline. Every decision is logged to ~/.claude/vzt-router/decisions.jsonl.

2. Chair-aware session profiles (SessionStart)

The protocol reads which model your session launched with and inverts the doctrine to match:

  • Fable chair → tokens are scarce: plan inline, delegate ALL execution down
  • Opus chair → clock is scarce: build inline, push mechanical work down
  • Sonnet chair → capability is scarce: escalate up only when a task earns it
  • Haiku chair → dispatcher mode: delegate almost everything

3. Model-pinned agent fleet (.claude/agents/)

Ten agents with model: + effort: frontmatter — Claude Code runs each on its pinned model regardless of your session model:

AgentModelEffortRole
vzt-plannerfablemaxNo-prior-art plans only — novel/greenfield architecture, one-way-door distributed-systems calls
vzt-oraclefablemaxRoot-causes impossible bugs; returns a fix packet, not a guess
vzt-architectopusmaxThe opus@max rung — routine planning, with a step-routing table (each step tagged with its cheapest sufficient tier)
vzt-heavy-builderopushighTightly-coupled multi-file surgery, algorithms, migrations
vzt-revieweropushighReviews only the load-bearing seam the plan flags
vzt-art-directoropushighAuthors DESIGN.md — visual taste, when none is written down yet
vzt-buildersonnetmediumThe workhorse — all routine implementation
vzt-stylistsonnetmediumApplies DESIGN.md — restyles, spacing, palette, dark mode
vzt-scouthaikulowRecon: find/count/summarize, read-only
vzt-mechanichaikulowMechanical edits: renames, formatting, bumps

4. Turn-level skills (skill model: override)

When up- or down-tier work needs the full conversation context (subagents start fresh), these switch the current turn's model in place:

  • /vzt-design <task> — plan this turn on Opus 5.5 @ max (the opus@max rung — the default planning turn)
  • /vzt-plan <task> — plan this turn on Fable 5.1 (only when the design has no prior art to reason from)
  • /vzt-fix <bug> — root-cause this turn on Fable 5.1
  • /vzt-build <step> — execute this turn on Sonnet 5.5
  • /vzt-quick <task> — mechanical turn on Haiku 5.5
  • /vzt-ui [extract|apply] — the visual lane: author a repo's DESIGN.md from its real token layer, then apply it. (No model pin — the tier depends on whether the taste cache exists; see below.)
  • /vzt-fable-mode — run this turn under the five frontier working gates (scope, evidence, attack, verify, report) (no model pin — runs on the active model)
  • /vzt-diagnose <symptom> — parallel hypothesis fan-out for a hard bug: N≤4 read-only agents each test one root cause with one real command and return CONFIRMED/REFUTED with the output pasted. Run it before escalating to /vzt-fix — cheap parallel evidence first, frontier reasoning only once it is earned. (No model pin — the probes run on Haiku/Sonnet.)
  • /vzt-ship <the system to build> — spec-first long-horizon execution: writes a SPEC to disk before any code, gates it with a command, then drives it as supervised background workers. See Long-horizon work below.

The session model returns on your next prompt.

The standard pipeline

you: "build feature X"  (a non-trivial, multi-part feature)
 └─ PLAN → vzt-architect (Opus 5.5, effort max)  ·   or /vzt-design for an in-context turn
     ·    (novel/greenfield design instead? → vzt-planner (Fable 5.1) or /vzt-plan)
     └─ plan with step-routing table + load-bearing seam flagged
         ├─ steps tagged sonnet → vzt-builder        (parallel)
         ├─ steps tagged haiku  → vzt-mechanic/scout (parallel)
         ├─ steps tagged opus   → vzt-heavy-builder
         ├─ steps tagged ui     → vzt-stylist  (Sonnet, applies DESIGN.md)
         │                        └─ no DESIGN.md yet? → vzt-art-director (Opus) writes it ONCE
         └─ seam review         → vzt-reviewer (Opus, only the risky seam)

On an Opus chair, a routine one-shot request skips planning entirely — the classifier delegates the build straight down to vzt-builder (Sonnet) and any mechanical part to vzt-mechanic (Haiku), while you stay on Opus as coordinator. Full walkthroughs per chair: Chair Profiles.

Long-horizon work — /vzt-ship

Scope language ("entire codebase", "from scratch", "greenfield", "end-to-end", "multi-tenant") used to route straight to Fable — the slower, more expensive model. That was a bug. Long-horizon work doesn't fail because the model isn't smart enough; it fails because context compaction eats the plan halfway through the run, and the back half gets built against a plan the chair no longer remembers. A slower model doesn't fix that. A plan on disk does.

Escalate the PROCESS, not the MODEL. Scope language now routes to Opus under a new task kind, HORIZON, and the gate is two-factor: scope language alone is still a planning question and stays on Fable ("design the architecture for the whole system"); scope + a build verb (build/implement/ship/create/scaffold/rewrite/...) is a shipping question and becomes HORIZON. Fable narrows to genuinely hard debugging (/vzt-fix) and stays ≤10% of turns.

A HORIZON classification points at /vzt-ship, which runs four phases:

  1. SPEC — forbidden from editing source. Writes .vzt/ship/<slug>/SPEC.md from templates/spec.md: contract, out-of-scope, the interfaces that cross unit boundaries (these become a serial "barrier" unit), a file manifest, and units whose FILES_IN_SCOPE sets are pairwise disjoint, each with one machine-checkable oracle chosen before the unit is built.
  2. GATE — vzt-agent ship-check <SPEC.md> is a command, not an opinion: it exits non-zero on overlapping scopes, a manifest file no unit owns, a unit with no oracle, an unknown agentType, or an undeclared external connection (see below).
  3. RUN — launches workflows/vzt-ship.js via the Workflow tool: barrier → parallel units → independent read-only verification of each oracle (builders never grade themselves) → bounded repair (≤2 rounds) → integration gate.
  4. LAND — verifies artifacts on disk, not reports, then stops before commit/deploy.

It survives compaction because the coherence lives on disk, not in the conversation: SPEC.md + LEDGER.jsonl on disk, vzt-agent ship-status reconstructs run state, and the UserPromptSubmit classifier hook re-injects a [VZT-SHIP] block on every prompt — compaction does not re-fire SessionStart, so the classifier is the only hook that survives it.

Optional: watch a run in an agent multiplexer (orca/)

workflows/vzt-ship.js is the headless path. When you'd rather watch the units work in parallel — live panes, diffs, agent state — the same gated SPEC drives a supervised run in an agent multiplexer. Three backends behind one --mux flag: orca (desktop ADE, default), herdr (terminal-native, persistent over SSH/mobile), or vscode (each unit opens as a native VS Code integrated terminal via the companion extension in vscode/ — no external binary, just VS Code):

vzt-agent ship-watch .vzt/ship/<slug>/SPEC.md               # orca (default)
vzt-agent ship-watch .vzt/ship/<slug>/SPEC.md --mux herdr   # herdr
vzt-agent ship-watch .vzt/ship/<slug>/SPEC.md --mux vscode  # native VS Code terminals

The vscode backend needs no multiplexer install: it uses git worktree + a filesystem queue the companion extension drains into terminals, and a Stop hook for idle detection. Setup + the honest constraints (VS Code can't rename terminal tabs, so PASS/FAIL shows in a "VZT Ship" output channel + status bar) are in docs/VSCODE.md.

For daily VS Code use, install the local companion once from this checkout:

npm --prefix vscode install
npm run install:vscode:local

After reloading VS Code, the extension is passively active in every workspace: the VZT: ... status bar item shows ready, setup needed, running, or blocked; VZT: Doctor reports hook and mux state; and VZT: Start Ship Watch From Spec launches a --mux vscode run from an existing SPEC.md.

One command: dispatch each dependency wave of units as a claude worktree pane, at most --max-concurrent (default 4, 0 = unlimited) in flight at once → wait for each to finish → auto-run its oracle, stamp its card, record the ledger → integration gate → stop at "ready to review + merge". A unit declaring dependsOn opens its worktree already seeded with its dependencies' finished work (committed, uncommitted, and untracked, applied and committed before launch), so "build against the interface the barrier wrote" is a worktree that actually has it rather than a race; a unit whose dependency did not pass is recorded BLOCKED and never dispatched. Each pane is auto-bootstrapped (orca/worktree-bootstrap.sh symlinks node_modules/.env* from the primary checkout, so a worktree can actually build), and the ledger resolves to the primary checkout so parallel worker writes are never lost or conflicted. This is not the fan-out that vzt-route rejects — units within a wave are pairwise-disjoint, not a race. Full guide: orca/README.md.

Claude + Codex collaboration

This repo carries a Codex-facing AGENTS.md and a shared docs/CLAUDE-CODEX-COLLABORATION.md contract. Claude Code owns orchestration, product interpretation, and HITL gates; Codex owns bounded implementation, mechanical refactors, focused review, and local verification.

Use files, not memory, for work that must survive compaction or an agent handoff: .vzt/team/handoff.md, .vzt/team/decisions.md, .vzt/team/verification.md, and dated review notes under .vzt/team/reviews/.

When this execution-layer repo changes routing doctrine, model tiers, worker briefs, hook wiring, ship orchestration, or VS Code mux behavior, mirror the corresponding protocol note into the sibling ../VZT-Tech-Consulting-Protocol checkout before release. If the sibling repo is dirty, patch only the relevant docs/templates and preserve unrelated work.

VS Code agent bus

Claude Code and Codex can sit in two split VS Code terminals and talk to each other through a durable local message bus. Terminals only host the processes; they are never the transport. Messages live in a WAL SQLite file ($VZT_BUS_DIR, default ~/.vzt/bus/bus.db) driven by short-lived vzt-agent bus calls, so queued mail survives a window reload. The only bytes ever typed into an agent terminal are one fixed wake line and, 400 ms later, a bare Enter. Full contract: .vzt/ship/vscode-agent-bus/SPEC.md.

Messages are typed (message, task, question, reply, gate, report) and move through an explicit state machine; tasks need an idempotency --key and run one at a time per agent.

vzt-agent bus register --alias A --kind claude|codex --window W --json
vzt-agent bus send --to ALIAS --type message|task|question --subject S --body B [--key K]
vzt-agent bus check --json            # read your mail (agent side, VZT_BUS_AGENT set)
vzt-agent bus ack --id ID
vzt-agent bus reply --id ID --body B
vzt-agent bus ask --to ALIAS --subject S --body B [--timeout-ms N]   # blocks until answered
vzt-agent bus complete --id TASK --outcome succeeded|failed
vzt-agent bus gate-resolve --id ID --decision approve|reject
vzt-agent bus agents --json           # registry with liveness: live | stale | never-started
vzt-agent bus inbox --json            # threads, for the sidebar
vzt-agent bus doctor --json           # node:sqlite, hook wiring, Codex hook trust
# used by the extension: argv, bootstrap-prompt, lease, wake-plan, wake-sent, hook

Exit codes: 0 ok, 2 usage/validation, 3 inside Orca (use orca orchestration there instead), 4 conflict, 5 ask timeout, 70 Node without node:sqlite (the bus needs Node >= 22.13).

Install. vzt-agent install wires four lifecycle hooks (SessionStart, UserPromptSubmit, Stop, PermissionRequest) into ~/.claude/settings.json and ~/.codex/hooks.json ($CODEX_HOME honoured). They no-op unless the terminal carries VZT_BUS_AGENT. Codex skips new hooks until you trust them: open codex, run /hooks, and trust the 4 vzt-bus hooks. vzt-agent bus doctor lists untrusted ones, and the VS Code Pair command refuses to launch Codex until they are trusted. The hook command is a byte-stable constant, so trust survives reinstalls.

Codex Stop-hook limitation. Claude receives mail at the end of a turn through a Stop-hook decision: block. Codex never does: a Stop-hook block can permanently wedge a Codex session (openai/codex#20783), so the Codex Stop hook prints nothing and Codex is reached only by the wake line, typed after it goes idle and confirmed by its next UserPromptSubmit beat (one retry Enter, then the agent is flagged wake_unconfirmed).

Live gate. Offline tests cover the bus; the real round trip is a manual gate because it launches real agents and spends quota:

vzt-agent install            # then trust the 4 Codex hooks via /hooks
node test/live/bus-roundtrip-live.mjs

It replays the extension's poller against Claude and Codex running under PTYs in this repo: a harness alias gives Claude a task to bus ask Codex for 6*7 and write the answer to .vzt/live-bus/<run>/answer.txt. Exit 0 needs the answer 42, the task completed/succeeded, and at least one confirmed Codex wake within 10 minutes; a failure prints the reason and both screen tails. It uses the vzt-agent on your PATH (the same one the hooks call), so update the global install first.

Orca-style workspaces

The vzt-mux extension (0.8.0) brings Orca's project → worktree → agent model into VS Code, on top of the agent bus above. Contract: .vzt/ship/vscode-orca-workspaces/SPEC.md.

Orcavzt-mux
projectProjects view
worktreeNew Agent Workspace
orchestration send/askvzt-agent bus send/ask
status glyphsProjects view glyphs
bellattention
Agent DashboardAgent Dashboard
  • Projects come from an explicit registry, ~/.vzt/projects.json (VZT_PROJECTS_FILE overrides it); nothing scans the disk. Each project opens in its own VS Code window.
  • Worktrees go to <parent of root>/<project>-worktrees/<slug> on branch <slug>. Removing one keeps the branch.
  • The bus is project-scoped: every worktree of a project shares one scope, so agents address each other by alias across worktrees. An alias is unique per project. Other projects stay unreachable unless --cross-workspace is given.
vzt-agent projects add <dir> [--name N] [--base REF] [--json]
vzt-agent projects remove <name> [--json]       # registry only; disk untouched
vzt-agent projects list [--json]                # projects with their git worktrees
vzt-agent worktree create --project N --name SLUG [--base REF] [--json]
vzt-agent worktree remove --project N --name SLUG [--force] [--json]

Exit codes: 0 ok, 2 usage/not found, 4 conflict (the name, path or branch already exists).

Visual work — DESIGN.md is a taste cache

Visual work was the one kind this protocol was blind to. "Restyle the dashboard", "fix the spacing", "the palette is off" matched no signal and fell into the zero-signal default bucket, where they got done from whatever the model imagined the product looked like. That is how one org ends up with thirty repos and thirty palettes.

The fix is not a better model. It is a file.

A DESIGN.md at the repo root moves visual taste off the model tier and onto disk — the same move /vzt-ship makes for long-horizon plans, for the same reason. Once the taste is written down, applying it is not judgement; it is execution:

taste comes fromtieragent
No DESIGN.mdthe modelOpusvzt-art-director — decide once, write it down
DESIGN.md existsthe fileSonnetvzt-stylist — apply it faithfully

The classifier gates on this with a filesystem check, structurally identical to the two-factor HORIZON gate except the second factor is a file rather than a second regex. So the routing control is a git-diffable artifact: create DESIGN.md and visual work routes down forever; delete it and taste work routes back up.

Two properties keep it honest. A DESIGN.md under 400 bytes doesn't count — a placeholder would down-route every visual request in the repo while containing no taste to apply, and a cache that lies is worse than no cache. And every generated DESIGN.md carries a ## Compliance section with a runnable check, recorded as a baseline and ratcheted, because a design doc nothing enforces is decorative: the repos that already carry 300-line design systems also carry a thousand-plus raw palette classes.

Start with /vzt-ui extract. Template: templates/DESIGN.md.

This is visual design. Technical design — architecture, schemas, APIs, migration plans — is a different lane: /vzt-design and vzt-architect.

External side effects — CONNECTIONS_IN_SCOPE

FILES_IN_SCOPE is the collision boundary for what a worker writes inside the repo. Until now nothing bounded what it could reach outside one — and the ship path is exactly where that gap bites hardest. orca/worktree-bootstrap.sh symlinks the primary checkout's node_modules and .env* into every unit's worktree, because a worktree that cannot build cannot be graded. The side effect is that a fan-out of parallel agents starts life holding whatever production credentials the repo holds, and the only thing standing between a builder and a live customer API is that nobody happened to tell it to call one.

So external access gets the same treatment as filesystem access — declared in the spec, checked by a command, default-deny:

// .vzt/connections.json — git-tracked, and never contains a credential
{
  "version": 1,
  "connections": [
    { "id": "stripe-test", "service": "stripe", "mode": "test",
      "credentialEnv": "STRIPE_TEST_SECRET_KEY", "allow": ["read", "write"] }
  ]
}
// a unit in SPEC.md, or CONNECTIONS_IN_SCOPE in a worker brief
{ "id": "u2-invoice", "filesInScope": ["src/invoice.ts"],
  "connectionsInScope": ["stripe-test"] }

ship-check exits non-zero when a unit names a connection the registry does not declare, when the registry is absent entirely, when it is malformed, or when an entry carries an inline token/secret/apiKey/key/credential/password — the registry names the env var, never the value, because a file whose whole job is to bound blast radius is the last place a live key should live.

Three properties keep it honest:

  • Omitted means none, not unrestricted. A unit that declares nothing is repo-local work — nearly all work — and pays no friction for it.
  • The absent registry is a red gate, not an open one. "No registry" means nothing has been declared, so a unit claiming a connection fails.
  • The boundary is rendered into the worker's prompt, on every unit, including the ones declaring nothing. A boundary that binds the plan but never reaches the agent is decorative — the worktree still has the symlinked .env, so the only enforcement the worker ever sees is the sentence stating the rule. Silence about outbound calls reads as permission to make them.

Template: templates/connections.json. Same move as SPEC.md and DESIGN.md, one axis out: put the boundary on disk, and a command can enforce what doctrine only requested.

Mobile store releases — approval is not delivery

The furthest-outside side effect the protocol touches is a store release, and it is the one place where the absence of an error is not evidence of success. Every failure shape here is silent: nothing throws, no test goes red, the console is green, and users simply never receive what you shipped.

Shipping is three events, not one — the store approves, the storefront starts serving, and users are told. The third is usually a row in your own database that nothing updates on its own. Measured twice on real releases: the storefront moved to a new version while the release row still named the old one, so the in-app "update available" banner stayed dark for every install until a human ran the sync.

The rest of the doctrine — an OTA reaching exactly one runtime (a version bump orphans everyone else, and publishing into an audience of zero looks identical to publishing well); store policy declarations that deadlock the API; a permission a dependency's manifest added for you; the rule that a capability gate and its permissions must move together because both ways of getting it wrong ship rather than fail a build; the Play service-account chain including the iam.disableServiceAccountKeyCreation org policy that is on by default — lives in skills/vzt-mobile-release/SKILL.md.

Two habits generalise beyond mobile and are worth stating here:

  • Verify the artifact, not the build log — and include a control, something that should still be present, so a passing check cannot be a broken extraction.
  • An oracle you have never seen fail is not an oracle. Break the invariant on purpose, watch the test go red, restore it. Cheap, and the only thing that distinguishes a real check from a comfortable one.

Guardrails

  • Escalation ladder — two failures at a rung escalates exactly one rung (haiku→sonnet→opus→opus@max→fable), stated aloud. Under-routing is self-healing.
  • Fable budget — ≤10% of turns; vzt-agent stats shows your distribution against the target. The opus@max rung absorbs the planning that used to land on Fable.
  • External side effects are default-deny — a worker reaches only the connections its brief declares against .vzt/connections.json; holding a credential is not permission to use it.
  • Delegation cap — never delegate work finishable in a handful of tool calls; prefer one sub-agent over several; once delegated, commit. Verify external artifacts (run the oracle, git diff the worker's output); never spawn a sub-agent to double-check your own inline work.
  • Store releases are measured, not assumed — approval, serving and announcing are three separate events; ask the public storefront, never the console. See Mobile store releases.
  • No frontier execution — plans always hand execution to cheaper tiers.
  • Advisory, not authoritarian — directives are context injections; Claude overrides them only with a stated reason.

Manual overrides

InputEffect
@fable / @opus / @sonnet / @haiku prefixForce a tier for that prompt
~ prefixBypass routing for that prompt
/vzt-route <task>Ask for an explicit routing decision
/vzt-route statsTier distribution vs. targets

CLI

vzt-agent install [--global] [--target <dir>]   # install + wire settings.json
vzt-agent uninstall [--global] [--target <dir>] # clean removal
vzt-agent doctor [--global]                     # health check
vzt-agent sync [--check] [--only <.claude dir>] # refresh every install on this machine (~/.vzt/installs.json)
vzt-agent stats                                 # routing distribution + Fable budget, /vzt-ship and ui taste-cache falsification
vzt-agent matrix                                # print the routing matrix
vzt-agent ship-check <SPEC.md>                  # gate a /vzt-ship spec — disjoint scopes, an oracle per unit
vzt-agent ship-start <SPEC.md>                  # open the run ledger for a gated spec
vzt-agent ship-note  <SPEC.md> '<json>'         # append one ledger line
vzt-agent ship-status [--target <dir>]          # reconstruct run state from disk (use after a compaction)

# Supervision layer (optional — parallel /vzt-ship runs in an agent multiplexer)
#   --mux orca (default) | herdr | vscode (native VS Code terminals; see docs/VSCODE.md)
vzt-agent ship-watch    <SPEC.md> [--mux orca|herdr|vscode] [--timeout-ms <n>]  # kick once: dispatch → wait → verify → gate
vzt-agent ship-dispatch <SPEC.md> [--mux orca|herdr|vscode] [--execute]        # one worktree+claude per unit
vzt-agent ship-supervise <SPEC.md> [--mux orca|herdr|vscode]                   # verify each oracle → shared ledger + mux card

Keeping installs current. Installs are copies, so every install records itself in ~/.vzt/installs.json and vzt-agent sync refreshes each one whose source content changed (the first run also adopts existing installs: ~/.claude plus ~/github-projects/*/.claude, or VZT_PROJECTS_DIRS). It runs automatically from two places. One is the protocol repo's git hooks: run npm run hooks:enable once and every commit, merge or rebase on main in the primary checkout syncs in the background (log in ~/.vzt/sync.log). The other is every SessionStart, which fires a detached sync --only <that .claude>. Refreshed project .claude/ files are left uncommitted for you. Set VZT_NO_AUTOSYNC=1 to opt out.

Get the bare vzt-agent command with npm install -g github:vonzelle-vzt/vzt-agent-protocol, or prefix any of the above with npx github:vonzelle-vzt/vzt-agent-protocol (from a clone: node cli/vzt-agent.js).

Maintenance gate

Before release, run:

npm run verify

That runs the main node:test suite, root and VS Code npm audits, the VS Code TypeScript compile, and an npm dry-run package check. The root package uses an explicit files allowlist so local .claude/, .codex/, .vzt/, and test state do not leak into install tarballs.

The process is the moat — the five Fable layers

Model choice alone isn't the whole story — the working discipline riding on top of it is. /vzt-fable-mode extracts the frontier tier's working process into five gates any tier can run. Each gate is a checkpoint the work must pass before moving on; skipping one turns the output into a guess, no matter which model produced it. A cheaper model running these gates beats a frontier model running none. The canonical long form lives in skills/vzt-fable-mode/SKILL.md.

Gate 1 — Scope before you act

State the plan before touching anything: what the brief actually asks for, the smallest change that satisfies it, and what is explicitly out of scope. Then play devil's advocate against your own plan once — list the unknowns and assumptions it rests on, and for each one say how you'll resolve it (read the file, run the command, ask). A plan whose unknowns are named is a plan; one without them is a guess with steps.

Gate 2 — Evidence before reasoning

Never reason about code you haven't looked at this session. Confirm files, symbols, APIs, and flags exist — read/grep them — before building on them. What the model remembers from training or an earlier session is a hypothesis, not evidence: partial recognition does not mean current knowledge, and a prompt implying a file exists does not mean one does. Verify, then reason.

Gate 3 — Attack your own approach

Before executing, try once to break the plan: what input, state, or ordering makes it wrong? What's the strongest argument this is the trigger and not the cause? Name the evidence that would refute the approach and go check it. If the attack lands, fix the plan now — it is exponentially cheaper than fixing the shipped version.

Gate 4 — Verify before declaring done

Every change gets a machine-checkable oracle — a test, a command, a curl, a rendered page — decided before the change is made, not invented after. Run it and paste the actual output. "Should work," "looks correct," and a green typecheck are not verification; behavior observed end-to-end is. If the oracle can't be run, say so explicitly instead of implying it was.

Gate 5 — Report only what you verified

No claim in the report that wasn't checked. Anything unverified is marked unverified or dropped — a finding you can't walk through end-to-end is a guess. An honest partial report ("3 done, 1 blocked on X") beats a padded complete-sounding one every time. Failures are stated plainly, with the output that shows them.

Why gates, not effort

The gates are about process, not effort: raising the effort dial does not compensate for a skipped gate — a skipped gate at max effort is still a guess. They also scale down: on Haiku each gate is one line of output; the discipline is identical, the prose is shorter. Combined with the orchestrator doctrine (frontier designs and verifies, Sonnet/Haiku execute and report back), this is what delivers the ~2–10× lower cost on routine steps at equal quality. The Cost/Intelligence/Taste columns in the routing matrix quantify that trade-off tier by tier, so the routing decision is a number, not a vibe.

How fable-mode activates

  • Always on at the Opus tier — Opus never runs bare. Every Opus surface carries the five gates by default: the vzt-architect, vzt-heavy-builder and vzt-reviewer agents state them as their first rule, the Opus chair profile injects them at session start, and every [VZT-ROUTE] directive that targets Opus restates them. Opus stays Opus (no model change) — it just always works with Fable's process. Frontier discipline, cheaper model.
  • Automatic in fleet executors — vzt-builder and vzt-mechanic carry the gate summary as Rule 1, so anything the router delegates to them runs the gates with no user action. vzt-planner and vzt-oracle (Fable) don't reference it — they're the source of the doctrine, not a consumer of it.
  • Manual on the chair — /vzt-fable-mode <task> loads the full skill into the current turn, the "elevate Sonnet" move for hard inline work. The model may also auto-invoke it when a task obviously calls for the discipline, but the slash command is the guaranteed path. Skip it for routine one-liners — the gates would just add overhead.
  • No model pin, by design — unlike /vzt-plan//vzt-fix (force Fable), /vzt-build (Sonnet), and /vzt-quick (Haiku), fable-mode runs on whatever model is already active. It changes how the current model works, not which model works: the router picks the tier and effort, fable-mode upgrades the discipline of whichever tier got picked. The two dials are independent.

Requirements

  • Claude Code ≥ 2.1.170 (skill/agent model: frontmatter incl. fable alias)
  • Node.js ≥ 18
  • A plan with access to Fable 5.1 (falls back gracefully: availableModels restrictions make blocked tiers inherit the session model)

Docs

Release notes

1.20.0 — Claude Code 2.1.290–2.1.299, and installs that keep themselves current

Installs keep themselves current. On 2026-10-17 vzt-agent doctor found the global ~/.claude install and all four project installs on this machine out of date. The global one was still injecting the retired model lineup on every session. The cause was that installs are copies and nothing re-ran install. Now install registers itself in ~/.vzt/installs.json, and the new vzt-agent sync (with --check) refreshes every install whose source content hash changed. It runs from the repo's .githooks (npm run hooks:enable), but only on main in the primary checkout, so unmerged ship-unit worktree code never reaches real projects. It also runs as a detached self-heal from every SessionStart. See CLI.

Doctrine updates from the October Claude Code releases:

  • Per-subagent effort and model in the prompt (2.1.292). A dispatch prompt can set effort for each subagent, and that overrides the session effort with no frontmatter needed. The chair profiles, both routing docs and the worker brief (new EFFORT / MODEL field) now say to set it per worker: low-effort Haiku scouts plus one high-effort reasoner.
  • Subagent pacing. Every worker brief's CONSTRAINTS now opens with the engineering team's measured line: "Time matters here: do not spend time that can be avoided, and the earlier a correct result is obtained, the better."
  • Haiku 5.5 has a 1M context window. vzt-scout reads whole trees instead of sampling.
  • Sonnet 5.5 cache reads cost $0.10/MTok, down from $0.20. This is noted in the cost comment. The input/output ratios (100/40/20/1) are unchanged.
  • Fast mode is GA on the Claude API for Opus 5.5 ("speed": "fast"), the API twin of /fast.
  • onFailure: "block" (2.1.295) is deliberately not used. Every managed hook is advisory and fail-open. A new test fails if install ever writes onFailure.
  • UserPromptSubmit now runs before skill and slash-command expansion (2.1.299). This doesn't affect the classifier: it already skips / prompts and never blocks.
  • Idle compaction (2.1.299). A session over 200K tokens is compacted while idle, right before the one-hour cache expires. /vzt-ship now treats any long idle (such as waiting on ship-watch) as a compaction: re-run ship-status and re-read the SPEC.
  • Noted, with no protocol change: desktop nested sessions, /btw tabs, cloud session handoff, Team network secrets, API credits, /usage warnings, the web-search refill and claude plugin install --marketplace.

1.19.0 — the 5.5 lineup: Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5

The doctrine named the previous generation (Fable 5, Opus 5, Sonnet 5, Haiku 4.5) in every hook, agent, skill and doc, while a few surfaces already said 5.5. That made the lineup inconsistent. Fleet agents pin aliases (model: opus), so they were already running the new models. Only the prose and the cost math were behind.

  • Names. Every shipped surface now names Fable 5.1 / Opus 5.5 / Sonnet 5.5 / Haiku 5.5. The retired-model guard in test/classifier.test.mjs now fails on the old names too, and on haiku-4-5. It matches Opus 5 but not Opus 5.5.
  • Cost ratios re-anchored to sticker pricing: Fable 5.1 $10/$50, Opus 5.5 $4/$20, Sonnet 5.5 $2/$10, Haiku 5.5 $0.10/$0.50 per MTok. The TIERS.cost multipliers move from 10/5/3/1 to 100/40/20/1. Haiku 5.5 is 20× cheaper than Sonnet 5.5 (above 100K-token prompts it is $0.50/$2.50, so 20/8/4/1). Because of that gap, pushing recon and mechanical work down to Haiku matters more than it did. Opus 5.5 costs 40% of Fable's price, down from half, so the opus@max planning rung costs less than before.
  • /fast on Opus 5.5 is $8/$40 per MTok, 2× standard. Before, the session-start hook quoted Opus 5's $10/$50.
  • Effort defaults. On the API, Opus 5.5 and Haiku 5.5 default to medium, while Sonnet 5.5 and Fable 5.1 default to high. Sonnet 5.5 recalibrated its levels. The routing docs now say to set effort explicitly instead of relying on the default.
  • The routing logic itself is unchanged: same signals, same rungs, same agents.

1.18.0 — the agent bus, and three silent ship-watch failures on Orca

Agent bus (VS Code). Claude Code and Codex can now pair in two split VS Code terminals and message each other through a durable local bus (vzt-agent bus <verb>, WAL SQLite at $VZT_BUS_DIR, default ~/.vzt/bus). Typed messages, an explicit state machine, idempotency keys, 300 s redelivery, 30 s leases, one task in flight per agent. Terminals only ever receive a fixed wake line plus a separate Enter. Claude also gets mail via a Stop-hook block; Codex never does, because a Stop-hook block can wedge a session (openai/codex#20783). The vzt-mux extension is 0.7.0 with VZT: Pair Claude + Codex, a vztBus sidebar view and vztMux.busCommand. Pair refuses unless workspace trust is on and the Codex hooks are trusted, then runs bus trust-claude (Claude Code 2.1.299 stops on its folder-trust dialog even with permissions bypassed). Live gate node test/live/bus-roundtrip-live.mjs passed in 31 s on 2026-10-14; re-run it after any claude/codex update. See VS Code agent bus and docs/VSCODE.md.

ship-watch on Orca — three silent failures fixed the same day:

  • e2254e5 pre-trusts each unit worktree in ~/.claude.json (cli/claude-trust.mjs). Before, every unit pane sat on the trust dialog and was failed at the 90 s start check.
  • ab479b2 creates unit worktrees with --base-branch <primary HEAD sha>. Before, Orca based them on origin/main, so local unpushed commits (even the run's own SPEC.md) were invisible to units.
  • 693c5ce + 5e6d444: the integration gate's bare worktree now gets the primary checkout's node_modules symlinked and runs the repo's orca.yaml setup.command. Before, gates that build failed ERR_MODULE_NOT_FOUND, and vzt-orca-flow's live doctor failed "pipeline state NOT initialized" with every unit green.

/vzt-ship also gains the cross-spec ordering rule (specs that share files run as a strict sequence).

1.17.0 — a worktree is not seeded, so "build against the interface" was fiction

🔴 Every git worktree add branched from the primary checkout's HEAD, on all three backends, with no merge, rebase, or cherry-pick anywhere in cli/. So a unit briefed "implement X against the interface the barrier wrote in types.ts" opened a tree where types.ts did not exist — the barrier had written it in a different worktree, on a different branch. The unit then either failed its own oracle or breached scope re-creating the file itself, and the only place the pieces ever met was the integration gate, at the very end, after every unit's budget was already spent.

The fix is a real dependency graph, not a naming convention. Spec units gain an optional dependsOn: ["<unit id>"]; ship-check now also rejects a dependsOn cycle (named — "u1 → u2 → u1", not "there is a cycle"), an unknown unit id, a self-reference, and naming the barrier (it is every unit's implicit dependency already). New pure exports in cli/ship-lib.mjs: planWaves(spec) (topological waves — a spec with no dependsOn anywhere still yields exactly one wave, so every existing SPEC behaves identically), depsOf(u), and pathInScope(p, scope).

Before a unit launches, seedFromDeps() applies each dependency's finished work into the dependent's worktree — transitively, in topological order, barrier first — via the same captureWorktreePatch the integration gate already used (committed, uncommitted, and untracked), then commits the result and records it as baseSha. Committing matters twice: it becomes the scope-audit baseline, so a seeded file is never mistaken for this unit's own write, and it keeps this unit's own captured patch free of its dependencies' content. Seeding is idempotent (git apply --check --reverse detects an already-applied seed), so a corrected, re-dispatched unit doesn't choke on its own seed commit.

ship-watch now runs units in dependency waves, not spec.units.map(dispatch). A wide spec used to start one claude process and one terminal per unit, all at once; a new --max-concurrent <n> flag (also VZT_MAX_CONCURRENT, default 4, 0 restores the old unlimited behaviour) bounds how many run at a time. A unit whose dependency did not PASS is recorded BLOCKED and never dispatched — its worktree would be seeded from a broken or absent base, so running it anyway would fail for a reason that has nothing to do with its own brief.

Runtime SCOPE_BREACH moves onto the supervised path. verifyAndRecord now audits git diff --name-only <baseSha> plus git ls-files --others --exclude-standard in the unit's own worktree before running the oracle. This verdict used to exist only in the headless workflows/vzt-ship.js path — on ship-watch a breach first surfaced as a failed git apply in the integration gate, at the very end, attributed to whichever unit happened to apply second. Seeded dependency files and gitignored build artifacts are correctly not breaches.

A reused worktree that has fallen behind gets a --- BASE DRIFT --- block appended to its prompt, naming the missing-commit count and the 5 most recent subjects it's missing; a freshly created worktree emits nothing.

The VS Code Ship Run tree groups units under collapsible Wave N nodes (units stay vztUnit, so Focus Terminal / Open Worktree Diff / Re-run Oracle are unchanged), adds two states — waiting (grey, a dependency hasn't passed yet, distinct from queued) and SCOPE_BREACH (red warning, deliberately a different icon from FAIL's red error) — and rolls a wave up to its worst member (SCOPE_BREACH > FAIL > blocked > working > queued > waiting

finished > PASS). A record from a pre-0.6.0 CLI has no wave and still renders as a flat list. Extension 0.6.0.

The headless path honours dependsOn too. A field one engine obeys and the other silently drops is worse than no field — the spec would promise an ordering that half the runs ignore. workflows/vzt-ship.js now dispatches wave by wave and records BLOCKED for a unit whose dependency did not pass. Workflow scripts cannot import, so it carries a hand copy of planWaves/depsOf, and a drift guard runs both against the same fixtures and fails if they ever disagree. It needs ordering only, not seeding: those agents share one checkout, so a unit that runs after its dependency simply finds the files there.

"If I close my lid, do my agents keep running?" With --mux vscode, a ship unit is a VS Code integrated terminal — a child of the extension host — so closing or reloading the window kills every in-flight agent. That was already true; what was new is that the tree now says so. Sentinels are files, so .started survived on disk, .idle never arrived, and a unit that died yesterday rendered as a cheerful spinner forever. The extension stamps activatedAt into host.json, and any unit dispatched before this host booted is now interrupted (orange debug-disconnect) with a tooltip saying why. A recorded verdict still wins — PASS is durable, liveness is not. Separately, ship-watch holds a caffeinate -i -w <pid> idle-sleep assertion for the length of the run (macOS; opt out with VZT_NO_CAFFEINATE=1), so walking away does not suspend a unit mid-turn. For a run that must outlive the editor entirely, use --mux herdr — its panes belong to a daemon, not to a window.

183 tests.

1.16.0 — review a diff in VS Code, and prove the agent got it

A herdr pane has no cursor, so every terminal review tool makes you retype path:line by hand to say which line you mean. VS Code's native diff editor and Comments API do it for free, and one command ships the whole batch back as a single agent.prompt. This is the differentiator; the tree view is table stakes — every rival plugin is read-only.

What the surface refuses to guess is the interesting part. The plan said to open "the agent's diff", and herdr cannot tell you what that is: measured on the live five-workspace fleet, every agent reports cwd and foreground_cwd as $HOME, because herdr's new_cwd is $HOME and the agent cds afterwards. WorkspaceInfo.worktree was unset on all five. So the diff is this window's repo — which VS Code knows exactly — and the agent is chosen explicitly. Nothing inferred, nothing inferred wrong.

  • agent.prompt's target is a PANE ID and nothing else. Probed read-only with agent.get, which takes the same target string: w5G:p1 resolves, while the workspace label, the workspace id and the terminal title each come back agent_not_found. pane_id travels end to end and no display name is ever resolved — a review landing in a different agent than the one you picked is the one failure this surface must not have.
  • Comments are grouped by file and ordered by line, never by click order. An agent handed comments in click order has to reconstruct the diff and will interleave two files while editing. Each comment carries its anchored source line, so "this" has a referent without re-reading the file.
  • A failed send keeps the comments. Losing a written review to a transport error is unforgivable, and a retry is one click away.

🔴 The review loop shipped broken, and every cheap probe passed. Measured on live herdr 0.7.5 against a real Claude Code agent: a single line submits (idle → working within 2s), while multi-line text lands in the composer as [Pasted text #1 +13 lines] and sits there — still idle 30s later, after a call that returned ok in 153ms. A review is always multi-line, so the surface reported "sent 2 comments", discarded the threads, and parked the review in an input box nobody was looking at. Fixed with an unconditional agent.send_keys enter after the prompt: polling for a status change is slower and races a fast agent, while a spare enter hits an empty composer and does nothing.

That bug is why this release also ships a live gate. npm run test:live stages its own throwaway project, splits a pane, starts a real agent, and runs two phases — a CONTROL that sends a bare agent.prompt with no enter and must stay stuck, then the real client which must leave idle within ~2s and be visible in the pane. The order is the point: a green FIXED phase alone is equally what a daemon that submits everything would give you, or an assertion reading the wrong field. It is deliberately not named *.test.mjs, so npm test cannot pick it up — an automated suite that silently needs a staged agent goes red for the wrong reason — and with no daemon it exits 2 / CANNOT RUN, never 0. The lesson generalises past herdr: a fake-daemon test asserts the bytes you wrote, not that anything happened. The bytes were correct in the broken version too.

Four traps found by making that gate stage its own agent, every one of them invisible from the daemon's side:

  • A pane read is a rendering, so its text is hard-wrapped at the pane width — a multi-word regex can straddle a newline and silently never match. A trust-dialog matcher written from how the dialog looks never fired, because the pane holds …you created or\none you trust?.
  • Readiness must be positive ("the composer is on screen"), never an absence ("no dialog is on screen"). A fresh agent draws a startup screen before its trust dialog, so an absence check passes in the gap. herdr reports idle throughout and agent.wait --until idle returns happily: from the daemon's side nothing is running, and a modal is just a program drawing characters.
  • pane.read over the socket needs source and answers result.read.text, both of which the CLI hides. Read a guessed field and you get undefined, which contains no marker — so the assertion passes while looking at nothing.
  • A freshly split pane is not yet at a shell prompt; agent.start answers agent_pane_busy, which reads as "in use" rather than "not ready yet".

The review body carries markers, never instructions. An earlier probe asked the agent to reply ACKNOWLEDGED as a delivery signal, and the agent refused — on the grounds that anyone able to file a review comment could otherwise steer the session. It is right: formatReview output is attacker-influenced whenever the review is not yours, and a gate must never depend on an agent choosing to obey injected text.

Also in this release: the two fleet defects the 1.15.0 notes claimed were pinned by tests, which did not exist — nothing under test/ referenced FleetModel, pane_created or pane_updated, so both fixes shipped unguarded while the suite stayed green at 113. Seven tests now run the real compiled model against a stubbed vscode, rather than grepping source: model.ts carries a long comment explaining each defect, so a source grep matches the explanation and passes on broken code. Suite is 131 and the reconnect half is covered too — the spin oracle runs in a child process on a hard deadline, because mutating the backoff to 0 starves the event loop and hangs the suite instead of failing it.

1.15.0 — a snapshot is a source, not the source

Adds a Herdr Fleet view to the vzt-mux extension: workspaces → tabs → agents, badged working / blocked / idle / done, with "N working · N blocked" in the status bar. Herdr stops being something you look at and becomes a daemon you query.

The extension is a CLIENT and must stay one. herdr owns every agent process; a VS Code window reloads on every extension update and dies with the app, so an agent parented to the extension host dies with it.

Four things everyone assumes about the herdr API, measured against a live 0.7.5 rather than read:

  • 89 methods, not 147.
  • agent.attach is CLI-only — there is no such socket method.
  • One request per connection. The server closes after answering, so a second write is EPIPE. Only events.subscribe holds the socket open.
  • pane_agent_status_changed cannot be subscribed globally; it requires a pane_id. The global pane.updated carries the whole PaneInfo including agent_status, and is the entire live-status mechanism.

Types are generated from herdr api schema --json on every compile, emitting HERDR_PROTOCOL; the client refuses to run against a daemon reporting a different number. That schema is not a JSON Schema — it is a container of five sibling schemas with refs rooted at the container, whose sub-schemas repeat 27 shared $defs — so the generator hoists, localises refs and collapses the repeats, asserting on every build that they are still structurally identical rather than trusting it.

Two defects found by measurement:

  • pane_created fires for a pane that ALREADY EXISTS with agent:null, and treating it like pane_updated silently drops a live agent from the tree until its next status change. Creation may no longer downgrade an agent already seen; pane_updated still may, or a finished agent lingers forever. The two are asserted to differ on purpose — closing the first defect by making both non-downgrading opens the second.
  • herdr's snapshot and its event stream disagree persistently. The stream pushed working twice while session.snapshot reported idle for 12s with no corrective event. The model follows the stream; the corrections are a re-seed on view visibility and an explicit Refresh, not a poll loop.

Nothing polls. The only timers under src/herdr/ are a 50ms redraw coalescer, a request timeout and reconnect backoff, and the view opens no socket at all until it first becomes visible.

1.14.0 — the visual lane: DESIGN.md is a taste cache

The protocol was blind to visual work. Eight agents, all tier-shaped; a grep for design system|ui|theme|brand|visual|css|tailwind across the whole repo matched nothing but two VS Code mux strings. So "restyle the dashboard", "fix the spacing" and "the palette is off" scored zero signals and fell into the default bucket — the same 48%-of-decisions hole the 1.9.1 audit found — where they got done from whatever the model imagined the product looked like.

This release adds the lane, and the interesting part is what decides its tier: a file, not the prompt.

  • templates/DESIGN.md — the artifact. Follows the awesome-design-md convention (YAML frontmatter a machine can parse, markdown an agent can read), with three sections that convention lacks: ## Token source of truth, ## Compliance, ## Known gaps. It also adds a variants: dimension the reference format has no room for — that frontmatter is flat because it describes marketing sites, and a flat file cannot express a scoped admin skin without silently flattening it into the global brand.
  • /vzt-ui, vzt-art-director (Opus, authors) and vzt-stylist (Sonnet, applies). An art director decides; a stylist executes the decision.
  • A two-factor gate in the classifier, structurally identical to HORIZON except the second factor is a statSync rather than a second regex. A real DESIGN.md routes visual work DOWN to Sonnet; its absence keeps taste work on Opus. The routing control is therefore a git-diffable artifact.

The doctrine: a DESIGN.md is a taste cache. It moves visual judgement off the model tier and onto disk — the same move /vzt-ship makes for long-horizon plans, and for the same reason. Escalate the PROCESS, not the MODEL; one axis over.

Three details that are load-bearing rather than decorative:

  • TASTE scores on Opus, SURFACE on Sonnet. The split is by who has to decide: "make it feel premium" needs someone to invent an answer; "fix the spacing" says what to change, not what it should become. Surface-on-Sonnet is the safety property — a visual false positive can never buy a costlier tier. Measured, not assumed: the existing longTrivial regression case contains "adjust the spacing" and asserts tier === 'sonnet'. Scoring SURFACE on Opus turns that test red on precisely the failure it exists to prevent.
  • A stub is not a cache. Under 400 bytes counts as absent. A placeholder would down-route every visual request in the repo forever while containing no taste to apply, and a cache that lies is worse than no cache.
  • ## Compliance ships a runnable oracle, baseline-and-ratchet. A design doc nothing enforces is decorative, and the audit that prompted this release found exactly that: repos carrying 300-line design systems and 1,197 raw Tailwind palette classes. The hex half of the check is filtered on purpose — var(--token, #hex) fallbacks are the correct defensive pattern and email templates genuinely cannot use CSS custom properties, so an unfiltered rule flags the best code in the repo and gets switched off within a week.

taste stays documentation. Deriving control flow from a column whose 10/9/8/4 values were assigned by feel would be a fake derivation, and it would let someone tidying a docs table silently re-tier the whole lane. TASTE_TIER is a named constant citing the column instead.

vzt-agent stats gains a visual (ui) line carrying the lane's own kill-switch, matching the one /vzt-ship already ships with: at ≥20 ui decisions, a cache-hit rate under 20% means nothing is reading a DESIGN.md, the lane is buying an Opus turn per prompt, and it should be deleted rather than defended.

Also fixed: AGENT_TYPES omitted the two new agents, so ship-check would have rejected any spec whose unit does UI work — the same drift that once hid vzt-architect. And the README claimed "Seven agents" while listing eight.

1.13.0 — queue records belong to a window

The last blocker on --mux vscode, and it was two bugs wearing one coat.

~/.vzt/vscode-mux/queue/ is one directory, but every open VS Code window runs its own extension host and every one of them polls it. Observed with 2 windows and 3 hosts:

  • Wrong-window routing — whichever host won the poll opened the terminal, possibly in a window you were not looking at, possibly running a different build. The likeliest explanation for identical runs behaving differently, and for a reload refreshing one host while an older one kept serving the queue.
  • Duplicate processing — claiming was readFileSync then unlinkSync, two steps, so two hosts could both read a record before either deleted it and both open a terminal for the same unit.

Now: the CLI stamps each record with workspaceRoot, and a host claims one only if that root matches an open workspace folder (containment either way, so a window opened on a subfolder or a parent still counts). The claim itself is an atomic rename — exactly one host wins, the loser gets ENOENT — which still matters when two windows legitimately have the same folder open.

Records without workspaceRoot come from an older CLI and are claimed by anyone: a version mismatch degrades to the old behaviour, not to a dead queue. If no open window owns the project, nobody claims it — the CLI now says exactly that instead of blaming the extension, and deletes the record rather than leaving it for some later, unrelated window to launch after the run has ended.

Proven with two competing stand-in hosts against one queue: the host owning an unrelated folder claimed nothing, the owning host claimed both units, 2/2 PASS and the integration gate merged. Extension 0.3.0 (the queue contract changed). 103 tests.

1.12.0 — Orca reaches parity, and a failed unit says why

Orca can finally skip permission prompts. worktree create --agent claude uses Orca's built-in launcher, which accepts no agent-specific flags — so orca was the one backend that could not pass --dangerously-skip-permissions, and an unsupervised unit hung on its first prompt. Orca is still the implicit default, so this was the untreated path most users would hit.

orca skills get orca-cli documents the fix and the trap. Dispatch is now the two-step Orca prescribes for a custom argv — worktree create without --agent, then terminal create --command '<full argv>' — and the agent handle comes from terminal create, never the worktree: a bare worktree create leaves a fallback shell as the first terminal, and waiting on that reports tui-idle instantly and grades the unit before it starts.

Orca gained the two-phase wait. It exposes no agent status states, but terminal read returns a monotonic latestCursor, and output is proof of life. Written from the documented CLI contract and not exercised end-to-end (no Orca runtime on the authoring machine), so it degrades on purpose: if a cursor cannot be read the phase is skipped and behaviour falls back to the single-phase wait that shipped before.

A failed unit now says why — on every backend. Orca can stream a running agent's output (terminal read); herdr cannot, and the VS Code extension API gives no read access to terminal contents at all. So a failing unit printed a bare FAIL and the diagnosis had to be rebuilt by hand — about an hour, to conclude "the agent never started". verifyAndRecord now prints the oracle command, the oracle's own output, the worktree, and the agent's Claude Code transcript path — or states that none exists, which is the diagnosis. Better than the capability it replaces: a transcript outlives the terminal.

A stale VS Code extension host is now visible. activate() stamps ~/.vzt/vscode-mux/host.json with the version actually loaded; doctor compares it to the installed manifest and says RELOAD THE WINDOW instead of green, and ship-watch warns before spending units on a host that will ignore the fix.

The chair follows a mid-session /model switch — corrected. 1.11.0 preferred settings.json; that was observed reading claude-fable-5[1m] while the session was demonstrably on Opus 5. Both file sources go stale in opposite directions, so the order is now payload → chair.json[sessionId] → settings.json. Every decision logs modelSource and payloadHadModel, so a wrong chair can be attributed instead of guessed at.

Not adopted, deliberately. Orca's orchestration (task DAGs, dispatch, decision gates, coordinator loops) covers the same ground as /vzt-ship, but the two optimize for different failures: /vzt-ship keeps coherence in a FILE that survives compaction and lets no builder grade itself — completion is an independent oracle — while Orca keeps coherence in the runtime and treats a worker's own worker_done as completion, with stronger provenance than our chair-written ledger. Neither subsumes the other.

Known open issue. ~/.vzt/vscode-mux/queue/ is global while extension hosts are per window, so with several windows open the hosts race to drain it — the terminal may open in a window you are not watching, running a different build. Scoping the queue per window is the fix.

101 tests.

1.11.0 — the VS Code mux grows a lifecycle, and the ledger learns to close

An audit of the protocol against its own live data. Every item below was reproduced before it was fixed.

Ship runs now terminate. ship-watch wrote only 3 of the 7 ledger line kinds its own reducer understands — no integration, no run_complete, no aborted. reduceLedger therefore reported every completed run as active forever, and the router hook re-injected a stale [VZT-SHIP] ACTIVE RUN block into every prompt in that repo with no TTL to clear it. All terminal lines are now written, including on the barrier-abort path.

A failing unit no longer reports as a pass. Two producers wrote unit_result in different dialects: the supervised path PASS/FAIL, the workflow path PASS/BLOCKED/ORACLE_FAIL/SCOPE_BREACH. FAIL matched nothing, so a run containing a failed unit told the rehydrating chair "all reported units passed" — a false green at exactly the moment, post-compaction, when the chair has no other source of truth. Both dialects now go through one exported FAILED_STATUSES set.

The integration gate stopped grading an empty tree. It ran against the primary checkout, which by construction contains none of the unit work — every unit lives in its own unmerged worktree. It now builds a temp worktree from HEAD and applies each passed unit's full divergence (committed, uncommitted and untracked — agents frequently do not commit) via a throwaway index, then runs the check there. Because ship-check enforces disjoint file scopes, a failed apply means a unit wrote outside its declared scope, and is reported as MERGE_CONFLICT.

The VS Code backend got herdr's state model. One hook script now serves three events — SessionStart→started, PermissionRequest→blocked, Stop→idle — and waitIdle is two-phase: wait for life, then wait for idle. Without a start signal a unit whose terminal never ran its command was indistinguishable from one still working, so it burned the entire unit budget before being graded against an empty worktree. Also fixed: sendText() into a still-initialising shell is silently discarded (now gated on shell integration, with a delay fallback), a .status rewrite was skipped on re-runs, and a throw during launch silently lost the unit.

A Ship Run tree (activity bar → VZT Ship): per-unit live status, focus its terminal, open its worktree diff while it is still being written, re-run its recorded oracle. Extension 0.2.0.

Routing fixes, each found in the live log (2,037 real decisions, 56% of them low-confidence):

  • The inspection family — audit, analyze, investigate, inspect — scored nothing. The audit prompt that found this could not be routed by the classifier it was auditing. Now opus:review.
  • security audit|review was an unconditional Fable escape hatch, bypassing the opus@max rung entirely (the demotion is gated to kind === 'plan'). Routine security review is Opus; a security hole stays Fable.
  • entire repo routed opus:horizon while whole repo routed sonnet:build — the scope nouns had diverged between the two alternations.
  • Length and brevity nudges overrode evidence instead of amplifying it: a long trivial prompt bought Opus on word count alone, and a sub-15-word prompt with a tied fable:debug signal lost to recon phrasing.
  • The chair follows a mid-session /model switch. chair.json was stamped only at SessionStart, so a switch never propagated — and because directive() branches on the chair's rank, a stale seat actively suppressed down-delegation on the most expensive tier.
  • vzt-agent stats de-duplicates double-routed prompts. A repo registering the classifier on top of the global one logged each prompt twice with divergent verdicts; the phantom rows pushed the Fable figure to 10.01% against a ≤10% target. Deduped: 9.9%. It also prints one decimal, so the gate stops contradicting itself at the boundary.

Install/guard fixes. install() now copies docs/ — the skills referenced docs/VSCODE.md by path and nothing ever installed it. Re-running install refreshes a managed hook whose command changed rather than leaving the stale one wired. The retired-model guard globs the whole doctrine surface instead of a hand-listed dozen (it was missing the two newest skills), and the doctrine-reference guard now covers docs/ and orca/.

Known gap, stated rather than papered over: orca units still cannot skip permission prompts — orca worktree create exposes no way to pass flags to the agent it launches. Orca also remains the implicit default, and the CLI now says so out loud when it falls through to it.

99 tests.

1.10.0 — Opus 5: the opus@max rung

Opus 5 launched, and the fleet had already moved — model: opus is an alias for the latest Opus (claude --help: "an alias for the latest model"), so every agent was running Opus 5 from launch day while the doctrine around it still described Opus 4.8. Nothing was broken; the guidance was just describing a model nobody was running — and in one case describing behavior Opus 5 had reversed.

  • New rung: haiku → sonnet → opus → opus@max → fable. Routine planning — architecture, tech specs, roadmaps, migration plans, PRD breakdown — no longer reaches the frontier tier. Opus 5 is a step change on deep reasoning at half Fable's cost ($5/$25 vs $10/$50), so that band now runs on Opus at max effort via the new vzt-architect agent and /vzt-design turn skill. Fable keeps what only Fable can do: planning with no prior art to reason from (novel, greenfield, from-scratch, or a one-way-door sharding / replication / consensus / multi-tenancy call) and impossible bugs. Fable budget ≤15% → ≤10%.
  • Implemented as a post-scoring demotion, not a signal rewrite. Every SIGNALS weight is untouched: a fable:plan win falls to opus:plan @ max unless FRONTIER_NOVEL matches. One block, reversible. The fable:debug band is deliberately not gated by it — an impossible bug is frontier work regardless of how ordinary the system it lives in sounds.
  • suggestEffort now emits max for opus+plan. That is the rung, and it is the only place the classifier suggests it — retiring the "never emits max" invariant 1.9.1 shipped.
  • Delegation cap — this reverses 1.9.x guidance, on purpose. Opus 4.8 under-reached for sub-agents, so the chair profile pushed parallel fan-out hard. Opus 5 reaches for them readily, and that guidance now compounds an existing bias into sub-agent sprawl. The Opus chair and vzt-heavy-builder now carry a ceiling: never delegate work finishable in a handful of tool calls, prefer one sub-agent over several, keep spawn counts low, and once you delegate, commit — never re-derive a worker's findings. Parallel waves are reframed from a default motion to genuinely independent tracks only.
  • Verification, scoped precisely. Opus 5 self-checks unprompted, so instructions telling it to verify buy nothing. The line matters and was drawn deliberately: verifying an external artifact — the oracle's output, a worker's diff on disk, observed behavior — is the whole point and is untouched, so fable-mode Gate 4, MACHINE_CHECK, "reporting ≠ persistence", and /vzt-ship's independent verification stage all survive unchanged. What's cut is self-verification: don't spawn a sub-agent to double-check your own inline work, and don't pad turns with re-verification passes.
  • Scope and concision clause on the Opus chair and in templates/worker-brief.md — Opus 5 expands scope and writes longer by default. Deliver the scope asked, no unrequested refactors or abstractions, lead with the outcome.
  • /fast is now priced in the doctrine. On Opus 5 fast mode bills $10/$50 per MTok — Fable-tier price for Opus-tier intelligence. Still the right lever when wall-clock beats token cost; explicitly a bad default.
  • Effort guidance re-tuned. "Fable-low ≈ Opus-high" was an Opus 4.8-era equivalence and is gone. Start xhigh for coding/agentic and high elsewhere, then sweep down — Opus 5 is unusually strong at low/medium, so effort defaults inherited from earlier models over-spend.
  • Anti-staleness guard (the fix for the root cause). Two new tests assert that every TIERS label's model name appears across ROUTING-MATRIX / vzt-route / README / CHAIR-PROFILES, and that no retired version string survives outside this release-notes section. The next model launch now fails a command instead of quietly rotting a dozen files. Add a row to the test's RETIRED table when a model is superseded.
  • 79 tests green (was 72). 8 agents, 9 skills.

1.9.1 — routing gap-fill (audit follow-through)

  • xhigh for hard Opus builds. The per-prompt classifier now suggests effort: xhigh for a high-confidence, multi-signal Opus build (a clearly-hard refactor + performance/concurrency + complexity task) — the current Claude Code default for hard coding/agentic, and what the vzt-heavy-builder it delegates to already runs at, so the inline suggestion matches the delegate. Single-signal Opus builds stay high; Opus review and horizon-supervision stay high; the classifier still never emits max.
  • /fast lever in the Opus chair profile. The Opus chair is explicitly wall-clock constrained, and /fast (Opus 4.8, ~2.5× output at premium tokens) is the direct lever — now named in the doctrine.
  • --mux vscode doctrine parity. The native VS Code backend shipped in 1.9.0 but the doctrine still read "two backends / orca|herdr"; threaded vscode through the Opus long-horizon profile and the vzt-route / vzt-ship skills.

1.9.0 — native --mux vscode + model-currency audit

  • Native VS Code multiplexer — a third --mux backend alongside orca/herdr. vzt-agent ship-watch <SPEC> --mux vscode opens each ship unit as its own native VS Code integrated terminal in its own git worktree, via a filesystem queue drained by the companion extension in vscode/ and a Stop-event idle sentinel hook. No external multiplexer binary. See docs/VSCODE.md.
  • Model-currency audit — verified the lineup is current (Fable 5 / Opus 4.8 / Sonnet 5 / Haiku 4.5; model: fable confirmed a valid Claude Code alias). Corrected the TIERS cost multipliers to today's sticker ratios (Fable 25×→10×, Opus 15×→5×; Sonnet/Haiku unchanged), so the printed "× cost saving" figures are honest. Adopted xhigh for the Opus vzt-heavy-builder — now the Claude Code default for hard coding/agentic work — while keeping the "no xhigh on routine work" guardrail.

1.8.0 — Herdr-supervised runs are the DEFAULT substrate for /vzt-ship

  • The /vzt-ship skill now drives supervised runs in a live agent multiplexer by default — when vzt-agent is on PATH and a mux is live, run vzt-agent ship-watch <SPEC.md> so each unit is a real claude agent in a watchable/attachable worktree pane (Herdr via VZT_MUX=herdr; omit --mux). The headless Workflow tool becomes the fallback (no mux live / vzt-agent off PATH) — resumable but not watchable; the skill says which driver it used. This ships in the files install() copies (skills/vzt-ship/SKILL.md + hooks/vzt-session-start.mjs), so every project pulls the default in on install — no per-repo CLAUDE.md edit needed.
  • ship-watch still STOPS at the green integration gate; never auto-merges.
  • Test isolation fix: the ship-dispatch default-mux test now clears VZT_MUX from the child env before asserting the code default (orca), and separately asserts that VZT_MUX=herdr makes herdr the default with no --mux. Prevents a machine-wide export VZT_MUX=herdr from turning the suite red. 70/70 tests.

1.7.0 — Herdr backend (agent-multiplexer supervision, --mux)

  • The supervision layer is now multiplexer-agnostic via --mux orca|herdr (default orca). Added Herdr (herdr.dev) — a terminal-native agent multiplexer (a binary, persistent over SSH/mobile) — as a second backend. ship-watch/ship-dispatch/ship-supervise all take --mux.
  • Refactored the Orca-specific code into a 5-method backend interface (dispatch / waitIdle / resolve / stamp / plan). The Orca path is unchanged; worktree-bootstrap.sh and the primary-checkout ledger resolution are already mux-agnostic.
  • Herdr prerequisites (one-time): brew install herdr, brew services start herdr, herdr integration install claude (so claude reports state → herdr agent wait). Verified live: worktree resolution, oracle-in-worktree, and workspace-label stamping (orca card / herdr workspace label). 68/68 tests green.

1.6.0 — Orca supervision layer

  • Orca accepted as a /vzt-ship supervision layer (not fan-out — that stays rejected; these units are pairwise-disjoint, not a race). The terminal-native protocol is unchanged; Orca just runs claude, so routing/hooks/subagents/skills inherit as-is. Reserve Orca for parallel ship runs.
  • New CLI: ship-watch (kick once — dispatch every unit as an Orca claude pane → wait for each → auto oracle + card + ledger → integration gate → stop at "ready to review + merge"; never auto-merges), ship-dispatch (the commands, dry-run or --execute), ship-supervise (verify each oracle → shared ledger
    • Orca card). Installed to ~/.orca/vzt/ alongside orca/worktree-bootstrap.sh.
  • worktree-bootstrap.sh symlinks node_modules/.env* from the primary checkout into each worktree — closing the "a worktree can't build" objection for the supervised case.
  • Ledger coherence fix: ship-note/ship-status now resolve LEDGER.jsonl to the primary checkout (git worktree list first entry). .vzt/ship/ is git-tracked, so worktrees used to fork their own ledger — worker writes were lost and branches conflicted. Byte-identical in a plain checkout.
  • See orca/README.md and skills/vzt-route/SKILL.md → "Accepted — Orca as the ship SUPERVISION layer".

1.5.0 — long-horizon release

  • Doctrine shift — "Escalate the PROCESS, not the MODEL." Scope language ("entire codebase", "from scratch", "greenfield", "end-to-end", "multi-tenant") no longer routes to Fable. It now routes to Opus under a new HORIZON task kind, gated two-factor: scope language alone stays a planning question on Fable; scope + a build verb becomes HORIZON and points at /vzt-ship.
  • New skill /vzt-ship — spec-first long-horizon execution. SPEC (no code) → GATE (vzt-agent ship-check, a command not an opinion) → RUN (barrier → parallel units → independent oracle verification → bounded repair → integration gate, via the Workflow tool) → LAND (verify artifacts on disk, stop before commit/deploy). Survives compaction: SPEC.md + LEDGER.jsonl on disk, and the classifier hook re-injects a [VZT-SHIP] block on every prompt since compaction doesn't re-fire SessionStart.
  • New CLI commands: ship-check, ship-start, ship-note, ship-status.
  • See Long-horizon work above.

1.3.0 — 2026-07-08

  • Fable discipline is now always on at the Opus tier. The five fable-mode gates (scope, evidence, attack, verify, report) are wired into every Opus surface: vzt-reviewer now carries them (previously only the builders did), the Opus chair profile injects them at session start, and Opus-targeted [VZT-ROUTE] directives restate them. Model routing is unchanged — Opus runs on Opus 4.8, but always with the frontier tier's working process.
  • New sync test asserting every Opus surface (both agents, the chair profile, the classifier directive) carries the gates.
  • README now documents all five gates in full (The process is the moat — the five Fable layers) and leads the quick start with the zero-clone npx github: install.
  • README cleanup: removed the third-party comparison section.

1.2.0 — 2026-07-08

  • Added worker-brief delegation doctrine: templates/worker-brief.md is the canonical template (TASK/CONTEXT/FILES_IN_SCOPE/OPERATION/ACCEPTANCE/ MACHINE_CHECK/EXPECT/CONSTRAINTS/REPORT).
  • Collision boundary: FILES_IN_SCOPE in a brief is a hard write boundary — vzt-builder, vzt-mechanic, and vzt-heavy-builder now all STOP and report rather than expanding scope if the task needs a file outside it.
  • Machine-checkable acceptance: vzt-planner's step-routing table now carries a machine_check command per step, chosen at plan time, not invented by the worker after the fact.
  • Reporting ≠ persistence: the SessionStart hook's Fable/Opus chair profiles and skills/vzt-route/SKILL.md now tell the orchestrator to verify worker artifacts on disk (git diff, re-run the check) before accepting a completion report.
  • Added a sync test asserting the template, the three worker agents, and the routing skill all carry the new contract.

1.1.0 — 2026-07-07

  • Added the vzt-fable-mode skill: the frontier tier's five working gates (scope, evidence, attack, verify, report) extracted into a portable process any tier can run — cited as a new rule 1 in vzt-builder, vzt-heavy-builder, and vzt-mechanic.
  • Effort is now a routing dimension: the classifier computes a suggested effort (suggestEffort()) per prompt and surfaces it in every [VZT-ROUTE] directive (@ effort low|medium|high), alongside an effort note: xhigh for hard coding/agentic (where the heavy-builder runs), not for routine work.
  • Added Cost/Intelligence/Taste columns to TIERS in the classifier hook, with matching columns in docs/ROUTING-MATRIX.md and skills/vzt-route/SKILL.md — a sync test now enforces the cost values match across all three.
  • Added orchestrator doctrine (frontier designs and verifies; Sonnet/Haiku execute and report back) to vzt-planner, skills/vzt-route/SKILL.md, the SessionStart hook's Fable/Opus profiles, and the CLAUDE.md snippet.

License

MIT © VZT Tech Consulting

↑↓ navigate openesc close