codex-model-router 1.2.0 → 2.1.0

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/README.md CHANGED
@@ -1,222 +1,158 @@
1
1
  # codex-model-router
2
2
 
3
- A small Node.js CLI that safely installs advisory Codex subagent routing for Terra, Luna, and Sol without replacing unrelated user configuration.
3
+ A small Node.js CLI that safely installs adaptive Codex subagent routing for Terra, Luna, and Sol without replacing unrelated user configuration or the user's selected primary model.
4
4
 
5
- > Routing is advisory. Terra reads the installed skill and decides when to delegate. This package does not intercept prompts or guarantee a hard model switch.
5
+ > Routing is advisory. Codex reads the installed skills and decides when to delegate. This package does not intercept prompts, guarantee a hard model switch, or modify AGENTS.md.
6
6
 
7
- ## Compatibility
7
+ ## Routing model
8
8
 
9
- Compatibility was verified on 2026-07-31 against:
9
+ The normal installation uses free mode:
10
10
 
11
- - Codex CLI `0.145.0` or newer
12
- - Node.js 18, 20, 22, and 24
13
- - Windows, Linux, and macOS GitHub-hosted runners
14
- - Codex project agents in `.codex/agents/`
15
- - Codex user agents in `$CODEX_HOME/agents/` or `~/.codex/agents/`
16
- - project skills in `.agents/skills/`
17
- - user skills in `~/.agents/skills/`
11
+ - The user-selected primary model handles conversation, clarification, follow-ups, final replies, and trivial work.
12
+ - Terra/high investigates, produces implementation-ready plans, verifies results, debugs, and replans.
13
+ - Luna/xhigh performs most clear, bounded, repetitive, or independently verifiable implementation work.
14
+ - Sol/medium is read-only and used only when Terra still cannot resolve core logic after focused investigation and one materially revised plan, or when the user explicitly requests Sol.
15
+ - Existing built-in and custom agents remain available and take precedence when they are a better match.
18
16
 
19
- The selected Codex account must have access to `gpt-5.6-terra`, `gpt-5.6-luna`, and `gpt-5.6-sol`. Older Codex versions may not recognize the generated agent or skill files.
17
+ A normal workflow is Terra -> Luna -> Terra. Implementation errors return to Luna as focused corrections. Plan or logic errors return to Terra for a revised plan before Luna continues. Sol is not a routine reviewer.
20
18
 
21
19
  ## Quick start
22
20
 
23
- Preview a project installation without changing files:
21
+ Preview without changing files:
24
22
 
25
23
  ```sh
26
- npx codex-model-router install --set-default --dry-run
24
+ npx codex-model-router install --dry-run
27
25
  ```
28
26
 
29
- Install Terra/high as the project default, add Luna and Sol, then verify:
27
+ Install free-mode routing and verify it:
30
28
 
31
29
  ```sh
32
- npx codex-model-router install --set-default
30
+ npx codex-model-router install
33
31
  npx codex-model-router doctor
34
32
  ```
35
33
 
36
- Remove only unchanged package-managed settings and files:
34
+ Remove only unchanged package-managed files and settings:
37
35
 
38
36
  ```sh
39
37
  npx codex-model-router uninstall
40
38
  ```
41
39
 
42
- ## Install
40
+ ## Optional Terra default
41
+
42
+ A normal install does not add or change top-level model settings. Users remain free to select any primary model through Codex.
43
43
 
44
- Run directly from npm without installing the CLI globally:
44
+ To explicitly set and track Terra/high as the selected scope's default:
45
45
 
46
46
  ```sh
47
- npx codex-model-router install
47
+ codex-model-router install --set-default
48
48
  ```
49
49
 
50
- Install the CLI command globally:
50
+ Running a later plain `install` returns package-managed default settings to free mode. User-modified model settings are preserved.
51
+
52
+ ## Agent reasoning
53
+
54
+ The managed agent defaults are Terra/high, Luna/xhigh, and Sol/medium. Override all agents or individual agents during installation:
51
55
 
52
56
  ```sh
53
- npm install -g codex-model-router
54
- codex-model-router install
57
+ codex-model-router install --agent-reasoning high
58
+ codex-model-router install --terra-reasoning xhigh --luna-reasoning low --sol-reasoning max
55
59
  ```
56
60
 
57
- Installing the CLI globally only makes the command available system-wide. It does not modify user-level Codex configuration. The separate `install --global` option applies routing to the current user's Codex configuration.
61
+ Supported values are `none`, `low`, `medium`, `high`, `xhigh`, and `max`. Per-agent options override `--agent-reasoning`. Later installs preserve unchanged package-managed reasoning choices; manually edited agent files remain protected.
58
62
 
59
- Install a specific GitHub release:
63
+ ## Install
64
+
65
+ Run from npm:
60
66
 
61
67
  ```sh
62
- npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v1.2.0.tar.gz
68
+ npx codex-model-router install
69
+ ```
70
+
71
+ Install the command globally:
72
+
73
+ ```sh
74
+ npm install -g codex-model-router
63
75
  codex-model-router install
64
76
  ```
65
77
 
66
- ## Project and global scope
78
+ Installing the CLI globally only makes the command available system-wide. Use `install --global` to install routing for the current user's Codex configuration.
67
79
 
68
- Project scope is the default:
80
+ Install a specific GitHub release:
69
81
 
70
82
  ```sh
83
+ npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v2.1.0.tar.gz
71
84
  codex-model-router install
72
85
  ```
73
86
 
74
- It manages only:
87
+ ## Managed files
88
+
89
+ Project scope is the default and manages only:
75
90
 
76
91
  ```text
77
- .codex/config.toml
92
+ .codex/config.toml # only with --set-default or a managed migration
93
+ .codex/agents/terra.toml
78
94
  .codex/agents/luna.toml
79
95
  .codex/agents/sol.toml
80
96
  .codex/model-router-state.json
81
- .codex/config.toml.codex-model-router.bak # only when needed
97
+ .codex/config.toml.codex-model-router.bak # only when needed
82
98
  .agents/skills/model-router/SKILL.md
99
+ .agents/skills/implementation-planning/SKILL.md
83
100
  ```
84
101
 
85
- During a mutating operation it may temporarily create:
86
-
87
- ```text
88
- .codex/model-router.lock
89
- .codex/model-router-transaction.json
90
- .codex/model-router-transaction-data/
91
- ```
92
-
93
- These transient files are removed after a successful operation or safe recovery.
94
-
95
- Use global scope only when the routing configuration should apply to every Codex project for the current user:
102
+ Use global scope with:
96
103
 
