model-orchestrator 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.31] - 2026-09-23
8
+
9
+ ### Added
10
+
11
+ - **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. `--dry` and `--dry-run` preview 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.
12
+ - **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.
13
+
14
+ ### Changed
15
+
16
+ - **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.
17
+ - **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.txt` keeps reference links together and gives plans and automatic effort their own section.
18
+ - **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.
19
+
20
+ ### Fixed
21
+
22
+ - **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. `--list` prints 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.
23
+
24
+ ### Security
25
+
26
+ - **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.
27
+
28
+ ## [0.1.30] - 2026-09-22
29
+
30
+ ### Fixed
31
+
32
+ - **Every hook count now matches the files on disk ([#35](https://github.com/aunysillyme/model-orchestrator/issues/35)).** The generated `CLAUDE.snippet.md` said "Two hooks were written" and named only `route-gate.mjs` and `subagent-context.mjs`, while a claude-code install writes and wires `route-metrics.mjs` too. It now names all three and how to read the metrics log. The same pass corrects `llms.txt` and `docs/install.md` (three hooks on a full install), `templates/agents/README.md` (lists `route-metrics.mjs`), and the README plugin section, which said the plugin ships three hooks: it ships the two read-only ones, and `route-metrics` comes only with the npm install. A new test fails if the snippet names fewer hooks than the plan writes.
33
+
7
34
  ## [0.1.29] - 2026-09-21
8
35
 
9
36
  ### Changed
@@ -400,7 +427,9 @@ First release.
400
427
  - Tests: a case per fix, judges proven to go red, mutation checks; `npm test` prints the current count.
401
428
  - 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.
402
429
 
403
- [Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.29...HEAD
430
+ [Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.31...HEAD
431
+ [0.1.31]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.30...v0.1.31
432
+ [0.1.30]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.29...v0.1.30
404
433
  [0.1.29]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.28...v0.1.29
405
434
  [0.1.28]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.27...v0.1.28
406
435
  [0.1.27]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.26...v0.1.27
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/model-orchestrator.svg)](https://www.npmjs.com/package/model-orchestrator) [![test](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml/badge.svg)](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
4
4
 
5
- **A model orchestrator that sends every task to the smallest model that can do it,** so small work goes to cheap tiers and fewer tokens go to frontier models. One command reads which AIs you actually have, then writes the routing rules, the subagents and the lane runner for exactly that set: Claude Code, Codex, Gemini, Grok, Qwen, Ollama.
5
+ **A model orchestrator for AI coding agents: routing rules, subagents and a lane runner that tell your agent which model handles each task,** so small work goes to cheap tiers and fewer tokens go to frontier models. Answer a few questions and it writes the setup for exactly the AIs you have: Claude Code, Codex, Antigravity (Google), Grok, Qwen, Ollama.
6
+
7
+ **The problem:** one agent does every task on its biggest model, so renaming a file costs the same as designing a system.
8
+
9
+ **What you get:** rules your agent follows to keep planning on the frontier model and hand routine work to cheaper tiers and the other AIs you already pay for, plus a log that shows where the work went.
6
10
 
7
11
  ```bash
8
12
  npx model-orchestrator
@@ -10,7 +14,7 @@ npx model-orchestrator
10
14
 
11
15
  <img src="docs/demo.gif" alt="A terminal running npx model-orchestrator with --dry: it prints the level, the AIs detected, both target folders and all 38 files it would write, then says nothing was written." width="100%" />
12
16
 
13
- Three questions, then 38 files. The same plan as text:
17
+ A few questions, then 38 files for a three-AI setup. The same plan as text (paths shown relative to the project folder):
14
18
 
15
19
  ```text
16
20
  Plan
@@ -18,17 +22,48 @@ Plan
18
22
  access claude-code, codex, grok
19
23
  primary claude-code
20
24
  tools codecalc
25
+ plans none stated
21
26
  folder ./ai-orchestrator
22
- project . (11 subagent files go here)
27
+ project . (8 subagents + 3 hooks go here)
23
28
  files 38
24
- - ROUTING.md multi-lane decision tree
25
- - TIERS.md DELEGATION_MATRIX.md which lane, at what effort
26
- - TASK_BUNDLE.md the brief every delegation carries
27
- - protocols/ build, propagate, gap-analysis, deep-research, and three more
28
- - [project] .claude/agents/ builder, deep-planner, code-reviewer, bulk-worker,
29
- live-researcher, reader, finding-verifier, done-verifier
30
- - [project] .claude/hooks/ route-gate, subagent-context, route-metrics
31
- - bin/cli-run.mjs bin/lanes.json the lane runner
29
+ - README.md
30
+ - TASK_BUNDLE.md
31
+ - protocols/README.md
32
+ - protocols/build-protocol.md
33
+ - protocols/deep-research.md
34
+ - protocols/docs-then-prove.md
35
+ - protocols/gap-analysis.md
36
+ - protocols/memory-and-record.md
37
+ - protocols/numbers-and-logic.md
38
+ - protocols/propagate.md
39
+ - ORCHESTRATOR.md
40
+ - [project] .claude/agents/builder.md
41
+ - [project] .claude/agents/bulk-worker.md
42
+ - [project] .claude/agents/code-reviewer.md
43
+ - [project] .claude/agents/deep-planner.md
44
+ - [project] .claude/agents/done-verifier.md
45
+ - [project] .claude/agents/finding-verifier.md
46
+ - [project] .claude/agents/live-researcher.md
47
+ - [project] .claude/agents/reader.md
48
+ - CLAUDE.snippet.md
49
+ - [project] .claude/hooks/route-gate.mjs
50
+ - [project] .claude/hooks/subagent-context.mjs
51
+ - [project] .claude/hooks/route-metrics.mjs
52
+ - settings.hooks.snippet.json
53
+ - CODECALC.md
54
+ - mcp/agy.mcp_config.json
55
+ - mcp/codex.config.toml
56
+ - mcp/mcpServers.json
57
+ - mcp/vscode.mcp.json
58
+ - mcp/zed.settings.json
59
+ - CLI-RUN.md
60
+ - DELEGATION_MATRIX.md
61
+ - RESEARCH_TRIAGE.md
62
+ - ROUTING.md
63
+ - TIERS.md
64
+ - bin/cli-run.mjs
65
+ - bin/lanes.json
66
+ - MANIFEST.json
32
67
 
33
68
  --dry: nothing written.
34
69
  ```
@@ -46,7 +81,34 @@ The recording above comes from the published package under `asciinema`, rendered
46
81
  - **Use it when:** you run more than one model or agent and want the expensive tier kept for planning and judgment.
47
82
  - **For agents:** [`llms.txt`](llms.txt) summarizes the package and links every doc; [`AGENTS.md`](AGENTS.md) has the headless commands.
48
83
 
