codex-model-router 2.0.0 → 2.2.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 +56 -5
- package/bin/codex-model-router.js +2 -2
- package/lib/agent-reasoning-cli.js +181 -0
- package/lib/experimental-v2.js +566 -0
- package/lib/manifest.js +70 -24
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The normal installation uses free mode:
|
|
|
10
10
|
|
|
11
11
|
- The user-selected primary model handles conversation, clarification, follow-ups, final replies, and trivial work.
|
|
12
12
|
- Terra/high investigates, produces implementation-ready plans, verifies results, debugs, and replans.
|
|
13
|
-
- Luna/
|
|
13
|
+
- Luna/xhigh performs most clear, bounded, repetitive, or independently verifiable implementation work.
|
|
14
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
15
|
- Existing built-in and custom agents remain available and take precedence when they are a better match.
|
|
16
16
|
|
|
@@ -49,6 +49,47 @@ codex-model-router install --set-default
|
|
|
49
49
|
|
|
50
50
|
Running a later plain `install` returns package-managed default settings to free mode. User-modified model settings are preserved.
|
|
51
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:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
codex-model-router install --agent-reasoning high
|
|
58
|
+
codex-model-router install --terra-reasoning xhigh --luna-reasoning low --sol-reasoning max
|
|
59
|
+
```
|
|
60
|
+
|
|
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.
|
|
62
|
+
|
|
63
|
+
## Optional experimental multi-agent V2
|
|
64
|
+
|
|
65
|
+
Codex currently has an experimental `multi_agent_v2` configuration that is not part of the public configuration reference. It may be unavailable for some accounts or Codex builds, and its keys or behavior may change without notice.
|
|
66
|
+
|
|
67
|
+
A normal install never enables it. Enable it explicitly for the current project only after accepting that compatibility risk:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
codex-model-router v2 enable --dry-run
|
|
71
|
+
codex-model-router v2 enable
|
|
72
|
+
codex-model-router v2 status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The managed block is:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[features.multi_agent_v2]
|
|
79
|
+
hide_spawn_agent_metadata = false
|
|
80
|
+
tool_namespace = "agents"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Disable only the unchanged block created by this package:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
codex-model-router v2 disable
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `--global` with `enable`, `disable`, or `status` to target the user-level Codex configuration. Pre-existing V2 settings are treated as user-managed and are never overwritten. A normal package uninstall also removes an unchanged package-managed V2 block; modified V2 content is preserved and reported.
|
|
90
|
+
|
|
91
|
+
Enabling this setting does not guarantee that every child agent resolves to the requested model. Verify actual subagent model usage from Codex session metadata when cost control depends on Luna being selected.
|
|
92
|
+
|
|
52
93
|
## Install
|
|
53
94
|
|
|
54
95
|
Run from npm:
|
|
@@ -69,7 +110,7 @@ Installing the CLI globally only makes the command available system-wide. Use `i
|
|
|
69
110
|
Install a specific GitHub release:
|
|
70
111
|
|
|
71
112
|
```sh
|
|
72
|
-
npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v2.
|
|
113
|
+
npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v2.2.0.tar.gz
|
|
73
114
|
codex-model-router install
|
|
74
115
|
```
|
|
75
116
|
|
|
@@ -78,11 +119,12 @@ codex-model-router install
|
|
|
78
119
|
Project scope is the default and manages only:
|
|
79
120
|
|
|
80
121
|
```text
|
|
81
|
-
.codex/config.toml # only with --set-default or a managed migration
|
|
122
|
+
.codex/config.toml # only with --set-default, V2 opt-in, or a managed migration
|
|
82
123
|
.codex/agents/terra.toml
|
|
83
124
|
.codex/agents/luna.toml
|
|
84
125
|
.codex/agents/sol.toml
|
|
85
126
|
.codex/model-router-state.json
|
|
127
|
+
.codex/model-router-v2-state.json # only after explicit `v2 enable`
|
|
86
128
|
.codex/config.toml.codex-model-router.bak # only when needed
|
|
87
129
|
.agents/skills/model-router/SKILL.md
|
|
88
130
|
.agents/skills/implementation-planning/SKILL.md
|
|
@@ -93,6 +135,7 @@ Use global scope with:
|
|
|
93
135
|
```sh
|
|
94
136
|
codex-model-router install --global
|
|
95
137
|
codex-model-router doctor --global
|
|
138
|
+
codex-model-router v2 enable --global
|
|
96
139
|
```
|
|
97
140
|
|
|
98
141
|
Global scope stores agents under `$CODEX_HOME/agents` and skills under `~/.agents/skills`. When `CODEX_HOME` is unset, it defaults to `~/.codex`.
|
|
@@ -119,8 +162,13 @@ Terra verifies separately whether implementation matches the plan and whether th
|
|
|
119
162
|
|
|
120
163
|
```text
|
|
121
164
|
codex-model-router install [--global] [--set-default] [--dry-run]
|
|
165
|
+
[--agent-reasoning <effort>]
|
|
166
|
+
[--terra-reasoning <effort>] [--luna-reasoning <effort>] [--sol-reasoning <effort>]
|
|
122
167
|
codex-model-router uninstall [--global] [--dry-run]
|
|
123
168
|
codex-model-router doctor [--global]
|
|
169
|
+
codex-model-router v2 enable [--global] [--dry-run]
|
|
170
|
+
codex-model-router v2 disable [--global] [--dry-run]
|
|
171
|
+
codex-model-router v2 status [--global]
|
|
124
172
|
codex-model-router --version
|
|
125
173
|
```
|
|
126
174
|
|
|
@@ -131,7 +179,8 @@ codex-model-router --version
|
|
|
131
179
|
- Preserves unrelated TOML settings, comments, BOM, ordering, and LF/CRLF line endings.
|
|
132
180
|
- Rejects malformed, non-UTF-8, duplicate, or unsafe configuration before writing.
|
|
133
181
|
- Rejects symlinks and Windows junctions in managed paths.
|
|
134
|
-
- Uses scope-level locking, atomic transactions, rollback, and conflict-safe recovery.
|
|
182
|
+
- Uses scope-level locking, atomic transactions, rollback, and conflict-safe recovery for normal routing installation.
|
|
183
|
+
- The optional V2 manager uses the same scope lock, atomic file replacement, an exact marked block, and a separate state file.
|
|
135
184
|
- Never overwrites post-interruption user changes.
|
|
136
185
|
- Dry-run creates no files, directories, backups, locks, journals, or state.
|
|
137
186
|
- Never modifies AGENTS.md, shell profiles, editor settings, hooks, MCP servers, telemetry, accounts, or environment variables.
|
|
@@ -140,6 +189,8 @@ codex-model-router --version
|
|
|
140
189
|
|
|
141
190
|
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.
|
|
142
191
|
|
|
143
|
-
|
|
192
|
+
The optional V2 setting is intentionally excluded from compatibility guarantees because it is not publicly documented by OpenAI. Failure or removal of that experimental setting does not affect the normal V1/custom-agent installation.
|
|
193
|
+
|
|
194
|
+
Restart Codex after installation or V2 changes so new agents, skills, and configuration are discovered. Use `doctor` for missing, user-modified, unsafe-state, lock, interrupted-transaction, or managed V2 reports.
|
|
144
195
|
|
|
145
196
|
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,181 @@
|
|
|
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 {
|
|
13
|
+
cleanupV2AfterUninstall,
|
|
14
|
+
reportV2Doctor,
|
|
15
|
+
runV2Command
|
|
16
|
+
} from "./experimental-v2.js";
|
|
17
|
+
import { run as runCore } from "./router.js";
|
|
18
|
+
|
|
19
|
+
const OPTION_TO_AGENT = Object.freeze({
|
|
20
|
+
"--terra-reasoning": "terra",
|
|
21
|
+
"--luna-reasoning": "luna",
|
|
22
|
+
"--sol-reasoning": "sol"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function usage() {
|
|
26
|
+
return `codex-model-router ${VERSION}
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
codex-model-router install [--global] [--set-default] [--dry-run]
|
|
30
|
+
[--agent-reasoning <effort>]
|
|
31
|
+
[--terra-reasoning <effort>]
|
|
32
|
+
[--luna-reasoning <effort>]
|
|
33
|
+
[--sol-reasoning <effort>]
|
|
34
|
+
codex-model-router uninstall [--global] [--dry-run]
|
|
35
|
+
codex-model-router doctor [--global]
|
|
36
|
+
codex-model-router v2 enable [--global] [--dry-run]
|
|
37
|
+
codex-model-router v2 disable [--global] [--dry-run]
|
|
38
|
+
codex-model-router v2 status [--global]
|
|
39
|
+
codex-model-router --version
|
|
40
|
+
|
|
41
|
+
Reasoning efforts: ${REASONING_EFFORTS.join(", ")}
|
|
42
|
+
Defaults: Terra=${DEFAULT_AGENT_REASONING.terra}, Luna=${DEFAULT_AGENT_REASONING.luna}, Sol=${DEFAULT_AGENT_REASONING.sol}
|
|
43
|
+
|
|
44
|
+
V2 commands manage an experimental, undocumented Codex setting and are never enabled by a normal install.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function optionValue(argv, index, option) {
|
|
48
|
+
const token = argv[index];
|
|
49
|
+
if (token.startsWith(`${option}=`)) return { value: token.slice(option.length + 1), consumed: 1 };
|
|
50
|
+
const value = argv[index + 1];
|
|
51
|
+
if (!value || value.startsWith("--")) return { error: `missing value for ${option}` };
|
|
52
|
+
return { value, consumed: 2 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseReasoningOptions(argv) {
|
|
56
|
+
const command = argv[0];
|
|
57
|
+
const cleaned = command ? [command] : [];
|
|
58
|
+
const overrides = {};
|
|
59
|
+
let all;
|
|
60
|
+
|
|
61
|
+
for (let index = 1; index < argv.length;) {
|
|
62
|
+
const token = argv[index];
|
|
63
|
+
const option = token.split("=", 1)[0];
|
|
64
|
+
const agent = OPTION_TO_AGENT[option];
|
|
65
|
+
if (option !== "--agent-reasoning" && !agent) {
|
|
66
|
+
cleaned.push(token);
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (command !== "install") return { error: `${option} is supported only for install` };
|
|
71
|
+
const parsed = optionValue(argv, index, option);
|
|
72
|
+
if (parsed.error) return parsed;
|
|
73
|
+
const reasoning = parsed.value.toLowerCase();
|
|
74
|
+
if (!REASONING_EFFORTS.includes(reasoning)) {
|
|
75
|
+
return { error: `unsupported reasoning effort: ${parsed.value}` };
|
|
76
|
+
}
|
|
77
|
+
if (agent) overrides[agent] = reasoning;
|
|
78
|
+
else all = reasoning;
|
|
79
|
+
index += parsed.consumed;
|
|
80
|
+
}
|
|
81
|
+
return { cleaned, overrides, all };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function digest(content) {
|
|
85
|
+
return createHash("sha256").update(content).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readRegularText(path) {
|
|
89
|
+
try {
|
|
90
|
+
const info = await lstat(path);
|
|
91
|
+
if (!info.isFile() || info.isSymbolicLink()) return null;
|
|
92
|
+
return await readFile(path, "utf8");
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error?.code === "ENOENT") return null;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function activeReasoning(content) {
|
|
100
|
+
const match = content.match(/^\s*model_reasoning_effort\s*=\s*"([^"\r\n]+)"\s*(?:#.*)?$/m);
|
|
101
|
+
return match && REASONING_EFFORTS.includes(match[1]) ? match[1] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function versionAtLeast(version, minimum) {
|
|
105
|
+
const parse = (value) => String(value || "0").split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
106
|
+
const left = parse(version);
|
|
107
|
+
const right = parse(minimum);
|
|
108
|
+
for (let index = 0; index < 3; index += 1) {
|
|
109
|
+
if (left[index] !== right[index]) return left[index] > right[index];
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function installedReasoning(argv, options) {
|
|
115
|
+
const global = argv.includes("--global");
|
|
116
|
+
const home = resolve(options.home ?? homedir());
|
|
117
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
118
|
+
const env = options.env ?? process.env;
|
|
119
|
+
const codexHome = global
|
|
120
|
+
? resolve(env.CODEX_HOME || join(home, ".codex"))
|
|
121
|
+
: join(cwd, ".codex");
|
|
122
|
+
const stateText = await readRegularText(join(codexHome, "model-router-state.json"));
|
|
123
|
+
if (!stateText) return { reasoning: {}, content: {}, packageVersion: null };
|
|
124
|
+
|
|
125
|
+
let state;
|
|
126
|
+
try {
|
|
127
|
+
state = JSON.parse(stateText);
|
|
128
|
+
} catch {
|
|
129
|
+
return { reasoning: {}, content: {}, packageVersion: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const physicalCodexHome = await realpath(codexHome);
|
|
133
|
+
const reasoning = {};
|
|
134
|
+
const content = {};
|
|
135
|
+
for (const name of AGENT_NAMES) {
|
|
136
|
+
const path = join(physicalCodexHome, "agents", `${name}.toml`);
|
|
137
|
+
const tracked = state.files?.[name];
|
|
138
|
+
const current = await readRegularText(path);
|
|
139
|
+
if (!tracked || !current || resolve(tracked.path) !== resolve(path) || digest(current) !== tracked.hash) continue;
|
|
140
|
+
const effort = activeReasoning(current);
|
|
141
|
+
if (!effort) continue;
|
|
142
|
+
reasoning[name] = effort;
|
|
143
|
+
content[name] = current;
|
|
144
|
+
}
|
|
145
|
+
return { reasoning, content, packageVersion: state.packageVersion };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function runCli(argv, options = {}) {
|
|
149
|
+
const output = options.output ?? console.log;
|
|
150
|
+
if (argv[0] === "v2") return runV2Command(argv.slice(1), options);
|
|
151
|
+
if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
|
|
152
|
+
output(usage());
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const parsed = parseReasoningOptions(argv);
|
|
157
|
+
if (parsed.error) {
|
|
158
|
+
output(`${parsed.error}\n\n${usage()}`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const installed = await installedReasoning(parsed.cleaned, options);
|
|
163
|
+
const selected = { ...DEFAULT_AGENT_REASONING };
|
|
164
|
+
if (versionAtLeast(installed.packageVersion, "2.1.0")) Object.assign(selected, installed.reasoning);
|
|
165
|
+
if (parsed.all) {
|
|
166
|
+
for (const name of AGENT_NAMES) selected[name] = parsed.all;
|
|
167
|
+
}
|
|
168
|
+
Object.assign(selected, parsed.overrides);
|
|
169
|
+
configureAgentReasoning(selected, installed.content);
|
|
170
|
+
|
|
171
|
+
const result = await runCore(parsed.cleaned, options);
|
|
172
|
+
const command = parsed.cleaned[0];
|
|
173
|
+
if (command === "doctor") {
|
|
174
|
+
const v2Result = await reportV2Doctor(parsed.cleaned.slice(1), options);
|
|
175
|
+
return result || v2Result;
|
|
176
|
+
}
|
|
177
|
+
if (command === "uninstall" && result === 0) {
|
|
178
|
+
return cleanupV2AfterUninstall(parsed.cleaned.slice(1), options);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
const STATE_FILE = "model-router-v2-state.json";
|
|
7
|
+
const LOCK_FILE = "model-router.lock";
|
|
8
|
+
const START_MARKER = "# >>> codex-model-router experimental multi-agent-v2 >>>";
|
|
9
|
+
const END_MARKER = "# <<< codex-model-router experimental multi-agent-v2 <<<";
|
|
10
|
+
const STATE_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
export const EXPERIMENTAL_V2_SETTINGS = Object.freeze({
|
|
13
|
+
hide_spawn_agent_metadata: false,
|
|
14
|
+
tool_namespace: "agents"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function digest(value) {
|
|
18
|
+
return createHash("sha256").update(value).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePath(path) {
|
|
22
|
+
const value = resolve(path);
|
|
23
|
+
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isWithin(path, root) {
|
|
27
|
+
const value = relative(resolve(root), resolve(path));
|
|
28
|
+
return value === "" || (!value.startsWith(`..${sep}`) && value !== ".." && !isAbsolute(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function nearestExistingAncestor(path) {
|
|
32
|
+
let current = resolve(path);
|
|
33
|
+
while (true) {
|
|
34
|
+
try {
|
|
35
|
+
await lstat(current);
|
|
36
|
+
return current;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code !== "ENOENT") throw error;
|
|
39
|
+
const parent = dirname(current);
|
|
40
|
+
if (parent === current) throw new Error(`no existing ancestor for ${path}`);
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function canonicalize(path) {
|
|
47
|
+
const absolute = resolve(path);
|
|
48
|
+
const ancestor = await nearestExistingAncestor(absolute);
|
|
49
|
+
return resolve(await realpath(ancestor), relative(ancestor, absolute));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function assertSafePath(target, root, anchor, label) {
|
|
53
|
+
const absoluteTarget = resolve(target);
|
|
54
|
+
const absoluteRoot = resolve(root);
|
|
55
|
+
const absoluteAnchor = resolve(anchor);
|
|
56
|
+
if (!isWithin(absoluteTarget, absoluteRoot) || !isWithin(absoluteTarget, absoluteAnchor)) {
|
|
57
|
+
throw new Error(`unsafe path for ${label}: ${absoluteTarget}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const physicalAnchor = await realpath(absoluteAnchor);
|
|
61
|
+
let current = absoluteAnchor;
|
|
62
|
+
const inspect = [current];
|
|
63
|
+
for (const component of relative(absoluteAnchor, absoluteTarget).split(sep).filter(Boolean)) {
|
|
64
|
+
current = join(current, component);
|
|
65
|
+
inspect.push(current);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let index = 0; index < inspect.length; index += 1) {
|
|
69
|
+
const candidate = inspect[index];
|
|
70
|
+
let info;
|
|
71
|
+
try {
|
|
72
|
+
info = await lstat(candidate);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error?.code === "ENOENT") break;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
if (info.isSymbolicLink()) throw new Error(`unsafe symbolic link or junction: ${candidate}`);
|
|
78
|
+
if (index < inspect.length - 1 && !info.isDirectory()) {
|
|
79
|
+
throw new Error(`unsafe non-directory path component: ${candidate}`);
|
|
80
|
+
}
|
|
81
|
+
const physical = await realpath(candidate);
|
|
82
|
+
const expected = resolve(physicalAnchor, relative(absoluteAnchor, candidate));
|
|
83
|
+
if (normalizePath(physical) !== normalizePath(expected)) {
|
|
84
|
+
throw new Error(`unsafe redirected path: ${candidate}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function locationFor(options, global) {
|
|
90
|
+
const home = await canonicalize(options.home ?? homedir());
|
|
91
|
+
const cwd = await canonicalize(options.cwd ?? process.cwd());
|
|
92
|
+
const env = options.env ?? process.env;
|
|
93
|
+
const codexHome = global
|
|
94
|
+
? resolve(env.CODEX_HOME || join(home, ".codex"))
|
|
95
|
+
: join(cwd, ".codex");
|
|
96
|
+
const anchor = global ? await nearestExistingAncestor(codexHome) : cwd;
|
|
97
|
+
const location = {
|
|
98
|
+
scope: global ? "global" : "project",
|
|
99
|
+
codexHome,
|
|
100
|
+
config: join(codexHome, "config.toml"),
|
|
101
|
+
state: join(codexHome, STATE_FILE),
|
|
102
|
+
lock: join(codexHome, LOCK_FILE),
|
|
103
|
+
anchor
|
|
104
|
+
};
|
|
105
|
+
await validateLocation(location);
|
|
106
|
+
return location;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function validateLocation(location) {
|
|
110
|
+
for (const [label, target] of Object.entries({
|
|
111
|
+
"Codex home": location.codexHome,
|
|
112
|
+
"config.toml": location.config,
|
|
113
|
+
"experimental V2 state": location.state,
|
|
114
|
+
"scope lock": location.lock
|
|
115
|
+
})) {
|
|
116
|
+
await assertSafePath(target, location.codexHome, location.anchor, label);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readText(path) {
|
|
121
|
+
try {
|
|
122
|
+
const bytes = await readFile(path);
|
|
123
|
+
const text = bytes.toString("utf8");
|
|
124
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) throw new Error(`${path} is not valid UTF-8`);
|
|
125
|
+
if (text.includes("\0")) throw new Error(`${path} contains a NUL byte`);
|
|
126
|
+
return { exists: true, text };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === "ENOENT") return { exists: false, text: "" };
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function atomicWrite(path, content) {
|
|
134
|
+
await mkdir(dirname(path), { recursive: true });
|
|
135
|
+
let mode = 0o600;
|
|
136
|
+
try {
|
|
137
|
+
mode = (await stat(path)).mode;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error?.code !== "ENOENT") throw error;
|
|
140
|
+
}
|
|
141
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
142
|
+
let handle;
|
|
143
|
+
try {
|
|
144
|
+
handle = await open(temporary, "wx", mode);
|
|
145
|
+
await handle.writeFile(content, "utf8");
|
|
146
|
+
await handle.sync();
|
|
147
|
+
await handle.close();
|
|
148
|
+
handle = undefined;
|
|
149
|
+
await rename(temporary, path);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
try { await handle?.close(); } catch {}
|
|
152
|
+
try { await rm(temporary, { force: true }); } catch {}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function newlineFor(content) {
|
|
158
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function managedBlock(newline) {
|
|
162
|
+
return [
|
|
163
|
+
START_MARKER,
|
|
164
|
+
"[features.multi_agent_v2]",
|
|
165
|
+
"hide_spawn_agent_metadata = false",
|
|
166
|
+
'tool_namespace = "agents"',
|
|
167
|
+
END_MARKER,
|
|
168
|
+
""
|
|
169
|
+
].join(newline);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function escapeRegExp(value) {
|
|
173
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function findManagedV2Block(content) {
|
|
177
|
+
const starts = [...content.matchAll(new RegExp(`^${escapeRegExp(START_MARKER)}\\r?$`, "gm"))];
|
|
178
|
+
const ends = [...content.matchAll(new RegExp(`^${escapeRegExp(END_MARKER)}\\r?$`, "gm"))];
|
|
179
|
+
if (!starts.length && !ends.length) return null;
|
|
180
|
+
if (starts.length !== 1 || ends.length !== 1 || ends[0].index <= starts[0].index) {
|
|
181
|
+
return { error: "duplicate, incomplete, or misordered experimental V2 markers" };
|
|
182
|
+
}
|
|
183
|
+
const start = starts[0].index;
|
|
184
|
+
let end = ends[0].index + ends[0][0].length;
|
|
185
|
+
if (content[end] === "\n") end += 1;
|
|
186
|
+
return { start, end, content: content.slice(start, end) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stripInlineComment(line) {
|
|
190
|
+
let quote = null;
|
|
191
|
+
let escaped = false;
|
|
192
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
193
|
+
const character = line[index];
|
|
194
|
+
if (quote) {
|
|
195
|
+
if (quote === '"' && character === "\\" && !escaped) {
|
|
196
|
+
escaped = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (character === quote && !escaped) quote = null;
|
|
200
|
+
escaped = false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (character === '"' || character === "'") quote = character;
|
|
204
|
+
else if (character === "#") return line.slice(0, index);
|
|
205
|
+
}
|
|
206
|
+
return line;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasUnmanagedV2(content) {
|
|
210
|
+
return content.split(/\r?\n/).some((line) => /\bmulti_agent_v2\b/.test(stripInlineComment(line)));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function appendBlock(original) {
|
|
214
|
+
const newline = newlineFor(original);
|
|
215
|
+
const block = managedBlock(newline);
|
|
216
|
+
const hasBom = original.charCodeAt(0) === 0xfeff;
|
|
217
|
+
const body = original.slice(hasBom ? 1 : 0);
|
|
218
|
+
let separator = "";
|
|
219
|
+
if (body.length > 0) {
|
|
220
|
+
if (body.endsWith(`${newline}${newline}`)) separator = "";
|
|
221
|
+
else if (body.endsWith(newline)) separator = newline;
|
|
222
|
+
else separator = `${newline}${newline}`;
|
|
223
|
+
} else if (hasBom) {
|
|
224
|
+
separator = newline;
|
|
225
|
+
}
|
|
226
|
+
return { content: `${original}${separator}${block}`, block, separator };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateState(raw, location) {
|
|
230
|
+
if (!raw || typeof raw !== "object" || raw.version !== STATE_VERSION) {
|
|
231
|
+
throw new Error("experimental V2 state is invalid or unsupported");
|
|
232
|
+
}
|
|
233
|
+
if (raw.scope !== location.scope) throw new Error("experimental V2 state scope mismatch");
|
|
234
|
+
if (normalizePath(raw.configPath) !== normalizePath(location.config)) {
|
|
235
|
+
throw new Error("experimental V2 state path mismatch");
|
|
236
|
+
}
|
|
237
|
+
if (typeof raw.createdConfig !== "boolean") throw new Error("experimental V2 createdConfig is invalid");
|
|
238
|
+
if (typeof raw.separator !== "string" || !["", "\n", "\n\n", "\r\n", "\r\n\r\n"].includes(raw.separator)) {
|
|
239
|
+
throw new Error("experimental V2 separator is invalid");
|
|
240
|
+
}
|
|
241
|
+
if (!/^[a-f0-9]{64}$/.test(raw.blockHash || "")) throw new Error("experimental V2 block hash is invalid");
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function loadState(location) {
|
|
246
|
+
const file = await readText(location.state);
|
|
247
|
+
if (!file.exists) return { exists: false, state: null };
|
|
248
|
+
let raw;
|
|
249
|
+
try {
|
|
250
|
+
raw = JSON.parse(file.text);
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error("experimental V2 state is not valid JSON");
|
|
253
|
+
}
|
|
254
|
+
return { exists: true, state: validateState(raw, location) };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function inspect(location) {
|
|
258
|
+
const state = await loadState(location);
|
|
259
|
+
const config = await readText(location.config);
|
|
260
|
+
const block = findManagedV2Block(config.text);
|
|
261
|
+
if (block?.error) return { status: "invalid", detail: block.error, state, config, block };
|
|
262
|
+
if (!state.exists) {
|
|
263
|
+
if (block) return { status: "untracked", detail: "package markers exist without state", state, config, block };
|
|
264
|
+
if (hasUnmanagedV2(config.text)) {
|
|
265
|
+
return { status: "unmanaged", detail: "pre-existing V2 configuration is preserved", state, config, block };
|
|
266
|
+
}
|
|
267
|
+
return { status: "disabled", state, config, block };
|
|
268
|
+
}
|
|
269
|
+
if (!config.exists) return { status: "missing", detail: "config.toml is missing", state, config, block };
|
|
270
|
+
if (!block) return { status: "missing", detail: "managed V2 block is missing", state, config, block };
|
|
271
|
+
if (digest(block.content) !== state.state.blockHash) {
|
|
272
|
+
return { status: "user-modified", detail: "managed V2 block changed", state, config, block };
|
|
273
|
+
}
|
|
274
|
+
return { status: "managed", state, config, block };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function inspectLock(location) {
|
|
278
|
+
const file = await readText(location.lock);
|
|
279
|
+
if (!file.exists) return { status: "none" };
|
|
280
|
+
let metadata;
|
|
281
|
+
try {
|
|
282
|
+
metadata = JSON.parse(file.text);
|
|
283
|
+
} catch {
|
|
284
|
+
return { status: "corrupt", detail: "lock file is not valid JSON" };
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
!Number.isInteger(metadata.pid) ||
|
|
288
|
+
metadata.pid <= 0 ||
|
|
289
|
+
typeof metadata.hostname !== "string" ||
|
|
290
|
+
typeof metadata.token !== "string"
|
|
291
|
+
) {
|
|
292
|
+
return { status: "corrupt", detail: "lock metadata is invalid" };
|
|
293
|
+
}
|
|
294
|
+
if (metadata.hostname !== hostname()) {
|
|
295
|
+
return { status: "active", metadata, detail: "lock belongs to another host" };
|
|
296
|
+
}
|
|
297
|
+
let alive = true;
|
|
298
|
+
try {
|
|
299
|
+
process.kill(metadata.pid, 0);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (["ESRCH", "EINVAL"].includes(error?.code)) alive = false;
|
|
302
|
+
else if (error?.code !== "EPERM") throw error;
|
|
303
|
+
}
|
|
304
|
+
return alive ? { status: "active", metadata } : { status: "stale", metadata };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function acquireLock(location, command) {
|
|
308
|
+
await mkdir(location.codexHome, { recursive: true });
|
|
309
|
+
await validateLocation(location);
|
|
310
|
+
|
|
311
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
312
|
+
const token = randomUUID();
|
|
313
|
+
let handle;
|
|
314
|
+
try {
|
|
315
|
+
handle = await open(location.lock, "wx", 0o600);
|
|
316
|
+
await handle.writeFile(`${JSON.stringify({
|
|
317
|
+
version: 1,
|
|
318
|
+
token,
|
|
319
|
+
pid: process.pid,
|
|
320
|
+
hostname: hostname(),
|
|
321
|
+
command,
|
|
322
|
+
scope: location.scope,
|
|
323
|
+
startedAt: new Date().toISOString()
|
|
324
|
+
}, null, 2)}\n`, "utf8");
|
|
325
|
+
await handle.sync();
|
|
326
|
+
await handle.close();
|
|
327
|
+
return token;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
try { await handle?.close(); } catch {}
|
|
330
|
+
if (error?.code !== "EEXIST") throw error;
|
|
331
|
+
const lock = await inspectLock(location);
|
|
332
|
+
if (lock.status === "none") continue;
|
|
333
|
+
if (lock.status === "stale") {
|
|
334
|
+
const before = await readText(location.lock);
|
|
335
|
+
const again = await readText(location.lock);
|
|
336
|
+
if (before.exists && again.exists && before.text === again.text) {
|
|
337
|
+
await rm(location.lock, { force: true });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const detail = lock.metadata
|
|
342
|
+
? `${lock.metadata.command || "operation"} pid ${lock.metadata.pid}`
|
|
343
|
+
: lock.detail || "unknown lock owner";
|
|
344
|
+
throw new Error(`scope is locked by ${detail}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
throw new Error("unable to acquire scope lock");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function releaseLock(location, token) {
|
|
351
|
+
if (!token) return;
|
|
352
|
+
try {
|
|
353
|
+
const lock = await readText(location.lock);
|
|
354
|
+
if (!lock.exists) return;
|
|
355
|
+
const metadata = JSON.parse(lock.text);
|
|
356
|
+
if (metadata.token === token) await rm(location.lock, { force: true });
|
|
357
|
+
} catch {}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function print(output, status, detail) {
|
|
361
|
+
output?.(`${status}: experimental-v2${detail ? ` (${detail})` : ""}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function enablePlan(current, location) {
|
|
365
|
+
const next = appendBlock(current.config.text);
|
|
366
|
+
return {
|
|
367
|
+
configContent: next.content,
|
|
368
|
+
stateContent: `${JSON.stringify({
|
|
369
|
+
version: STATE_VERSION,
|
|
370
|
+
scope: location.scope,
|
|
371
|
+
configPath: location.config,
|
|
372
|
+
createdConfig: !current.config.exists,
|
|
373
|
+
separator: next.separator,
|
|
374
|
+
blockHash: digest(next.block),
|
|
375
|
+
enabledAt: new Date().toISOString()
|
|
376
|
+
}, null, 2)}\n`,
|
|
377
|
+
configStatus: current.config.exists ? "update" : "create"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function disablePlan(current) {
|
|
382
|
+
let before = current.config.text.slice(0, current.block.start);
|
|
383
|
+
const separator = current.state.state.separator;
|
|
384
|
+
if (separator && before.endsWith(separator)) before = before.slice(0, -separator.length);
|
|
385
|
+
const restored = `${before}${current.config.text.slice(current.block.end)}`;
|
|
386
|
+
const empty = restored.replace(/^\uFEFF/, "").trim() === "";
|
|
387
|
+
return {
|
|
388
|
+
restored,
|
|
389
|
+
deleteConfig: current.state.state.createdConfig && empty
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function restoreOriginalConfig(location, config) {
|
|
394
|
+
if (config.exists) await atomicWrite(location.config, config.text);
|
|
395
|
+
else await rm(location.config, { force: true });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function enable(location, options) {
|
|
399
|
+
const preview = await inspect(location);
|
|
400
|
+
if (preview.status === "managed") {
|
|
401
|
+
print(options.output, "skip", "already enabled and managed");
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
if (preview.status === "unmanaged") {
|
|
405
|
+
print(options.output, "preserve", preview.detail);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
if (preview.status !== "disabled") {
|
|
409
|
+
print(options.output, "fail", preview.detail || preview.status);
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
if (options.dryRun) {
|
|
413
|
+
const plan = enablePlan(preview, location);
|
|
414
|
+
print(options.output, `would-${plan.configStatus}`, "config.toml");
|
|
415
|
+
print(options.output, "would-create", "experimental V2 state");
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let token;
|
|
420
|
+
try {
|
|
421
|
+
token = await acquireLock(location, "v2-enable");
|
|
422
|
+
const current = await inspect(location);
|
|
423
|
+
if (current.status !== "disabled") {
|
|
424
|
+
print(options.output, "fail", `state changed during enable: ${current.status}`);
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
const plan = enablePlan(current, location);
|
|
428
|
+
await atomicWrite(location.config, plan.configContent);
|
|
429
|
+
try {
|
|
430
|
+
await atomicWrite(location.state, plan.stateContent);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
await restoreOriginalConfig(location, current.config);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
print(options.output, plan.configStatus, "config.toml");
|
|
436
|
+
print(options.output, "create", "experimental V2 state");
|
|
437
|
+
return 0;
|
|
438
|
+
} finally {
|
|
439
|
+
await releaseLock(location, token);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function disable(location, options) {
|
|
444
|
+
const preview = await inspect(location);
|
|
445
|
+
if (preview.status === "disabled" || preview.status === "unmanaged") {
|
|
446
|
+
if (!options.onlyManaged) print(options.output, preview.status === "disabled" ? "skip" : "preserve", preview.detail || "not enabled by this package");
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
if (preview.status !== "managed") {
|
|
450
|
+
print(options.output, "preserve", preview.detail || preview.status);
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
if (options.dryRun) {
|
|
454
|
+
const plan = disablePlan(preview);
|
|
455
|
+
print(options.output, plan.deleteConfig ? "would-remove" : "would-update", "config.toml");
|
|
456
|
+
print(options.output, "would-remove", "experimental V2 state");
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let token;
|
|
461
|
+
try {
|
|
462
|
+
token = await acquireLock(location, "v2-disable");
|
|
463
|
+
const current = await inspect(location);
|
|
464
|
+
if (current.status !== "managed") {
|
|
465
|
+
print(options.output, "preserve", `state changed during disable: ${current.status}`);
|
|
466
|
+
return 1;
|
|
467
|
+
}
|
|
468
|
+
const plan = disablePlan(current);
|
|
469
|
+
if (plan.deleteConfig) await rm(location.config, { force: true });
|
|
470
|
+
else await atomicWrite(location.config, plan.restored);
|
|
471
|
+
try {
|
|
472
|
+
await rm(location.state, { force: true });
|
|
473
|
+
} catch (error) {
|
|
474
|
+
await atomicWrite(location.config, current.config.text);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
print(options.output, plan.deleteConfig ? "remove" : "update", "config.toml");
|
|
478
|
+
print(options.output, "remove", "experimental V2 state");
|
|
479
|
+
return 0;
|
|
480
|
+
} finally {
|
|
481
|
+
await releaseLock(location, token);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function status(location, options) {
|
|
486
|
+
const current = await inspect(location);
|
|
487
|
+
const mapping = {
|
|
488
|
+
managed: ["healthy", "managed experimental setting"],
|
|
489
|
+
disabled: ["disabled", "not enabled"],
|
|
490
|
+
unmanaged: ["preserve", current.detail],
|
|
491
|
+
untracked: ["unsafe-state", current.detail],
|
|
492
|
+
missing: ["missing", current.detail],
|
|
493
|
+
"user-modified": ["user-modified", current.detail],
|
|
494
|
+
invalid: ["invalid", current.detail]
|
|
495
|
+
};
|
|
496
|
+
const [label, detail] = mapping[current.status] || ["invalid", current.status];
|
|
497
|
+
if (!(options.quietIfDisabled && current.status === "disabled")) print(options.output, label, detail);
|
|
498
|
+
return ["managed", "disabled", "unmanaged"].includes(current.status) ? 0 : 1;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parseArgs(argv) {
|
|
502
|
+
if (!argv.length || argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
503
|
+
const [action, ...flags] = argv;
|
|
504
|
+
if (!["enable", "disable", "status"].includes(action)) return { error: "unknown V2 action" };
|
|
505
|
+
const parsed = { action, global: false, dryRun: false };
|
|
506
|
+
for (const flag of flags) {
|
|
507
|
+
if (flag === "--global") parsed.global = true;
|
|
508
|
+
else if (flag === "--dry-run" && action !== "status") parsed.dryRun = true;
|
|
509
|
+
else return { error: `unsupported option for v2 ${action}: ${flag}` };
|
|
510
|
+
}
|
|
511
|
+
return parsed;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function v2Usage() {
|
|
515
|
+
return `Experimental Codex multi-agent V2 settings\n\nUsage:\n codex-model-router v2 enable [--global] [--dry-run]\n codex-model-router v2 disable [--global] [--dry-run]\n codex-model-router v2 status [--global]\n\nThis is an undocumented Codex setting and may change or stop working without notice.`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function runV2Command(argv, options = {}) {
|
|
519
|
+
const output = options.output ?? console.log;
|
|
520
|
+
const parsed = parseArgs(argv);
|
|
521
|
+
if (parsed.help) {
|
|
522
|
+
output(v2Usage());
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
if (parsed.error) {
|
|
526
|
+
output(`${parsed.error}\n\n${v2Usage()}`);
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const location = await locationFor(options, parsed.global);
|
|
531
|
+
if (parsed.action === "enable") return enable(location, { output, dryRun: parsed.dryRun });
|
|
532
|
+
if (parsed.action === "disable") return disable(location, { output, dryRun: parsed.dryRun });
|
|
533
|
+
return status(location, { output });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
print(output, "fail", error.message);
|
|
536
|
+
return 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function scopeFlags(argv) {
|
|
541
|
+
return { global: argv.includes("--global"), dryRun: argv.includes("--dry-run") };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function reportV2Doctor(argv, options = {}) {
|
|
545
|
+
const output = options.output ?? console.log;
|
|
546
|
+
try {
|
|
547
|
+
const location = await locationFor(options, scopeFlags(argv).global);
|
|
548
|
+
return status(location, { output, quietIfDisabled: true });
|
|
549
|
+
} catch (error) {
|
|
550
|
+
print(output, "fail", error.message);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export async function cleanupV2AfterUninstall(argv, options = {}) {
|
|
556
|
+
const output = options.output ?? console.log;
|
|
557
|
+
try {
|
|
558
|
+
const flags = scopeFlags(argv);
|
|
559
|
+
const location = await locationFor(options, flags.global);
|
|
560
|
+
if (!(await readText(location.state)).exists) return 0;
|
|
561
|
+
return disable(location, { output, dryRun: flags.dryRun, onlyManaged: true });
|
|
562
|
+
} catch (error) {
|
|
563
|
+
print(output, "fail", error.message);
|
|
564
|
+
return 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
package/lib/manifest.js
CHANGED
|
@@ -87,7 +87,7 @@ Return only the artifact required by the current stage.
|
|
|
87
87
|
const luna = `name = "luna"
|
|
88
88
|
description = "Implements clear, bounded work from an approved plan."
|
|
89
89
|
model = "gpt-5.6-luna"
|
|
90
|
-
model_reasoning_effort = "
|
|
90
|
+
model_reasoning_effort = "xhigh"
|
|
91
91
|
sandbox_mode = "workspace-write"
|
|
92
92
|
developer_instructions = """
|
|
93
93
|
Implement the current approved plan with the smallest coherent change.
|
|
@@ -155,45 +155,91 @@ Omit irrelevant fields and vague instructions. Preserve verified work, revise on
|
|
|
155
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.
|
|
156
156
|
`;
|
|
157
157
|
|
|
158
|
-
export const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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"
|
|
164
171
|
});
|
|
165
172
|
|
|
166
|
-
export const MANAGED_FILE_NAMES = Object.freeze(["terra", "luna", "sol", "skill", "planning"]);
|
|
167
173
|
export const AGENT_NAMES = Object.freeze(["terra", "luna", "sol"]);
|
|
168
174
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
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"]);
|
|
176
192
|
|
|
177
|
-
export const
|
|
178
|
-
terra:
|
|
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: {
|
|
179
203
|
name: "terra",
|
|
180
204
|
model: "gpt-5.6-terra",
|
|
181
205
|
model_reasoning_effort: "high",
|
|
182
206
|
sandbox_mode: "read-only"
|
|
183
|
-
}
|
|
184
|
-
luna:
|
|
207
|
+
},
|
|
208
|
+
luna: {
|
|
185
209
|
name: "luna",
|
|
186
210
|
model: "gpt-5.6-luna",
|
|
187
|
-
model_reasoning_effort: "
|
|
211
|
+
model_reasoning_effort: "xhigh",
|
|
188
212
|
sandbox_mode: "workspace-write"
|
|
189
|
-
}
|
|
190
|
-
sol:
|
|
213
|
+
},
|
|
214
|
+
sol: {
|
|
191
215
|
name: "sol",
|
|
192
216
|
model: "gpt-5.6-sol",
|
|
193
217
|
model_reasoning_effort: "medium",
|
|
194
218
|
sandbox_mode: "read-only"
|
|
195
|
-
}
|
|
196
|
-
}
|
|
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();
|
|
197
243
|
|
|
198
244
|
export const SKILL_EXPECTATIONS = Object.freeze({
|
|
199
245
|
skill: Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-model-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
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",
|
|
11
|
-
"test": "node
|
|
10
|
+
"check": "node --check bin/codex-model-router.js && node --check lib/agent-reasoning-cli.js && node --check lib/experimental-v2.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/run-tests.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
|
|
11
|
+
"test": "node scripts/run-tests.js",
|
|
12
12
|
"test:package": "node scripts/package-smoke.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"codex-cli",
|
|
28
28
|
"openai",
|
|
29
29
|
"model-router",
|
|
30
|
+
"multi-agent",
|
|
30
31
|
"subagent",
|
|
31
32
|
"subagents",
|
|
32
33
|
"agent-routing",
|