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 +71 -135
- package/bin/codex-model-router.js +2 -2
- package/lib/agent-reasoning-cli.js +160 -0
- package/lib/manifest.js +186 -28
- package/lib/router-core.js +88 -37
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,222 +1,158 @@
|
|
|
1
1
|
# codex-model-router
|
|
2
2
|
|
|
3
|
-
A small Node.js CLI that safely installs
|
|
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.
|
|
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
|
-
##
|
|
7
|
+
## Routing model
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The normal installation uses free mode:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
|
|
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
|
|
21
|
+
Preview without changing files:
|
|
24
22
|
|
|
25
23
|
```sh
|
|
26
|
-
npx codex-model-router install --
|
|
24
|
+
npx codex-model-router install --dry-run
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
Install
|
|
27
|
+
Install free-mode routing and verify it:
|
|
30
28
|
|
|
31
29
|
```sh
|
|
32
|
-
npx codex-model-router install
|
|
30
|
+
npx codex-model-router install
|
|
33
31
|
npx codex-model-router doctor
|
|
34
32
|
```
|
|
35
33
|
|
|
36
|
-
Remove only unchanged package-managed
|
|
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
|
-
##
|
|
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
|
-
|
|
44
|
+
To explicitly set and track Terra/high as the selected scope's default:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
|
|
47
|
+
codex-model-router install --set-default
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
Run from npm:
|
|
60
66
|
|
|
61
67
|
```sh
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
+
Existing files at managed paths are never overwritten. Later manual edits are preserved and reported by `doctor`.
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
codex-model-router install --set-default
|
|
116
|
-
```
|
|
113
|
+
## Prompt and cache efficiency
|
|
117
114
|
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
## Planning rules
|
|
128
122
|
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
163
|
-
- Uses
|
|
164
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import { runCli } from "../lib/agent-reasoning-cli.js";
|
|
3
3
|
|
|
4
|
-
process.exitCode = await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
115
|
+
const skill = `---
|
|
67
116
|
name: model-router
|
|
68
|
-
description:
|
|
117
|
+
description: Adaptively route code planning and implementation while preserving the selected primary model and existing agents.
|
|
69
118
|
---
|
|
70
119
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
84
|
-
|
|
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: "
|
|
88
|
-
|
|
89
|
-
|
|
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: "
|
|
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
|
});
|
package/lib/router-core.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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" : "
|
|
563
|
+
flags.setDefault ? "already matches or contains protected values" : "primary model preserved"
|
|
528
564
|
);
|
|
529
565
|
}
|
|
530
566
|
|
|
531
|
-
|
|
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
|
|
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 (
|
|
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
|
|
1026
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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",
|
|
1195
|
+
healthy = doctorStatus(messages, "invalid", name, invalid) && healthy;
|
|
1145
1196
|
} else {
|
|
1146
|
-
doctorStatus(messages, "healthy",
|
|
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.
|
|
4
|
-
"description": "Safely install Codex
|
|
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
|
},
|