49
- Built from a working system: the routing rules, the protocols and the lane runner here run in production every day, generalized so they transfer to any stack.
84
+ ## After you install
85
+
86
+ For the Claude Code setup above, follow the activation summary from the project folder:
87
+
88
+ 1. **Rules:** copy the block in `ai-orchestrator/CLAUDE.snippet.md` into `CLAUDE.md` (create it if missing).
89
+ 2. **Hooks:** merge `ai-orchestrator/settings.hooks.snippet.json` into `.claude/settings.json` (create it if missing).
90
+ 3. **Smoke test:** run `node ./ai-orchestrator/bin/cli-run.mjs --doctor` to check the enabled lanes. Add `--run` to send each lane one tiny prompt.
91
+
92
+ Run `claude` from the project folder to load the subagents. Follow the sign-in and companion-tool steps printed for your selection; the same steps are saved in `ai-orchestrator/README.md`.
93
+
94
+ A real call through the lane runner, captured from a fresh install on 2026-09-23 (Codex CLI 0.154.0). Your agent sends a small read to Codex at low effort, and `cli-run` prints one status line with the route it used:
95
+
96
+ ```text
97
+ $ node ai-orchestrator/bin/cli-run.mjs codex "In one sentence, what is ai-orchestrator/ROUTING.md for?" --effort low
98
+ cli-run[codex] ok rc=0 class=ok refused=null 18.9s raw=20418B route=lane default/low :: turn.completed
99
+ ```
100
+
101
+ Each call also appends one line to `~/.ai-orchestrator/cli-run.log.jsonl` with the lane, the model and effort requested and resolved, the verdict, the exit code, the seconds and the deliverable size, so you can see where the work went. A run that produces no deliverable exits non-zero: on the same install, `--expect-file summary.md` for a file the lane never wrote printed `no_deliverable rc=10 class=empty` and a fix line.
102
+
103
+ ## Part of a set
104
+
105
+ Three open-source tools that work on their own and fit together:
106
+
107
+ | Repo | What it gives you |
108
+ |---|---|
109
+ | [agent-personalizer](https://github.com/aunysillyme/agent-personalizer) | One interview writes the profile and rules every AI you use reads, kept in sync from one source. |
110
+ | **model-orchestrator** | Routing rules that tell your agent which model handles each task, so frontier models do the hard work and cheaper tiers do the rest. |
111
+ | [website-build-skill](https://github.com/aunysillyme/website-build-skill) | A skill pack that teaches your AI current website-building expertise: research, design, code, accessibility, performance, search and security. |
50
112
 
51
113
  ## The three levels
52
114
 
@@ -58,6 +120,7 @@ Built from a working system: the routing rules, the protocols and the lane runne
58
120
  | **3 · Advanced** | a virtual machine | everything above, plus a gateway config rendered from the API keys you hold (asked separately from your CLIs), pinned images, box rules, privacy gates, and a weekly gap-analysis job with "what watches it" written down |
59
121
 
60
122
  Levels explained: [Part 1](docs/part-1-beginner.md) · [Part 2](docs/part-2-intermediate.md) · [Part 3](docs/part-3-advanced.md).
123
+
61
124
  ## The AIs it knows about
62
125
 
63
126
  | Id | What | Level |
@@ -69,13 +132,13 @@ Levels explained: [Part 1](docs/part-1-beginner.md) · [Part 2](docs/part-2-inte
69
132
  | `hermes` | Hermes Agent: the free tier | 2+ |
70
133
  | `qwen` | Qwen Code CLI with a cheap metered model: structured bulk | 2+ |
71
134
  | `ollama` | local models: the privacy lane | 2+ |
72
- | `claude-app`, `chatgpt-app`, `gemini-app` | chat apps with no CLI: level 1 via a paste block | 1 |
135
+ | `claude-app`, `chatgpt-app`, `gemini-app` | chat apps: level 1 via a paste block | 1 |
73
136
 
74
137
  `npx model-orchestrator --list` prints the catalog with install and sign-in notes. Details: [docs/catalog.md](docs/catalog.md).
75
138
 
76
139
  ## Measuring routing
77
140
 
78
- A routing rule nobody measures is a rule nobody knows is followed. On a claude-code install, `route-metrics.mjs` turns every turn, dispatch and subagent start/stop into one JSON line under `~/.ai-orchestrator/route-metrics.jsonl`, including the lane your agent named in its own `<!-- route: <lane> | <why> -->` marker.
141
+ See where your agent sends the work. On a claude-code install, `route-metrics.mjs` turns every turn, dispatch and subagent start/stop into one JSON line under `~/.ai-orchestrator/route-metrics.jsonl`, including the lane your agent named in its own `<!-- route: <lane> | <why> -->` marker.
79
142
 
80
143
  ```bash
81
144
  node .claude/hooks/route-metrics.mjs --summary # since the log began
@@ -93,17 +156,17 @@ The hooks and subagents also ship as a plugin, so they install and update throug
93
156
  /plugin install model-orchestrator@model-orchestrator
94
157
  ```
95
158
 
96
- It ships the three hooks and the eight subagents, each with an explicit tool list, and loads them namespaced as `model-orchestrator:builder`. The routing rules come from `npx model-orchestrator`, which is the step that reads your setup and writes rules to match it. `plugin/` is generated from `templates/`, and `test/plugin.test.js` holds the bundle to that shape: committed output matches the generator, hooks stay read-only, every agent keeps its tool list. Details: [plugin/README.md](plugin/README.md).
159
+ It ships the two read-only hooks (`route-gate`, `subagent-context`) and the eight subagents, each with an explicit tool list, and loads them namespaced as `model-orchestrator:builder`. The routing rules come from `npx model-orchestrator`, which is the step that reads your setup and writes rules to match it. `plugin/` is generated from `templates/`, and `test/plugin.test.js` holds the bundle to that shape: committed output matches the generator, hooks stay read-only, every agent keeps its tool list. The third hook, `route-metrics`, writes a log, so it comes only with the npm install. Details: [plugin/README.md](plugin/README.md).
97
160
 
98
161
  ## Companion tools (all optional)
99
162
 
100
163
  An orchestrator routes work. Three companion tools cover the rest of what a working agent needs, exact numbers, a memory, and current library docs:
101
164
 
102
- | Tool | Closes | Default |
165
+ | Tool | What it gives you | Set up |
103
166
  |---|---|---|
104
- | [codecalc](https://github.com/The-40-Thieves/codecalc) | guessed numbers: exact arithmetic, code execution in 31 languages, logic checks; offline, no key | yes |
105
- | [obsidian-tc](https://github.com/The-40-Thieves/obsidian-tc) | no durable memory: hybrid search, backlinks, compare-and-swap writes; local by default | no |
106
- | [Context7](https://github.com/upstash/context7) | stale library recall: current, version-specific docs pulled into the prompt | no |
167
+ | [codecalc](https://github.com/The-40-Thieves/codecalc) | exact arithmetic, code execution in 31 languages and logic checks, running offline | by default |
168
+ | [obsidian-tc](https://github.com/The-40-Thieves/obsidian-tc) | durable memory with hybrid search, backlinks and compare-and-swap writes; local by default | when you pick it |
169
+ | [Context7](https://github.com/upstash/context7) | current, version-specific library docs pulled into the prompt | when you pick it |
107
170
 
108
171
  Selecting one writes a doc and the config snippets for your agents, so the install stays yours to run. What each needs first, and how Context7 and codecalc pair up (docs say what an API should do, a run proves what it does): [docs/companions.md](docs/companions.md). Every level carries the three rules they serve either way: `protocols/numbers-and-logic.md`, `protocols/memory-and-record.md` and `protocols/docs-then-prove.md`.
109
172
 
@@ -119,23 +182,29 @@ Selecting one writes a doc and the config snippets for your agents, so the insta
119
182
 
120
183
  ## Common questions
121
184
 
122
- ### How do I cut token usage across Claude Code, Codex and Gemini?
185
+ ### How do I cut token usage across Claude Code, Codex and Antigravity (Google)?
123
186
 
124
187
  Install for the tools you have, then let the generated `ROUTING.md` decide the tier per task: bulk, reading and verification go to the fast tier or a cheaper CLI lane, and the deep tier only plans and judges. On Claude Code, execution goes to the `builder` subagent by default and the main session plans and verifies. Every lane call through `cli-run` logs the model and effort it ran with, so you can check where the tokens went.
125
188
 
126
189
  ### How do I route tasks to cheaper models?
127
190
 
128
- The rules route by role, complexity and stakes (see [Routing by role, complexity and stakes](#routing-by-role-complexity-and-stakes)). Role picks the agent, complexity moves the effort, stakes move the tier. A task a cheap tier finishes correctly stays on the cheap tier, and the frontier tokens go to the work that earns them.
191
+ The rules route by role, complexity and stakes (see [Routing by role, complexity and stakes](docs/how-it-routes.md#routing-by-role-complexity-and-stakes)). Role picks the agent, complexity moves the effort, stakes move the tier. A task a cheap tier finishes correctly stays on the cheap tier, and the frontier tokens go to the work that earns them.
129
192
 
130
193
  ### Where does this sit next to an LLM router or an AI gateway?
131
194
 
132
195
  One layer up, and they compose. This routes at the task level, through instructions your agent follows and a runner for agent CLIs. Request-level routers and gateways (RouteLLM, LiteLLM, OpenRouter, claude-code-router) forward the model on every API call, and they sit underneath this happily: pick the lane here, let the gateway carry the call.
133
196
 
134
- ### Can an agent install and run it without a person?
197
+ ### How does an agent install and run it headlessly?
135
198
 
136
- Yes. `--yes` with `--level`, `--ais` and `--project` runs headless, `--dry-run` previews the plan, and `--list` prints every supported AI. Your existing files stay as they are: activation snippets land beside them, ready to merge when you choose.
199
+ Use the CLI flags. `--yes` with `--level`, `--ais` and `--project` runs headless, `--dry-run` previews the plan, and `--list` prints every supported AI. Your own documents are kept unless you pass `--force`; activation snippets are ready to merge. `MANIFEST.json` and `bin/lanes.json` are rewritten each run. Runtime files upgrade when they match the recorded hash; edited copies are kept and named. See [re-running an install](docs/install.md#what-a-run-does) for `--update-docs` and `--upgrade-runtime`.
137
200
 
138
201
 
202
+ ## Uninstall
203
+
204
+ Remove unedited files recorded by the installer; edited files stay and are listed.
205
+ Run `npx model-orchestrator --uninstall --dir ./ai-orchestrator --project .` (add `--dry` to preview).
206
+ Remove the pasted rules block and merged hooks entry by hand. [Removal details](docs/install.md#uninstall).
207
+
139
208
  ## Read next
140
209
 
141
210
  | Doc | What is in it |
@@ -150,14 +219,14 @@ Yes. `--yes` with `--level`, `--ais` and `--project` runs headless, `--dry-run`
150
219
  <summary><strong>Platform support, and every test this suite skips</strong></summary>
151
220
 
152
221
 
153
- Node 18 or newer. No dependencies. Works on macOS and Linux; the level 3 box templates assume Ubuntu. Windows: CI runs the suite on `windows-latest` (Node 18, 20, 22), including lane execution end to end through `cli-run` against a fake CLI installed the same way npm installs a real one (a `.cmd` shim). `cli-run` never runs a lane through `cmd.exe` when it can avoid it: it resolves the shim to the Node script underneath and spawns Node directly, so a prompt reaching a real lane never passes through a Windows shell. A `.cmd` or `.bat` lane that cannot be resolved that way (an old or hand-edited shim) is refused with exit 13 and a message saying how to fix it, rather than run through `cmd.exe`: a batch file re-reads its arguments after `cmd.exe` has parsed them once, and no escaping fully contains a prompt through both passes. Install, detection, the hooks and `cli-run`'s `taskkill` tree kill are tested on Windows too, including SIGTERM/SIGINT to the wrapper (Windows has no OS-level signals: both terminate it unconditionally, verified there rather than treated the same as POSIX). Five narrow skips remain on Windows, each for a POSIX behavior the OS or the CI shell genuinely does not have, and each named here because a test that is quietly skipped reads as a test that passed: `statSync().mode`'s executable bit (NTFS has none, so that one assertion is conditional inside a test that otherwise runs everywhere); a lane dying mid-run from a real POSIX signal (a real Windows lane cannot die "by signal"); running `weekly-audit.sh`'s watchdog functions for real under Git Bash's job control, both the end-to-end run and the `bounded()` timeout check (the script itself only ever runs on the Ubuntu box it targets); and a `mkfifo` FIFO at the rules path, the one case that proves `route-gate.mjs` cannot HANG on a non-regular file, since Windows has no `mkfifo` to build one (the guard behind it is covered on every OS by a directory at the same path); and an untracked `mkfifo` FIFO in the repository `cli-run --audit` sizes, the case that proves `--effort auto` never opens a non-regular file (the symlink half of that test runs on every OS). The list is not prose on trust: `test/prose.test.js` counts every `skip:` in the suite and fails if one of them is not documented here.
222
+ Node 18 or newer, with zero runtime dependencies. Works on macOS and Linux; the level 3 box templates assume Ubuntu. Windows: CI runs the suite on `windows-latest` (Node 18, 20, 22), including lane execution end to end through `cli-run` against a fake CLI installed the same way npm installs a real one (a `.cmd` shim). `cli-run` never runs a lane through `cmd.exe` when it can avoid it: it resolves the shim to the Node script underneath and spawns Node directly, so a prompt reaching a real lane never passes through a Windows shell. A `.cmd` or `.bat` lane that cannot be resolved that way (an old or hand-edited shim) is refused with exit 13 and a message saying how to fix it, rather than run through `cmd.exe`: a batch file re-reads its arguments after `cmd.exe` has parsed them once, and no escaping fully contains a prompt through both passes. Install, detection, the hooks and `cli-run`'s `taskkill` tree kill are tested on Windows too, including SIGTERM/SIGINT to the wrapper (Windows has no OS-level signals: both terminate it unconditionally, verified there rather than treated the same as POSIX). The Windows skip list covers POSIX behavior, with each skip pinned by `test/prose.test.js`: `statSync().mode`'s executable bit (NTFS has none, so that one assertion is conditional inside a test that otherwise runs everywhere); a lane dying mid-run from a real POSIX signal (a real Windows lane cannot die "by signal"); running `weekly-audit.sh`'s watchdog functions for real under Git Bash's job control, both the end-to-end run and the `bounded()` timeout check (the script itself only ever runs on the Ubuntu box it targets); and a `mkfifo` FIFO at the rules path, the one case that proves `route-gate.mjs` cannot HANG on a non-regular file, since Windows has no `mkfifo` to build one (the guard behind it is covered on every OS by a directory at the same path); and an untracked `mkfifo` FIFO in the repository `cli-run --audit` sizes, the case that proves `--effort auto` never opens a non-regular file (the symlink half of that test runs on every OS). `test/prose.test.js` counts every `skip:` in the suite and requires this list to document each one.
154
223
 
155
224
  **Privacy.** The installer sends no telemetry and makes no network call of its own once it is running. Two things around that are worth being exact about:
156
225
 
157
226
  - `npx model-orchestrator` is itself a download: npm fetches this package from the registry before any of it runs. `npm install -g model-orchestrator` once, then run `model-orchestrator`, if you would rather that happen exactly one time.
158
- - A missing vendor CLI is *printed*, not installed. In an interactive run the installer offers to run one pinned `npm install -g` per package and only runs the ones you answer yes to; with `--yes` or `--no-install` it answers no for you and prints the command instead. Vendor shell installers (Antigravity, Grok) are only ever printed, alongside the `curl … | less` you would use to read one before running it.
227
+ - For a missing vendor CLI, the installer prints the install command. An interactive run offers to run one pinned `npm install -g` per package with your confirmation; `--yes` and `--no-install` keep installation in your hands. Vendor shell installers (Antigravity, Grok) are only ever printed, alongside the `curl … | less` you would use to read one before running it.
159
228
 
160
- `cli-run` talks to nothing but the vendor CLI you name.
229
+ `cli-run` calls the vendor CLI you name.
161
230
 
162
231
 
163
232
  </details>
@@ -166,7 +235,7 @@ Node 18 or newer. No dependencies. Works on macOS and Linux; the level 3 box tem
166
235
  <summary><strong>Vendor version compatibility</strong></summary>
167
236
 
168
237
 
169
- **This package detects that a binary exists. It does not check its version, and a present binary is not a working lane.** `--doctor` reports presence, and with `--run` sends one lane a one-word canary; neither validates that the vendor's flags, output shape or auth still match what the generated files assume.
238
+ **Detection checks whether a binary is present.** Use the compatibility table below to compare vendor versions. `--doctor` reports presence; add `--run` to send every enabled lane a small canary and check its sign-in and output against the runner's success criteria.
170
239
 
171
240
  The lane wiring and the output judges were written against these versions, which are the ones this release was exercised on:
172
241
 
@@ -186,18 +255,24 @@ Generated from `src/catalog.js` by `npm run gen:catalog`; `npm test` fails if th
186
255
 
187
256
  <!-- vendor-table:end -->
188
257
 
189
- One number per lane, and it is the same number the installer pins: where a lane installs from npm, `builtAgainst` in the catalog *is* the pin, so "built against" and "pinned to" can never be two answers. That pin is a floor, not a ceiling: these CLIs ship breaking flag changes on their own schedules, so a newer version may work perfectly, or may change a flag the generated wiring passes. When a lane starts failing after a vendor upgrade, compare against this table first.
258
+ For npm-installed lanes, `builtAgainst` in the catalog supplies both the compatibility table and the install pin. Newer vendor versions may work or may change a flag the generated wiring uses. When a lane starts failing after a vendor upgrade, compare against this table first.
190
259
 
191
260
  **The live canary runs on your machine, with your credentials.** That is what `node bin/cli-run.mjs --doctor --run` is: it sends every enabled lane one tiny prompt through your own sign-ins and reports `canary ok` or `canary FAILED rc=` per lane. Run it after install, and again after any vendor upgrade.
192
261
 
193
- It deliberately does not run in this repository's CI. A canary is only meaningful against real credentials, and there are no credentials a maintainer could supply that would tell **you** anything about **your** lanes: your sign-ins, your quota, your vendor versions. A maintainer-credential canary in CI would prove one machine works and bill someone per run to do it. So CI runs the full suite against stub lanes on Ubuntu, macOS and Windows, Node 18/20/22, plus a packaged install into a clean consumer, and the live check ships to you instead.
262
+ CI runs the full suite against stub lanes on Ubuntu, macOS and Windows, Node 18/20/22, plus a packaged install into a clean consumer. Run the live check locally to verify your own sign-ins, quota and vendor versions.
194
263
 
195
264
 
196
265
  </details>
197
266
 
198
267
  ## Contributing
199
268
 
200
- Add an AI to `src/catalog.js` and every prompt, table, config and doc picks it up. Run `npm test`. Keep templates free of logic and free of anything that looks like a credential. The rest is in [CONTRIBUTING.md](CONTRIBUTING.md); releases in [RELEASING.md](RELEASING.md); security reports in [SECURITY.md](SECURITY.md).
269
+ Contributions are welcome:
270
+
271
+ - **New AIs:** add an entry to `src/catalog.js`; prompts, tables, configs and docs use the catalog.
272
+ - **Vendor updates:** contribute a lane fixture captured from a newer vendor version and the test that checks it.
273
+ - **Docs:** fix an unclear instruction or add a reproducible example.
274
+
275
+ Run `npm test` with your change. Keep templates free of logic and credential values. See [CONTRIBUTING.md](CONTRIBUTING.md), [RELEASING.md](RELEASING.md) and [SECURITY.md](SECURITY.md).
201
276
 
202
277
  ## Credits
203
278
 
package/bin/cli.js CHANGED
@@ -17,13 +17,14 @@ import { planFiles, writeFiles, resolveSelection, resolveTools, resolveApis, dir
17
17
  // duplicated: resolve npm's own cmd-shim and run node on it directly, no
18
18
  // shell, or fall back to the escaped cmd.exe path it also provides.
19
19
  import { windowsSpawnPlan } from './cli-run.mjs';
20
+ import { uninstallFiles } from '../src/uninstall.js';
20
21
 
21
22
  // One strict parse. Unknown flags, missing values and duplicates are usage
22
23
  // errors (exit 2) before anything is planned, so a typo like --dryy can never
23
24
  // turn a dry run into a real one.
24
25
  const SPEC = {
25
26
  level: 'value', ais: 'value', primary: 'value', dir: 'value', project: 'value', tools: 'value', apis: 'value', plans: 'value',
26
- yes: 'bool', force: 'bool', dry: 'bool', 'dry-run': 'bool', 'no-install': 'bool', 'no-tools': 'bool', 'no-apis': 'bool', 'effort-auto': 'bool', 'upgrade-runtime': 'bool', 'update-docs': 'bool', list: 'bool', help: 'bool', h: 'bool', version: 'bool', v: 'bool'
27
+ yes: 'bool', force: 'bool', dry: 'bool', 'dry-run': 'bool', uninstall: 'bool', 'no-install': 'bool', 'no-tools': 'bool', 'no-apis': 'bool', 'effort-auto': 'bool', 'upgrade-runtime': 'bool', 'update-docs': 'bool', list: 'bool', help: 'bool', h: 'bool', version: 'bool', v: 'bool'
27
28
  };
28
29
  export function parseArgs(argv) {
29
30
  const out = {};
@@ -94,6 +95,7 @@ if (flag('help') || flag('h')) {
94
95
  Usage
95
96
  npx model-orchestrator interactive
96
97
  npx model-orchestrator --list show the AI catalog and exit
98
+ npx model-orchestrator --uninstall --dir <dir> --project <project>
97
99
  npx model-orchestrator --yes --level 2 --ais claude-code,codex,grok [--primary claude-code] [--dir ./ai-orchestrator] [--project .]
98
100
 
99
101
  Flags
@@ -116,6 +118,7 @@ Flags
116
118
  --update-docs regenerate the documents a previous run wrote and nobody edited since (hash-checked against
117
119
  MANIFEST.json), so a changed selection reaches ROUTING.md and friends; edited documents are kept
118
120
  and reported, and nothing happens without a manifest
121
+ --uninstall remove unedited managed files recorded in MANIFEST.json; keep and list edited files
119
122
  --dry, --dry-run print the plan, write nothing
120
123
  --no-install never offer to run npm installs
121
124
  --list print the catalog
@@ -131,6 +134,12 @@ if (flag('list')) {
131
134
  for (const a of AIS) {
132
135
  const here = a.bin ? (which(a.bin) ? 'installed' : 'not on PATH') : 'app';
133
136
  console.log(`${a.id.padEnd(13)} ${a.name}\n${''.padEnd(13)} level ${a.minLevel}+ · ${a.access} · ${here}\n${''.padEnd(13)} ${a.role}`);
137
+ const install = a.install.npm
138
+ ? `npm install -g ${npmSpec(a)}`
139
+ : a.install.script
140
+ ? `curl -fsSL ${a.install.script} -o /tmp/${a.id}-install.sh && less /tmp/${a.id}-install.sh && bash /tmp/${a.id}-install.sh`
141
+ : a.install.url + (a.install.brew ? ` (or: brew install ${a.install.brew})` : '');
142
+ console.log(`${''.padEnd(13)} install: ${install}\n${''.padEnd(13)} sign in: ${a.auth}`);
134
143
  if (a.plans) for (const p of a.plans) console.log(`${''.padEnd(13)} plan ${p.id}: ${p.name} (${p.headroom} headroom, checked ${p.checked}, ${p.source})`);
135
144
  }
136
145
  console.log('\nmetered API providers (--apis a,b, level 3 gateway only):');
@@ -141,7 +150,7 @@ if (flag('list')) {
141
150
  }
142
151
 
143
152
  const yes = flag('yes');
144
- const rl = yes ? null : makeAsker({ input: stdin, output: stdout });
153
+ const rl = yes || flag('uninstall') ? null : makeAsker({ input: stdin, output: stdout });
145
154
  const ask = (q, fallback) => (rl ? rl.ask(q, fallback) : Promise.resolve(fallback));
146
155
 
147
156
  function bad(msg) {
@@ -173,6 +182,20 @@ function plansFromManifest(manifest, selected) {
173
182
  }
174
183
 
175
184
  async function main() {
185
+ if (flag('uninstall')) {
186
+ const allowed = new Set(['uninstall', 'dir', 'project', 'dry', 'dry-run', 'yes']);
187
+ const incompatible = Object.keys(parsed.out).filter((name) => !allowed.has(name));
188
+ if (incompatible.length) bad(`--uninstall cannot be combined with ${incompatible.map((name) => '--' + name).join(', ')}`);
189
+ const dir = resolve(opt('dir') || './ai-orchestrator');
190
+ const project = resolve(opt('project') || '.');
191
+ const dry = flag('dry') || flag('dry-run');
192
+ const actions = uninstallFiles({ dir, project, dry });
193
+ console.log(dry ? 'Uninstall preview (--dry): nothing changed.' : 'Uninstall complete.');
194
+ for (const action of actions) console.log(action);
195
+ console.log('\nManual steps: remove the pasted model-orchestrator block from CLAUDE.md and its merged hook entries from .claude/settings.json. Keep your other rules and hooks.');
196
+ console.log('For another primary agent, remove its pasted activation block from its rules file.');
197
+ return;
198
+ }
176
199
  // Says what this generates, not what it guarantees. The old line promised
177
200
  // routing this package does not perform: lane choice is an instruction an
178
201
  // agent follows, never something enforced here (#11).
@@ -346,11 +369,15 @@ async function main() {
346
369
  const files = planFiles({ level, selected, primary, dir, project, tools, apis, plans, effortAuto });
347
370
  const lvl = LEVELS.find((l) => l.id === level);
348
371
  const agentFiles = files.filter((f) => f.root === 'project');
372
+ const projectKinds = [
373
+ [agentFiles.filter((f) => primary?.agentsDir && toPosixRel(f.rel).startsWith(primary.agentsDir + '/')).length, 'subagents'],
374
+ [agentFiles.filter((f) => toPosixRel(f.rel).startsWith('.claude/hooks/')).length, 'hooks']
375
+ ].filter(([count]) => count).map(([count, kind]) => `${count} ${kind}`).join(' + ');
349
376
  const statedPlans = Object.entries(plans).map(([id, p]) => `${id}=${p.id}`).join(', ');
350
- console.log(`\nPlan\n level ${lvl.id} ${lvl.name}\n access ${selected.map((a) => a.id).join(', ')}\n primary ${primary ? primary.id : 'none'}${primaryAutoPicked ? ` (chosen for you from ${candidates.map((a) => a.id).join(', ')}; pass --primary to decide it yourself)` : ''}\n tools ${tools.map((t) => t.id).join(', ') || 'none'}\n plans ${statedPlans || 'none stated'}` + (level >= 3 ? `\n api keys ${apis.map((p) => p.id).join(', ') || 'none'}` : '') + `\n folder ${dir}\n project ${project}${agentFiles.length ? ' (' + agentFiles.length + ' subagent files go here)' : ''}\n files ${files.length}`);
377
+ console.log(`\nPlan\n level ${lvl.id} ${lvl.name}\n access ${selected.map((a) => a.id).join(', ')}\n primary ${primary ? primary.id : 'none'}${primaryAutoPicked ? ` (chosen for you from ${candidates.map((a) => a.id).join(', ')}; pass --primary to decide it yourself)` : ''}\n tools ${tools.map((t) => t.id).join(', ') || 'none'}\n plans ${statedPlans || 'none stated'}` + (level >= 3 ? `\n api keys ${apis.map((p) => p.id).join(', ') || 'none'}` : '') + `\n folder ${dir}\n project ${project}${projectKinds ? ' (' + projectKinds + ' go here)' : ''}\n files ${files.length}`);
351
378
  if (plansKept) console.log(' plans kept from the previous run');
352
379
  if (agentFiles.length && !opt('project')) {
353
- console.log(`\nNote: --project was not given, so the ${agentFiles.length} subagent file(s) go to the current directory (${project}). Pass --project to put them somewhere else.`);
380
+ console.log(`\nNote: --project defaults to the current directory, so the ${projectKinds} go to the current directory (${project}). Pass --project to put them somewhere else.`);
354
381
  }
355
382
  if (level >= 2 && !selected.some((a) => a.cliRun)) {
356
383
  console.log('\nWarning: no executable lanes selected; delegation is inactive. Use level 1 for a single-agent setup, or add a supported CLI. Doctor will exit 13 until a lane is enabled.');
@@ -471,5 +498,5 @@ function isEntryPoint() {
471
498
  }
472
499
  if (isEntryPoint()) main().catch((e) => {
473
500
  console.error('model-orchestrator: ' + (e && e.message ? e.message : e));
474
- process.exit(e && e.code === 'EOF' ? 2 : 1);
501
+ process.exit(e && ['EOF', 'UNINSTALL'].includes(e.code) ? 2 : 1);
475
502
  });
package/docs/install.md CHANGED
@@ -11,7 +11,7 @@ The installer asks a few things, then writes a folder:
11
11
  2. **Which AIs do you have access to?** (it marks the ones already on your PATH)
12
12
  3. **Which one is your primary agent?** (the one that runs the system)
13
13
 
14
- It never writes a secret, never runs a vendor shell script for you, and never overwrites a document you already have unless you pass `--force`. Two exceptions, both stated when they happen: `MANIFEST.json` and `bin/lanes.json` are machine-owned and rewritten on every run so a changed selection applies; runtime files (`cli-run`, the audit job, compose, gateway config, setup script) are upgraded when the installed copy matches the hash a previous run recorded, kept and reported as a conflict when you edited them, and kept as unverifiable when no manifest exists (`--upgrade-runtime` replaces runtime files only). The same hash rule is available for documents on request: `--update-docs` regenerates the documents a previous run wrote and nobody edited, so a changed selection reaches `ROUTING.md` and the delegation matrix without `--force`; edited documents are kept and named. Docs and protocols go to `--dir` (default `./ai-orchestrator`); subagent definitions (and, on Claude Code, two hook scripts) go to the project root your agent runs from (`--project`, default the current directory), because that is the only place Claude Code and Antigravity read them. It ends with an activation summary: what to copy where, which sign-ins, and one smoke command. Uninstall: follow the generated README. Inspect the manifest and remove only the individual managed subagent files you no longer need, preserve edited or pre-existing files, and remove your manually pasted activation block. Never delete a shared subagent folder.
14
+ It never writes a secret, never runs a vendor shell script for you, and never overwrites a document you already have unless you pass `--force`. Two exceptions, both stated when they happen: `MANIFEST.json` and `bin/lanes.json` are machine-owned and rewritten on every run so a changed selection applies; runtime files (`cli-run`, the audit job, compose, gateway config, setup script) are upgraded when the installed copy matches the hash a previous run recorded, kept and reported as a conflict when you edited them, and kept as unverifiable when no manifest exists (`--upgrade-runtime` replaces runtime files only). The same hash rule is available for documents on request: `--update-docs` regenerates the documents a previous run wrote and nobody edited, so a changed selection reaches `ROUTING.md` and the delegation matrix without `--force`; edited documents are kept and named. Docs and protocols go to `--dir` (default `./ai-orchestrator`); subagent definitions (and, on Claude Code, three hook scripts) go to the project root your agent runs from (`--project`, default the current directory), because that is the only place Claude Code and Antigravity read them. It ends with an activation summary: what to copy where, which sign-ins, and one smoke command. Use `--uninstall` to remove unedited managed files, then remove your manually merged activation entries. See [Uninstall](#uninstall).
15
15
 
16
16
  ## Plans and automatic effort
17
17
 
@@ -25,7 +25,7 @@ An install has two targets, and a scripted run should set both.
25
25
  | `--dir` | `./ai-orchestrator` | the docs, protocols and (level 2+) `bin/cli-run.mjs`. Named after what it contains, not after this package, so a project can hold one without looking like a checkout of it. Pass `--dir ./model-orchestrator` if you prefer the package name. |
26
26
  | `--project` | the current directory | the subagent definitions, and the rules file your agent reads. Only Claude Code (`.claude/agents/`) and Antigravity (`.agents/agents/`) get files here, because that is the only place those CLIs look. Claude Code also gets three hook scripts in `.claude/hooks/`, wired by a settings snippet you merge yourself. |
27
27
 
28
- `--project` defaulting to the current directory is the one that surprises people: run the command from your home folder with Claude Code as the primary and five agent files land in your home folder. The installer prints the resolved project path in the plan and says when you left it at the default. Set it.
28
+ `--project` defaulting to the current directory is the one that surprises people: run the command from your home folder with Claude Code as the primary and the subagent and hook files land in your home folder. The installer prints the resolved project path in the plan and says when you left it at the default. Set it.
29
29
 
30
30
  ## Non-interactive
31
31
 
@@ -41,6 +41,29 @@ npx model-orchestrator --yes --level 2 --ais claude-code,codex --project ~/my-ap
41
41
  npx model-orchestrator --yes --level 2 --ais claude-code,codex,grok --primary claude-code --dir ./ai-orchestrator --project . --update-docs # added a lane: regenerate the docs you never edited
42
42
  ```
43
43
 
44
+ ## Uninstall
45
+
46
+ Use the same `--dir` and `--project` paths you installed with. The uninstall reads `MANIFEST.json` under `--dir` and checks every recorded path before removing any file.
47
+
48
+ ```bash
49
+ # Preview the exact removals and kept files.
50
+ npx model-orchestrator --uninstall --dir ./ai-orchestrator --project ./my-app --dry
51
+ # Remove the files whose content still matches the recorded hash.
52
+ npx model-orchestrator --uninstall --dir ./ai-orchestrator --project ./my-app
53
+ ```
54
+
55
+ - **Unedited files:** removed when their content hash matches the manifest.
56
+ - **Edited files:** kept and listed by path, with the manifest retained so you can review them.
57
+ - **Other files:** anything outside the manifest stays, including your own files in shared `.claude/agents/` and `.claude/hooks/` folders.
58
+ - **Directories:** removed only when the manifest records that the installer created them and they are empty after removal. Older manifests leave directories in place because they carry no directory ownership record.
59
+ - **Manifest:** removed last, only when every managed file has been removed.
60
+ - **Path safety:** an absolute path, traversal entry, or symlink is refused before any removal. Entries must stay inside their declared `--dir` or `--project` root.
61
+ - **Missing manifest:** exits 2 and names the expected `MANIFEST.json` path.
62
+ - **Target paths:** `--dir` and `--project` must match the installation paths in the manifest. A mismatch exits 2 before removal.
63
+ - **Preview:** `--dry` (or `--dry-run`) lists the planned removals and writes nothing.
64
+
65
+ The command prints the remaining manual steps: remove the pasted model-orchestrator block from `CLAUDE.md` and its merged hook entries from `.claude/settings.json`. Keep your other rules and hooks. With another primary agent, remove its pasted activation block from the corresponding rules file.
66
+
44
67
  ## What gets written (level 3, everything)
45
68
 
46
69
  ```
@@ -70,4 +93,3 @@ ai-orchestrator/
70
93
  | [`docs/`](docs/README.md) | the three parts and the catalog |
71
94
  | [`plugin/`](plugin/README.md) | the Claude Code plugin, generated from `templates/` by `npm run gen:plugin`; `.claude-plugin/marketplace.json` at the root lists it |
72
95
  | [`test/`](test/README.md) | `npm test`: judges proven to go red, catalog integrity, planner, end-to-end install in a temp dir; `.github/workflows/test.yml` runs it on Ubuntu, macOS and Windows, Node 18/20/22 |
73
-
package/llms.txt CHANGED
@@ -1,12 +1,12 @@
1
1
  # model-orchestrator
2
2
 
3
- > Model orchestrator for AI coding agents and LLMs (Claude Code, Codex, Gemini, Grok, Qwen, Ollama). One installer writes routing rules, subagent definitions and a CLI lane runner for the AI tools you already have. The rules tell your agent which model, subagent or CLI to use for each task, so small work goes to cheap tiers and fewer tokens go to frontier models. It sits above the request layer: your agent reads the rules and picks the lane, so the decision stays readable, versioned and editable. Request-level routers and gateways compose underneath it.
3
+ > Model orchestrator for AI coding agents and LLMs (Claude Code, Codex, Antigravity (Google), Grok, Qwen, Ollama). One installer writes routing rules, subagent definitions and a CLI lane runner for the AI tools you already have. The rules tell your agent which model, subagent or CLI to use for each task, so small work goes to cheap tiers and fewer tokens go to frontier models. It sits above the request layer: your agent reads the rules and picks the lane, so the decision stays readable, versioned and editable. Request-level routers and gateways compose underneath it.
4
4
 
5
5
  Install and run: `npx model-orchestrator` (interactive), or headless: `npx model-orchestrator --yes --level 2 --ais claude-code,codex --project . --dir ./ai-orchestrator`. Preview without writing: add `--dry-run`. List every supported AI: `npx model-orchestrator --list`. Node 18 or newer, zero runtime dependencies, MIT licence.
6
6
 
7
- Levels: 1 beginner (one agent or chat app), 2 intermediate (several agent CLIs, each called through `cli-run`), 3 advanced (adds a virtual machine with a gateway and a scheduled audit job). On Claude Code the install also delegates execution to subagents by default and ships two hooks (UserPromptSubmit, SubagentStart) that inject the routing table every turn.
7
+ Levels: 1 beginner (one agent or chat app), 2 intermediate (several agent CLIs, each called through `cli-run`), 3 advanced (adds a virtual machine with a gateway and a scheduled audit job). On Claude Code the install also delegates execution to subagents by default and ships three hooks: `route-gate` (UserPromptSubmit) and `subagent-context` (SubagentStart) inject the routing table every turn, and `route-metrics` logs route markers and subagent dispatches.
8
8
 
9
- Claude Code plugin: `/plugin marketplace add aunysillyme/model-orchestrator`, then `/plugin install model-orchestrator@model-orchestrator`. It installs the two hooks and eight subagents; the routing rules still come from `npx model-orchestrator`, and the routing log (`route-metrics.mjs`) is npm-only.
9
+ Claude Code plugin: `/plugin marketplace add aunysillyme/model-orchestrator`, then `/plugin install model-orchestrator@model-orchestrator`. It installs the two read-only hooks (`route-gate`, `subagent-context`) and eight subagents; the routing rules still come from `npx model-orchestrator`, and the routing log (`route-metrics.mjs`) is npm-only.
10
10
 
11
11
  ## Docs
12
12
 
@@ -24,14 +24,14 @@ Claude Code plugin: `/plugin marketplace add aunysillyme/model-orchestrator`, th
24
24
 
25
25
  - [Claude Code plugin](https://github.com/aunysillyme/model-orchestrator/blob/main/plugin/README.md): installing the hooks and subagents with `/plugin install`, what the plugin reads, and what it leaves to the installer
26
26
  - [CLI runner](https://github.com/aunysillyme/model-orchestrator/blob/main/bin/README.md): `cli-run` lanes, exit codes, the route logged per run
27
-
28
- ## Plans and automatic effort
29
-
30
- Use `--plans AI=plan` to state a subscription plan and receive volume-allocation guidance. `--effort-auto` is opt-in and writes bounded `auto` effort only for selected high or max headroom CLI lanes. Auto is medium or high, never above high; name xhigh explicitly for security-critical or irreversible work.
31
27
  - [Templates](https://github.com/aunysillyme/model-orchestrator/blob/main/templates/README.md): the routing, tiers, task bundle and protocol files the installer renders
32
28
  - [Changelog](https://github.com/aunysillyme/model-orchestrator/blob/main/CHANGELOG.md): every release and the issue behind each fix
33
29
  - [Agent instructions](https://github.com/aunysillyme/model-orchestrator/blob/main/AGENTS.md): running the installer from an agent, and contributing
34
30
 
31
+ ## Plans and automatic effort
32
+
33
+ - [Plans and automatic effort](https://github.com/aunysillyme/model-orchestrator/blob/main/docs/install.md#plans-and-automatic-effort): `--plans AI=plan` states a subscription plan so the guidance allocates volume by headroom; opt-in `--effort-auto` sizes effort per call on high or max headroom CLI lanes, medium or high, with xhigh named explicitly for security-critical or irreversible work
34
+
35
35
  ## Optional
36
36
 
37
37
  - [Security policy](https://github.com/aunysillyme/model-orchestrator/blob/main/SECURITY.md)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "model-orchestrator",
3
- "version": "0.1.29",
4
- "description": "Model orchestrator for AI coding agents and LLMs: Claude Code, Codex, Gemini, Grok, Qwen, Ollama. Routing rules tell your agent which model, subagent or CLI to use for each task, so small work goes to cheap tiers and fewer tokens go to frontier models. One installer, plus a CLI runner that logs every route.",
3
+ "version": "0.1.31",
4
+ "description": "Model orchestrator for Claude Code, Codex, Antigravity (Google): routing rules so small work goes to cheap tiers and fewer tokens go to frontier models.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "model-orchestrator": "bin/cli.js"
package/src/install.js CHANGED
@@ -793,8 +793,17 @@ export function readManifest(dir) {
793
793
  // existing documents are kept unless --force, or --update-docs for the ones a previous run wrote and nobody edited.
794
794
  export function writeFiles(files, opts) {
795
795
  const { dir, force = false, dry = false, upgradeRuntime = false, updateDocs = false } = opts;
796
- const prevHashes = (opts.prevManifest && opts.prevManifest.files) || null;
797
796
  const roots = { dir, project: opts.project || dir };
797
+ const previous = opts.prevManifest;
798
+ const sameRoot = (kind) => {
799
+ if (typeof previous?.[kind] !== 'string') return false;
800
+ try { return realRoot(previous[kind]).root === realRoot(roots[kind]).root; }
801
+ catch { return false; }
802
+ };
803
+ const sameRoots = { dir: sameRoot('dir'), project: sameRoot('project') };
804
+ const belongsHere = (key) => typeof key === 'string' && sameRoots[key.startsWith('[project] ') ? 'project' : 'dir'];
805
+ // A hash or directory from another project cannot establish ownership here.
806
+ const prevHashes = previous?.files ? Object.fromEntries(Object.entries(previous.files).filter(([key]) => belongsHere(key))) : null;
798
807
  const groups = { dir: files.filter((f) => (f.root || 'dir') === 'dir'), project: files.filter((f) => f.root === 'project') };
799
808
  const problems = [];
800
809
  for (const k of ['dir', 'project']) {
@@ -815,6 +824,9 @@ export function writeFiles(files, opts) {
815
824
  const docsConflict = []; // --update-docs: documents kept because you edited them
816
825
  const docsUnverifiable = []; // --update-docs: documents kept because there is no manifest to compare against
817
826
  const created = [];
827
+ // Only directories actually created by this install are owned. Preserve the
828
+ // previous inventory on reruns; legacy manifests deliberately own none.
829
+ const createdDirectories = new Set(Array.isArray(previous?.directories) ? previous.directories.filter(belongsHere) : []);
818
830
  const originals = new Map(); // abs -> {content, mode} of files --force overwrote, restored on failure
819
831
  // Files that exist and were NOT rewritten this run. MANIFEST.json must record the hash of
820
832
  // what is on disk for them (the previous run's hash, or nothing when there was no manifest),
@@ -890,7 +902,7 @@ export function writeFiles(files, opts) {
890
902
  }
891
903
  }
892
904
  let content = f.content;
893
- if (f.rel === 'MANIFEST.json' && keptKeys.size) {
905
+ if (f.rel === 'MANIFEST.json') {
894
906
  const m = JSON.parse(content);
895
907
  for (const kk of Object.keys(m.files || {})) {
896
908
  if (!keptKeys.has(kk)) continue;
@@ -901,7 +913,24 @@ export function writeFiles(files, opts) {
901
913
  }
902
914
  if (!dry) {
903
915
  if (exists) originals.set(abs, { content: readFileSync(abs), mode: statSync(abs).mode });
916
+ const missingDirectories = [];
917
+ let parent = dirname(abs);
918
+ while (parent === root || parent.startsWith(root + sep)) {
919
+ if (existsSync(parent)) break;
920
+ // Keep the project container itself; only its generated child
921
+ // directories belong to this package.
922
+ if (k !== 'project' || parent !== root) missingDirectories.push(parent);
923
+ const up = dirname(parent);
924
+ if (up === parent) break;
925
+ parent = up;
926
+ }
904
927
  mkdirSync(dirname(abs), { recursive: true });
928
+ for (const path of missingDirectories) createdDirectories.add((k === 'project' ? '[project] ' : '') + (toPosixRel(relative(root, path)) || '.'));
929
+ if (f.rel === 'MANIFEST.json') {
930
+ const m = JSON.parse(content);
931
+ m.directories = [...createdDirectories].sort();
932
+ content = JSON.stringify(m, null, 2) + '\n';
933
+ }
905
934
  writeFileSync(abs, content, { flag: exists ? 'w' : 'wx' });
906
935
  if (!exists) created.push(abs);
907
936
  chmodSync(abs, f.mode);
@@ -0,0 +1,163 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, rmdirSync, unlinkSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { isAbsolute, join, relative, resolve, sep, win32 } from 'node:path';
4
+ import { dirProblems, realRoot } from './install.js';
5
+
6
+ const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
7
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
8
+ const refused = (message) => Object.assign(new Error('refusing to uninstall: ' + message), { code: 'UNINSTALL' });
9
+
10
+ function stat(path) {
11
+ try { return lstatSync(path); }
12
+ catch (error) {
13
+ if (error.code === 'ENOENT') return null;
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ function targetRoot(path) {
19
+ const problems = dirProblems(path);
20
+ if (problems.length) throw refused(problems.join('; '));
21
+ const st = stat(resolve(path));
22
+ if (st?.isSymbolicLink()) throw refused(`target is a symlink: ${path}`);
23
+ if (st && !st.isDirectory()) throw refused(`target is not a directory: ${path}`);
24
+ // As in the installer, ancestors above the chosen root may be system aliases
25
+ // (for example /tmp on macOS). Entries inside the root may never be symlinks.
26
+ return realRoot(path).root;
27
+ }
28
+
29
+ function entry(key, roots, directory = false) {
30
+ if (typeof key !== 'string') throw refused('manifest paths must be strings');
31
+ const project = key.startsWith('[project] ');
32
+ const rel = project ? key.slice('[project] '.length) : key;
33
+ const root = project ? roots.project : roots.dir;
34
+ const isDirRoot = directory && !project && rel === '.';
35
+ if (!isDirRoot && (!rel || isAbsolute(rel) || win32.isAbsolute(rel) || /[\\:\x00-\x1f\x7f]/.test(rel)
36
+ || rel.split('/').some((part) => !part || part === '.' || part === '..'))) {
37
+ throw refused(`invalid manifest path: ${key}`);
38
+ }
39
+ const abs = resolve(root, rel);
40
+ if (!isDirRoot && (abs === root || !abs.startsWith(root + sep))) throw refused(`path leaves its target root: ${key}`);
41
+ return { key, root, abs, directory };
42
+ }
43
+
44
+ function inspect(item) {
45
+ const { root, abs, directory } = item;
46
+ const parts = relative(root, abs).split(sep).filter(Boolean);
47
+ let cur = root;
48
+ // Recheck the root as well as the entry's parents before each removal.
49
+ for (let i = -1; i < parts.length; i++) {
50
+ if (i >= 0) cur = join(cur, parts[i]);
51
+ const st = stat(cur);
52
+ if (!st) return null;
53
+ if (st.isSymbolicLink()) throw refused(`symlink at ${cur}`);
54
+ const wantDirectory = i < parts.length - 1 || directory;
55
+ if (wantDirectory ? !st.isDirectory() : !st.isFile()) throw refused(`unexpected file type at ${cur}`);
56
+ if (i === parts.length - 1) return st;
57
+ }
58
+ }
59
+
60
+ function readRegular(item) {
61
+ const expected = inspect(item);
62
+ if (!expected) return null;
63
+ // Nonblocking open also protects against a regular file being replaced by a
64
+ // FIFO between lstat and open. O_NOFOLLOW protects the final component.
65
+ const fd = openSync(item.abs, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
66
+ try {
67
+ const actual = fstatSync(fd);
68
+ if (!actual.isFile() || actual.dev !== expected.dev || actual.ino !== expected.ino) throw refused(`file changed during inspection: ${item.abs}`);
69
+ return { bytes: readFileSync(fd), stat: actual };
70
+ } finally { closeSync(fd); }
71
+ }
72
+
73
+ function removeFile(item, expectedHash) {
74
+ const current = readRegular(item);
75
+ if (!current) return;
76
+ if (hash(current.bytes) !== expectedHash) throw refused(`keep edited ${item.abs}: changed during uninstall`);
77
+ const last = inspect(item);
78
+ if (!last || last.dev !== current.stat.dev || last.ino !== current.stat.ino) throw refused(`file changed during uninstall: ${item.abs}`);
79
+ unlinkSync(item.abs);
80
+ }
81
+
82
+ // Validate every path and type before removing anything. The manifest is an
83
+ // inventory, never authority to expand the two roots supplied by the caller.
84
+ export function uninstallFiles({ dir, project, dry = false }) {
85
+ const roots = { dir: targetRoot(dir), project: targetRoot(project) };
86
+ const manifest = entry('MANIFEST.json', roots);
87
+ const saved = readRegular(manifest);
88
+ if (!saved) throw refused(`missing manifest: ${join(resolve(dir), 'MANIFEST.json')}`);
89
+ let data;
90
+ try { data = JSON.parse(saved.bytes.toString('utf8')); }
91
+ catch { throw refused(`invalid JSON in ${manifest.abs}`); }
92
+ if (!object(data) || data.generator !== 'model-orchestrator' || !object(data.files)) throw refused(`invalid manifest schema: ${manifest.abs}`);
93
+ for (const name of ['dir', 'project']) {
94
+ if (typeof data[name] !== 'string' || targetRoot(data[name]) !== roots[name]) {
95
+ throw refused(`--${name} differs from the manifest; use the original installation path`);
96
+ }
97
+ }
98
+ if (data.directories !== undefined && !Array.isArray(data.directories)) throw refused('manifest directories must be an array');
99
+
100
+ const files = Object.entries(data.files).map(([key, digest]) => {
101
+ const item = entry(key, roots);
102
+ if (item.abs === manifest.abs) throw refused(`manifest cannot manage itself: ${key}`);
103
+ if (typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest)) throw refused(`invalid content hash for ${key}`);
104
+ return { ...item, digest };
105
+ });
106
+ const directories = (data.directories || []).map((key) => entry(key, roots, true));
107
+ const seen = new Set([manifest.abs]);
108
+ for (const item of [...files, ...directories]) {
109
+ if (seen.has(item.abs)) throw refused(`duplicate manifest path: ${item.key}`);
110
+ seen.add(item.abs);
111
+ inspect(item);
112
+ }
113
+
114
+ const actions = [];
115
+ const pending = [];
116
+ let edited = false;
117
+ for (const item of files) {
118
+ const current = readRegular(item);
119
+ if (!current) actions.push(' missing file ' + item.abs);
120
+ else if (hash(current.bytes) !== item.digest) {
121
+ edited = true;
122
+ actions.push(' keep edited ' + item.abs);
123
+ } else {
124
+ pending.push(item);
125
+ actions.push(' remove file ' + item.abs);
126
+ }
127
+ }
128
+ if (edited) actions.push(' keep manifest ' + manifest.abs);
129
+ else actions.push(' remove file ' + manifest.abs);
130
+
131
+ // Compute empty directories against the same plan used by the real run.
132
+ // Foreign entries and kept edits prevent their parents from being removed.
133
+ const disappearing = new Set(pending.map((item) => item.abs));
134
+ if (!edited) disappearing.add(manifest.abs);
135
+ const empty = [];
136
+ directories.sort((a, b) => b.abs.split(sep).length - a.abs.split(sep).length || a.abs.localeCompare(b.abs));
137
+ for (const item of directories) {
138
+ if (!inspect(item)) continue;
139
+ if (readdirSync(item.abs).every((name) => disappearing.has(join(item.abs, name)))) {
140
+ disappearing.add(item.abs);
141
+ empty.push(item);
142
+ }
143
+ }
144
+
145
+ if (!dry) {
146
+ // A changed type or symlink detected here still refuses the whole run.
147
+ for (const item of [...files, ...directories, manifest]) inspect(item);
148
+ for (const item of pending) removeFile(item, item.digest);
149
+ if (!edited) removeFile(manifest, hash(saved.bytes));
150
+ }
151
+ for (const item of empty) {
152
+ if (!dry) {
153
+ if (!inspect(item)) continue;
154
+ try { rmdirSync(item.abs); }
155
+ catch (error) {
156
+ if (error.code === 'ENOTEMPTY' || error.code === 'EEXIST') continue;
157
+ throw error;
158
+ }
159
+ }
160
+ actions.push(' remove directory ' + item.abs);
161
+ }
162
+ return actions;
163
+ }
@@ -10,6 +10,6 @@ Loading surfaces for the primary agent. The installer writes exactly one of thes
10
10
  | `grok`, `hermes` | nothing agent-specific | rules travel with the prompt or the task bundle |
11
11
  | a chat app | `PASTE-INTO-YOUR-AGENT.md` | no files to load; paste into custom instructions |
12
12
 
13
- `snippets/` are rendered with the chosen agent's name and rules file. Nothing here is appended to a file the user already has. `snippets/route-gate.mjs`, `snippets/subagent-context.mjs`, and `snippets/settings.hooks.snippet.json` are claude-code only: two hooks and the settings block that wires them, installed to `.claude/hooks/` and next to `CLAUDE.snippet.md`.
13
+ `snippets/` are rendered with the chosen agent's name and rules file. Nothing here is appended to a file the user already has. `snippets/route-gate.mjs`, `snippets/subagent-context.mjs`, `snippets/route-metrics.mjs`, and `snippets/settings.hooks.snippet.json` are claude-code only: three hooks and the settings block that wires them, installed to `.claude/hooks/` and next to `CLAUDE.snippet.md`.
14
14
 
15
15
  `claude-code/` and `agy/` both ship the same agent set: one per tier, plus `finding-verifier`, `done-verifier` and `reader`. Add an agent to one folder and its README, and the other.
@@ -30,4 +30,4 @@ Anything durable is searched for before it is written and its folder index is co
30
30
 
31
31
  Subagents were written to `{{AGENTS_DIR}}` (the project root, which is where Claude Code reads project-level agents; `--project` changes it). Run `claude` from `{{PROJECT_DIR}}` and they are available as {{AGENTS_LIST_LINE}}.
32
32
 
33
- Two hooks were written to `{{AGENTS_DIR}}/../hooks/` (`.claude/hooks/`): `route-gate.mjs` injects the routing table on every prompt, and `subagent-context.mjs` reminds a spawned subagent where the rules and the task-bundle format live. Merge `settings.hooks.snippet.json`, written next to this file, into `.claude/settings.json` to wire them in.
33
+ Three hooks were written to `{{AGENTS_DIR}}/../hooks/` (`.claude/hooks/`): `route-gate.mjs` injects the routing table on every prompt, `subagent-context.mjs` reminds a spawned subagent where the rules and the task-bundle format live, and `route-metrics.mjs` appends each turn's route marker and subagent dispatch to a local `route-metrics.jsonl` log (read it with `node .claude/hooks/route-metrics.mjs --summary`). Merge `settings.hooks.snippet.json`, written next to this file, into `.claude/settings.json` to wire all three in.
@@ -17,7 +17,7 @@ uvx 'codecalc[full]' setup --write # merges the codecalc entry into your clie
17
17
 
18
18
  Pinned form, if you want the version this installer was released with: `uvx 'codecalc[full]=={{CODECALC_PIN}}' setup --write`. `[full]` is the edition that actually runs everything documented (about 120 MB). Base `codecalc` is execution only; symbolic tools then return a `dependency_missing` error naming the extra, never a silent failure.
19
19
 
20
- ## Agents `setup` does not register (snippets in `mcp/`)
20
+ ## Register more agents with `mcp/` snippets
21
21
 
22
22
  | Agent | File to edit | Snippet |
23
23
  |---|---|---|
@@ -32,11 +32,11 @@ Merge the block; do not replace the file. Every other server you have stays as i
32
32
 
33
33
  ## Install the skill too
34
34
 
35
- The tools cannot help a model that never reaches for them. codecalc ships `SKILL.md` inside the package and `setup --write` copies it for Claude Code. For other agents, copy it into that agent's skills folder (Antigravity and Qwen Code read the same `SKILL.md` format). `protocols/numbers-and-logic.md` in this folder is the house rule that points at it.
35
+ The skill teaches your agent when to call the tools. codecalc ships `SKILL.md` inside the package and `setup --write` copies it for Claude Code. For other agents, copy it into that agent's skills folder (Antigravity and Qwen Code read the same `SKILL.md` format). `protocols/numbers-and-logic.md` in this folder is the house rule that points at it.
36
36
 
37
- ## What it is not
37
+ ## Where it fits
38
38
 
39
- Not a cloud sandbox for multi-tenant loads, not a replacement for a vendor's built-in interpreter when zero setup matters more than measurement. It assumes a single-operator, local, stdio setup. It earns its keep when the correctness of a claim, not "it ran", is the point.
39
+ Use codecalc for a single-operator, local stdio setup where you need to measure and verify a claim. Its calculator, sandbox and logic tools give your agent repeatable checks for arithmetic, code behavior and reasoning.
40
40
 
41
41
  ## On a box (level 3)
42
42