Skip to content

Commit 289e325

Browse files
AceGreenmanclaude
andcommitted
Add @razroo/iso-route — one model policy, every harness
Declare a default model + named roles once; iso-route compiles that policy into each harness's native config: .claude/settings.json, .codex/config.toml (with per-role profiles), opencode.json (with per-agent overrides), and an advisory README note for Cursor. Emits a resolved role map alongside the Claude settings so iso-harness can stamp per-subagent frontmatter consistently. Honest about ceilings: warns when a harness can't bind models programmatically (Cursor) or lacks runtime fallback (everywhere). Fallback chains are recorded in the resolved map but not encoded into harness config — runtime routing belongs in proxy layers (OpenRouter, LiteLLM), not a build-time transpiler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cdca720 commit 289e325

18 files changed

Lines changed: 1157 additions & 2 deletions

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ Today, writing agent instructions is fragmented on two axes:
2525

2626
Three core packages compose into a build pipeline that fixes both,
2727
[`@razroo/iso`](./packages/iso) runs the whole chain as one command,
28-
[`@razroo/iso-eval`](./packages/iso-eval) scores whether the resulting
29-
agent actually completes real tasks, and
28+
[`@razroo/iso-route`](./packages/iso-route) compiles one model policy
29+
into each harness's config so you can swap models everywhere with a
30+
single edit, [`@razroo/iso-eval`](./packages/iso-eval) scores whether
31+
the resulting agent actually completes real tasks, and
3032
[`@razroo/iso-trace`](./packages/iso-trace) parses production transcripts
3133
so you can see what your agent *really* does in the wild:
3234

@@ -87,6 +89,15 @@ project that exercises the wrapper end-to-end.
8789
MCP servers into `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/*.mdc`,
8890
`.opencode/agents/*.md`, etc., so all four harnesses stay in lockstep.
8991