97
104
  ```sh
98
105
  codex-model-router install --global
99
106
  codex-model-router doctor --global
100
107
  ```
101
108
 
102
- Global scope manages the equivalent paths under `$CODEX_HOME` and stores the skill under `~/.agents/skills/model-router/`. When `CODEX_HOME` is unset, it defaults to `~/.codex`.
103
-
104
- ## Existing settings
105
-
106
- A normal install adds Terra/high only when the top-level values are absent. Existing model values are preserved:
107
-
108
- ```sh
109
- codex-model-router install
110
- ```
109
+ Global scope stores agents under `$CODEX_HOME/agents` and skills under `~/.agents/skills`. When `CODEX_HOME` is unset, it defaults to `~/.codex`.
111
110
 
112
- To explicitly set and track Terra/high:
111
+ Existing files at managed paths are never overwritten. Later manual edits are preserved and reported by `doctor`.
113
112
 
114
- ```sh
115
- codex-model-router install --set-default
116
- ```
113
+ ## Prompt and cache efficiency
117
114
 
118
- `--set-default` changes only:
115
+ The installed model-router skill is intentionally short. Detailed planning rules live in the separate implementation-planning skill and are loaded only for nontrivial or uncertain changes.
119
116
 
120
- ```toml
121
- model = "gpt-5.6-terra"
122
- model_reasoning_effort = "high"
123
- ```
117
+ Within the same agent thread, accepted state is kept and concise deltas are appended. A new agent thread receives the latest self-contained snapshot. After three deltas or a material contract or logic change, Terra produces a new snapshot.
124
118
 
125
- The exact prior values are recorded. Uninstall restores them only while the current values still match what the package installed. Later user edits are preserved and reported instead of overwritten.
119
+ Prompt-cache reuse is best effort: genuinely new information cannot be a cache hit on first use. Stable wording and ordering improve repeated-prefix reuse, but correctness takes priority over cache behavior.
126
120
 
127
- Existing Luna, Sol, or skill files are never overwritten. Unrelated TOML settings, comments, BOM, ordering, and LF/CRLF line endings are preserved.
121
+ ## Planning rules
128
122
 
129
- ## Installed routing
123
+ For nontrivial changes, Terra identifies only applicable correctness-critical details, including exact files and symbols, names, contracts, parameter flow, callers, ordered logic, invariants, fixed decisions, safe local choices, return conditions, and acceptance criteria.
130
124
 
131
- - Terra/high handles normal questions, coding, debugging, fixes, tests, and implementation.
132
- - Luna/high handles deterministic repeated edits, bulk patterns, searches, formatting, counting, extraction, and summaries.
133
- - Sol/medium/workspace-write handles security-sensitive or high-regression-risk review and implementation.
134
- - Sol normally reviews first and may edit files only when Terra explicitly delegates implementation or a confirmed fix.
135
- - Simple questions do not spawn a subagent.
125
+ Information is marked as CONFIRMED, PROPOSED, or UNKNOWN. Luna does not begin implementation while an UNKNOWN can materially change behavior, targets, contracts, parameter flow, control flow, or compatibility.
136
126
 
137
- Users may edit model names, reasoning levels, and `sandbox_mode` directly in the generated TOML files. Later installs preserve manual edits, and `doctor` reports changed managed files.
127
+ Terra verifies separately whether implementation matches the plan and whether the plan plus implementation satisfy the original requirement. After three cycles without new evidence or a changed decision, the workflow stops instead of repeating the same approach.
138
128
 
139
129
  ## Commands
140
130
 
141
131
  ```text
142
132
  codex-model-router install [--global] [--set-default] [--dry-run]
133
+ [--agent-reasoning <effort>]
134
+ [--terra-reasoning <effort>] [--luna-reasoning <effort>] [--sol-reasoning <effort>]
143
135
  codex-model-router uninstall [--global] [--dry-run]
144
136
  codex-model-router doctor [--global]
145
137
  codex-model-router --version
146
138
  ```
147
139
 
148
- `doctor` is read-only. Exit code `0` means healthy; exit code `1` means action is required.
149
-
150
- Common statuses:
151
-
152
- - `healthy`: the managed item matches its expected state.
153
- - `preserve` or `user-modified`: user content was detected and left unchanged.
154
- - `missing` or `invalid`: a required item is absent or malformed.
155
- - `unsafe-state`: state paths or path components are unsafe.
156
- - `locked` or `stale-lock`: another operation is active or a previous process ended unexpectedly.
157
- - `unfinished`, `recoverable`, `conflicting`, or `corrupt`: an interrupted transaction needs attention.
140
+ `doctor` is read-only. Exit code 0 means healthy; exit code 1 means action is required.
158
141
 
159
142
  ## Safety behavior
160
143
 
144
+ - Preserves unrelated TOML settings, comments, BOM, ordering, and LF/CRLF line endings.
161
145
  - Rejects malformed, non-UTF-8, duplicate, or unsafe configuration before writing.
162
- - Rejects symlinks and Windows junctions in managed paths so writes cannot escape the selected scope.
163
- - Uses a scope-level process lock to prevent concurrent install/uninstall races.
164
- - Uses an atomic transaction journal and private snapshots to recover interrupted multi-file operations.
165
- - Never overwrites post-interruption user changes during recovery.
166
- - Uses atomic file replacement and rollback for handled failures.
167
- - Removes or restores only unchanged package-managed content.
168
- - Never modifies `AGENTS.md`, shell profiles, editor settings, hooks, MCP servers, telemetry, accounts, or environment variables.
146
+ - Rejects symlinks and Windows junctions in managed paths.
147
+ - Uses scope-level locking, atomic transactions, rollback, and conflict-safe recovery.
148
+ - Never overwrites post-interruption user changes.
169
149
  - Dry-run creates no files, directories, backups, locks, journals, or state.
170
- - Tests use temporary directories and never touch real Codex configuration.
171
-
172
- ## Troubleshooting
173
-
174
- ### Agents or skills do not appear
175
-
176
- Restart Codex after installation. Verify the selected scope with `doctor` or `doctor --global`, and confirm that your Codex version is at least the compatibility baseline above.
177
-
178
- ### `user-modified`
179
-
180
- The file or setting was changed after installation. The CLI intentionally leaves it untouched. Review the reported path and decide manually whether to retain the custom value or reinstall the package template under a different name.
181
-
182
- ### `unsafe-state` or unsafe path component
183
-
184
- Do not delete the reported file blindly. Check whether `.codex`, `.agents`, `CODEX_HOME`, or one of their parent directories is a symlink or Windows junction. Use a normal directory inside the intended project or user scope.
185
-
186
- ### Active or stale lock
187
-
188
- An active lock includes the operation and process ID. Wait for that process to finish. A later mutating command automatically removes a stale lock only when the recorded local process is no longer alive. A corrupt or remote-host lock must be inspected manually before removal.
150
+ - Never modifies AGENTS.md, shell profiles, editor settings, hooks, MCP servers, telemetry, accounts, or environment variables.
189
151
 
