odai-dsh-agent 0.2.21 → 0.2.23
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/LICENSE +21 -21
- package/README.md +155 -157
- package/build/bin/odai-dsh-agent.mjs +18 -36
- package/build/bin/odai-dsh-agent.mjs.map +1 -1
- package/build/src/installer.d.mts +1 -6
- package/build/src/installer.d.mts.map +1 -1
- package/build/src/installer.mjs +3 -83
- package/build/src/installer.mjs.map +1 -1
- package/client/client.js +36 -58
- package/control-center.cordis.patch.yml +3 -3
- package/package.json +90 -90
- package/preset/odai/agent.cordis.yml +260 -260
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/control-center-runtime.d.mts +1 -0
- package/preset/odai/runtime/control-center-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/control-center-runtime.mjs +20 -0
- package/preset/odai/runtime/control-center-runtime.mjs.map +1 -1
- package/preset/odai/runtime/index.d.mts.map +1 -1
- package/preset/odai/runtime/index.mjs +9 -7
- package/preset/odai/runtime/index.mjs.map +1 -1
- package/preset/odai/runtime/{session-compat.d.mts → legacy-session-repair.d.mts} +7 -7
- package/preset/odai/runtime/legacy-session-repair.d.mts.map +1 -0
- package/preset/odai/runtime/{session-compat.mjs → legacy-session-repair.mjs} +1 -1
- package/preset/odai/runtime/legacy-session-repair.mjs.map +1 -0
- package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/lifecycle-runtime.mjs +10 -2
- package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts +19 -1
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-gap.mjs +180 -16
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
- package/preset/odai/runtime/role-overlays.mjs +9 -9
- package/preset/odai/runtime/role-overlays.mjs.map +1 -1
- package/preset/odai/runtime/router.d.mts +1 -0
- package/preset/odai/runtime/router.d.mts.map +1 -1
- package/preset/odai/runtime/router.mjs +5 -1
- package/preset/odai/runtime/router.mjs.map +1 -1
- package/preset/odai/runtime/routing-context.d.mts +9 -2
- package/preset/odai/runtime/routing-context.d.mts.map +1 -1
- package/preset/odai/runtime/routing-context.mjs +30 -9
- package/preset/odai/runtime/routing-context.mjs.map +1 -1
- package/preset/odai/runtime/runtime-support.d.mts +0 -1
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.mjs.map +1 -1
- package/preset/odai/runtime/runtime-types.d.mts +1 -3
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-types.mjs +1 -5
- package/preset/odai/runtime/runtime-types.mjs.map +1 -1
- package/preset/odai/runtime/session-evidence.d.mts.map +1 -1
- package/preset/odai/runtime/session-evidence.mjs +1 -3
- package/preset/odai/runtime/session-evidence.mjs.map +1 -1
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.mjs +23 -3
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
- package/preset/odai/skills/odai/SKILL.md +68 -68
- package/preset/odai/skills/odai/agents/openai.yaml +4 -4
- package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
- package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
- package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
- package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +28 -28
- package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +14 -14
- package/preset/odai/skills/odai/manifest.json +52 -52
- package/preset/odai/skills/odai/references/craft.md +54 -54
- package/preset/odai/skills/odai/references/dao.md +31 -31
- package/preset/odai/skills/odai/references/leverage.md +50 -50
- package/preset/odai/skills/odai/references/planning.md +83 -83
- package/preset/odai/skills/odai/references/support.md +30 -30
- package/preset/odai/skills/odai/references/verification.md +24 -24
- package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
- package/preset/odai/skills/odai/scripts/build-routing.mjs +315 -315
- package/preset/odai/skills/odai/scripts/install-routing.mjs +393 -393
- package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
- package/preset/odai/skills/odai/scripts/run-role.mjs +285 -285
- package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
- package/preset/odai/runtime/session-compat.d.mts.map +0 -1
- package/preset/odai/runtime/session-compat.mjs.map +0 -1
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import process from "node:process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const skillRoot = path.resolve(scriptDir, "..");
|
|
10
|
-
const manifest = JSON.parse(readFileSync(path.join(skillRoot, "manifest.json"), "utf8"));
|
|
11
|
-
if (manifest?.schemaVersion !== 2 || !manifest.roleFiles || !manifest.referenceFiles || !Array.isArray(manifest.requiredFiles)) {
|
|
12
|
-
throw new Error("Odai manifest owner topology is unavailable");
|
|
13
|
-
}
|
|
14
|
-
const ownerNames = Object.freeze({
|
|
15
|
-
roleFiles: ["controller", "researcher", "planner", "reviewer", "frontend"],
|
|
16
|
-
referenceFiles: ["dao", "planning", "craft", "verification", "support", "leverage", "care", "human-safety"],
|
|
17
|
-
});
|
|
18
|
-
for (const [group, expectedNames] of Object.entries(ownerNames)) {
|
|
19
|
-
const entries = manifest[group];
|
|
20
|
-
if (entries === null || typeof entries !== "object" || Array.isArray(entries)
|
|
21
|
-
|| JSON.stringify(Object.keys(entries).sort()) !== JSON.stringify([...expectedNames].sort())) {
|
|
22
|
-
throw new Error(`Invalid manifest owner set: ${group}`);
|
|
23
|
-
}
|
|
24
|
-
const paths = Object.values(entries);
|
|
25
|
-
if (new Set(paths).size !== paths.length) throw new Error(`Duplicate manifest owner path: ${group}`);
|
|
26
|
-
}
|
|
27
|
-
const ownedPaths = [...Object.values(manifest.roleFiles), ...Object.values(manifest.referenceFiles)];
|
|
28
|
-
if (new Set(ownedPaths).size !== ownedPaths.length) throw new Error("Role and reference owners must use distinct paths");
|
|
29
|
-
|
|
30
|
-
const canonicalRoot = realpathSync(skillRoot);
|
|
31
|
-
const requiredFiles = new Set(manifest.requiredFiles);
|
|
32
|
-
|
|
33
|
-
function ownerFilePath(group, name) {
|
|
34
|
-
const relativeSource = manifest[group]?.[name];
|
|
35
|
-
if (typeof relativeSource !== "string" || relativeSource.trim() === "" || relativeSource.includes("\\")
|
|
36
|
-
|| path.isAbsolute(relativeSource) || /^[A-Za-z]:/u.test(relativeSource)
|
|
37
|
-
|| relativeSource.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
38
|
-
throw new Error(`Unsafe manifest owner path: ${group}.${name}`);
|
|
39
|
-
}
|
|
40
|
-
if (!requiredFiles.has(relativeSource)) throw new Error(`Manifest owner is undeclared: ${group}.${name}`);
|
|
41
|
-
const source = realpathSync(path.resolve(skillRoot, relativeSource));
|
|
42
|
-
const nested = path.relative(canonicalRoot, source);
|
|
43
|
-
if (nested === "" || nested === ".." || nested.startsWith(`..${path.sep}`) || path.isAbsolute(nested)) {
|
|
44
|
-
throw new Error(`Manifest owner escapes the canonical root: ${group}.${name}`);
|
|
45
|
-
}
|
|
46
|
-
return source;
|
|
47
|
-
}
|
|
48
|
-
for (const [group, names] of Object.entries(ownerNames)) {
|
|
49
|
-
for (const name of names) ownerFilePath(group, name);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const argv = process.argv.slice(2);
|
|
53
|
-
|
|
54
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
55
|
-
console.log(`Usage:
|
|
56
|
-
node skills/odai/scripts/build-routing.mjs --host <codex|claude|copilot> --out <directory> \\
|
|
57
|
-
--controller-model <model> --planner-model <model> --reviewer-model <model> \\
|
|
58
|
-
[--researcher-model <model>] [--frontend-model <model>] [--controller-effort <effort>] \\
|
|
59
|
-
[--researcher-effort <effort>] [--planner-effort <effort>] [--reviewer-effort <effort>] \\
|
|
60
|
-
[--frontend-effort <effort>] [--verifier-command <command>]
|
|
61
|
-
|
|
62
|
-
生成 odai 的可选宿主 auto 路由适配器。一个持续总控持有实施与最终交付,只在多源证据压缩、独立规划、独立验收或前端专业制作能改变结果时调用相应责任;researcher 与 frontend 映射默认不生成。这里不跨 provider、不增加第二总控或隐藏的每轮前置流程。`);
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
assertKnownArgs();
|
|
67
|
-
const host = option("--host");
|
|
68
|
-
const out = option("--out");
|
|
69
|
-
const models = {
|
|
70
|
-
controller: option("--controller-model"),
|
|
71
|
-
researcher: option("--researcher-model"),
|
|
72
|
-
planner: option("--planner-model"),
|
|
73
|
-
reviewer: option("--reviewer-model"),
|
|
74
|
-
frontend: option("--frontend-model"),
|
|
75
|
-
};
|
|
76
|
-
const efforts = {
|
|
77
|
-
controller: option("--controller-effort"),
|
|
78
|
-
researcher: option("--researcher-effort"),
|
|
79
|
-
planner: option("--planner-effort"),
|
|
80
|
-
reviewer: option("--reviewer-effort"),
|
|
81
|
-
frontend: option("--frontend-effort"),
|
|
82
|
-
};
|
|
83
|
-
const verifierCommand = option("--verifier-command") || "node .codex/odai-verify-routing.mjs";
|
|
84
|
-
const requiredRoles = ["controller", "planner", "reviewer"];
|
|
85
|
-
const roles = Object.freeze([
|
|
86
|
-
...requiredRoles,
|
|
87
|
-
...(models.researcher ? ["researcher"] : []),
|
|
88
|
-
...(models.frontend ? ["frontend"] : []),
|
|
89
|
-
]);
|
|
90
|
-
const descriptions = {
|
|
91
|
-
controller: "持续持有用户目标、全局状态、修正回路与最终交付。",
|
|
92
|
-
researcher: "只在多源决定证据压缩有实测收益时返回可追溯来源账本。",
|
|
93
|
-
planner: "只在独立判断能改变路线时形成有界的证据化规划。",
|
|
94
|
-
reviewer: "只在独立判断能改变放行结果时依据真实证据验收。",
|
|
95
|
-
frontend: "只在界面设计或前端制作存在专业缺口时形成可验证成品。",
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (!new Set(["codex", "claude", "copilot"]).has(host)) throw new Error(`Unsupported routing host: ${host || "(missing)"}`);
|
|
99
|
-
if (!out) throw new Error("--out requires a directory");
|
|
100
|
-
for (const role of requiredRoles) if (!models[role]) throw new Error(`--${role}-model is required`);
|
|
101
|
-
if (efforts.researcher && !models.researcher) throw new Error("--researcher-effort requires --researcher-model");
|
|
102
|
-
if (efforts.frontend && !models.frontend) throw new Error("--frontend-effort requires --frontend-model");
|
|
103
|
-
if (host === "copilot" && Object.values(efforts).some(Boolean)) {
|
|
104
|
-
throw new Error("Copilot custom-agent profiles do not provide a portable reasoning-effort field");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const policy = "conditional";
|
|
108
|
-
const target = path.resolve(out, host);
|
|
109
|
-
mkdirSync(target, { recursive: true });
|
|
110
|
-
if (host === "codex") buildCodex(target);
|
|
111
|
-
else if (host === "claude") buildClaude(target);
|
|
112
|
-
else buildCopilot(target);
|
|
113
|
-
|
|
114
|
-
const metadata = {
|
|
115
|
-
id: `odai-routing-${host}`,
|
|
116
|
-
host,
|
|
117
|
-
generatedFrom: "skills/odai/scripts/build-routing.mjs",
|
|
118
|
-
mode: "single-controller-conditional-routing",
|
|
119
|
-
mapping: Object.fromEntries(roles.map((role) => [role, {
|
|
120
|
-
provider: host,
|
|
121
|
-
model: models[role],
|
|
122
|
-
reasoning_effort: efforts[role] || null,
|
|
123
|
-
}])),
|
|
124
|
-
routing_policy: {
|
|
125
|
-
mode: policy,
|
|
126
|
-
controller_identity: "persistent-task-thread",
|
|
127
|
-
controller_owns_final_delivery: true,
|
|
128
|
-
researcher_activation: models.researcher
|
|
129
|
-
? "only-when-multi-source-decision-evidence-compression-has-measured-net-benefit"
|
|
130
|
-
: "unconfigured",
|
|
131
|
-
planner_activation: "only-when-independent-judgment-can-change-route",
|
|
132
|
-
reviewer_activation: "only-when-independent-judgment-can-change-release",
|
|
133
|
-
frontend_activation: models.frontend
|
|
134
|
-
? "only-when-interface-design-or-production-needs-specialist-capability"
|
|
135
|
-
: "unconfigured",
|
|
136
|
-
controller_owns_implementation: true,
|
|
137
|
-
sufficient_controller_defaults_to_single_pass: true,
|
|
138
|
-
},
|
|
139
|
-
runtime_verification: host === "codex"
|
|
140
|
-
? { mode: "post-run-executable", command: verifierCommand }
|
|
141
|
-
: { mode: "host-native-evidence-required", command: null },
|
|
142
|
-
activation: activation(host),
|
|
143
|
-
profiles: roles.map((role) => ({ name: `odai-${role}`, purpose: descriptions[role] })),
|
|
144
|
-
};
|
|
145
|
-
writeFileSync(path.join(target, "ADAPTER.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
146
|
-
console.log(target);
|
|
147
|
-
|
|
148
|
-
function buildCodex(root) {
|
|
149
|
-
const sourceDir = path.join(skillRoot, "assets", "codex-agents");
|
|
150
|
-
const configRoot = path.join(root, ".codex");
|
|
151
|
-
const agentsRoot = path.join(configRoot, "agents");
|
|
152
|
-
const contractsRoot = path.join(configRoot, "role-contracts");
|
|
153
|
-
mkdirSync(agentsRoot, { recursive: true });
|
|
154
|
-
mkdirSync(contractsRoot, { recursive: true });
|
|
155
|
-
writeRendered(path.join(sourceDir, "config.toml"), path.join(configRoot, "config.toml"), {
|
|
156
|
-
__ODAI_CONTROLLER_MODEL_LINE__: `model = ${JSON.stringify(models.controller)}`,
|
|
157
|
-
__ODAI_CONTROLLER_EFFORT_LINE__: tomlEffortLine(efforts.controller),
|
|
158
|
-
__ODAI_CONTROLLER_BODY__: roleBody("controller", "codex"),
|
|
159
|
-
__ODAI_AGENT_SECTIONS__: codexAgentSections(),
|
|
160
|
-
});
|
|
161
|
-
for (const role of roles.slice(1)) {
|
|
162
|
-
writeRendered(path.join(sourceDir, "role.toml"), path.join(agentsRoot, `odai-${role}.toml`), {
|
|
163
|
-
__ODAI_ROLE_MODEL__: JSON.stringify(models[role]),
|
|
164
|
-
__ODAI_ROLE_EFFORT_LINE__: tomlEffortLine(efforts[role]),
|
|
165
|
-
__ODAI_ROLE_BODY__: roleBody(role, "codex"),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
copyScript("verify-routing.mjs", path.join(configRoot, "odai-verify-routing.mjs"));
|
|
169
|
-
copyScript("run-role.mjs", path.join(configRoot, "odai-run-role.mjs"));
|
|
170
|
-
for (const role of roles) writeFileSync(path.join(contractsRoot, `odai-${role}.md`), roleBody(role, "codex"), "utf8");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function buildClaude(root) {
|
|
174
|
-
const source = path.join(skillRoot, "assets", "claude-agents", "agent.md");
|
|
175
|
-
const agentsRoot = path.join(root, ".claude", "agents");
|
|
176
|
-
mkdirSync(agentsRoot, { recursive: true });
|
|
177
|
-
for (const role of roles) {
|
|
178
|
-
writeRendered(source, path.join(agentsRoot, `odai-${role}.md`), {
|
|
179
|
-
__ODAI_ROLE__: role,
|
|
180
|
-
__ODAI_ROLE_DESCRIPTION__: descriptions[role],
|
|
181
|
-
__ODAI_ROLE_MODEL__: models[role],
|
|
182
|
-
__ODAI_ROLE_EFFORT_LINE__: yamlEffortLine(efforts[role]),
|
|
183
|
-
__ODAI_PERMISSION_MODE__: ["researcher", "planner", "reviewer"].includes(role) ? "plan" : "default",
|
|
184
|
-
__ODAI_TOOLS_LINE__: "",
|
|
185
|
-
__ODAI_ROLE_BODY__: roleBody(role, "claude"),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
writeFileSync(path.join(root, ".claude", "settings.patch.json"), `${JSON.stringify({ agent: "odai-controller" }, null, 2)}\n`, "utf8");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function buildCopilot(root) {
|
|
192
|
-
const source = path.join(skillRoot, "assets", "copilot-agents", "agent.md");
|
|
193
|
-
const agentsRoot = path.join(root, ".github", "agents");
|
|
194
|
-
mkdirSync(agentsRoot, { recursive: true });
|
|
195
|
-
for (const role of roles) {
|
|
196
|
-
writeRendered(source, path.join(agentsRoot, `odai-${role}.agent.md`), {
|
|
197
|
-
__ODAI_ROLE__: role,
|
|
198
|
-
__ODAI_ROLE_DESCRIPTION__: descriptions[role],
|
|
199
|
-
__ODAI_ROLE_MODEL__: models[role],
|
|
200
|
-
__ODAI_DISABLE_MODEL_INVOCATION__: role === "controller" ? "true" : "false",
|
|
201
|
-
__ODAI_USER_INVOCABLE__: role === "controller" ? "true" : "false",
|
|
202
|
-
__ODAI_TOOLS__: role === "researcher"
|
|
203
|
-
? '["view", "glob", "grep"]'
|
|
204
|
-
: ["planner", "reviewer"].includes(role) ? '["view", "glob", "grep", "shell"]' : '["*"]',
|
|
205
|
-
__ODAI_ROLE_BODY__: roleBody(role, "copilot"),
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
writeFileSync(path.join(root, "LAUNCH.json"), `${JSON.stringify({
|
|
209
|
-
command: ["copilot", `--model=${models.controller}`, "--agent=odai-controller"],
|
|
210
|
-
reason: "Copilot has no portable project setting that makes a custom agent the default main controller.",
|
|
211
|
-
autoModelUnsupported: true,
|
|
212
|
-
}, null, 2)}\n`, "utf8");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function roleBody(role, hostName) {
|
|
216
|
-
const source = ownerFilePath("roleFiles", role);
|
|
217
|
-
if (!existsSync(source)) throw new Error(`Missing canonical routing role body: ${source}`);
|
|
218
|
-
const names = hostName === "codex"
|
|
219
|
-
? { researcher: "odai_researcher", planner: "odai_planner", reviewer: "odai_reviewer", frontend: "odai_frontend" }
|
|
220
|
-
: { researcher: "odai-researcher", planner: "odai-planner", reviewer: "odai-reviewer", frontend: "odai-frontend" };
|
|
221
|
-
const rendered = renderText(readFileSync(source, "utf8"), {
|
|
222
|
-
__ODAI_POLICY__: policy,
|
|
223
|
-
__ODAI_RESEARCHER_ROLE__: models.researcher ? names.researcher : "researcher(当前适配器未配置映射,不能调用)",
|
|
224
|
-
__ODAI_PLANNER_ROLE__: names.planner,
|
|
225
|
-
__ODAI_REVIEWER_ROLE__: names.reviewer,
|
|
226
|
-
__ODAI_FRONTEND_ROLE__: models.frontend ? names.frontend : "frontend(当前适配器未配置映射,不能调用)",
|
|
227
|
-
__ODAI_RUNTIME_VERIFICATION__: hostName === "codex"
|
|
228
|
-
? "实际路由必须用宿主返回的线程、角色、模型与用量证据核对,不从调用请求推断成功。"
|
|
229
|
-
: "只有宿主原生运行证据能识别实际角色与模型时,路由才算已核实。",
|
|
230
|
-
__ODAI_HOST_NOTE__: hostName === "copilot" ? "Copilot Auto 会覆盖角色模型选择;需要区分角色时不使用 Auto。" : "",
|
|
231
|
-
}, source);
|
|
232
|
-
const craft = role === "frontend"
|
|
233
|
-
? readFileSync(ownerFilePath("referenceFiles", "craft"), "utf8").trim()
|
|
234
|
-
: "";
|
|
235
|
-
if (hostName !== "codex") {
|
|
236
|
-
return [rendered, ...(craft ? ["## Canonical 制作工艺", craft] : [])].join("\n\n");
|
|
237
|
-
}
|
|
238
|
-
const canonical = readFileSync(path.join(skillRoot, "SKILL.md"), "utf8")
|
|
239
|
-
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
240
|
-
const responsibility = role === "controller"
|
|
241
|
-
? "你是唯一总控,持有完整目标、全局状态、修正回路与最终交付。"
|
|
242
|
-
: `你只承担 ${role} 责任,不是第二个总控。`;
|
|
243
|
-
return [
|
|
244
|
-
`以下 canonical odai 是所有责任共享的内核;宿主角色契约只限制本责任,不得另建流程。${responsibility}`,
|
|
245
|
-
canonical,
|
|
246
|
-
...(craft ? ["## Canonical 制作工艺", craft] : []),
|
|
247
|
-
"## 宿主角色契约",
|
|
248
|
-
rendered,
|
|
249
|
-
].join("\n\n");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function codexAgentSections() {
|
|
253
|
-
return roles.slice(1).flatMap((role) => [
|
|
254
|
-
`[agents.odai_${role}]`,
|
|
255
|
-
`description = ${JSON.stringify(descriptions[role])}`,
|
|
256
|
-
`config_file = ${JSON.stringify(`agents/odai-${role}.toml`)}`,
|
|
257
|
-
"",
|
|
258
|
-
]).join("\n").trimEnd();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function activation(value) {
|
|
262
|
-
if (value === "claude") return { main: "新会话由托管设置选择 odai-controller。", reload: "重启 Claude Code 或重新加载配置。" };
|
|
263
|
-
if (value === "copilot") return {
|
|
264
|
-
main: `使用 copilot --model=${models.controller} --agent=odai-controller 启动。`,
|
|
265
|
-
reload: "安装或更新后重启 Copilot CLI。",
|
|
266
|
-
limitation: "Copilot Auto 会覆盖角色模型选择。",
|
|
267
|
-
};
|
|
268
|
-
return {
|
|
269
|
-
main: "普通会话由托管配置选择单一总控,其余责任按真实缺口调用。",
|
|
270
|
-
reload: "修改后开启新的 Codex 会话。",
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function copyScript(name, targetFile) {
|
|
275
|
-
const source = path.join(skillRoot, "scripts", name);
|
|
276
|
-
if (!existsSync(source)) throw new Error(`Missing canonical routing script: ${source}`);
|
|
277
|
-
writeFileSync(targetFile, readFileSync(source), "utf8");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function assertKnownArgs() {
|
|
281
|
-
const known = new Set([
|
|
282
|
-
"--host", "--out", "--controller-model", "--researcher-model", "--planner-model",
|
|
283
|
-
"--reviewer-model", "--frontend-model", "--controller-effort", "--researcher-effort",
|
|
284
|
-
"--planner-effort", "--reviewer-effort", "--frontend-effort", "--verifier-command",
|
|
285
|
-
]);
|
|
286
|
-
for (let index = 0; index < argv.length; index += 2) {
|
|
287
|
-
const name = argv[index];
|
|
288
|
-
if (!known.has(name)) throw new Error(`Unknown option: ${name}`);
|
|
289
|
-
const value = argv[index + 1] || "";
|
|
290
|
-
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function option(name) {
|
|
295
|
-
const index = argv.indexOf(name);
|
|
296
|
-
if (index < 0) return "";
|
|
297
|
-
const value = argv[index + 1] || "";
|
|
298
|
-
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
299
|
-
return value;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function tomlEffortLine(value) { return value ? `model_reasoning_effort = ${JSON.stringify(value)}` : ""; }
|
|
303
|
-
function yamlEffortLine(value) { return value ? `effort: ${value}\n` : ""; }
|
|
304
|
-
function writeRendered(source, targetFile, replacements) {
|
|
305
|
-
if (!existsSync(source)) throw new Error(`Missing canonical routing source: ${source}`);
|
|
306
|
-
mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
307
|
-
writeFileSync(targetFile, renderText(readFileSync(source, "utf8"), replacements, source), "utf8");
|
|
308
|
-
}
|
|
309
|
-
function renderText(input, replacements, source = "generated routing text") {
|
|
310
|
-
let text = input;
|
|
311
|
-
for (const [key, value] of Object.entries(replacements)) text = text.replaceAll(key, value);
|
|
312
|
-
const unresolved = text.match(/__ODAI_[A-Z0-9_]+__/g);
|
|
313
|
-
if (unresolved) throw new Error(`Unresolved routing template fields in ${source}: ${[...new Set(unresolved)].join(", ")}`);
|
|
314
|
-
return text;
|
|
315
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const skillRoot = path.resolve(scriptDir, "..");
|
|
10
|
+
const manifest = JSON.parse(readFileSync(path.join(skillRoot, "manifest.json"), "utf8"));
|
|
11
|
+
if (manifest?.schemaVersion !== 2 || !manifest.roleFiles || !manifest.referenceFiles || !Array.isArray(manifest.requiredFiles)) {
|
|
12
|
+
throw new Error("Odai manifest owner topology is unavailable");
|
|
13
|
+
}
|
|
14
|
+
const ownerNames = Object.freeze({
|
|
15
|
+
roleFiles: ["controller", "researcher", "planner", "reviewer", "frontend"],
|
|
16
|
+
referenceFiles: ["dao", "planning", "craft", "verification", "support", "leverage", "care", "human-safety"],
|
|
17
|
+
});
|
|
18
|
+
for (const [group, expectedNames] of Object.entries(ownerNames)) {
|
|
19
|
+
const entries = manifest[group];
|
|
20
|
+
if (entries === null || typeof entries !== "object" || Array.isArray(entries)
|
|
21
|
+
|| JSON.stringify(Object.keys(entries).sort()) !== JSON.stringify([...expectedNames].sort())) {
|
|
22
|
+
throw new Error(`Invalid manifest owner set: ${group}`);
|
|
23
|
+
}
|
|
24
|
+
const paths = Object.values(entries);
|
|
25
|
+
if (new Set(paths).size !== paths.length) throw new Error(`Duplicate manifest owner path: ${group}`);
|
|
26
|
+
}
|
|
27
|
+
const ownedPaths = [...Object.values(manifest.roleFiles), ...Object.values(manifest.referenceFiles)];
|
|
28
|
+
if (new Set(ownedPaths).size !== ownedPaths.length) throw new Error("Role and reference owners must use distinct paths");
|
|
29
|
+
|
|
30
|
+
const canonicalRoot = realpathSync(skillRoot);
|
|
31
|
+
const requiredFiles = new Set(manifest.requiredFiles);
|
|
32
|
+
|
|
33
|
+
function ownerFilePath(group, name) {
|
|
34
|
+
const relativeSource = manifest[group]?.[name];
|
|
35
|
+
if (typeof relativeSource !== "string" || relativeSource.trim() === "" || relativeSource.includes("\\")
|
|
36
|
+
|| path.isAbsolute(relativeSource) || /^[A-Za-z]:/u.test(relativeSource)
|
|
37
|
+
|| relativeSource.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
38
|
+
throw new Error(`Unsafe manifest owner path: ${group}.${name}`);
|
|
39
|
+
}
|
|
40
|
+
if (!requiredFiles.has(relativeSource)) throw new Error(`Manifest owner is undeclared: ${group}.${name}`);
|
|
41
|
+
const source = realpathSync(path.resolve(skillRoot, relativeSource));
|
|
42
|
+
const nested = path.relative(canonicalRoot, source);
|
|
43
|
+
if (nested === "" || nested === ".." || nested.startsWith(`..${path.sep}`) || path.isAbsolute(nested)) {
|
|
44
|
+
throw new Error(`Manifest owner escapes the canonical root: ${group}.${name}`);
|
|
45
|
+
}
|
|
46
|
+
return source;
|
|
47
|
+
}
|
|
48
|
+
for (const [group, names] of Object.entries(ownerNames)) {
|
|
49
|
+
for (const name of names) ownerFilePath(group, name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const argv = process.argv.slice(2);
|
|
53
|
+
|
|
54
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
55
|
+
console.log(`Usage:
|
|
56
|
+
node skills/odai/scripts/build-routing.mjs --host <codex|claude|copilot> --out <directory> \\
|
|
57
|
+
--controller-model <model> --planner-model <model> --reviewer-model <model> \\
|
|
58
|
+
[--researcher-model <model>] [--frontend-model <model>] [--controller-effort <effort>] \\
|
|
59
|
+
[--researcher-effort <effort>] [--planner-effort <effort>] [--reviewer-effort <effort>] \\
|
|
60
|
+
[--frontend-effort <effort>] [--verifier-command <command>]
|
|
61
|
+
|
|
62
|
+
生成 odai 的可选宿主 auto 路由适配器。一个持续总控持有实施与最终交付,只在多源证据压缩、独立规划、独立验收或前端专业制作能改变结果时调用相应责任;researcher 与 frontend 映射默认不生成。这里不跨 provider、不增加第二总控或隐藏的每轮前置流程。`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
assertKnownArgs();
|
|
67
|
+
const host = option("--host");
|
|
68
|
+
const out = option("--out");
|
|
69
|
+
const models = {
|
|
70
|
+
controller: option("--controller-model"),
|
|
71
|
+
researcher: option("--researcher-model"),
|
|
72
|
+
planner: option("--planner-model"),
|
|
73
|
+
reviewer: option("--reviewer-model"),
|
|
74
|
+
frontend: option("--frontend-model"),
|
|
75
|
+
};
|
|
76
|
+
const efforts = {
|
|
77
|
+
controller: option("--controller-effort"),
|
|
78
|
+
researcher: option("--researcher-effort"),
|
|
79
|
+
planner: option("--planner-effort"),
|
|
80
|
+
reviewer: option("--reviewer-effort"),
|
|
81
|
+
frontend: option("--frontend-effort"),
|
|
82
|
+
};
|
|
83
|
+
const verifierCommand = option("--verifier-command") || "node .codex/odai-verify-routing.mjs";
|
|
84
|
+
const requiredRoles = ["controller", "planner", "reviewer"];
|
|
85
|
+
const roles = Object.freeze([
|
|
86
|
+
...requiredRoles,
|
|
87
|
+
...(models.researcher ? ["researcher"] : []),
|
|
88
|
+
...(models.frontend ? ["frontend"] : []),
|
|
89
|
+
]);
|
|
90
|
+
const descriptions = {
|
|
91
|
+
controller: "持续持有用户目标、全局状态、修正回路与最终交付。",
|
|
92
|
+
researcher: "只在多源决定证据压缩有实测收益时返回可追溯来源账本。",
|
|
93
|
+
planner: "只在独立判断能改变路线时形成有界的证据化规划。",
|
|
94
|
+
reviewer: "只在独立判断能改变放行结果时依据真实证据验收。",
|
|
95
|
+
frontend: "只在界面设计或前端制作存在专业缺口时形成可验证成品。",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!new Set(["codex", "claude", "copilot"]).has(host)) throw new Error(`Unsupported routing host: ${host || "(missing)"}`);
|
|
99
|
+
if (!out) throw new Error("--out requires a directory");
|
|
100
|
+
for (const role of requiredRoles) if (!models[role]) throw new Error(`--${role}-model is required`);
|
|
101
|
+
if (efforts.researcher && !models.researcher) throw new Error("--researcher-effort requires --researcher-model");
|
|
102
|
+
if (efforts.frontend && !models.frontend) throw new Error("--frontend-effort requires --frontend-model");
|
|
103
|
+
if (host === "copilot" && Object.values(efforts).some(Boolean)) {
|
|
104
|
+
throw new Error("Copilot custom-agent profiles do not provide a portable reasoning-effort field");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const policy = "conditional";
|
|
108
|
+
const target = path.resolve(out, host);
|
|
109
|
+
mkdirSync(target, { recursive: true });
|
|
110
|
+
if (host === "codex") buildCodex(target);
|
|
111
|
+
else if (host === "claude") buildClaude(target);
|
|
112
|
+
else buildCopilot(target);
|
|
113
|
+
|
|
114
|
+
const metadata = {
|
|
115
|
+
id: `odai-routing-${host}`,
|
|
116
|
+
host,
|
|
117
|
+
generatedFrom: "skills/odai/scripts/build-routing.mjs",
|
|
118
|
+
mode: "single-controller-conditional-routing",
|
|
119
|
+
mapping: Object.fromEntries(roles.map((role) => [role, {
|
|
120
|
+
provider: host,
|
|
121
|
+
model: models[role],
|
|
122
|
+
reasoning_effort: efforts[role] || null,
|
|
123
|
+
}])),
|
|
124
|
+
routing_policy: {
|
|
125
|
+
mode: policy,
|
|
126
|
+
controller_identity: "persistent-task-thread",
|
|
127
|
+
controller_owns_final_delivery: true,
|
|
128
|
+
researcher_activation: models.researcher
|
|
129
|
+
? "only-when-multi-source-decision-evidence-compression-has-measured-net-benefit"
|
|
130
|
+
: "unconfigured",
|
|
131
|
+
planner_activation: "only-when-independent-judgment-can-change-route",
|
|
132
|
+
reviewer_activation: "only-when-independent-judgment-can-change-release",
|
|
133
|
+
frontend_activation: models.frontend
|
|
134
|
+
? "only-when-interface-design-or-production-needs-specialist-capability"
|
|
135
|
+
: "unconfigured",
|
|
136
|
+
controller_owns_implementation: true,
|
|
137
|
+
sufficient_controller_defaults_to_single_pass: true,
|
|
138
|
+
},
|
|
139
|
+
runtime_verification: host === "codex"
|
|
140
|
+
? { mode: "post-run-executable", command: verifierCommand }
|
|
141
|
+
: { mode: "host-native-evidence-required", command: null },
|
|
142
|
+
activation: activation(host),
|
|
143
|
+
profiles: roles.map((role) => ({ name: `odai-${role}`, purpose: descriptions[role] })),
|
|
144
|
+
};
|
|
145
|
+
writeFileSync(path.join(target, "ADAPTER.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
146
|
+
console.log(target);
|
|
147
|
+
|
|
148
|
+
function buildCodex(root) {
|
|
149
|
+
const sourceDir = path.join(skillRoot, "assets", "codex-agents");
|
|
150
|
+
const configRoot = path.join(root, ".codex");
|
|
151
|
+
const agentsRoot = path.join(configRoot, "agents");
|
|
152
|
+
const contractsRoot = path.join(configRoot, "role-contracts");
|
|
153
|
+
mkdirSync(agentsRoot, { recursive: true });
|
|
154
|
+
mkdirSync(contractsRoot, { recursive: true });
|
|
155
|
+
writeRendered(path.join(sourceDir, "config.toml"), path.join(configRoot, "config.toml"), {
|
|
156
|
+
__ODAI_CONTROLLER_MODEL_LINE__: `model = ${JSON.stringify(models.controller)}`,
|
|
157
|
+
__ODAI_CONTROLLER_EFFORT_LINE__: tomlEffortLine(efforts.controller),
|
|
158
|
+
__ODAI_CONTROLLER_BODY__: roleBody("controller", "codex"),
|
|
159
|
+
__ODAI_AGENT_SECTIONS__: codexAgentSections(),
|
|
160
|
+
});
|
|
161
|
+
for (const role of roles.slice(1)) {
|
|
162
|
+
writeRendered(path.join(sourceDir, "role.toml"), path.join(agentsRoot, `odai-${role}.toml`), {
|
|
163
|
+
__ODAI_ROLE_MODEL__: JSON.stringify(models[role]),
|
|
164
|
+
__ODAI_ROLE_EFFORT_LINE__: tomlEffortLine(efforts[role]),
|
|
165
|
+
__ODAI_ROLE_BODY__: roleBody(role, "codex"),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
copyScript("verify-routing.mjs", path.join(configRoot, "odai-verify-routing.mjs"));
|
|
169
|
+
copyScript("run-role.mjs", path.join(configRoot, "odai-run-role.mjs"));
|
|
170
|
+
for (const role of roles) writeFileSync(path.join(contractsRoot, `odai-${role}.md`), roleBody(role, "codex"), "utf8");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildClaude(root) {
|
|
174
|
+
const source = path.join(skillRoot, "assets", "claude-agents", "agent.md");
|
|
175
|
+
const agentsRoot = path.join(root, ".claude", "agents");
|
|
176
|
+
mkdirSync(agentsRoot, { recursive: true });
|
|
177
|
+
for (const role of roles) {
|
|
178
|
+
writeRendered(source, path.join(agentsRoot, `odai-${role}.md`), {
|
|
179
|
+
__ODAI_ROLE__: role,
|
|
180
|
+
__ODAI_ROLE_DESCRIPTION__: descriptions[role],
|
|
181
|
+
__ODAI_ROLE_MODEL__: models[role],
|
|
182
|
+
__ODAI_ROLE_EFFORT_LINE__: yamlEffortLine(efforts[role]),
|
|
183
|
+
__ODAI_PERMISSION_MODE__: ["researcher", "planner", "reviewer"].includes(role) ? "plan" : "default",
|
|
184
|
+
__ODAI_TOOLS_LINE__: "",
|
|
185
|
+
__ODAI_ROLE_BODY__: roleBody(role, "claude"),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
writeFileSync(path.join(root, ".claude", "settings.patch.json"), `${JSON.stringify({ agent: "odai-controller" }, null, 2)}\n`, "utf8");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildCopilot(root) {
|
|
192
|
+
const source = path.join(skillRoot, "assets", "copilot-agents", "agent.md");
|
|
193
|
+
const agentsRoot = path.join(root, ".github", "agents");
|
|
194
|
+
mkdirSync(agentsRoot, { recursive: true });
|
|
195
|
+
for (const role of roles) {
|
|
196
|
+
writeRendered(source, path.join(agentsRoot, `odai-${role}.agent.md`), {
|
|
197
|
+
__ODAI_ROLE__: role,
|
|
198
|
+
__ODAI_ROLE_DESCRIPTION__: descriptions[role],
|
|
199
|
+
__ODAI_ROLE_MODEL__: models[role],
|
|
200
|
+
__ODAI_DISABLE_MODEL_INVOCATION__: role === "controller" ? "true" : "false",
|
|
201
|
+
__ODAI_USER_INVOCABLE__: role === "controller" ? "true" : "false",
|
|
202
|
+
__ODAI_TOOLS__: role === "researcher"
|
|
203
|
+
? '["view", "glob", "grep"]'
|
|
204
|
+
: ["planner", "reviewer"].includes(role) ? '["view", "glob", "grep", "shell"]' : '["*"]',
|
|
205
|
+
__ODAI_ROLE_BODY__: roleBody(role, "copilot"),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(path.join(root, "LAUNCH.json"), `${JSON.stringify({
|
|
209
|
+
command: ["copilot", `--model=${models.controller}`, "--agent=odai-controller"],
|
|
210
|
+
reason: "Copilot has no portable project setting that makes a custom agent the default main controller.",
|
|
211
|
+
autoModelUnsupported: true,
|
|
212
|
+
}, null, 2)}\n`, "utf8");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function roleBody(role, hostName) {
|
|
216
|
+
const source = ownerFilePath("roleFiles", role);
|
|
217
|
+
if (!existsSync(source)) throw new Error(`Missing canonical routing role body: ${source}`);
|
|
218
|
+
const names = hostName === "codex"
|
|
219
|
+
? { researcher: "odai_researcher", planner: "odai_planner", reviewer: "odai_reviewer", frontend: "odai_frontend" }
|
|
220
|
+
: { researcher: "odai-researcher", planner: "odai-planner", reviewer: "odai-reviewer", frontend: "odai-frontend" };
|
|
221
|
+
const rendered = renderText(readFileSync(source, "utf8"), {
|
|
222
|
+
__ODAI_POLICY__: policy,
|
|
223
|
+
__ODAI_RESEARCHER_ROLE__: models.researcher ? names.researcher : "researcher(当前适配器未配置映射,不能调用)",
|
|
224
|
+
__ODAI_PLANNER_ROLE__: names.planner,
|
|
225
|
+
__ODAI_REVIEWER_ROLE__: names.reviewer,
|
|
226
|
+
__ODAI_FRONTEND_ROLE__: models.frontend ? names.frontend : "frontend(当前适配器未配置映射,不能调用)",
|
|
227
|
+
__ODAI_RUNTIME_VERIFICATION__: hostName === "codex"
|
|
228
|
+
? "实际路由必须用宿主返回的线程、角色、模型与用量证据核对,不从调用请求推断成功。"
|
|
229
|
+
: "只有宿主原生运行证据能识别实际角色与模型时,路由才算已核实。",
|
|
230
|
+
__ODAI_HOST_NOTE__: hostName === "copilot" ? "Copilot Auto 会覆盖角色模型选择;需要区分角色时不使用 Auto。" : "",
|
|
231
|
+
}, source);
|
|
232
|
+
const craft = role === "frontend"
|
|
233
|
+
? readFileSync(ownerFilePath("referenceFiles", "craft"), "utf8").trim()
|
|
234
|
+
: "";
|
|
235
|
+
if (hostName !== "codex") {
|
|
236
|
+
return [rendered, ...(craft ? ["## Canonical 制作工艺", craft] : [])].join("\n\n");
|
|
237
|
+
}
|
|
238
|
+
const canonical = readFileSync(path.join(skillRoot, "SKILL.md"), "utf8")
|
|
239
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
240
|
+
const responsibility = role === "controller"
|
|
241
|
+
? "你是唯一总控,持有完整目标、全局状态、修正回路与最终交付。"
|
|
242
|
+
: `你只承担 ${role} 责任,不是第二个总控。`;
|
|
243
|
+
return [
|
|
244
|
+
`以下 canonical odai 是所有责任共享的内核;宿主角色契约只限制本责任,不得另建流程。${responsibility}`,
|
|
245
|
+
canonical,
|
|
246
|
+
...(craft ? ["## Canonical 制作工艺", craft] : []),
|
|
247
|
+
"## 宿主角色契约",
|
|
248
|
+
rendered,
|
|
249
|
+
].join("\n\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function codexAgentSections() {
|
|
253
|
+
return roles.slice(1).flatMap((role) => [
|
|
254
|
+
`[agents.odai_${role}]`,
|
|
255
|
+
`description = ${JSON.stringify(descriptions[role])}`,
|
|
256
|
+
`config_file = ${JSON.stringify(`agents/odai-${role}.toml`)}`,
|
|
257
|
+
"",
|
|
258
|
+
]).join("\n").trimEnd();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function activation(value) {
|
|
262
|
+
if (value === "claude") return { main: "新会话由托管设置选择 odai-controller。", reload: "重启 Claude Code 或重新加载配置。" };
|
|
263
|
+
if (value === "copilot") return {
|
|
264
|
+
main: `使用 copilot --model=${models.controller} --agent=odai-controller 启动。`,
|
|
265
|
+
reload: "安装或更新后重启 Copilot CLI。",
|
|
266
|
+
limitation: "Copilot Auto 会覆盖角色模型选择。",
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
main: "普通会话由托管配置选择单一总控,其余责任按真实缺口调用。",
|
|
270
|
+
reload: "修改后开启新的 Codex 会话。",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function copyScript(name, targetFile) {
|
|
275
|
+
const source = path.join(skillRoot, "scripts", name);
|
|
276
|
+
if (!existsSync(source)) throw new Error(`Missing canonical routing script: ${source}`);
|
|
277
|
+
writeFileSync(targetFile, readFileSync(source), "utf8");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function assertKnownArgs() {
|
|
281
|
+
const known = new Set([
|
|
282
|
+
"--host", "--out", "--controller-model", "--researcher-model", "--planner-model",
|
|
283
|
+
"--reviewer-model", "--frontend-model", "--controller-effort", "--researcher-effort",
|
|
284
|
+
"--planner-effort", "--reviewer-effort", "--frontend-effort", "--verifier-command",
|
|
285
|
+
]);
|
|
286
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
287
|
+
const name = argv[index];
|
|
288
|
+
if (!known.has(name)) throw new Error(`Unknown option: ${name}`);
|
|
289
|
+
const value = argv[index + 1] || "";
|
|
290
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function option(name) {
|
|
295
|
+
const index = argv.indexOf(name);
|
|
296
|
+
if (index < 0) return "";
|
|
297
|
+
const value = argv[index + 1] || "";
|
|
298
|
+
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function tomlEffortLine(value) { return value ? `model_reasoning_effort = ${JSON.stringify(value)}` : ""; }
|
|
303
|
+
function yamlEffortLine(value) { return value ? `effort: ${value}\n` : ""; }
|
|
304
|
+
function writeRendered(source, targetFile, replacements) {
|
|
305
|
+
if (!existsSync(source)) throw new Error(`Missing canonical routing source: ${source}`);
|
|
306
|
+
mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
307
|
+
writeFileSync(targetFile, renderText(readFileSync(source, "utf8"), replacements, source), "utf8");
|
|
308
|
+
}
|
|
309
|
+
function renderText(input, replacements, source = "generated routing text") {
|
|
310
|
+
let text = input;
|
|
311
|
+
for (const [key, value] of Object.entries(replacements)) text = text.replaceAll(key, value);
|
|
312
|
+
const unresolved = text.match(/__ODAI_[A-Z0-9_]+__/g);
|
|
313
|
+
if (unresolved) throw new Error(`Unresolved routing template fields in ${source}: ${[...new Set(unresolved)].join(", ")}`);
|
|
314
|
+
return text;
|
|
315
|
+
}
|