92+
- **[`packages/iso-route`](./packages/iso-route)**[`@razroo/iso-route`](https://www.npmjs.com/package/@razroo/iso-route)
93+
One model policy, every harness. Declare a default model plus named
94+
roles (`planner`, `fast-edit`, `reviewer`, …) in a single
95+
`models.yaml`; iso-route compiles that into `.claude/settings.json`,
96+
`.codex/config.toml`, `opencode.json`, and a resolved role map that
97+
`iso-harness` consumes when stamping per-subagent frontmatter. Honest
98+
about ceilings — warns loudly where a harness (e.g. Cursor) can't bind
99+
models programmatically.
100+
90101
- **[`packages/iso-eval`](./packages/iso-eval)**[`@razroo/iso-eval`](https://www.npmjs.com/package/@razroo/iso-eval)
91102
Behavioral eval runner for the produced harness. Snapshots a workspace
92103
per task, hands it to a runner with the task prompt, then scores the
@@ -117,6 +128,7 @@ iso/
117128
├── isolint/ # portable prose
118129
├── iso-harness/ # one source, every harness
119130
├── iso/ # one command for the whole pipeline
131+
├── iso-route/ # one model policy → per-harness config
120132
├── iso-eval/ # behavioral eval on the produced harness
121133
└── iso-trace/ # parse + query real agent transcripts (observability)
122134
```

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/iso-route/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Razroo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/iso-route/README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# @razroo/iso-route
2+
3+
**One model policy, every harness.**
4+
5+
`agentmd`, `isolint`, and `iso-harness` get your *prompts* to every coding
6+
agent. `@razroo/iso-route` does the same thing for your *model choices*:
7+
you declare a default model plus named roles once, and iso-route compiles
8+
that policy into the config file each harness actually reads —
9+
`.claude/settings.json`, `.codex/config.toml`, `opencode.json` — plus a
10+
README note for Cursor (which has no file-based model binding).
11+
12+
Use it to swap Opus for Sonnet everywhere with a single edit, pin a
13+
cheaper model to a `fast-edit` role, or send a `reviewer` role to a
14+
different provider entirely.
15+
16+
> **v0.1 scope:** emits config files for Claude Code, Codex, and OpenCode,
17+
> and a resolved role map (`iso-route.resolved.json`) that `iso-harness`
18+
> consumes when it stamps per-subagent frontmatter. Fallback chains are
19+
> recorded in the resolved map but *not* encoded into any harness config
20+
> — runtime routing lives in proxy layers (OpenRouter, LiteLLM), not
21+
> iso-route.
22+
23+
## Install
24+
25+
```bash
26+
npm install -D @razroo/iso-route
27+
```
28+
29+
## Policy shape
30+
31+
```yaml
32+
# models.yaml
33+
default:
34+
provider: anthropic
35+
model: claude-sonnet-4-6
36+
37+
roles:
38+
planner:
39+
provider: anthropic
40+
model: claude-opus-4-7
41+
reasoning: high
42+
43+
fast-edit:
44+
provider: anthropic
45+
model: claude-haiku-4-5
46+
47+
reviewer:
48+
provider: openai
49+
model: gpt-5
50+
fallback:
51+
- { provider: anthropic, model: claude-sonnet-4-6 }
52+
```
53+
54+
Valid providers: `anthropic`, `openai`, `google`, `xai`, `deepseek`,
55+
`mistral`, `groq`, `ollama`, `openrouter`, `local`.
56+
Valid `reasoning` levels: `low`, `medium`, `high`.
57+
58+
## Fan-out mapping
59+
60+
| Field | Claude Code | Codex | OpenCode | Cursor |
61+
| ------------------------- | ------------------------------------ | ---------------------------------------------------- | -------------------------------------- | -------------------------------- |
62+
| `default.model` | `.claude/settings.json` `model` | `.codex/config.toml` `model` | `opencode.json` top-level `model` | README note only |
63+
| `roles.<name>.model` | resolved map (iso-harness stamps) | `[profiles.<name>]` in `config.toml` | `agent.<name>.model` in `opencode.json`| advisory row in README note |
64+
| `reasoning` | closest model tier | `model_reasoning_effort` | provider-specific | advisory |
65+
| `fallback[]` | resolved map only (runtime unsupported) | resolved map only | resolved map only | resolved map only |
66+
| provider auth | env var convention | `[model_providers.<name>]` block | `provider` block with `npm` package | — |
67+
68+
Cursor has no programmatic way to bind a model to a rule or chat, so
69+
iso-route emits a README note at `.cursor/iso-route.md` and warns at build
70+
time. Everything else gets a real config file.
71+
72+
## CLI
73+
74+
```bash
75+
iso-route build models.yaml --out .
76+
iso-route build models.yaml --targets claude,codex --dry-run
77+
iso-route plan models.yaml
78+
```
79+
80+
`build` writes per-harness files under `--out` (defaults to `.`). Add
81+
`--dry-run` to preview without touching disk. `plan` prints the resolved
82+
role table so you can eyeball what each harness will see.
83+
84+
## Library API
85+
86+
```ts
87+
import { build, loadPolicy } from "@razroo/iso-route";
88+
89+
const result = build({ source: "./models.yaml", out: "./.out", dryRun: true });
90+
for (const w of result.warnings) console.warn(w);
91+
```
92+
93+
Individual emitters are exported too (`emitClaude`, `emitCodex`,
94+
`emitOpenCode`, `emitCursor`) if you only need one target.
95+
96+
## How this fits the rest of the pipeline
97+
98+
```
99+
agent.md → agentmd lint → agentmd render → isolint lint → iso-harness build
100+
+
101+
models.yaml → iso-route build ┘
102+
103+
104+
project with CLAUDE.md, settings.json,
105+
config.toml, opencode.json, …
106+
```
107+
108+
`iso-harness` owns *what the agent reads*. `iso-route` owns *which model
109+
reads it*. They share one output directory and are designed to be run
110+
back-to-back — the `@razroo/iso` wrapper will compose them for you.
111+
112+
## What iso-route is NOT
113+
114+
- **Not a request-level router.** Picking a cheaper model per-request
115+
based on prompt complexity belongs in a proxy (OpenRouter, LiteLLM,
116+
Portkey, Not Diamond). iso-route is a build-time transpiler, not an
117+
inference-path component.
118+
- **Not a model catalog.** It validates provider names, not model IDs.
119+
If you type a model name your provider doesn't recognize, you'll find
120+
out at runtime. A catalog package may land in v0.2.
121+
122+
## License
123+
124+
MIT — see [LICENSE](./LICENSE).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Example iso-route policy.
2+
#
3+
# The shape is intentionally small: one default model every harness falls back
4+
# to, plus named roles that harness-specific machinery (Claude Code subagents,
5+
# Codex profiles, OpenCode agents) can bind to.
6+
#
7+
# iso-route ONLY handles model selection. Prompts, rules, and subagent bodies
8+
# are owned by agentmd + iso-harness. Everything here compiles to *config*,
9+
# not prose.
10+
11+
default:
12+
provider: anthropic
13+
model: claude-sonnet-4-6
14+
15+
roles:
16+
planner:
17+
provider: anthropic
18+
model: claude-opus-4-7
19+
reasoning: high
20+
21+
fast-edit:
22+
provider: anthropic
23+
model: claude-haiku-4-5
24+
25+
reviewer:
26+
provider: openai
27+
model: gpt-5
28+
fallback:
29+
- { provider: anthropic, model: claude-sonnet-4-6 }
30+
31+
local-smoke:
32+
provider: ollama
33+
model: qwen2.5-coder:7b

packages/iso-route/package.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "@razroo/iso-route",
3+
"version": "0.1.0",
4+
"description": "Author one model policy; fan out to every harness that supports it. Translates role-based model selection into settings.json, config.toml, opencode.json, and a machine-readable resolved map.",
5+
"license": "MIT",
6+
"type": "module",
7+
"sideEffects": false,
8+
"main": "dist/index.js",
9+
"types": "dist/index.d.ts",
10+
"bin": {
11+
"iso-route": "dist/cli.js"
12+
},
13+
"files": [
14+
"dist/**/*.js",
15+
"dist/**/*.d.ts",
16+
"examples",
17+
"README.md",
18+
"LICENSE"
19+
],
20+
"scripts": {
21+
"build": "tsc -p tsconfig.json",
22+
"clean": "rm -rf dist",
23+
"dev": "tsc -p tsconfig.json --watch",
24+
"test": "node --test --import tsx tests/*.test.ts",
25+
"typecheck": "tsc -p tsconfig.json --noEmit",
26+
"example": "tsx src/cli.ts build examples/models.yaml --out examples/out --dry-run",
27+
"ci": "npm run typecheck && npm test",
28+
"prepublishOnly": "npm run clean && npm run build && npm test"
29+
},
30+
"dependencies": {
31+
"yaml": "^2.6.0"
32+
},
33+
"devDependencies": {
34+
"@types/node": "^22.10.0",
35+
"tsx": "^4.19.1",
36+
"typescript": "^5.6.2"
37+
},
38+
"engines": {
39+
"node": ">=20.6.0"
40+
},
41+
"repository": {
42+
"type": "git",
43+
"url": "git+https://github.com/razroo/iso.git",
44+
"directory": "packages/iso-route"
45+
},
46+
"homepage": "https://github.com/razroo/iso/tree/main/packages/iso-route#readme",
47+
"bugs": {
48+
"url": "https://github.com/razroo/iso/issues"
49+
},
50+
"keywords": [
51+
"agent",
52+
"routing",
53+
"model",
54+
"harness",
55+
"ai",
56+
"coding-agent",
57+
"claude-code",
58+
"cursor",
59+
"codex",
60+
"opencode"
61+
],
62+
"publishConfig": {
63+
"access": "public"
64+
}
65+
}

packages/iso-route/src/build.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { mkdirSync, writeFileSync } from "node:fs";
2+
import { dirname, resolve } from "node:path";
3+
import { loadPolicy } from "./parser.js";
4+
import { emitClaude } from "./targets/claude.js";
5+
import { emitCodex } from "./targets/codex.js";
6+
import { emitCursor } from "./targets/cursor.js";
7+
import { emitOpenCode } from "./targets/opencode.js";
8+
import type { BuildResult, EmitResult, HarnessTarget, ModelPolicy } from "./types.js";
9+
10+
export const ALL_TARGETS: HarnessTarget[] = ["claude", "codex", "opencode", "cursor"];
11+
12+
export interface BuildOptions {
13+
source: string;
14+
out: string;
15+
targets?: HarnessTarget[];
16+
dryRun?: boolean;
17+
}
18+
19+
export function build(opts: BuildOptions): BuildResult {
20+
const policy = loadPolicy(opts.source);
21+
const targets = opts.targets ?? ALL_TARGETS;
22+
const outDir = resolve(opts.out);
23+
const emits: EmitResult[] = [];
24+
for (const t of targets) emits.push(emitFor(t, policy));
25+
26+
if (!opts.dryRun) {
27+
for (const e of emits) {
28+
for (const f of e.files) {
29+
const full = resolve(outDir, f.path);
30+
mkdirSync(dirname(full), { recursive: true });
31+
writeFileSync(full, f.contents);
32+
}
33+
}
34+
}
35+
36+
const warnings = emits.flatMap((e) => e.warnings);
37+
return { policy, emits, warnings };
38+
}
39+
40+
function emitFor(target: HarnessTarget, policy: ModelPolicy): EmitResult {
41+
switch (target) {
42+
case "claude":
43+
return emitClaude(policy);
44+
case "codex":
45+
return emitCodex(policy);
46+
case "opencode":
47+
return emitOpenCode(policy);
48+
case "cursor":
49+
return emitCursor(policy);
50+
}
51+
}

0 commit comments

Comments
 (0)