190
- ### Interrupted transaction
191
-
192
- Run `doctor` first. A mutating command automatically rolls back `unfinished` or `recoverable` transactions when all hashes still match. `conflicting` means a managed file changed after interruption; preserve that file and resolve the journal manually rather than forcing an overwrite. `corrupt` means the journal or snapshot cannot be trusted.
193
-
194
- ### Untracked backup already exists
195
-
196
- The CLI refuses to replace `config.toml.codex-model-router.bak`. Compare it with `config.toml`, move it to a clearly named safe location, and retry only after confirming its ownership.
197
-
198
- ### Permission or `CODEX_HOME` errors
199
-
200
- For project scope, verify write access to the project. For global scope, print and verify `CODEX_HOME`; do not run with elevated privileges merely to bypass an unexplained path error.
201
-
202
- ### Manual backup recovery
203
-
204
- The managed `.bak` is the exact pre-change configuration. Before restoring it, stop Codex, preserve the current `config.toml` under a new name, verify both files, and restore only within the same project/global scope.
205
-
206
- ### npx uses an old version
207
-
208
- Request the exact version first:
209
-
210
- ```sh
211
- npx --yes codex-model-router@1.2.0 --version
212
- ```
213
-
214
- When necessary, inspect the npm cache with `npm cache verify`; avoid deleting unrelated cache directories blindly.
215
-
216
- ## Security and release integrity
152
+ ## Compatibility
217
153
 
218
- Security vulnerabilities should be reported privately according to [SECURITY.md](SECURITY.md).
154
+ Compatibility is tested on Node.js 18, 20, 22, and 24 across Windows, Linux, and macOS runners. The selected Codex account must have access to gpt-5.6-terra, gpt-5.6-luna, and gpt-5.6-sol.
219
155
 
220
- Releases use npm Trusted Publishing through GitHub Actions, verified provenance, exact tag/version validation, current-version-only release notes, workflow linting, and a clean public-registry end-to-end install/doctor/uninstall check before the GitHub Release is created. Maintainer setup and release steps are documented in [MAINTAINERS.md](MAINTAINERS.md).
156
+ Restart Codex after installation so new agents and skills are discovered. Use `doctor` for missing, user-modified, unsafe-state, lock, or interrupted-transaction reports.
221
157
 
