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