Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog 2.0.0 and the project uses Semantic Versioning; on 0.y.z anything may change.
Unreleased
Fixed
- The product website navigation follows the section being read, keeps the active sidebar link visible, and labels its portfolio breadcrumb Home.
1.0.2 - 2026-09-28
Fixed
- A refused symlinked or changing file (such as
MANIFEST.json) now names its path and the fix: replace the link with the file it points to. Since 1.0.1, rerunning the installer over a symlinked manifest stops instead of treating it as a fresh install. - A path shown in a refusal escapes control characters, so a crafted file name cannot write terminal escape sequences; a directory or other non-file gets "expected a regular file" without symlink advice.
- The acceptance-check protocol states the timeout's reach: it stops the command's process group, and a descendant that starts its own session (for example with
setsid) keeps running.
Added
- Product website at
model-orchestrator.aunysillyme.dev, with repository-generated documentation, changelog, and the model-orchestrator trailer. Main-branch pushes rebuild the site through Vercel; the published version is checked against npm.
1.0.1 - 2026-09-27
Security
aunx route-metricsalways runs the packaged metrics implementation. A checkout can no longer substitute its own JavaScript when a user requests a summary.- Installer and uninstall manifests use bounded regular-file reads with symlink refusal. Acceptance-check and routing JSON reads share the same bounded reader.
- Acceptance-check timeouts and interrupts clean up the command's process group or Windows process tree, including ordinary descendants.
- Weekly gateway probes pass credentials through stdin instead of temporary files, reject multiline keys, and remove gateway/provider credentials from audit subprocesses. The service now reads a separate
weekly-audit.env; see the generated jobs README when upgrading. - Newly generated companion launch commands use the catalog's exact versions, and npm publication uses a reviewed exact npm version. Existing client entries are preserved; follow the companion upgrade steps.
Fixed
- Security and installation documentation now match current activation rules, model inheritance, companion setup and credential boundaries.
1.0.0 - 2026-09-27
Added
- The
aunxcommand. The existing package now ships a short command alongsidemodel-orchestrator. Run the installer withaunx, call an AI throughaunx cli-run, scaffold a task brief and context file, execute acceptance checks, get a routing suggestion or summarize your local routing activity. - A build process from requirements to verified use. Shared context, executable acceptance checks, live capability probes, scoped assignments, background heartbeat guidance, split-build ownership and one independent audit step with a companion reviewer.
- Reproducible proof. Measurement scripts, dated results with method and sample size, a generated proof page and a weekly refresh workflow. The test suite rejects expired figures.
Changed
- Automatic project activation: interactive installs apply the main agent's catalog-supported rules block and Claude Code hooks under the existing single confirmation, with backups. The edit screen and
--no-applykeep activation manual; headless--yesrequires--apply-snippetsas before. - Remaining actions: every non-dry install runs the local CLI presence check automatically. "What's left for you" omits completed activation and informational items, checks Codex's reliable sign-in status, and makes other sign-in instructions conditional. Live canaries remain opt-in.
- Companion registration and cleanup: selected companions register in supported project MCP config when activation is enabled; global configuration stays manual. Activation ownership lets uninstall remove unchanged applied blocks and added hooks or MCP entries while preserving unrelated content and backups.
- Stack-dependent assignment: planning, building, review, verification, research, bulk work, reading and private work are assigned from selected capability facts. Generated stack tables explain each choice; the manifest stores the current assignment and
aunx routereads it. Independent review requires a known different model family, and private work requires local execution. - Model tiers: generated agent definitions use planning, working and cheap model tiers. Current plan-to-model mappings are unverified, so definitions leave the model to the user's tool configuration while preserving effort guidance.
- One-confirm installation: interactive setup detects available AI tools and shows a proposed level, main agent, paths and role table before one confirmation. The edit menu changes individual settings; an empty detection asks for the user's AIs first. Headless
--yesstill requires explicit level and AI selection. - Capability catalog: AI entries carry
summaryandfacts; role claims and duplicated capability fields are retired. The generated catalog marks unknown facts and inherited vendor claims as unverified. - Companions are opt-in. Default and
--yesinstalls select none. Missing tools appear together under "Install these yourself", with their official commands and links. - Public terms match the work. Task brief, context file, acceptance checks, subscription lanes, pay-per-token lanes, planning model, working model and cheap model.
- Documentation starts with the model router. New command examples, search-phrased questions, a trailer hero, an install walkthrough and a concise security-review history with regression evidence.
- A smaller publication surface. Development-only
scripts/and the old raw audit brief are excluded; reproducible proof scripts remain available.
Fixed
- Coherent level installs: routing uses installed agents or available role fallbacks, review guidance accounts for the main agent's model family, smoke paths are quoted, and README first reads and output-contract checks match the level and selected workers.
- Level 3 setup: initialize the configured local model inside Compose, verify
local-small, and configure the scheduled service's executable search path. - Accurate level descriptions: qualify activation and agent sets by main agent, keep companions opt-in, and describe the weekly job's fixed lane, observed data and configurable budget and privacy policies.
- Post-install review round: oversized settings and MCP JSON files are refused before any write, the rules block keeps a CRLF file's line endings, Claude Code sign-in is hidden only when its status reports signed in, Ollama install text appears only when it is absent and names the configured model, CLI main agents without a rules file get accurate wording, and a new rules file is labelled "create".
Removed
- Automatic vendor installation. The installer never runs third-party installs.
--no-installremains accepted for existing scripts.
Upgrading from 0.1.x
- Keep your existing
--dirand--project. Run with--update-docs --dryfirst, then remove--dryto apply. - Unedited
TASK_BUNDLE.mdmigrates toTASK_BRIEF.md; an edited legacy brief is kept and named for manual migration. - Unchanged runtime files upgrade automatically.
--upgrade-runtimereplaces runtime files only and preserves document edits. - Existing codecalc guides and snippets stay managed; uninstall removes them only when unedited. New installs select companions explicitly with
--tools. - Every previous installer flag remains accepted. Runner exit codes, logs, hook contracts and safe uninstall behavior remain compatible.
0.1.35 - 2026-09-25
Changed
- The description names all three things the installer writes. npm, the GitHub About box and
package.jsonnow read: rules, subagents and a lane runner tell agents which model to use. The README opening already listed the lane runner; the description stopped at rules and subagents. - Every GitHub topic is an npm keyword.
coding-agentswas a topic with no matching keyword.
0.1.34 - 2026-09-24
Changed
- The package description says what it installs and who picks the model. npm and the GitHub About box now read: rules and subagents tell your agent which model does each task, matching the README opening; the old line read as if the package itself sent work to a tier.
0.1.33 - 2026-09-24
Added
- Opt-in snippet application.
--apply-snippetsapplies a replaceable Claude Code rules block and merges hooks with timestamped backups, validation before writes, dry-run previews and manual uninstall guidance. (#41) - Model orchestrator or a model proxy. README and
llms.txtexplain the request layer, when to pick each approach and how they compose. (#44)
Fixed
- Selected lane guidance. Delegation rows and related cost and research advice render from selected catalog lane categories, keeping absent categories out of active picks. (#42)
- Rules folder relocation. Project-contained snippets use relative paths; external rules paths carry relocation guidance. Installer hooks that read rules honour
MODEL_ORCHESTRATOR_RULES_DIR, and the generated README names the path case. (#43)
0.1.32 - 2026-09-23
Changed
- README shows what the install gives you, not a file dump. The 50-line dry-run file list under the demo became a six-row table naming each part and what it does for you; the preview command stays, and the full file list is one link away in
docs/install.md.
0.1.31 - 2026-09-23
Added
- Manifest-based uninstall.
--uninstall --dir <dir> --project <project>removes managed files whose content matches the recorded hash, keeps and names edited files, and preserves files outside the manifest.--dryand--dry-runpreview the same removals. The manifest is the last file removed and stays when edits remain. New installs record the directories they create so uninstall can remove them when empty; older manifests leave directories in place. The command prints the manual steps for removing pasted rules and merged hooks. A missing manifest exits 2 and names its expected path. - Activation and related tools in the README. A captured install supplies the rules, hooks and smoke-test steps. A shared table links agent-personalizer and website-build-skill, and a short uninstall section links the full instructions.
Changed
- Presentation leads with the task and payoff. The opening states that the rules tell your agent which model handles each task, followed by the problem, the setup and the routing log. README sections follow the install flow, with platform and vendor details kept in collapsed sections. Contributing names new catalog entries, vendor fixtures and documentation fixes as welcome contributions.
- Short package description and consistent names. The description fits the search-card budget while preserving the purpose clause. The opening and agent summary name Antigravity (Google).
llms.txtkeeps reference links together and gives plans and automatic effort their own section. - Positive section headings. The plugin README names what the hooks do and what npx installs; codecalc explains where it fits; the pull request template names work kept for a later change. Safety guarantees retain their explicit wording.
Fixed
- Setup output matches the generated files. The plan counts subagents and hooks separately from its file list, and the README contains captured dry-run output with project-relative paths.
--listprints each AI's catalog install instructions and sign-in note. The README describes how reruns preserve documents, rewrite machine-owned configuration and upgrade untouched runtime files, and its routing link points to the detailed document.
Security
- Uninstall validates the entire manifest before removing files. Absolute paths, traversal, symlinked entries, malformed hashes and mismatched target roots are refused. Foreign files in shared project folders stay. A rerun targeting another project carries previous ownership records only for roots that still match. Regression tests cover these refusals, edit preservation, directory ownership, changed projects, legacy manifests, previews and missing manifests.
0.1.30 - 2026-09-22
Fixed
- Every hook count now matches the files on disk (#35). The generated
CLAUDE.snippet.mdsaid "Two hooks were written" and named onlyroute-gate.mjsandsubagent-context.mjs, while a claude-code install writes and wiresroute-metrics.mjstoo. It now names all three and how to read the metrics log. The same pass correctsllms.txtanddocs/install.md(three hooks on a full install),templates/agents/README.md(listsroute-metrics.mjs), and the README plugin section, which said the plugin ships three hooks: it ships the two read-only ones, androute-metricscomes only with the npm install. A new test fails if the snippet names fewer hooks than the plan writes.
0.1.29 - 2026-09-21
Changed
- The seven principles state what to do, rather than what fails. "A gate you cannot fail is not a gate" became "Every gate can come back wrong. A checkpoint earns its place by being answerable both ways"; "Exit 0 is not a deliverable" became "Check for the artifact. A deliverable is a file, a commit or a line you can point at"; "A write nobody can find again did not happen" became "A write stays findable". Same rules, same gates, stated forward. This finishes the copy pass 0.1.28 started, which left the principle list untouched.
0.1.28 - 2026-09-21
Added
- A recording of the command doing its job, at the top of the README.
docs/demo.gif(60 KB) showsnpx model-orchestrator ... --drytyped and run: the level, the AIs, both target folders, all 38 files it would write, and the closing line that nothing was written.scripts/record-demo.shre-records it from the PUBLISHED package inside a temp folder, so the frames stay the program's own output rather than a staged screen, and anyone can reproduce them withbrew install asciinema agg. The text plan stays in the README under the image, and the image carries alt text describing what it prints, so the page still reads with images off. --summarynow answers the question the package exists for: how much work left the main session.work sent off the main sessioncounts covered turns whose route marker named any lane that is not an inline name, over covered turns. It is derived only from lane names the log already holds, with no price table and no token estimate, because the log holds neither.--inline a,brenames what counts as inline, since the lane vocabulary belongs to your ownROUTING.md. A log with no lane markers yet says so instead of printing a number.
Changed
- Every line of user-facing copy states what the package is and does. The opening bullet was "What it is not: a proxy, a gateway or an API router", which told a new reader what to stop expecting before they knew what they were looking at. It now reads "Where it sits: above the request layer. Your agent reads the rules and picks the lane", and request-level routers are described as composing underneath rather than as the thing this is not. Same for the plugin section, the companion-tool intro and the gateway question in Common Questions.
test/copy.test.jskeeps the boundary it was written to protect: the OVERCLAIM guard is unchanged, and two positive phrases are now required in both README andllms.txt, so the claim cannot quietly widen and the copy cannot quietly lose it.
0.1.27 - 2026-09-21
Changed
- README restructured for a first-time reader: 32,083 bytes to 17,586, with the first screen now a claim, the install command and a real
--dryplan. The old opening was a 200-word paragraph followed by "What it is not", the flag-conflict rules and the uninstall procedure, all before the reader had seen the tool do anything. The detail moved rather than went away:docs/install.md(every flag, the two target folders, headless examples, the full file list),docs/how-it-routes.md(role, complexity and stakes; the three verifier agents; pinning model and effort),docs/guarantees.md(enforced by code, delegated to a vendor flag, or only an instruction),docs/companions.md(the full companion-tool table).docs/README.mdandllms.txtindex all four. Platform support and the vendor compatibility table stay in the README inside collapsed<details>blocks, becausescripts/gen-catalog.jswrites the vendor table between markers there andtest/prose.test.jsrequires the skipped-test explanations to live in the README; collapsing them keeps both mechanisms pointed at the same file. The opening line still satisfiestest/copy.test.js: it names the package, carries the shared purpose clause, and keeps the 0.1.11 correction that it does not select models itself.
0.1.26 - 2026-09-19
Added
- obsidian-tc on Windows: the answer is that no client needs a
cmd /csnippet, andOBSIDIAN-TC.mdnow says so with the code it was read from. All five obsidian-tc snippets spawn"command": "npx", and on Windowsnpxisnpx.cmd, which a bareCreateProcesswill not start. Each client was read instead of assumed, and all five resolve it themselves: Zed hands the command to the system shell (crates/context_server/src/transport/stdio_transport.rs,ShellBuilder::new(&Shell::System, ..), from its PR #42382, 2025-12-10), VS Code'sformatSubprocessArgumentsinsrc/vs/workbench/api/node/extHostMcpNode.tsresolves the executable and re-spawns withshell: truefor a.bator.cmd, the Codex CLI callswhich::which_inincodex-rs/rmcp-client/src/program_resolver.rs, and Cursor and Antigravity'sagyboth reachnpxthrough@modelcontextprotocol/sdk, whoseclient/stdio.jsimportscross-spawnand re-invokes a non-.exeas%COMSPEC% /d /s /c. That last point is wider than those two clients: every published SDK from 1.23.0 through 1.30.0 depends oncross-spawn ^7.0.5, soshell: falsein that transport is not the whole story. The doc also names the one Windows case that can still fail and is not about.cmd: Zed prefers PowerShell, which resolves a barenpxto npm'snpx.ps1shim, and a.ps1will not run under theRestrictedexecution policy that is Windows' client default. A test pins the snippet set, fails if any.windowsfile appears, and fails if the doc drops a citation. Not run on a Windows machine.
Fixed
- Context7's Windows guidance said something untrue about Zed, and its
cmdwrapper was missing/d(#34 follow-up). 0.1.25 shippedmcp/context7.zed.windows.settings.jsonon the premise that Zed spawns"command": "npx"directly and so cannot start it. Reading Zed's own source for the obsidian-tc work above showed it has launched MCP stdio servers through the system shell since PR #42382 (2025-12-10), so a current Zed starts the plain snippet. The file stays, because it is still the fix for an older Zed and for the PowerShell execution-policy case, butCONTEXT7.mdno longer sends every Windows user to it and now carries both reasons. The snippet's args change from["/c", "npx", ...]to["/d", "/c", "npx", ...]: without/d,cmdfirst runs whatever sits in the Command ProcessorAutoRunregistry value, which can print non-JSON into the protocol stream.cross-spawnpasses/d /s /cfor the same reason. The#34test follows the new args and now also fails if the doc stops naming why a current Zed does not need the file.
0.1.25 - 2026-09-19
Fixed
- Context7 on Windows: a Zed snippet that can start the local server (#34).
mcp/context7.zed.settings.jsonspawns"command": "npx", and on Windowsnpxisnpx.cmd, a batch file a client cannot spawn as a bare command. Newmcp/context7.zed.windows.settings.jsonruns"command": "cmd"with"args": ["/c", "npx", "-y", "@upstash/context7-mcp@<pin>"], the shape Context7's own client guide ships for Windows.CONTEXT7.mdgains a "Localnpxon Windows" table saying which Zed snippet to use on which OS, and the same by-hand change for any other client taking the local alternative. The remote snippets spawn nothing and are unchanged. A test pins the Windows args to the POSIX args behind/c npxand the catalog pin; not yet run on a Windows machine.
0.1.24 - 2026-09-18
Added
- Context7 as a third companion tool, paired with codecalc. Context7 (Upstash) hands the agent current, version-specific documentation and code examples for any library, SDK, API or CLI, hosted or run locally with
npx. It answers what a library is documented to do; codecalc still answers what the code actually does by running it. A new protocol,protocols/docs-then-prove.md, states the rule the pairing serves: pull current docs before writing against anything unconfirmed this session, then a run proves it, and where a doc and a run disagree the run wins. Optional and off by default, like obsidian-tc (--tools context7, or the interactive question); unlike the other two companions it always makes a network call, so it is the one to skip offline.src/catalog.jscarries the entry (repo, role, install, requirements, the clients it self-registers with);CONTEXT7_STATUSrenders both selected and not-selected wording the same wayCODECALC_STATUSandOBSIDIAN_TC_STATUSalready do, at every level and inROUTING.md. templates/tools/context7/CONTEXT7.mdplus seven per-clientmcp/snippets, each read from that client's own docs rather than shared across clients that do not actually share a config shape:context7.claude-code.mcp.json("type": "http"next tourl, required or Claude Code skips the server),context7.mcpServers.json(Cursor's own documented shape,urlwith notype),context7.vscode.mcp.json,context7.qwen.settings.json(httpUrl, noturl, plus the non-credentialAcceptheader upstream ships),context7.zed.settings.json(localnpx, pinned to the catalog'scontext7version),context7.codex.config.toml,context7.agy.mcp_config.json. Claude Desktop has no file: its remote connection is a UI step (Settings > Connectors > Add Custom Connector), documented inCONTEXT7.mdinstead of a snippet nothing there reads.- Every context7 snippet ships keyless by default. The anonymous tier works with no header, while an unexpanded or empty
Bearer ${CONTEXT7_API_KEY}makes every call return "Invalid API key" (probed live againsthttps://mcp.context7.com/mcp), and clients like Codexhttp_headersnever expand it.CONTEXT7.mdhas a "Higher rate limits (optional key)" section with one mechanism per client: Codexbearer_token_env_var, Claude Code${VAR}expansion in.mcp.jsonheaders, Claude Desktop's own Connectors key field, an exported shell variable for localnpx, and "check your client's docs" where expansion is not confirmed; the fallback for a client that does not pass its environment to a spawned child is to stay anonymous, never to paste the key into a snippet's args. A test fails if a shipped snippet carriesAuthorizationorCONTEXT7_API_KEY.
0.1.23 - 2026-09-15
Added
cli-runsays WHY a lane failed. Every run lands in one class with its own exit code:auth14,quota15,rejected16,refused17,cut_short18, next to the existingempty10,no_output11,timeout12 andunavailable13. A missing API key, a spent quota and an unknown model id used to share exit 10 or the vendor's own code, and each needs a different response: a missing key is not a model fault, and retrying a spent quota cannot help. Signals are read from each lane's authoritative error fields only, never the model's prose, with precedence auth, quota, rejected, refused, cut_short, empty.refused=Non every run. Tool calls a hook or deny rule blocked, counted from qwen'spermission_denials, agy's deny-rule steps, codex's routerRejected(lines, and grok's session transcript (itssessionIdis charset-checked, the path is contained to the sessions root, lines must name the same session, and the read is capped at 5 MiB).nullwhen a lane gives no signal. A deliverable with refused calls is still exit 0.- A problem line and a fix line on the terminal for every failure, and for an
okrun with refused calls, so a calling agent can relay "this is what went wrong, this is the fix" and ask. - Terminal output is redacted before it prints: JSON credential keys,
Authorization:values, bearer values, URL query credentials, and common key prefixes, redacted before any clipping. - The durable log gains
classandrefused. Both are fixed values; the log still never holds provider text, the problem line or stderr.
Changed
- A nonzero vendor exit is no longer passed through as cli-run's exit code. The class owns the code, and the vendor's own code stays in the log as
cli_rc. A nonzero exit nothing else explains iscut_short(18), and it is still neverok. If a script comparedcli-run's exit code with a specific vendor code, comparecli_rcin the log instead;!= 0checks are unaffected. - A lane killed by a signal, or output past the 16 MiB buffer, is
cut_short(18), not 10. Exit 10 now means only an empty run or an unmet--expect-*contract. - agy's judge refuses a non-object terminal
resultasbad_last_event(wasbad_status), and qwen's judge refuses a non-stringerror.messageaserror_message_not_string, so both classify ascut_short. hermes' stderr cause (degraded free tier, bad--toolsets) is now named in its detail line.
0.1.22 - 2026-09-12
Added
- SuperGrok Plus as a
grokplan (high headroom). xAI's pricing page lists it with "Significantly higher usage across Chat, Imagine, Voice & Build", so--plans grok=supergrok-plusnow works and counts as a high-headroom lane for plan guidance and--effort-auto. SuperGrok Heavy stays out: the page states no Build usage for it. A test holds both.
0.1.21 - 2026-09-12
Added
- Subscription plans, stated by you and never guessed. The installer asks which plan you hold for Claude Code, Codex,
agyandgrok(interactive, or--plans codex=pro-20x,agy=ultra-5x;--plans noneclears). Each plan row insrc/catalog.jscarries only a name, a headroom level (base,high,max), its official source page and the date it was checked; no prices and no model ids, because both change faster than releases.--listprints them. - Plan guidance in the generated docs. The lanes table gains a
Plancolumn, andROUTING.mdandDELEGATION_MATRIX.mdgain a plan guidance block: high and max headroom lanes take volume (scoped builds, pre-ship second-family audits, first-pass research), base headroom lanes keep short second opinions. Headroom moves volume only; who reviews what does not change. --effort autoincli-run. Accepted as a flag or as alanes.jsondefault on every lane with an effort flag (qwen still refuses it). A call sizes from prompt length: under 4,000 characters ismedium, otherwisehigh. A codex--auditalways runs athighand logs the larger of prompt length and changed lines as evidence. Auto never resolves abovehigh:xhigh,maxandultraare sent only when named. The durable log gainseffort_resolved,effort_basis(explicit,prompt_chars,audit_floor,none),effort_scopeandeffort_truncated.- Bounded change counting for audits. Untracked files are listed NUL-delimited, only regular files are opened (FIFOs, devices and symlinks are skipped by
lstat), and the pass stops at 200 files, 256 KiB per file, 2 MiB total or 2 seconds, marking the evidence truncated. An unbornHEADor any git failure falls back to prompt length.
Changed
- Automatic effort is opt-in. A high or max headroom plan does not change
bin/lanes.jsonby itself;--effort-auto(or yes to the question) writes"effort": "auto"for exactly those cli-run lanes. Claude Code is never written there, since it is not a cli-run lane and an entry for it would fail the whole file closed. With no plan stated,bin/lanes.jsonis byte-identical to 0.1.20 andMANIFEST.jsonrecords no plan keys. A re-run that omits--planskeeps the previous plans. - Five documented Windows skips, not four. The FIFO half of the new untracked-file test needs
mkfifo; its symlink half runs everywhere.
0.1.20 - 2026-09-12
Added
- A Claude Code plugin.
/plugin marketplace add aunysillyme/model-orchestrator, then/plugin install model-orchestrator@model-orchestrator, installsroute-gate.mjs,subagent-context.mjsand the eight subagents without merging a settings snippet by hand. The bundle lives inplugin/, listed by.claude-plugin/marketplace.jsonat the repo root. It passesclaude plugin validate --strict, the check Anthropic's community marketplace review runs, and all eight checks of Sigistry's public plugin verification methodology (1.2), run standalone before release as a quality bar; the plugin is not listed there. - The plugin is generated, never a second copy.
npm run gen:pluginrendersplugin/from the sametemplates/the installer uses, andtest/plugin.test.jsfails when the committed bundle drifts from that, whenplugin.json's version is notpackage.json's, whenhooks/hooks.jsonreferences a hook that is not shipped, when a plugin hook gains a network call, a file write, credential access, dynamic evaluation or a subprocess, when an agent has notools:line or a review-type agent carries Write or Edit, and when the plugin README loses its install commands. Each check was proved red against the real files before it was trusted. - The plugin's route gate works without an install step. A plugin cannot be rendered per project, so its
route-gate.mjsreads the installer's default locations,ai-orchestrator/ROUTING.mdthenai-orchestrator/ORCHESTRATOR.md, and takes the first that exists. Something at the first path that is not a readable file (a directory, a FIFO) is reported, never skipped for the second. With neither present it tells Claude on every prompt, and the user once at session start, to runnpx model-orchestrator, so a project with no rules is never a silent no-op.
Changed
builder,deep-plannerandlive-researchernow declare their tools, for npm installs too. Until now they carried notools:line and inherited every tool the session had, MCP tools included.buildergetsRead, Write, Edit, Glob, Grep, Bash;deep-plannergetsRead, Glob, Grep(its prompt already says it never edits);live-researchergetsWebSearch, WebFetch. This narrows what those three agents can do in an existing install once regenerated: if you relied onbuildercalling an MCP tool, ordeep-plannerrunning a command, add the tool to that agent'stools:line or delete the line.route-gate.mjstakes a list of rules paths instead of one. An installer render is a one-element list with no setup hint, so an npm install behaves exactly as before; a test pins that render.
Fixed
route-gate.mjscould emit more than Claude Code's 10,000-character hook output cap, on npm installs too. The routing table was capped at 4,000 characters, but a fallback message embeds the resolved project path and the error text, so a 12,000-characterCLAUDE_PROJECT_DIRproduced 12,146 characters from an installer render. Every string the hook emits is now capped at 8,000 characters, with a test on both renders. Found by the pre-release audit round and reproduced before the fix.- The plugin's hook-safety test could not see an async write or subprocess.
writeFileSync?matcheswriteFileSynandwriteFileSync, neverwriteFile, and everyprocess.envread was exempt. The check now matches the Sync and async form of every file write and subprocess call, refuses dynamicimport(andrequire(, allows static imports ofnode:fsandnode:pathonly, requiresopenSyncto open read-only, and allows no environment variable butCLAUDE_PROJECT_DIR, with a red case for each. Found by the same audit round.
Not changed
route-metrics.mjsstill installs withnpx model-orchestrator, unchanged. It is left out of the plugin only, because it writes a log to disk and the plugin ships only hooks that read.
0.1.19 - 2026-09-12
Added
- The
route-gate.mjsnon-regular-file guard is now tested on every OS, not only wheremkfifoexists. The FIFO test is the only one that can prove the HANG the guard exists to prevent (a naivereadFileSyncon a writer-less FIFO blocks forever), and Windows has nomkfifoto build one, so that test was skipped there and nothing exercised!st.isFile()on Windows at all. A directory at the same rules path reaches the same guard before anyopenorreadcall, on every OS, so the guard itself is covered everywhere and the win32 skip is no longer its only coverage. - A guard that refuses an undocumented test skip.
test/prose.test.jspins each skip to its file, its exact marker and a phrase the README has to carry, then counts every{ skip:in the suite and fails if the totals disagree. Proved in both directions: adding a skip anywhere fails it, and removing a skip's explanation from the README fails it.
Fixed
- The README's Windows skip list named three of the four skips. The
mkfifoskip intest/hooks.test.jshad never been documented, in the README or the changelog, while the sentence above it read "a few narrow skips remain" and enumerated the rest. A skipped test reads as a test that passed, so an undocumented skip is a coverage claim nobody made deliberately. All four are named now, the conditionalstatSync().modeassertion is labelled as the one-assertion case it is rather than a skipped test, and the sentence says outright that the list is enforced by a test rather than maintained by hand.
0.1.18 - 2026-09-11
Fixed
- The level 3 box's weekly audit could record a hung
--versionprobe as a version string instead of "UNVERIFIED: timed out".weekly-audit.sh'skilltreekilled a hung process's children before the process itself, so in that gap the probe's own shell could print and exit 0, andbounded()reported success. Seen once on macOS CI ("codex never" in the report); 0 of 20 local runs reproduced it, so it is rare but real on the box.killtreenow freezes each process (SIGSTOP) before walking its children, and the watchdog leaves a marker when it fires sobounded()returns 124 whatever order the kills land in. A new test forces the bad ordering and checks the timeout still reads as one. - On Windows,
bin/cli-run.mjscould not run any lane at all:spawn()threwEINVALfor every.cmdbinary, which is how npm installs every agent CLI there. Since Node's fix for CVE-2024-27980 (18.20.2, 20.12.2, and every 22.x), spawning a.bat/.cmdtarget withoutshell: truethrows instead of silently running it through an unsafely-escapedcmd.exe. This was a real, shipped defect, not a test gap: a Windows user following this README could not have run a single lane before this release.bin/cli-run.mjsnow resolves the.cmdshim to the Node script npm's owncmd-shimtool wrote underneath it and spawns Node directly on that script (resolveCmdShim,windowsSpawnPlan), so a prompt (untrusted text this tool does not control) never passes through a shell at all in the common case. A lane whose.cmd/.batcannot be resolved that way (an old or hand-edited shim) is refused with exit 13 and a message saying how to fix it, never run throughcmd.exe: a batch file re-reads its arguments through%*aftercmd.exehas parsed them once, and no escaping fully contains a prompt through both passes. The escapedcmd.exepath (the algorithm documented at qntm.org/cmd and used bycross-spawn, withwindowsVerbatimArguments) survives only as an explicit opt-in for the installer's ownnpm install -g <pinned spec>, whose arguments never include user text. Verified against the real, byte-for-byte output ofcmd-shim@9.0.2(the package npm itself uses), not a guessed shape; the escaping is pinned to exact expected strings for&,|,^,%,", a trailing backslash and a literal newline intest/judges.test.js. - A reconfigure's terminal report (
runtime upgraded:,runtime CONFLICT, kept:,documents kept:, and the rest) printed a Windows install's file paths with backslashes, while every other path this tool prints in generated text uses forward slashes;src/install.js's manifest key was already posix-normalized, but the label built alongside it for the human-readable report was not.writeFiles()now builds both from the same posix-normalized value. - A
--diroutside the project root, or thevm/level-3 templates'INSTALL_DIR/INSTALL_DIR_SH/INSTALL_DIR_SYSTEMD, ran through this host's ownpath.resolve(), which reads a leading/as drive-relative on win32, wrong for both: thevm/templates describe a REMOTE Linux box (weekly-audit.shis bash,weekly-audit.serviceis a systemd unit, neither of which can run anywhere but Linux), and the outside-project case is documentation prose, not a local filesystem path. An absolute--dirgiven as a bare POSIX path now renders unchanged on every host for both; a real local Windows path (one naming a drive) is untouched, since that case never took this branch. killTree's win32 branch spawned a baretaskkill, which depends on PATH containingSystem32; when it does not, the spawn'sENOENTreaches this tool as an unhearderrorevent on the returned process and crashes the whole run over what should be a best-effort cleanup step. Found onwindows-latestCI the first time a lane actually ran end to end there, once theEINVALfix above stopped hiding it.taskkillPath()now resolves the executable under%SystemRoot%(falling back through%windir%to a fixed path), independent of PATH, andkillTreeattaches an error listener so a spawn failure can never crash the wrapper.bin/cli.js's own opt-innpm install -g <ai>prompt had the identicalEINVAL-shaped defect as the lane spawn above, in a different file, since it calledspawnSync('npm', ...)directly with no shell. It now resolvesnpmwithwhich()and runs it through the samewindowsSpawnPlan()bin/cli-run.mjsexports, instead of a second, duplicated fix.
Changed
- The three Windows test-skip groups tracked in #30 are gone, replaced by two narrower, individually-justified skips found by actually running the unskipped suite on
windows-latestCI, plus the one pre-existing skip this pass never touched (statSync().mode's executable bit; NTFS has nothing equivalent).test/cli.test.js's fake lane binaries now install as an npm-style.cmdshim (verified against the realcmd-shim@9.0.2output) pointing at a small Node script, the same shape a real vendor CLI's install takes throughwindowsSpawnPlan()above, instead of a bespokesh.exebridge that never exercised cli-run.mjs's own spawn path at all; every test in that group but one, and the#12upgrade-path pair (fixed by the report-label change above), now runs unconditionally.test/install.test.js's 7 formerly-skipped tests split on what their path assertion actually describes: the ones naming avm/remote-box path stay literal POSIX (now true on every host, per theINSTALL_DIR*fix above); the ones naming a LOCAL path (where this run wrote files on this host) now build their expectation withresolve()/join()instead of a hardcoded POSIX literal, so they assert the same real, platform-native value the product renders rather than a string that only happened to match on POSIX. Two of those seven also had their own, separate bug once actually run on Windows: their fakecurl/jq/node/codexbinaries were placed on a hand-built PATH containing the literal strings/usr/binand/bin, which name nothing on that OS; they now prepend the stub directory to the REALprocess.env.PATH(this job already runs under Git Bash, so that PATH already carries whatbashitself needs) instead of replacing it with a POSIX-only guess. - New skip: a lane dying mid-run from a real POSIX signal genuinely cannot be reproduced on win32. cli-run.mjs's
r.signal || r.status === nullbranch exists for a real lane crashing or being sent a signal, but a real Windows lane is a plainnode <script>process (via the resolved cmd-shim), so it cannot die "by signal" any more than the product it is testing can; Windows has no OS-level POSIX signals. The only way this file's fixture can even simulate a signal death is a nestedsh -c "...; kill -TERM $$"(writeShellStub's win32 branch has to bridge throughshfor the shell body to run at all), which puts an extra node process between cli-run.mjs and the dying shell; measured onwindows-latestCI, MSYS bash's own self-kill status leaks through as a plain nonzero exit code (3840), which this tool already handles correctly, just under a different, honest verdict (exit_nonzero, notkilled). #13(SIGTERM/SIGINT to the wrapper) is Windows-aware now, not skipped, for both signals: Windows has no OS-level signals at all, proven onwindows-latestCI (the wrapper died as{code: null, signal: sig}for SIGTERM AND SIGINT alike; a hypothesis that SIGINT gets a real, catchable console-control event there was tried first and measured false in this exact scenario, not assumed). The test now expects an unhandled termination for both signals on win32; the graceful exit-143/130-and-kill-the-lane-first behavior stays a POSIX guarantee, asserted as before on every other OS.- New skip:
#10's watchdog-kill test, and the new#10bbounded()timeout check, for the same reason.weekly-audit.sh'sbounded()/killtree()rely onpgrep -Pand killing a backgrounded subshell's process tree, real bash job control this script only ever runs under on the box it targets (a systemd-scheduled job on Ubuntu, never something a Windows user runs locally). Actually executing that watchdog against a genuinely hanging stub underwindows-latestCI's Git Bash, rather than just rendering and syntax-checking the script (which the rest of this test group does, and which passes), hung past a 20s outer timeout: MSYS's job-control emulation does not reliably propagate akill -KILLto the underlying Windows process tree of a backgrounded( subshell ) &, a known class of MSYS/Cygwin limitation, not a defect in the generated script. - One test-report-label assertion in
test/install.test.jsand one intest/cli.test.jsstill hardcodedpath.join()'s native separator for what is now posix-normalized generated text (the report-label fix above); both now match the posix form. docs/audit-brief.mdgained a section on the Windows spawn path: what runs, why no shell in the common case, and why a lane is refused rather than run throughcmd.exe, and how the installer's one opt-incmd.execall escapes its arguments.
0.1.17 - 2026-09-11
Changed
- User-facing text now uses plain language instead of security-audit jargon. Words like "risk", "attack lane", "adversarial", "blast radius", "fail closed" and "threat model" read as alarming to someone deciding whether to try the tool, so they scared off exactly the readers this project needs. No rule any of them described changed, only the wording: "risk" is now "stakes" everywhere it names a routing input (with a one-line definition added to
README.mdandTIERS.md), "attack lane" / "Stage 5 Attack" / "attack pass" are now "challenge lane" / "Stage 5 Challenge" / "challenge pass", "adversarial" (auditor, read, critique, turn, pass) is now "second-opinion", "blast radius" is now "everything it touches", "fail(s) closed" is now "refuses by default", and "threat model" is now "security notes" in the files that link to it.test/prose.test.jsgained a permanent check (no alarming security wording in user-facing text) over the purely-prose, user-facing surface (docs/,templates/,README.md,llms.txt,CONTRIBUTING.md, the PR template) so the old wording cannot silently creep back in.docs/audit-brief.md,SECURITY.md,CODE_OF_CONDUCT.mdand code identifiers/comments (for example theATTACK_LANErender var) are unchanged, since these words are expected or load-bearing there.
0.1.16 - 2026-09-11
Added
- A third claude-code-only hook,
route-metrics.mjs, answers "is my agent actually routing and delegating?" A routing rule nobody measures is a rule nobody knows is followed. Wired to five events (UserPromptSubmit,PreToolUseonAgent/Task,SubagentStart,SubagentStop,Stop), it appends one JSON line per event to~/.ai-orchestrator/route-metrics.jsonl(the same directory and home resolutionbin/cli-run.mjsalready logs to): a turn, a subagent dispatch (subagent_type, background flag), a subagent start and stop (so a duration can be computed from a small state file keyed bysha256(agent_id)), and the lane parsed from a new hidden marker,<!-- route: <lane> | <why> -->, that the route-gate block now asks every reply to end with. Only named, charset-bounded fields ever reach the log; prompt text, tool descriptions, the raw assistant message, and the marker's "why" half never do.node .claude/hooks/route-metrics.mjs --summary [--since <ISO date>]reports turns, route-marker coverage, lanes by count, dispatches bysubagent_type, dispatches with no matching start, and mean/max duration per agent type. Plain Node, zero deps, prints nothing to stdout on any event, fail-open (a miss is a missing log line, never a blocked turn). Installed and wired only when claude-code is the primary, same no-overwrite rules as the other two hooks. Seedocs/audit-brief.mdfor the full threat-model writeup. - CI now runs on
windows-latesttoo, node 18/20/22, alongside Ubuntu and macOS.defaults.run.shell: bashmakes every workflow step Git Bash on the Windows runner instead of the defaultpwsh, so the same script runs on all three OSes with no parallel Windows rewrite.
Fixed
finding-verifierandcode-reviewer(claude-code) called themselves unqualified "Read-only" in their descriptions while carrying an unrestrictedBashgrant, the same overclaimdone-verifiershipped with in 0.1.15 and was fixed there; nothing in that grant stops either from running a mutating command. Both descriptions and bodies now say plainly that they carry no file-editing tools and that Bash is bound by the prompt, not the tool grant.templates/agents/claude-code/README.mdandtemplates/agents/snippets/claude-code.mdare corrected the same way. The done-verifier-only test is replaced with one that walks every claude-code agent file: any agent whosetools:line includesBashmust qualify any "Read-only" claim, checked against its own file and against every generated doc surface.CLAUDE.mdandCONTRIBUTING.mdeach pinned a fixed test count that drifted the moment a test was added or removed, the same class of driftAGENTS.mdalready avoided by saying the suite prints the current number instead. Both now say the same thingAGENTS.mddoes. A new test intest/prose.test.jsfails if any top-level.mdstates a fixed count of test cases.which()(src/detect.js, and its standalone copy inbin/cli-run.mjs) never found a Windows lane binary, because a PATH entry there never holds a baregrok: npm and vendor installers dropgrok.cmd(or.exe/.bat), the same way any Windows shell resolves a bare command through%PATHEXT%.which()now tries the bare name first (a no-op on POSIX, and still matching an already-extensioned name on Windows), then each%PATHEXT%suffix.platformis a parameter (defaultprocess.platform), the same patternkillTree(pid, platform, deps)already used, so the win32 branch has a real test (test/detect.test.js, new) on every OS this suite runs on.homeis a parameter too, for the same testability reason: a dev machine with a real vendor CLI already on~/.local/binmade the new win32 tests false-negative until it was injectable.- Committed text files were checked out as CRLF on
windows-latest, breaking every test that compares a file's exact bytes against a string built in memory with\n(docs/catalog.mdvs.catalogMarkdown(), the README's generated vendor-table section,llms.txt's first line, and a backslash-continued shell example's line-rejoin regex in a test). New.gitattributes(* text=auto eol=lf) forces LF on checkout regardless of a contributor's or a runner'score.autocrlf;test/fixtures/(real captured vendor output, byte-exact on purpose) is marked-textso line-ending normalization never touches it. - A large block of
test/install.test.jsandtest/cli.test.jscompared a planned file'sf.rel(built withpath.join, so backslash-separated on win32) against a hardcoded forward-slash literal ('protocols/build-protocol.md','vm/README.md','.claude/agents/deep-planner.md', and about a dozen more), which is never equal on Windows; several other assertions hardcoded a POSIX:PATHdelimiter and/usr/bin,/binabsolute paths, or replaced a spawned child'senvoutright and droppedPATH/USERPROFILE/the rest of the parent environment Windows itself needs. Path literals now go throughjoin(...); PATH construction goes throughnode:path'sdelimiter; every replacedenvobject spreadsprocess.envfirst and setsUSERPROFILEalongsideHOME(os.homedir()does not consultHOMEon win32). - On a real Windows install, a reconfigure's "applied:" line always said "nothing" and the existing-runtime upgrade/conflict checks never fired, because
bin/cli.jschecked a written file'sf.rel(backslash-separated on win32, built bypath.join) directly againstMACHINE_OWNED/RUNTIME(src/install.js, hand-written with forward slashes), which never match on that OS.fileClass()already normalized before checking; it now exports that normalizer (toPosixRel) forbin/cli.js's two direct checks to use too. Both take an optionalseparatorparameter (default the realpath.sep) so the win32 case has a test (test/install.test.js) provable from any host. Found viatest/cli.test.js's "#6: rerunning with an added lane..." on windows-latest CI. - A replaced child
envobject could carry bothPATHand the host's own differently-casedPathkey at once ({ ...process.env, PATH: x }adds a new key next to whichever case the real environment block used; Windows env vars are case-insensitive, plain JS object keys are not), so which one a spawned child actually saw was implementation-defined, not last-key-wins.test/cli.test.js'smergeEnv()replaces any existing case-variant of an overridden key instead of adding a second one. test/cli.test.js's fake lane binaries are#!/bin/shscripts, which Windows cannot execute asargv[0](no shebang interpretation inCreateProcess).writeShellStub()/writeNodeStub()write a.cmdlauncher beside the POSIX file on win32 that hands off to Git Bash'ssh.exeornode;which()'s detection of the resulting binary works, but running one all the way throughcli-run.mjs's own spawn path is not yet reliable on Windows CI for a reason this pass did not fully root-cause. Those specific tests (and the#12upgrade-path pair, which diverges in its own, separately unclear way) are skipped on win32 with a stated reason rather than shipped flaky or silently broken;killTree's win32 branch andwhich()'s%PATHEXT%resolution each keep their own direct, passing test. A handful of other tests assert an exact POSIX--dir/--projectpath string verbatim in output, whichpath.resolve()reinterprets as drive-relative on win32 (a real design question - should a level-3--dirdescribing a remote Linux box's path ever go through the local host's path semantics at all? - out of scope to decide here) and are skipped the same way.statSync().mode's executable bit is a POSIX-only assertion, dropped on win32 rather than asserted against a filesystem that has no equivalent concept.
0.1.15 - 2026-09-10
The portable parts of a live routing revision, delegate by default, gated on one verified fact rather than a guess: code.claude.com/docs/en/sub-agents states that a non-fork Claude Code subagent's initial context includes "every level of the CLAUDE.md hierarchy the main conversation loads", and that the built-in Explore and Plan agents skip it. No other lane in this catalog has that documented, so everything below is gated on subagentsLoadRules(primary), currently true for claude-code alone; every other primary keeps its original wording unchanged.
Added
subagentsLoadRules: trueon the claude-code catalog entry, with the doc quote as its comment. Drives every new render var below throughsrc/install.js; nothing here is a template branch, per the house rule that templates carry no logic.- Builder executes by default, on claude-code.
ROUTING.mdrule 5, its "Who builds" section, the "Add an endpoint" example, and the claude-codeCLAUDE.snippet.mdnow say: the orchestrator plans, briefs, verifies and talks to the human; it stays inline only when (a) the brief would cost as much as the work, (b) the task needs this conversation's own context, or (c) it is the human's decision or the final verification of delegated work. Every other primary keeps "the orchestrator builds it directly." - Two hooks, claude-code only:
route-gate.mjs(UserPromptSubmit) andsubagent-context.mjs(SubagentStart), written to.claude/hooks/, wired by asettings.hooks.snippet.jsonthe user merges into.claude/settings.jsonthemselves, never written over one they have.route-gate.mjsreads the<!-- route-gate:start -->...<!-- route-gate:end -->block out of the rendered routing rules file and injects it every turn, so the table is read from the one place it is generated, not recited from memory; a missing file or block still exits 0 with a one-line fallback naming the path it looked for.subagent-context.mjsinjects a static reminder of where the rules andTASK_BUNDLE.mdlive and that a delegate does not route further or verify its own work as final. Both are plain Node, zero deps, bounded reads, fail-open by design (a miss is a stray context string, not a gate): seedocs/audit-brief.md. done-verifierandreader, two new fast-tier agents with no file-editing tools, in both the claude-code and agy formats.done-verifierprobes a tracker item's stated done-signal (a file, a commit, a URL, a log line, a count) and returns MET, NOT_MET or UNVERIFIABLE; it never closes or edits anything.readerreads and digests many files or notes and returns exactly what the brief asks (facts, quotes citedpath:line, an index, a digest); unlikebulk-worker, it never classifies, tags, transforms or writes.readeris read-only by tool grant on both formats (noBash);done-verifieron claude-code carriesBashfor its probes, bound only by its prompt, not by the grant, and its description says so; on agy,commandExecutionPolicy: offblocks command execution mechanically instead.ROUTING.md's decision tree,TIERS.md's effort table, and both agent-folder READMEs now name them.- The inline threshold gets a measurement instead of a guessed figure. "A subagent starts with your CLAUDE.md and tool definitions already loaded, so it has a fixed start-up cost before it does anything. Measure yours once: spawn a subagent with a one-line task and read its token count. Work smaller than that stays inline." (claude-code
ROUTING.mdandCLAUDE.snippet.mdonly; no private number shipped.)
Changed
- The package now says what it is for in the first line people and agents read. npm search, GitHub search and the installer banner showed "Routing instructions and a CLI runner", which named the parts and not the purpose. The description, the README opening and the banner now lead with the goal (each task to the right model, agent or LLM, fewer frontier tokens) while keeping the 0.1.11 correction intact: routing is an instruction your agent follows, and the README still states it does not automatically compare prices or select models. A test holds all three surfaces to that. New: an "At a glance" block and question-shaped "Common questions" in the README, request-level alternatives named for readers who want a proxy,
llms.txtat the root and a headless-use section inAGENTS.md(both now ship in the package), and search keywords matching what comparable routers use. - Corrected the unqualified premise "a subagent holds none of these rules" everywhere it appeared (
TASK_BUNDLE.md,ORCHESTRATOR.md, the claude-code snippet,docs/part-1-beginner.md,docs/part-2-intermediate.md, README principle 6, andROUTING.md's "Who builds"). The corrected fact: a Claude Code subagent loads CLAUDE.md and so keeps the standing rules, just not this task's scope; a second CLI or a fresh chat window may still hold none of it. "Absence is denial" is unchanged; only the premise about who is absent what was wrong. - The claude-code snippet's closing "available as ..." agent list is generated from the files actually shipped in
templates/agents/claude-code/, never hand-typed. It had drifted once already:finding-verifiershipped in 0.1.14 and was missing from this sentence until now.claudeAgentIds()insrc/install.jsreads the folder; a test ties the rendered list to it. - The delegate-by-default gate now reaches every generated surface it should, not just three of them.
build-protocol.md's "Roles, as capabilities" table and its "Why the builder does not hand off the main build" line,builder.md's description, andROUTING.md's "Plan big, execute small" modifier still said, on a claude-code install, that the orchestrator writes the main build itself, never hands it off whole, and that a delegate inherits none of the session's rules: the exact premise the rest of this release corrects. All four now render throughsubagentsLoadRules(primary)the same way the decision tree and "Who builds" already did; every other primary is unchanged. A semantic-regression test asserts a claude-code install contains none of the old phrasing and a codex install still does.
Fixed
- Both new hooks could hang, and
route-gate.mjscould read an unbounded or blocking file (pre-release audit finding, never shipped).readFileSync(0)in bothroute-gate.mjsandsubagent-context.mjsblocked until stdin reached EOF, so a caller that piped input in without closing its end (or ran the hook from a bare TTY) left the process running indefinitely; reproduced withsleep 3 | CLAUDE_PROJECT_DIR=... node route-gate.mjsstill running past 1.5s. Separately,route-gate.mjsread the whole rules file into memory before bounding it (readFileSync(path).slice(0, MAX_READ)), so a FIFO planted at the rules path blocked forever on open, and a very large file was read in full before being truncated. Fixed in both hooks: stdin is now drained asynchronously against a 250ms hard cap, never blocking past it.route-gate.mjsadditionallystatSyncs the resolved path and refuses anything that is notisFile()(a FIFO, socket, device or directory, symlink target included) before ever calling open, then reads through a single fixed 64 KB buffer viaopenSync/readSync, closed in afinally, so neither the read time nor the memory used depends on the file's on-disk size. Tests: an open, never-closed stdin pipe now exits within 1s for both hooks; a FIFO at the rules path returns the fallback instead of hanging; a 200 MB sparse rules file completes in well under a second with output still capped.
Three refinements to the routing model, from a review by @shawnwows. The theme is the same in all three: a routing decision that was implied, inherited or asserted is now stated, pinned or checked.
Added
--modeland--efforton every lane, and a route recorded per run. A lane with no flag and nodefaultsentry inbin/lanes.jsonruns on its own config file, whichcli-runcannot see: a CLI configured months ago at a low reasoning effort keeps auditing at that effort while the routing docs describe an adversarial pass, and nothing raises an error. Each vendor spells the flags differently andcli-runtranslates (grok -m/--reasoning-effort,codex -m/-c model_reasoning_effort="X",agy --model/--effort,hermes -m/--reasoning,qwen -mand no reasoning flag), each one read from that CLI's own--help. Flags beatdefaults,defaultsbeats nothing,--doctorprints what each lane is pinned to, and the log carriesmodel_requested,effort_requested,model_sourceandeffort_sourceon every record, including runs refused before the lane started. It records no "actual": one lane of five (grok) reports a model id in its own output and the other four report none, so the field would be populated for one lane and empty for four, and it would be a provider-supplied string, which the durable log never holds.--efforton qwen is a usage error rather than a silent drop, and route values are charset-bounded because a model id becomes an argv element and, on codex, part of a TOML value.finding-verifier, a sixth subagent, in both agent formats. Review and scanner findings no longer go straight to a repair. It reads the cited line, states what would trigger the problem, hunts for the guard, caller or test that makes it impossible, and returns CONFIRMED, NOT_REPRODUCED or INCONCLUSIVE per finding. Only CONFIRMED earns a change; INCONCLUSIVE is never rounded up to be safe or down to be tidy. Bound into the build protocol as Stage 5a, intoROUTING.md, and into the Claude Code activation snippet. The reproduction rule already existed in Stage 5; it had no owner, no separate model family and no way to say "I could not settle this".- Complexity and risk as inputs, alongside role (
TIERS.md). Complexity moves the effort: a worker executing a finished plan needs less reasoning than the reviewer judging its output. Risk (security, privacy, data loss, irreversible) moves the tier and who reads the result, because none of those failures is fixable by editing the code afterwards. A one-line change to an auth check is simple and high-risk at once, and the risk decides. Deliberately two rules and two small tables rather than a role by complexity by risk matrix: an 80-cell table is not maintained, and an unmaintained routing table is worse than none because it is believed.
Changed
--modelis no longer qwen-only.--safe-modestill is.- The route is resolved before the "lane disabled" and "binary missing" refusals, so those records carry it too. Found by the pre-release audit: a run refused for a missing binary is still a run that requested a route, and a failure record without one is the gap this release exists to close.
bin/lanes.jsongains an optionaldefaultsblock. It fails closed with the rest of the file: an unknown lane, an unknown key, a value outside the charset, or an effort pinned on a lane with no reasoning flag refuses every lane until it is fixed, rather than being skipped quietly.- The generated activation list gains a step about pinning the route, and
--doctoroutput gains a route column with a plain sentence about what "not pinned" means.
0.1.13 - 2026-09-08
Three issues from a fresh first-run walkthrough of 0.1.12 (#26, #27, #28). Same class as 0.1.12's five: a surface describing an install that did not happen. A fourth, #25, was filed and closed as a mistake on the reporter's side, not a defect: the warning it said was missing has been printed since 0.1.12 and the repro had been read through a truncated pipe.
Fixed
- The "Then prove it took" list no longer sends a level 1 reader to a file level 1 never wrote (#27). Step 4 told every reader, at every level, to pick a lane out of
bin/lanes.jsonand runnode bin/cli-run.mjs. Level 1 writes nobin/at all, and step 3 immediately above it hedged correctly with "At level 2+" while step 4 did not. The list is nowproofSteps()insrc/install.js, gated on level the same wayactivationSteps()is, and the template renders it. Two tests: the README's section must equal the array exactly for every level and primary, and nobin/path may appear in it that the plan did not write. - The box setup no longer tells you to sign in to CLIs you did not pick (#26).
templates/advanced/vm/README.mdstep 3 was a fixed sentence namingcodex login --device-auth,grok login --device-authandagy. A level 3 install of claude-code, codex, qwen and ollama was told to sign in to two CLIs it does not have and never told about the one it does. The step now renders each selected CLI's ownauthstring from the catalog. Everything else in that file was already computed from the selection, which is what made the one hardcoded line easy to miss. - A selected local runtime is finally told to install itself (#26).
activationSteps()filtered onkind === 'agent-cli', so Ollama, which has a binary and a download page, appeared in no ordered list at any level. Its only mention was one row of a URL table inDELEGATION_MATRIX.md. It now gets a step naming the download page and theollama pull <model>that has to follow it. - The tool block stopped saying the same word twice (#28). Every run that selected a tool printed
optional: Optional. Needs Python 3.10+ and uv., because the label repeated the note's own first word. The label isnote:now. The note keeps the word, because--listand the interactive picker print it bare with no label.
Changed
--primaryis documented as what it is.--helpcalled it "required when several qualify", and then a--yesrun with several candidates silently picked one in catalog order. The run now names the choice in the plan (primary claude-code (chosen for you from claude-code, codex; pass --primary to decide it yourself)) and the help says the same thing. Behaviour is unchanged: the default was sensible, only the promise was wrong.
0.1.12 - 2026-09-08
Five issues from one first-run walkthrough of 0.1.11 (#20 to #24). Every one of them is the same failure: a page describing an install that did not happen. Each fix removes the second copy of a fact rather than correcting it.
Fixed
- The generated README no longer describes a different install from the one the terminal just printed (#20). The activation list existed twice: once as an array built in
bin/cli.js, once as prose intemplates/common/README.mdthat assumed a chat app. A level 2 Claude Code install was told, on the page it was pointed at, to pastePASTE-INTO-YOUR-AGENT.md, a file that run never wrote, and a level 1 chat install was told its rules file wasyour agent's instructions file, a leftover placeholder.activationSteps()andsnippetFor()now live insrc/install.jsand both surfaces render the same array, so the page can only ever name the file that was written. A test renders every level against every possible primary and fails if the README omits a printed step or names any other agent's snippet. - A chat install no longer claims a project root it never created (#21). Level 1 with a chat app writes no project files, and the README still printed
--projectas "where your agent reads rules and subagents" next to "subagent definitions: none". It now says there is no project root and why. A CLI primary that reads a rules file but gets no subagent folder (codex, qwen) keeps its project path and gains the missing half: whether this run created that folder. - The chat activation line is a sentence again (#22). It read
paste ... into Claude app or claude.ai (chat only, no CLI)'s custom instructions or Project: the catalog's disambiguating note sat inside a possessive. Chat entries in the catalog now carrychatNameandchatSurface, and the line readsopen the Claude app or claude.ai and paste the block in <path> into its custom instructions or a Project. The catalog note stays where it is useful, in the picker list. - "Built against" and "pinned to" are one number per lane, by construction (#23). The README's compatibility table was hand-written and the installer's npm pins were edited separately, so a user comparing them found claude at 2.1.226 and 2.1.260, codex at 0.153.4 and 0.153.2, with no rule for which to trust.
builtAgainstinsrc/catalog.jsis now the single source:npm run gen:catalogrenders the README table from it, the npm pin is that value wherever a lane installs from npm, and the tests fail if the table drifts, if a pin disagrees with itsbuiltAgainst, or if a recorded fixture's vendor version disagrees with either. The pins moved to the exercised versions rather than the table moving to the pins, because the exercised version is the one with evidence behind it. - The README stops pinning a release tag the registry has moved past (#23). The GitHub one-liner still said
#v0.1.7while npm served 0.1.11. It now points at main, says where the tags are, and a test fails on any#vX.Y.Zin the README that is not this package's own version. - The documented example sets both write targets (#24). The first non-interactive example set
--dirand left--projectat the current directory, so a copied command run from a home folder dropped five agent files into it. Both flags are now set in the example, a table explains what lands where and why--dirdefaults to a folder named for its contents, and the installer prints a line when--projectwas left at the default and subagent files are going there. A test fails on any documented--yesexample that sets one target and not the other. - Privacy describes what actually runs (#24). It claimed the only network step was an
npm install -gyou approve, when the thing the user runs isnpx(a download in itself) and the installer under--yesprints vendor install commands without running them. Both are now stated, along with--yesselecting the recommended companion tool unless--no-toolsis passed.
Changed
- Vendor CLI pins move to the versions this release was exercised against:
@anthropic-ai/claude-code@2.1.226,@openai/codex@0.153.4,@qwen-code/qwen-code@0.22.3. A pin is a floor, not a ceiling: newer versions may work, and the table exists so a lane that breaks after a vendor upgrade has something to compare against. npm run gen:catalognow regenerates two surfaces,docs/catalog.mdand the README vendor table between itsvendor-tablemarkers.
0.1.11 - 2026-09-07
Fixed
- The cost claim the audit rejected was still live in three places outside the README. #11 asked for the opening promise to be narrowed, and 0.1.2 narrowed it in
README.mdonly. "Route every task to the cheapest AI that does it well" survived inpackage.json'sdescription, which is what npm search results show, in the repository's GitHub description, which is what GitHub search shows, and in the installer's own banner, printed to every user on every run. All three now say what the package generates instead of what it guarantees: "Routing instructions and a CLI runner for your AI tools." The cheapest-capable-lane guidance in the docs and templates is untouched; that is the product's advice, not a promise about what the code enforces.
0.1.10 - 2026-09-07
Closes the last two verification items on #11. Both had been described as needing vendor sign-ins or infrastructure that was not available. Both turned out to be doable with what was already here, and doing them found a real defect.
Fixed
- The generated weekly audit orphaned a temp file on every timeout. A run killed by the unit's
TimeoutStartSecdies on SIGKILL, so no trap and no cleanup line of ours can run, and itsreports/.audit-<stamp>-XXXXXXfile was left behind forever. The job now sweeps.audit-*older than a day at start. A day is far outside the unit's own 900s deadline, so a temp belonging to a run still in flight can never be swept. Found by actually starting the unit on Ubuntu; the previous text-only assertion could not see it.
Added
test/fixtures/: raw output captured from real vendor CLI runs, withmanifest.jsonrecording the vendor version, the exact flags, the exit code, and what each fixture proves. Every other judge test in this repository uses shapes written by hand. The capture earned itself immediately: real codex 0.153.4 emits anitem.completedwhose item istype:"error"(a skills-budget warning) beforeturn.completed, which no synthetic fixture contained, and real agy returns"OK\n"with a trailing newline.test/fixtures.test.jsruns every judge against them.test/systemd/run-on-ubuntu.shand its README: starts the generated job as a real systemd user unit and proves that the timeout kills the whole cgroup (a detached grandchild does not survive it), that a failed rerun preserves the previous report, that a malformed gateway key exits 2 before anything is written, and that the new sweep removes an aged orphan while leaving a fresh one alone. 11/11 on Ubuntu 24.04.4 LTS, systemd 255. Not part ofnpm test, which has no systemd to run against.
Notes
test/fixtures/README.mdstates its own gaps rather than hiding them: qwen's success shape is still synthetic because its key was not present in the capture environment, andclaudeandollamahave no judge, so no fixture.
0.1.9 - 2026-09-06
Added
--versionand-von the installer, printing the package version and exiting before anything else is validated, so it answers from a broken or half-configured directory.- A "Vendor version compatibility" section in the README, naming the exact vendor CLI version each lane was built against, and saying plainly that the installer checks a binary's presence and never its version. Closes the compatibility-statement item on #11.
Fixed
-hand-vare now parsed. Both were listed in the flag table but unreachable, because parsing required a--prefix and rejected every single-dash argument. Strictness is unchanged:-xis stillunexpected argument,--versionnstillunknown flag.
Changed
- The live-canary question on #11 is answered by design rather than left open: the canary is
cli-run --doctor --run, which runs on the user's machine against the user's own sign-ins. A maintainer-credential canary in CI would prove one machine works and bill per run, so it is documented as deliberately absent.
0.1.8 - 2026-09-06
Added
authorinpackage.json, so npm shows a byline:aunysillyme (https://github.com/aunysillyme).- The installer's last line now points at the repository, on the reasoning that the end of a successful install is the moment a user is most likely to act on it.
0.1.7 - 2026-09-05
Fixed
- Preserve unrelated subagents in uninstall guidance and explain manual activation cleanup.
- Provide a chat activation block below 1,500 characters and explain protocol uploads.
- Warn about zero-lane setups; doctor reports inactive with exit 13, including with --run.
- Add a first-task walkthrough and clarify agent-directed routing, connection checks and output contracts.
- Correct the contributor exit-code reference.
0.1.6 - 2026-09-05
Issues #16 to #19, filed against 0.1.5. Each reproduced before the fix; each fix has a test.
Added
--dry-runas an alias for--dry; the package script was already nameddry-run(#16).cli-run:killTree()ends a lane's process tree withtaskkill /T /Fon Windows instead of killing only the root process. Windows is still not exercised by CI and stays documented as unsupported; the branch is unit-tested by argv capture (#18).
Fixed
cli-run --expect-jsonaccepts a response that is exactly one markdown code fence around JSON (\``json ... ````, CRLF included). Prose before or after the fence still fails the contract, because then the deliverable is not the JSON (#17).--yeswith several candidate agents and no--primarynow prefers claude-code, then the first agent that can load subagent definitions, then the first listed.--ais codex,agyused to pick codex and write no subagents (#19).
0.1.5 - 2026-09-05
First release published by the workflow, with provenance.
Added
- Published to npm as
model-orchestrator(0.1.4 was the first publish, by hand).npx model-orchestratoris now the install line; the GitHub route stays for pinned or unreleased runs. .github/workflows/release.yml: on av*tag, checks the tag againstpackage.json, runs the tests, and publishes with provenance through npm trusted publishing (no stored token). Needs the one-time trusted-publisher setup on npmjs.com described inRELEASING.md.
Changed
RELEASING.md: the first npm publish is manual and must not pass--provenance(npm only generates provenance inside a supported CI runner); later releases go through the workflow.
0.1.4 - 2026-09-05
Added
--update-docs: after a selection change, regenerate the documents a previous run wrote and nobody edited since. The check is the same hash rule the runtime class uses: an installed copy that matches the hashMANIFEST.jsonrecorded is regenerated and named under "documents updated"; one that differs is kept and named under "document CONFLICT, kept"; without a manifest every changed document is kept as UNVERIFIABLE.--forcestill replaces everything;--dryreports and writes nothing. The reconfiguration hint names the flag.
Fixed
MANIFEST.jsonrecorded the hash of content a run planned for a document it then kept, so the next hash check read every kept document (and, on a reconfiguration, every kept runtime file) as edited. The manifest now records the previous run's hash for kept files and no entry when there was no previous manifest, so the file classes tell the truth about what is on disk. Project-root agent definitions are keyed under[project].
0.1.3 - 2026-09-05
Professional-repo pass and the eight findings from the agy scored audit (overall 9.4/10; the findings are in the issue tracker's audit record).
Added
- Community files:
CONTRIBUTING.md,CODE_OF_CONDUCT.md(Contributor Covenant 2.1),MAINTAINERS.md,RELEASING.md,AGENTS.mdandCLAUDE.mdfor contributors' agents, yml issue forms with blank issues disabled, a pull request template,CODEOWNERS,.editorconfig, Dependabot for the workflow actions. test/prose.test.js: fails on an em dash anywhere in the repo's text files, so the house rule is checked rather than requested.- README badges (CI, licence, Node) and a one-line privacy statement.
Fixed
--help:--primarywas described as "level 1 only"; it applies at every level and is required when several agents qualify.docs/catalog.md: install lines now carry the same npm pin the installer uses (scripts/gen-catalog.jswent throughnpmSpec, closing the last gap #8 left open).--upgrade-runtime: the report names the runtime files it replaced; before, the files were replaced and the "runtime upgraded:" line never printed.cli-run --doctor: prints a note when the primary agent is absent from the lane list, so "1 enabled lane(s): codex" after a Claude Code + Codex install no longer reads as a missed install.templates/README.md: listed four protocols and one companion tool; there are six and two. A test now checks that table against the tree.- Agent snippets name the routing file for the level (
ORCHESTRATOR.mdat 1,ROUTING.mdat 2 and 3) instead of a conditional clause;common/README.mdsays why both files exist at level 2.
Changed
- CI:
cli-run --doctoris no longer masked with|| true; exit 0 or 10 (an enabled lane's binary absent on the runner) passes, anything else fails the job. - The tarball ships
CHANGELOG.mdandSECURITY.md(added tofiles). - CI: actions pinned to full commit SHAs with a version comment,
permissions: contents: read, one run per branch withcancel-in-progress,fail-fast: falseso one leg's failure does not hide another's result. SECURITY.mdstates a response window (7 days to acknowledge, 30 to fix or decline), that only the latest release receives fixes, and the scope.- This changelog reshaped to Keep a Changelog 2.0.0 with dated releases and compare links.
0.1.2 - 2026-09-05
Follow-up audit of 0.1.1 (issues #12 to #15).
Fixed
cli-run: SIGINT/SIGTERM to the wrapper kill the lane's process group before exiting 130/143, with the handlers registered before the spawn so a slow runner cannot signal between the two (#13).cli-run: stdout and stderr go through streaming UTF-8 decoders and limits are counted in bytes, so a multibyte character split across chunks survives (#14).cli-run:--expect-filesnapshots the target before the run and requires it to be new or changed, so a pre-existing artifact fails however recent (#15).
Changed
- Installer: three file classes.
MANIFEST.jsonrecords the generator version and a hash per generated file; runtime files (cli-run.mjs, the audit job, unit and timer, compose, gateway config, setup script) are upgraded when the installed copy is provably untouched, kept and reported as a conflict when edited, kept and reported as unverifiable when no manifest exists;--upgrade-runtimereplaces runtime files only (#12). The README discloses the machine-owned and runtime exceptions to "never overwrite".
0.1.1 - 2026-09-05
Audit follow-up (issues #1 to #10 on the repo).
Fixed
cli-run: lanes run in their own process group and the group is killed on timeout or buffer overrun (#1); the durable log stores only a fixed reason code (#4); nonzero vendor exits pass through with anexit_nonzeroverdict and a bounded stderr head on the terminal (#9).- Weekly audit: temp-and-rename so a failed rerun never truncates the last good report, failed output kept beside it (#3); every probe under a watchdog,
TimeoutStartSec=900,UNVERIFIEDlines for timed-out probes (#10). - Installer: one
npmSpechelper so the interactive install, the printed command, the table and the box script use the same pinned version (#8).
Added
cli-run:--expect-fileand--expect-jsonopt-in contracts, with the guarantee of a bare run stated exactly (#5).- Weekly audit:
--auditfor codex and an explicit boundary note for other lanes (#2). - Installer:
MANIFEST.jsonandbin/lanes.jsonare machine-owned and rewritten on every run, with a requested-vs-applied report on reconfiguration (#6). - README leads with the GitHub route pinned to the release until the npm publish, and a table stating which properties are enforced, delegated or instructions (#7, #11). CI installs the packed tarball into a clean consumer and runs it.
0.1.0 - 2026-09-04
First release.
Added
- Installer: three levels, access-aware AI selection, primary-agent loading surface, companion-tool questions (codecalc recommended, obsidian-tc optional), strict flags, containment preflight, exclusive create with rollback, no vendor scripts run.
bin/cli-run.mjs: one entrypoint for grok, codex, agy, hermes and qwen with each lane's native success signal; exit 10 on a run that produced nothing, fail-closedlanes.json, signal handling, digest-only log,--doctor.- Templates: six protocols (build, propagate, gap analysis, deep research, numbers and logic, memory and record), task bundle, single-agent and multi-lane routing, tiers, generated delegation matrix, research triage, VM tier (gateway config by env-var name, compose on loopback, box rules, privacy gates, weekly audit timer).
- Tests: a case per fix, judges proven to go red, mutation checks;
npm testprints the current count. - Adversarial audit: two Codex rounds plus a two-engine review (Codex, Antigravity); findings and fixes in
docs/audit-brief.md. After the review: subagents go to the project root (--project), snippet paths computed from--dir, lane sections rendered from the selection, a primary agent required, level 3 asks for API keys separately from CLIs, images and CLI installs pinned, an activation summary at the end of every install.