222
- Codex references: [Subagents](https://developers.openai.com/codex/subagents), [Build skills](https://developers.openai.com/codex/build-skills), and [Configuration reference](https://developers.openai.com/codex/config-reference).
158
+ Security vulnerabilities should be reported privately according to [SECURITY.md](SECURITY.md). Maintainer setup and release steps are documented in [MAINTAINERS.md](MAINTAINERS.md).
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "../lib/router.js";
2
+ import { runCli } from "../lib/agent-reasoning-cli.js";
3
3
 
4
- process.exitCode = await run(process.argv.slice(2), { output: console.log });
4
+ process.exitCode = await runCli(process.argv.slice(2), { output: console.log });
@@ -0,0 +1,160 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, realpath } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import {
6
+ AGENT_NAMES,
7
+ configureAgentReasoning,
8
+ DEFAULT_AGENT_REASONING,
9
+ REASONING_EFFORTS,
10
+ VERSION
11
+ } from "./manifest.js";
12
+ import { run as runCore } from "./router.js";
13
+
14
+ const OPTION_TO_AGENT = Object.freeze({
15
+ "--terra-reasoning": "terra",
16
+ "--luna-reasoning": "luna",
17
+ "--sol-reasoning": "sol"
18
+ });
19
+
20
+ function usage() {
21
+ return `codex-model-router ${VERSION}
22
+
23
+ Usage:
24
+ codex-model-router install [--global] [--set-default] [--dry-run]
25
+ [--agent-reasoning <effort>]
26
+ [--terra-reasoning <effort>]
27
+ [--luna-reasoning <effort>]
28
+ [--sol-reasoning <effort>]
29
+ codex-model-router uninstall [--global] [--dry-run]
30
+ codex-model-router doctor [--global]
31
+ codex-model-router --version
32
+
33
+ Reasoning efforts: ${REASONING_EFFORTS.join(", ")}
34
+ Defaults: Terra=${DEFAULT_AGENT_REASONING.terra}, Luna=${DEFAULT_AGENT_REASONING.luna}, Sol=${DEFAULT_AGENT_REASONING.sol}`;
35
+ }
36
+
37
+ function optionValue(argv, index, option) {
38
+ const token = argv[index];
39
+ if (token.startsWith(`${option}=`)) return { value: token.slice(option.length + 1), consumed: 1 };
40
+ const value = argv[index + 1];
41
+ if (!value || value.startsWith("--")) return { error: `missing value for ${option}` };
42
+ return { value, consumed: 2 };
43
+ }
44
+
45
+ function parseReasoningOptions(argv) {
46
+ const command = argv[0];
47
+ const cleaned = command ? [command] : [];
48
+ const overrides = {};
49
+ let all;
50
+
51
+ for (let index = 1; index < argv.length;) {
52
+ const token = argv[index];
53
+ const option = token.split("=", 1)[0];
54
+ const agent = OPTION_TO_AGENT[option];
55
+ if (option !== "--agent-reasoning" && !agent) {
56
+ cleaned.push(token);
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (command !== "install") return { error: `${option} is supported only for install` };
61
+ const parsed = optionValue(argv, index, option);
62
+ if (parsed.error) return parsed;
63
+ const reasoning = parsed.value.toLowerCase();
64
+ if (!REASONING_EFFORTS.includes(reasoning)) {
65
+ return { error: `unsupported reasoning effort: ${parsed.value}` };
66
+ }
67
+ if (agent) overrides[agent] = reasoning;
68
+ else all = reasoning;
69
+ index += parsed.consumed;
70
+ }
71
+ return { cleaned, overrides, all };
72
+ }
73
+
74
+ function digest(content) {
75
+ return createHash("sha256").update(content).digest("hex");
76
+ }
77
+
78
+ async function readRegularText(path) {
79
+ try {
80
+ const info = await lstat(path);
81
+ if (!info.isFile() || info.isSymbolicLink()) return null;
82
+ return await readFile(path, "utf8");
83
+ } catch (error) {
84
+ if (error?.code === "ENOENT") return null;
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ function activeReasoning(content) {
90
+ const match = content.match(/^\s*model_reasoning_effort\s*=\s*"([^"\r\n]+)"\s*(?:#.*)?$/m);
91
+ return match && REASONING_EFFORTS.includes(match[1]) ? match[1] : null;
92
+ }
93
+
94
+ function versionAtLeast(version, minimum) {
95
+ const parse = (value) => String(value || "0").split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
96
+ const left = parse(version);
97
+ const right = parse(minimum);
98
+ for (let index = 0; index < 3; index += 1) {
99
+ if (left[index] !== right[index]) return left[index] > right[index];
100
+ }
101
+ return true;
102
+ }
103
+
104
+ async function installedReasoning(argv, options) {
105
+ const global = argv.includes("--global");
106
+ const home = resolve(options.home ?? homedir());
107
+ const cwd = resolve(options.cwd ?? process.cwd());
108
+ const env = options.env ?? process.env;
109
+ const codexHome = global
110
+ ? resolve(env.CODEX_HOME || join(home, ".codex"))
111
+ : join(cwd, ".codex");
112
+ const stateText = await readRegularText(join(codexHome, "model-router-state.json"));
113
+ if (!stateText) return { reasoning: {}, content: {}, packageVersion: null };
114
+
115
+ let state;
116
+ try {
117
+ state = JSON.parse(stateText);
118
+ } catch {
119
+ return { reasoning: {}, content: {}, packageVersion: null };
120
+ }
121
+
122
+ const physicalCodexHome = await realpath(codexHome);
123
+ const reasoning = {};
124
+ const content = {};
125
+ for (const name of AGENT_NAMES) {
126
+ const path = join(physicalCodexHome, "agents", `${name}.toml`);
127
+ const tracked = state.files?.[name];
128
+ const current = await readRegularText(path);
129
+ if (!tracked || !current || resolve(tracked.path) !== resolve(path) || digest(current) !== tracked.hash) continue;
130
+ const effort = activeReasoning(current);
131
+ if (!effort) continue;
132
+ reasoning[name] = effort;
133
+ content[name] = current;
134
+ }
135
+ return { reasoning, content, packageVersion: state.packageVersion };
136
+ }
137
+
138
+ export async function runCli(argv, options = {}) {
139
+ const output = options.output ?? console.log;
140
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
141
+ output(usage());
142
+ return 0;
143
+ }
144
+
145
+ const parsed = parseReasoningOptions(argv);
146
+ if (parsed.error) {
147
+ output(`${parsed.error}\n\n${usage()}`);
148
+ return 1;
149
+ }
150
+
151
+ const installed = await installedReasoning(parsed.cleaned, options);
152
+ const selected = { ...DEFAULT_AGENT_REASONING };
153
+ if (versionAtLeast(installed.packageVersion, "2.1.0")) Object.assign(selected, installed.reasoning);
154
+ if (parsed.all) {
155
+ for (const name of AGENT_NAMES) selected[name] = parsed.all;
156
+ }
157
+ Object.assign(selected, parsed.overrides);
158
+ configureAgentReasoning(selected, installed.content);
159
+ return runCore(parsed.cleaned, options);
160
+ }
package/lib/manifest.js CHANGED
@@ -5,12 +5,13 @@ const packageJson = require("../package.json");
5
5
 
6
6
  export const VERSION = packageJson.version;
7
7
 
8
+ // Explicit opt-in only. A normal install leaves the user's primary model unchanged.
8
9
  export const DEFAULTS = Object.freeze({
9
10
  model: "gpt-5.6-terra",
10
11
  model_reasoning_effort: "high"
11
12
  });
12
13
 
13
- const luna = `name = "luna"
14
+ const legacyLunaV12 = `name = "luna"
14
15
  description = "Low-risk helper for repeated edits, searches, formatting, extraction, counting, and summaries."
15
16
  model = "gpt-5.6-luna"
16
17
  model_reasoning_effort = "high"
@@ -21,7 +22,7 @@ Escalate ambiguous, security-sensitive, or logic-heavy decisions to Terra.
21
22
  """
22
23
  `;
23
24
 
24
- const sol = `name = "sol"
25
+ const legacySolWorkspaceWrite = `name = "sol"
25
26
  description = "High-capability specialist for security-sensitive or high-regression-risk work."
26
27
  model = "gpt-5.6-sol"
27
28
  model_reasoning_effort = "medium"
@@ -35,7 +36,18 @@ Do not expand the scope beyond the delegated task.
35
36
  """
36
37
  `;
37
38
 
38
- const skill = `---
39
+ const legacySolReadOnly = `name = "sol"
40
+ description = "Read-only reviewer for security-sensitive or high-regression-risk logic."
41
+ model = "gpt-5.6-sol"
42
+ model_reasoning_effort = "medium"
43
+ sandbox_mode = "read-only"
44
+ developer_instructions = """
45
+ Review only. Focus on security, authentication, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state, and regression risk.
46
+ Report concrete findings to Terra; do not apply fixes.
47
+ """
48
+ `;
49
+
50
+ const legacySkillWorkspaceWrite = `---
39
51
  name: model-router
40
52
  description: Route Codex work between Terra, Luna, and Sol with the fewest required agents.
41
53
  ---
@@ -46,50 +58,196 @@ Use Sol for security, authentication, authorization, permissions, secrets, destr
46
58
  Do not spawn a subagent for a simple question. Use the minimum number of agents and run Luna with Sol only when both independent tasks are required.
47
59
  `;
48
60
 
49
- export const TEMPLATES = Object.freeze({
50
- luna: Object.freeze({ relative: ["agents", "luna.toml"], content: luna }),
51
- sol: Object.freeze({ relative: ["agents", "sol.toml"], content: sol }),
52
- skill: Object.freeze({ content: skill })
53
- });
61
+ const legacySkillReadOnly = `---
62
+ name: model-router
63
+ description: Route Codex work between Terra, Luna, and Sol with the fewest required agents.
64
+ ---
54
65
 
55
- const legacySolReadOnly = `name = "sol"
56
- description = "Read-only reviewer for security-sensitive or high-regression-risk logic."
66
+ Terra handles ordinary questions, coding, debugging, fixes, testing, and implementation. Never create a Terra subagent.
67
+ Use Luna only for deterministic low-risk repeated edits, bulk patterns, read-heavy searches, formatting, counting, extraction, or summaries; prefer Luna when the same clear operation repeats at least three times.
68
+ Use Sol only as a read-only reviewer for security, authentication, authorization, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state changes, high-regression-risk logic, or an explicit review. Terra applies fixes.
69
+ Do not spawn a subagent for a simple question. Use the minimum number of agents and run Luna with Sol only when both independent tasks are required.
70
+ `;
71
+
72
+ const terra = `name = "terra"
73
+ description = "Investigates, plans, verifies, debugs, and replans nontrivial code changes."
74
+ model = "gpt-5.6-terra"
75
+ model_reasoning_effort = "high"
76
+ sandbox_mode = "read-only"
77
+ developer_instructions = """
78
+ Own focused investigation, compact implementation-ready planning, verification, debugging, and replanning.
79
+ Use the implementation-planning skill only for nontrivial or uncertain changes.
80
+ Never invent repository facts; preserve valid work and revise only affected parts.
81
+ Verify both plan conformance and whether the plan plus implementation satisfy the user's requirement.
82
+ Use Sol only when focused investigation and one materially revised plan still leave the core logic unresolved.
83
+ Return only the artifact required by the current stage.
84
+ """
85
+ `;
86
+
87
+ const luna = `name = "luna"
88
+ description = "Implements clear, bounded work from an approved plan."
89
+ model = "gpt-5.6-luna"
90
+ model_reasoning_effort = "xhigh"
91
+ sandbox_mode = "workspace-write"
92
+ developer_instructions = """
93
+ Implement the current approved plan with the smallest coherent change.
94
+ Preserve fixed behavior, contracts, scope, parameter flow, control flow, callers, and invariants.
95
+ Use local choice only for equivalent implementation details.
96
+ Return to Terra when the plan conflicts with current code, is stale, omits a required target or caller, or needs a new correctness-affecting decision.
97
+ Report the plan version, changed files and symbols, deviations, logical checks, and remaining uncertainty.
98
+ Do not self-approve.
99
+ """
100
+ `;
101
+
102
+ const sol = `name = "sol"
103
+ description = "Last-resort advisor for core logic Terra cannot resolve."
57
104
  model = "gpt-5.6-sol"
58
105
  model_reasoning_effort = "medium"
59
106
  sandbox_mode = "read-only"
60
107
  developer_instructions = """
61
- Review only. Focus on security, authentication, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state, and regression risk.
62
- Report concrete findings to Terra; do not apply fixes.
108
+ Analyze only the unresolved logical conflict.
109
+ Identify unsupported assumptions, the likely root cause, required invariants, viable correction directions, and remaining uncertainty.
110
+ Do not perform routine planning, implementation, verification, or final review.
111
+ Return findings to Terra; do not manage Luna.
63
112
  """
64
113
  `;
65
114
 
66
- const legacySkillReadOnly = `---
115
+ const skill = `---
67
116
  name: model-router
68
- description: Route Codex work between Terra, Luna, and Sol with the fewest required agents.
117
+ description: Adaptively route code planning and implementation while preserving the selected primary model and existing agents.
69
118
  ---
70
119
 
71
- Terra handles ordinary questions, coding, debugging, fixes, testing, and implementation. Never create a Terra subagent.
72
- Use Luna only for deterministic low-risk repeated edits, bulk patterns, read-heavy searches, formatting, counting, extraction, or summaries; prefer Luna when the same clear operation repeats at least three times.
73
- Use Sol only as a read-only reviewer for security, authentication, authorization, permissions, secrets, destructive actions, financial logic, SQL writes, concurrency, complex state changes, high-regression-risk logic, or an explicit review. Terra applies fixes.
74
- Do not spawn a subagent for a simple question. Use the minimum number of agents and run Luna with Sol only when both independent tasks are required.
120
+ Preserve repository rules and existing specialized agents. Explicit user instructions win.
121
+ The selected primary model handles conversation, clarification, follow-ups, final replies, and trivial work.
122
+ For code changes: ask the user when expected behavior is unclear; use Terra to investigate, plan, verify, debug, and replan; use Luna for clear bounded implementation; use Sol only when Terra still cannot resolve core logic after focused investigation and one materially revised plan, or when the user explicitly requests Sol.
123
+ Use the implementation-planning skill for nontrivial or uncertain changes.
124
+ Luna may choose equivalent local details only; approved behavior, contracts, scope, parameter flow, control flow, and invariants remain fixed.
125
+ Terra verifies both plan conformance and requirement correctness. Preserve verified work and revise only affected parts.
126
+ Stop after three cycles without new evidence or a changed decision. Use the fewest agents needed.
127
+ Within one agent thread append concise deltas; a new thread receives the latest self-contained snapshot.
128
+ `;
129
+
130
+ const planning = `---
131
+ name: implementation-planning
132
+ description: Create or revise a compact implementation-ready plan for nontrivial code changes involving multiple symbols, contracts, parameters, callers, control flow, or uncertain targets.
133
+ ---
134
+
135
+ Inspect only enough code to confirm correctness-critical facts. Mark information as CONFIRMED, PROPOSED, or UNKNOWN. Do not implement while an UNKNOWN can change behavior, targets, contracts, parameter flow, control flow, or compatibility.
136
+
137
+ Produce one compact current snapshot using only applicable fields:
138
+
139
+ - TASK, REQUIREMENT_VERSION, PLAN_VERSION
140
+ - OBJECTIVE: observable result
141
+ - TARGETS: exact files, symbols, and change type
142
+ - SYMBOLS: exact new names and naming basis
143
+ - CONTRACTS: current and proposed signatures, parameters, defaults, returns, and failure behavior
144
+ - FLOW: source -> caller -> parameter -> transformation -> use -> destination
145
+ - LOGIC: ordered steps and branches
146
+ - CALLERS: affected callers and argument changes
147
+ - INVARIANTS: behavior that must remain true
148
+ - FIXED: decisions Luna cannot change
149
+ - LOCAL_CHOICE: safe implementation freedom
150
+ - RETURN_REQUIRED: conditions requiring Terra
151
+ - DONE_WHEN: acceptance conditions
152
+
153
+ Omit irrelevant fields and vague instructions. Preserve verified work, revise only affected sections, and identify superseded decisions. Create a new snapshot after three deltas or any material contract or logic change.
154
+
155
+ Verification answers separately: did implementation match the plan, and did the plan plus implementation satisfy the request? Use PASS, IMPLEMENTATION_FIX, PLAN_REVISION, EVIDENCE_GAP, or REQUIREMENT_CLARIFICATION.
75
156
  `;
76
157
 
77
- export const LEGACY_TEMPLATES = Object.freeze({
78
- luna: Object.freeze([]),
79
- sol: Object.freeze([legacySolReadOnly]),
80
- skill: Object.freeze([legacySkillReadOnly])
158
+ export const REASONING_EFFORTS = Object.freeze([
159
+ "none",
160
+ "low",
161
+ "medium",
162
+ "high",
163
+ "xhigh",
164
+ "max"
165
+ ]);
166
+
167
+ export const DEFAULT_AGENT_REASONING = Object.freeze({
168
+ terra: "high",
169
+ luna: "xhigh",
170
+ sol: "medium"
81
171
  });
82
172
 
83
- export const AGENT_EXPECTATIONS = Object.freeze({
84
- luna: Object.freeze({
173
+ export const AGENT_NAMES = Object.freeze(["terra", "luna", "sol"]);
174
+
175
+ const AGENT_BASE_TEMPLATES = Object.freeze({ terra, luna, sol });
176
+
177
+ function withReasoning(content, reasoning) {
178
+ const pattern = /^model_reasoning_effort = "[^"]+"$/m;
179
+ if (!pattern.test(content)) throw new Error("agent template is missing model_reasoning_effort");
180
+ return content.replace(pattern, `model_reasoning_effort = "${reasoning}"`);
181
+ }
182
+
183
+ export const TEMPLATES = {
184
+ terra: { relative: Object.freeze(["agents", "terra.toml"]), content: terra },
185
+ luna: { relative: Object.freeze(["agents", "luna.toml"]), content: luna },
186
+ sol: { relative: Object.freeze(["agents", "sol.toml"]), content: sol },
187
+ skill: { relative: Object.freeze(["model-router", "SKILL.md"]), content: skill },
188
+ planning: { relative: Object.freeze(["implementation-planning", "SKILL.md"]), content: planning }
189
+ };
190
+
191
+ export const MANAGED_FILE_NAMES = Object.freeze(["terra", "luna", "sol", "skill", "planning"]);
192
+
193
+ export const LEGACY_TEMPLATES = {
194
+ terra: [],
195
+ luna: [legacyLunaV12, withReasoning(luna, "max")],
196
+ sol: [legacySolWorkspaceWrite, legacySolReadOnly],
197
+ skill: [legacySkillWorkspaceWrite, legacySkillReadOnly],
198
+ planning: []
199
+ };
200
+
201
+ export const AGENT_EXPECTATIONS = {
202
+ terra: {
203
+ name: "terra",
204
+ model: "gpt-5.6-terra",
205
+ model_reasoning_effort: "high",
206
+ sandbox_mode: "read-only"
207
+ },
208
+ luna: {
85
209
  name: "luna",
86
210
  model: "gpt-5.6-luna",
87
- model_reasoning_effort: "high"
88
- }),
89
- sol: Object.freeze({
211
+ model_reasoning_effort: "xhigh",
212
+ sandbox_mode: "workspace-write"
213
+ },
214
+ sol: {
90
215
  name: "sol",
91
216
  model: "gpt-5.6-sol",
92
217
  model_reasoning_effort: "medium",
93
- sandbox_mode: "workspace-write"
218
+ sandbox_mode: "read-only"
219
+ }
220
+ };
221
+
222
+ export function configureAgentReasoning(overrides = {}, migrateFrom = {}) {
223
+ const selected = { ...DEFAULT_AGENT_REASONING, ...overrides };
224
+ for (const [name, reasoning] of Object.entries(selected)) {
225
+ if (!AGENT_NAMES.includes(name)) throw new Error(`unknown agent: ${name}`);
226
+ if (!REASONING_EFFORTS.includes(reasoning)) {
227
+ throw new Error(`unsupported reasoning effort for ${name}: ${reasoning}`);
228
+ }
229
+ }
230
+
231
+ for (const name of AGENT_NAMES) {
232
+ const previous = migrateFrom[name];
233
+ if (typeof previous === "string" && !LEGACY_TEMPLATES[name].includes(previous)) {
234
+ LEGACY_TEMPLATES[name].push(previous);
235
+ }
236
+ TEMPLATES[name].content = withReasoning(AGENT_BASE_TEMPLATES[name], selected[name]);
237
+ AGENT_EXPECTATIONS[name].model_reasoning_effort = selected[name];
238
+ }
239
+ return Object.freeze({ ...selected });
240
+ }
241
+
242
+ configureAgentReasoning();
243
+
244
+ export const SKILL_EXPECTATIONS = Object.freeze({
245
+ skill: Object.freeze({
246
+ name: "model-router",
247
+ required: Object.freeze(["primary model", "Terra", "Luna", "Sol", "three cycles"])
248
+ }),
249
+ planning: Object.freeze({
250
+ name: "implementation-planning",
251
+ required: Object.freeze(["CONFIRMED", "PROPOSED", "UNKNOWN", "FIXED", "LOCAL_CHOICE", "RETURN_REQUIRED", "PLAN_REVISION"])
94
252
  })
95
253
  });
@@ -17,15 +17,18 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
17
  import { applyEdits, insertionEdit, removalEdit, scanToml } from "./toml.js";
18
18
  import {
19
19
  AGENT_EXPECTATIONS,
20
+ AGENT_NAMES,
20
21
  DEFAULTS,
21
22
  LEGACY_TEMPLATES,
23
+ MANAGED_FILE_NAMES,
24
+ SKILL_EXPECTATIONS,
22
25
  TEMPLATES,
23
26
  VERSION
24
27
  } from "./manifest.js";
25
28
 
26
29
  export { VERSION };
27
30
 
28
- const STATE_VERSION = 3;
31
+ const STATE_VERSION = 4;
29
32
  const LOCK_FILE = "model-router.lock";
30
33
  const JOURNAL_FILE = "model-router-transaction.json";
31
34
  const TRANSACTION_DIR = "model-router-transaction-data";
@@ -213,9 +216,11 @@ function expectedPaths({ cwd, home, global, env }) {
213
216
  config: join(codexHome, "config.toml"),
214
217
  state: join(codexHome, "model-router-state.json"),
215
218
  backup: join(codexHome, "config.toml.codex-model-router.bak"),
219
+ terra: join(codexHome, ...TEMPLATES.terra.relative),
216
220
  luna: join(codexHome, ...TEMPLATES.luna.relative),
217
221
  sol: join(codexHome, ...TEMPLATES.sol.relative),
218
- skill: join(skillsHome, "model-router", "SKILL.md"),
222
+ skill: join(skillsHome, ...TEMPLATES.skill.relative),
223
+ planning: join(skillsHome, ...TEMPLATES.planning.relative),
219
224
  lock: join(codexHome, LOCK_FILE),
220
225
  journal: join(codexHome, JOURNAL_FILE),
221
226
  transactionData: join(codexHome, TRANSACTION_DIR)
@@ -239,6 +244,7 @@ async function validateLocationSafety(location) {
239
244
  location.config,
240
245
  location.state,
241
246
  location.backup,
247
+ location.terra,
242
248
  location.luna,
243
249
  location.sol,
244
250
  location.lock,
@@ -248,7 +254,7 @@ async function validateLocationSafety(location) {
248
254
  for (const target of codexTargets) {
249
255
  await assertNoIndirection(target, location.codexHome, codexAnchor, target);
250
256
  }
251
- const skillsTargets = [location.skillsRoot, location.skillsHome, location.skill];
257
+ const skillsTargets = [location.skillsRoot, location.skillsHome, location.skill, location.planning];
252
258
  for (const target of skillsTargets) {
253
259
  await assertNoIndirection(target, location.skillsRoot, skillsAnchor, target);
254
260
  }
@@ -295,7 +301,7 @@ function normalizeLegacyState(raw, location) {
295
301
  source: item.previous == null ? "inserted" : "replaced"
296
302
  };
297
303
  }
298
- for (const name of ["luna", "sol", "skill"]) {
304
+ for (const name of MANAGED_FILE_NAMES) {
299
305
  const item = raw.files[name];
300
306
  if (!item) continue;
301
307
  assertExactPath(item.path, location[name], name);
@@ -312,7 +318,7 @@ function validateState(raw, location) {
312
318
  if (!raw || typeof raw !== "object") throw new Error("state is not an object");
313
319
  let state;
314
320
  if (!raw.version) state = normalizeLegacyState(raw, location);
315
- else if (raw.version === 2 || raw.version === STATE_VERSION) state = structuredClone(raw);
321
+ else if ([2, 3, STATE_VERSION].includes(raw.version)) state = structuredClone(raw);
316
322
  else throw new Error(`unsupported state version: ${raw.version}`);
317
323
 
318
324
  state.version = STATE_VERSION;
@@ -410,10 +416,12 @@ function stateJson(state) {
410
416
  async function missingDirectories(location) {
411
417
  const candidates = [
412
418
  location.codexHome,
419
+ dirname(location.terra),
413
420
  dirname(location.luna),
414
421
  location.skillsRoot,
415
422
  location.skillsHome,
416
- dirname(location.skill)
423
+ dirname(location.skill),
424
+ dirname(location.planning)
417
425
  ];
418
426
  const unique = [...new Set(candidates.map((value) => resolve(value)))].sort((a, b) => a.length - b.length);
419
427
  const missing = [];
@@ -429,6 +437,29 @@ function patchInstallConfig(original, state, setDefault) {
429
437
  const additions = [];
430
438
  const nextValues = structuredClone(state.config.values || {});
431
439
 
440
+ if (!setDefault) {
441
+ for (const [key, item] of Object.entries(nextValues)) {
442
+ const assignment = scan.targets.get(key);
443
+ if (!assignment) {
444
+ delete nextValues[key];
445
+ continue;
446
+ }
447
+ if (assignment.parsedValue.kind !== "string") {
448
+ throw new Error(`${key} must be a TOML string`);
449
+ }
450
+ if (semanticValue(assignment) === item.installed) {
451
+ if (item.previousRaw === null) edits.push(removalEdit(assignment));
452
+ else edits.push({ start: assignment.valueStart, end: assignment.valueEnd, text: item.previousRaw });
453
+ }
454
+ delete nextValues[key];
455
+ }
456
+ return {
457
+ content: edits.length ? applyEdits(original, edits) : original,
458
+ values: nextValues,
459
+ changed: edits.length > 0
460
+ };
461
+ }
462
+
432
463
  for (const [key, installed] of Object.entries(DEFAULTS)) {
433
464
  const assignment = scan.targets.get(key);
434
465
  const tracked = nextValues[key];
@@ -441,7 +472,6 @@ function patchInstallConfig(original, state, setDefault) {
441
472
  nextValues[key] = tracked || { installed, previousRaw: null, source: "inserted" };
442
473
  continue;
443
474
  }
444
- if (!setDefault) continue;
445
475
  if (current === installed) continue;
446
476
  edits.push({ start: assignment.valueStart, end: assignment.valueEnd, text: quoteToml(installed) });
447
477
  nextValues[key] = tracked || {
@@ -509,26 +539,43 @@ async function planInstall(scope, flags) {
509
539
  state.config.values = patched.values;
510
540
 
511
541
  if (patched.changed) {
512
- if (!state.backup && config.exists) {
513
- const backup = await readText(location.backup);
514
- if (backup.exists) {
515
- failPlan(plan, `untracked backup already exists: ${location.backup}`);
516
- return plan;
542
+ const empty = patched.content.replace(/^\uFEFF/, "").trim() === "";
543
+ if (!flags.setDefault && state.config.createdFile && empty) {
544
+ addDelete(plan, location.config, "config.toml");
545
+ state.config.createdFile = false;
546
+ } else {
547
+ if (flags.setDefault && !state.backup && config.exists) {
548
+ const backup = await readText(location.backup);
549
+ if (backup.exists) {
550
+ failPlan(plan, `untracked backup already exists: ${location.backup}`);
551
+ return plan;
552
+ }
553
+ state.backup = { path: location.backup, hash: digest(Buffer.from(original, "utf8")) };
554
+ addWrite(plan, location.backup, original, "create", "config backup");
517
555
  }
518
- state.backup = { path: location.backup, hash: digest(Buffer.from(original, "utf8")) };
519
- addWrite(plan, location.backup, original, "create", "config backup");
556
+ addWrite(plan, location.config, patched.content, config.exists ? "update" : "create", "config.toml");
520
557
  }
521
- addWrite(plan, location.config, patched.content, config.exists ? "update" : "create", "config.toml");
522
558
  } else {
523
559
  addMessage(
524
560
  plan,
525
561
  "preserve",
526
562
  "config.toml",
527
- flags.setDefault ? "already matches or contains protected values" : "existing defaults preserved"
563
+ flags.setDefault ? "already matches or contains protected values" : "primary model preserved"
528
564
  );
529
565
  }
530
566
 
531
- for (const name of ["luna", "sol", "skill"]) {
567
+ if (!flags.setDefault && Object.keys(state.config.values).length === 0 && state.backup) {
568
+ const backup = await readText(location.backup);
569
+ if (!backup.exists) addMessage(plan, "skip", "config backup", "already missing");
570
+ else if (digest(Buffer.from(backup.text, "utf8")) === state.backup.hash) {
571
+ addDelete(plan, location.backup, "config backup");
572
+ } else {
573
+ addMessage(plan, "preserve", "config backup", "user-modified");
574
+ }
575
+ state.backup = null;
576
+ }
577
+
578
+ for (const name of MANAGED_FILE_NAMES) {
532
579
  const current = await readText(location[name]);
533
580
  const tracked = state.files[name];
534
581
  const template = TEMPLATES[name].content;
@@ -599,7 +646,7 @@ async function planUninstall(scope) {
599
646
  }
600
647
  state.config.values = {};
601
648
 
602
- for (const name of ["luna", "sol", "skill"]) {
649
+ for (const name of MANAGED_FILE_NAMES) {
603
650
  const tracked = state.files[name];
604
651
  if (!tracked) continue;
605
652
  const current = await readText(location[name]);
@@ -1015,16 +1062,15 @@ function getString(assignments, key) {
1015
1062
  return assignment?.parsedValue?.kind === "string" ? assignment.parsedValue.value : null;
1016
1063
  }
1017
1064
 
1018
- function validateSkill(content) {
1065
+ function validateSkill(content, expectation) {
1019
1066
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
1020
1067
  if (!match) return "invalid front matter";
1021
1068
  const front = match[1];
1022
1069
  const body = match[2];
1023
- if (!/^name:\s*model-router\s*$/m.test(front)) return "incorrect skill name";
1070
+ if (!front.split(/\r?\n/).some((line) => line.trim() === `name: ${expectation.name}`)) return "incorrect skill name";
1024
1071
  if (!/^description:\s*\S.+$/m.test(front)) return "missing skill description";
1025
- const requirements = [/Terra/i, /Luna/i, /Sol/i, /workspace-write/i, /simple question/i, /minimum number|fewest/i];
1026
- if (requirements.some((pattern) => !pattern.test(body))) return "missing required routing rule";
1027
- return null;
1072
+ const missing = expectation.required.find((term) => !body.toLowerCase().includes(term.toLowerCase()));
1073
+ return missing ? `missing required rule: ${missing}` : null;
1028
1074
  }
1029
1075
 
1030
1076
  function doctorStatus(messages, status, label, detail) {
@@ -1088,19 +1134,22 @@ async function doctor(scope) {
1088
1134
  doctorStatus(messages, "healthy", "state", `schema v${state.version}`);
1089
1135
 
1090
1136
  const config = await readText(location.config);
1091
- if (!config.exists) {
1137
+ const managedConfigKeys = Object.keys(state.config.values || {});
1138
+ if (!managedConfigKeys.length) {
1139
+ doctorStatus(messages, "healthy", "config.toml", config.exists ? "primary model preserved" : "free mode");
1140
+ } else if (!config.exists) {
1092
1141
  healthy = doctorStatus(messages, "missing", "config.toml") && healthy;
1093
1142
  } else {
1094
1143
  try {
1095
1144
  const scan = scanToml(config.text);
1096
- for (const [key, expected] of Object.entries(DEFAULTS)) {
1145
+ for (const key of managedConfigKeys) {
1146
+ const expected = DEFAULTS[key];
1097
1147
  const assignment = scan.targets.get(key);
1098
1148
  if (!assignment) healthy = doctorStatus(messages, "missing", key) && healthy;
1099
1149
  else if (assignment.parsedValue.kind !== "string") {
1100
1150
  healthy = doctorStatus(messages, "invalid", key, "must be a TOML string") && healthy;
1101
1151
  } else if (assignment.parsedValue.value !== expected) {
1102
- const status = state.config.values[key] ? "user-modified" : "user-override";
1103
- healthy = doctorStatus(messages, status, key, assignment.parsedValue.value ?? assignment.rawValue) && healthy;
1152
+ healthy = doctorStatus(messages, "user-modified", key, assignment.parsedValue.value ?? assignment.rawValue) && healthy;
1104
1153
  } else {
1105
1154
  doctorStatus(messages, "healthy", key, expected);
1106
1155
  }
@@ -1110,7 +1159,7 @@ async function doctor(scope) {
1110
1159
  }
1111
1160
  }
1112
1161
 
1113
- for (const name of ["luna", "sol"]) {
1162
+ for (const name of AGENT_NAMES) {
1114
1163
  const current = await readText(location[name]);
1115
1164
  if (!current.exists) {
1116
1165
  healthy = doctorStatus(messages, "missing", name) && healthy;
@@ -1133,17 +1182,19 @@ async function doctor(scope) {
1133
1182
  }
1134
1183
  }
1135
1184
 
1136
- const skill = await readText(location.skill);
1137
- if (!skill.exists) {
1138
- healthy = doctorStatus(messages, "missing", "skill") && healthy;
1139
- } else {
1140
- const invalid = validateSkill(skill.text);
1141
- if (state.files.skill && digest(Buffer.from(skill.text, "utf8")) !== state.files.skill.hash) {
1142
- healthy = doctorStatus(messages, "user-modified", "skill", invalid || "managed file hash changed") && healthy;
1185
+ for (const [name, expectation] of Object.entries(SKILL_EXPECTATIONS)) {
1186
+ const current = await readText(location[name]);
1187
+ if (!current.exists) {
1188
+ healthy = doctorStatus(messages, "missing", name) && healthy;
1189
+ continue;
1190
+ }
1191
+ const invalid = validateSkill(current.text, expectation);
1192
+ if (state.files[name] && digest(Buffer.from(current.text, "utf8")) !== state.files[name].hash) {
1193
+ healthy = doctorStatus(messages, "user-modified", name, invalid || "managed file hash changed") && healthy;
1143
1194
  } else if (invalid) {
1144
- healthy = doctorStatus(messages, "invalid", "skill", invalid) && healthy;
1195
+ healthy = doctorStatus(messages, "invalid", name, invalid) && healthy;
1145
1196
  } else {
1146
- doctorStatus(messages, "healthy", "skill");
1197
+ doctorStatus(messages, "healthy", name);
1147
1198
  }
1148
1199
  }
1149
1200
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "codex-model-router",
3
- "version": "1.2.0",
4
- "description": "Safely install Codex subagent routing for Terra, Luna, and Sol.",
3
+ "version": "2.1.0",
4
+ "description": "Safely install adaptive free-mode Codex routing for Terra, Luna, and Sol.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "codex-model-router": "./bin/codex-model-router.js"
8
8
  },
9
9
  "scripts": {
10
- "check": "node --check bin/codex-model-router.js && node --check lib/manifest.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
10
+ "check": "node --check bin/codex-model-router.js && node --check lib/agent-reasoning-cli.js && node --check lib/manifest.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
11
11
  "test": "node --test",
12
12
  "test:package": "node scripts/package-smoke.js"
13
13
  },