odai-dsh-agent 0.0.1

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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/bin/odai-dsh-agent.mjs +87 -0
  4. package/package.json +68 -0
  5. package/preset/odai/agent.cordis.yml +260 -0
  6. package/preset/odai/preset.yml +3 -0
  7. package/preset/odai/runtime/governance.mjs +120 -0
  8. package/preset/odai/runtime/index.mjs +584 -0
  9. package/preset/odai/runtime/router.mjs +290 -0
  10. package/preset/odai/runtime/routing-config.mjs +322 -0
  11. package/preset/odai/skills/odai/SKILL.md +65 -0
  12. package/preset/odai/skills/odai/agents/openai.yaml +4 -0
  13. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -0
  14. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -0
  15. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -0
  16. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -0
  17. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -0
  18. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -0
  19. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -0
  20. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -0
  21. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -0
  22. package/preset/odai/skills/odai/assets/task-state.md +12 -0
  23. package/preset/odai/skills/odai/references/craft.md +53 -0
  24. package/preset/odai/skills/odai/references/dao.md +30 -0
  25. package/preset/odai/skills/odai/references/leverage.md +45 -0
  26. package/preset/odai/skills/odai/references/support.md +36 -0
  27. package/preset/odai/skills/odai/references/verification.md +23 -0
  28. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -0
  29. package/preset/odai/skills/odai/scripts/build-routing.mjs +239 -0
  30. package/preset/odai/skills/odai/scripts/install-routing.mjs +375 -0
  31. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -0
  32. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -0
  33. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -0
  34. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -0
  35. package/scripts/verify-agent-load.mjs +217 -0
  36. package/src/installer.mjs +270 -0
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const runtimeSource = path.join(skillRoot, "scripts", "odai-hook.mjs");
15
+ const policySource = path.join(skillRoot, "assets", "hooks-policy.example.json");
16
+ const supportedHosts = ["codex", "claude", "copilot", "gemini", "grok", "kimi"];
17
+
18
+ const args = parseArgs(process.argv.slice(2));
19
+ if (args.help) {
20
+ printHelp();
21
+ process.exit(0);
22
+ }
23
+ if (!args.out) fail("缺少 --out <directory>");
24
+ if (!args.host) fail("缺少 --host <codex|claude|copilot|gemini|grok|kimi|all>");
25
+
26
+ const hosts = args.host === "all" ? supportedHosts : [args.host];
27
+ for (const host of hosts) {
28
+ if (!supportedHosts.includes(host)) fail(`不支持的 host:${host}`);
29
+ }
30
+
31
+ const outputRoot = path.resolve(args.out);
32
+ mkdirSync(outputRoot, { recursive: true });
33
+ for (const host of hosts) {
34
+ const target = path.join(outputRoot, host);
35
+ assertEmpty(target);
36
+ mkdirSync(target, { recursive: true });
37
+ buildAdapter(host, target);
38
+ console.log(`${host}: ${target}`);
39
+ }
40
+
41
+ function buildAdapter(host, target) {
42
+ const metadata = {
43
+ version: 1,
44
+ host,
45
+ policyPath: "<project>/.odai/hooks.json",
46
+ defaultBehavior: "No policy file means no-op.",
47
+ generatedFrom: "skills/odai/scripts/build-hooks.mjs",
48
+ capabilities: host === "grok" ? ["pre-tool"] : ["pre-tool", "stop-check"],
49
+ };
50
+
51
+ if (host === "copilot") {
52
+ copy(target, ".odai/hooks/odai-hook.mjs", runtimeSource);
53
+ copy(target, ".odai/hooks.example.json", policySource);
54
+ writeJson(target, ".github/hooks/odai.json", copilotHooks());
55
+ metadata.format = "project-overlay";
56
+ metadata.install = "Merge this directory into the repository root, review .github/hooks/odai.json, then create .odai/hooks.json from the example.";
57
+ } else {
58
+ copy(target, "scripts/odai-hook.mjs", runtimeSource);
59
+ copy(target, "examples/hooks.json", policySource);
60
+
61
+ if (host === "codex") {
62
+ writeJson(target, ".codex-plugin/plugin.json", codexPluginManifest());
63
+ writeJson(target, "hooks/hooks.json", groupedHooks("${PLUGIN_ROOT}", host, "PreToolUse", "Stop"));
64
+ metadata.format = "codex-plugin";
65
+ metadata.install = "Install or link this directory as a Codex plugin, review and trust its hooks, then create <project>/.odai/hooks.json.";
66
+ } else if (host === "claude") {
67
+ writeJson(target, ".claude-plugin/plugin.json", pluginManifest("Claude Code"));
68
+ writeJson(target, "hooks/hooks.json", groupedHooks("${CLAUDE_PLUGIN_ROOT}", host, "PreToolUse", "Stop"));
69
+ metadata.format = "claude-plugin";
70
+ metadata.install = "Install this directory as a Claude Code plugin, then create <project>/.odai/hooks.json.";
71
+ } else if (host === "grok") {
72
+ writeJson(target, ".claude-plugin/plugin.json", pluginManifest("Grok Build"));
73
+ writeJson(target, "hooks/hooks.json", groupedHooks("${GROK_PLUGIN_ROOT}", host, "PreToolUse", null));
74
+ metadata.format = "grok-plugin-using-claude-compatible-manifest";
75
+ metadata.install = "Install this directory with Grok Build's plugin manager, trust the hook, then create <project>/.odai/hooks.json.";
76
+ metadata.note = "Grok Build exposes PreToolUse as the blocking boundary; this adapter intentionally has no Stop verifier.";
77
+ } else if (host === "gemini") {
78
+ writeJson(target, "gemini-extension.json", {
79
+ name: "odai-hooks",
80
+ version: "0.1.0",
81
+ description: "Optional deterministic boundaries for odai.",
82
+ });
83
+ writeJson(target, "hooks/hooks.json", groupedHooks("${extensionPath}", host, "BeforeTool", "AfterAgent"));
84
+ metadata.format = "gemini-extension";
85
+ metadata.install = "Link or install this directory as a Gemini CLI extension, then create <project>/.odai/hooks.json.";
86
+ } else if (host === "kimi") {
87
+ writeJson(target, "kimi.plugin.json", kimiManifest());
88
+ metadata.format = "kimi-plugin";
89
+ metadata.install = "Run /plugins install <this-directory> in Kimi Code CLI, enable it, then create <project>/.odai/hooks.json.";
90
+ }
91
+ }
92
+
93
+ writeJson(target, "ADAPTER.json", metadata);
94
+ }
95
+
96
+ function groupedHooks(rootVariable, host, preEvent, stopEvent) {
97
+ const commandRoot = `${rootVariable}/scripts/odai-hook.mjs`;
98
+ const preHandler = {
99
+ type: "command",
100
+ command: `node "${commandRoot}" pre-tool --host ${host}`,
101
+ timeout: 5,
102
+ };
103
+ const stopHandler = {
104
+ type: "command",
105
+ command: `node "${commandRoot}" stop --host ${host}`,
106
+ timeout: 600,
107
+ };
108
+ if (host === "gemini") {
109
+ preHandler.name = "odai-write-boundary";
110
+ stopHandler.name = "odai-acceptance-check";
111
+ }
112
+ const hooks = {
113
+ [preEvent]: [
114
+ {
115
+ matcher: writeMatcher(host),
116
+ hooks: [preHandler],
117
+ },
118
+ ],
119
+ };
120
+ if (stopEvent) {
121
+ hooks[stopEvent] = [
122
+ {
123
+ matcher: "*",
124
+ hooks: [stopHandler],
125
+ },
126
+ ];
127
+ }
128
+ const result = { hooks };
129
+ if (host !== "gemini") {
130
+ result.description = "Optional odai boundaries. Inactive unless the project defines .odai/hooks.json.";
131
+ }
132
+ return result;
133
+ }
134
+
135
+ function copilotHooks() {
136
+ const command = "node .odai/hooks/odai-hook.mjs";
137
+ return {
138
+ version: 1,
139
+ hooks: {
140
+ preToolUse: [
141
+ {
142
+ type: "command",
143
+ matcher: writeMatcher("copilot"),
144
+ command: `${command} pre-tool --host copilot`,
145
+ cwd: ".",
146
+ timeoutSec: 5,
147
+ },
148
+ ],
149
+ agentStop: [
150
+ {
151
+ type: "command",
152
+ command: `${command} stop --host copilot`,
153
+ cwd: ".",
154
+ timeoutSec: 600,
155
+ },
156
+ ],
157
+ },
158
+ };
159
+ }
160
+
161
+ function kimiManifest() {
162
+ return {
163
+ name: "odai-hooks",
164
+ version: "0.1.0",
165
+ description: "Optional deterministic boundaries for odai.",
166
+ interface: {
167
+ displayName: "odai Hooks",
168
+ shortDescription: "Protect explicit read-only paths and run declared acceptance checks.",
169
+ },
170
+ hooks: [
171
+ {
172
+ event: "PreToolUse",
173
+ matcher: writeMatcher("kimi"),
174
+ command: "node ./scripts/odai-hook.mjs pre-tool --host kimi",
175
+ timeout: 5,
176
+ },
177
+ {
178
+ event: "Stop",
179
+ command: "node ./scripts/odai-hook.mjs stop --host kimi",
180
+ timeout: 600,
181
+ },
182
+ ],
183
+ };
184
+ }
185
+
186
+ function pluginManifest(hostName) {
187
+ return {
188
+ name: "odai-hooks",
189
+ version: "0.1.0",
190
+ description: `Optional deterministic odai boundaries for ${hostName}.`,
191
+ };
192
+ }
193
+
194
+ function codexPluginManifest() {
195
+ return {
196
+ id: "odai-hooks",
197
+ name: "odai-hooks",
198
+ version: "0.1.0",
199
+ description: "Optional deterministic boundaries for odai.",
200
+ author: {
201
+ name: "orzi",
202
+ url: "https://github.com/orziz",
203
+ },
204
+ homepage: "https://github.com/orziz/odai",
205
+ repository: "https://github.com/orziz/odai",
206
+ license: "MIT",
207
+ keywords: ["odai", "hooks", "guardrails"],
208
+ interface: {
209
+ displayName: "odai Hooks",
210
+ shortDescription: "Protect explicit read-only paths and run declared checks.",
211
+ longDescription: "Optional deterministic hook guardrails for projects that use odai.",
212
+ developerName: "orzi",
213
+ category: "Developer Tools",
214
+ capabilities: ["PreToolUse guard", "Stop acceptance check"],
215
+ websiteURL: "https://github.com/orziz/odai",
216
+ brandColor: "#1F6FEB",
217
+ defaultPrompt: "Use odai with the project's explicit hook boundaries.",
218
+ },
219
+ };
220
+ }
221
+
222
+ function writeMatcher(host) {
223
+ if (host === "copilot") return "create|edit|str_replace_editor|apply_patch";
224
+ if (host === "gemini") return "write_file|replace|apply_patch|edit|create";
225
+ return "Edit|Write|apply_patch|create|edit|write_file|str_replace_editor";
226
+ }
227
+
228
+ function writeJson(root, relativePath, value) {
229
+ const target = path.join(root, relativePath);
230
+ mkdirSync(path.dirname(target), { recursive: true });
231
+ writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8");
232
+ }
233
+
234
+ function copy(root, relativePath, source) {
235
+ const target = path.join(root, relativePath);
236
+ mkdirSync(path.dirname(target), { recursive: true });
237
+ copyFileSync(source, target);
238
+ }
239
+
240
+ function assertEmpty(target) {
241
+ if (!existsSync(target)) return;
242
+ if (readdirSync(target).length > 0) fail(`输出目录非空,拒绝覆盖:${target}`);
243
+ }
244
+
245
+ function parseArgs(argv) {
246
+ const result = { host: "", out: "", help: false };
247
+ for (let index = 0; index < argv.length; index += 1) {
248
+ const value = argv[index];
249
+ if (value === "--help" || value === "-h") result.help = true;
250
+ else if (value === "--host") result.host = argv[++index] || "";
251
+ else if (value === "--out") result.out = argv[++index] || "";
252
+ else fail(`未知参数:${value}`);
253
+ }
254
+ return result;
255
+ }
256
+
257
+ function printHelp() {
258
+ console.log(`Build optional odai hook adapters.
259
+
260
+ Usage:
261
+ node skills/odai/scripts/build-hooks.mjs --host <host|all> --out <directory>
262
+
263
+ Hosts:
264
+ ${supportedHosts.join(", ")}
265
+
266
+ The builder never installs hooks or edits a target project. Each generated
267
+ adapter is inactive until that project defines .odai/hooks.json.`);
268
+ }
269
+
270
+ function fail(message) {
271
+ console.error(message);
272
+ process.exit(1);
273
+ }
@@ -0,0 +1,239 @@
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
+ [--planning-policy <auto|stage>] [--controller-effort <effort>] [--planner-effort <effort>] \\
17
+ [--executor-effort <effort>] [--reviewer-effort <effort>] [--verifier-command <command>]
18
+
19
+ 生成 odai 的可选宿主路由适配器。auto 保留一个持续总控,只在独立规划、有界执行或独立验收能改变结果时调用相应责任。stage 仅生成从任务起点显式运行的 Codex 实验 runner,不注入每轮 Hook。这里不跨 provider 、不增加第二总控。`);
20
+ process.exit(0);
21
+ }
22
+
23
+ const host = option("--host");
24
+ const out = option("--out");
25
+ const requestedPolicy = option("--planning-policy") || "auto";
26
+ const models = {
27
+ controller: option("--controller-model"),
28
+ planner: option("--planner-model"),
29
+ executor: option("--executor-model") || option("--controller-model"),
30
+ reviewer: option("--reviewer-model"),
31
+ };
32
+ const efforts = {
33
+ controller: option("--controller-effort"),
34
+ planner: option("--planner-effort"),
35
+ executor: option("--executor-effort") || option("--controller-effort"),
36
+ reviewer: option("--reviewer-effort"),
37
+ };
38
+ const verifierCommand = option("--verifier-command") || "node .codex/odai-verify-routing.mjs";
39
+ const roles = ["controller", "planner", "executor", "reviewer"];
40
+ const descriptions = {
41
+ controller: "持续持有用户目标、全局状态、修正回路与最终交付。",
42
+ planner: "只在独立判断能改变路线时形成有界的证据化规划。",
43
+ executor: "只按冻结方案实施,不重新解释请求、选路或批准交付。",
44
+ reviewer: "只在独立判断能改变放行结果时依据真实证据验收。",
45
+ };
46
+
47
+ if (!new Set(["codex", "claude", "copilot"]).has(host)) throw new Error(`Unsupported routing host: ${host || "(missing)"}`);
48
+ if (!out) throw new Error("--out requires a directory");
49
+ for (const role of roles) if (!models[role]) throw new Error(`--${role}-model is required`);
50
+ if (!new Set(["auto", "stage"]).has(requestedPolicy)) throw new Error(`Unsupported --planning-policy: ${requestedPolicy}`);
51
+ if (requestedPolicy === "stage" && host !== "codex") throw new Error("stage routing currently requires Codex");
52
+ if (host === "copilot" && Object.values(efforts).some(Boolean)) {
53
+ throw new Error("Copilot custom-agent profiles do not provide a portable reasoning-effort field");
54
+ }
55
+
56
+ const policy = requestedPolicy === "stage" ? "stage" : "conditional";
57
+ const target = path.resolve(out, host);
58
+ mkdirSync(target, { recursive: true });
59
+ if (host === "codex") buildCodex(target);
60
+ else if (host === "claude") buildClaude(target);
61
+ else buildCopilot(target);
62
+
63
+ const metadata = {
64
+ id: `odai-routing-${host}`,
65
+ host,
66
+ generatedFrom: "skills/odai/scripts/build-routing.mjs",
67
+ mode: policy === "stage" ? "single-controller-stage-routing" : "single-controller-conditional-routing",
68
+ mapping: Object.fromEntries(roles.map((role) => [role, {
69
+ provider: host,
70
+ model: models[role],
71
+ reasoning_effort: efforts[role] || null,
72
+ }])),
73
+ routing_policy: {
74
+ mode: policy,
75
+ requested_mode: requestedPolicy,
76
+ controller_identity: "persistent-task-thread",
77
+ controller_owns_final_delivery: true,
78
+ planner_activation: "only-when-independent-judgment-can-change-route",
79
+ executor_activation: "only-after-plan-is-frozen-and-handoff-has-net-value",
80
+ reviewer_activation: "only-when-independent-judgment-can-change-release",
81
+ bounded_fresh_execution_context: policy === "stage",
82
+ controller_reentry_on_failure: policy === "stage",
83
+ sufficient_controller_defaults_to_single_pass: true,
84
+ shared_evidence_chain: policy === "stage",
85
+ },
86
+ runtime_verification: host === "codex"
87
+ ? { mode: "post-run-executable", command: verifierCommand }
88
+ : { mode: "host-native-evidence-required", command: null },
89
+ activation: activation(host),
90
+ profiles: roles.map((role) => ({ name: `odai-${role}`, purpose: descriptions[role] })),
91
+ };
92
+ writeFileSync(path.join(target, "ADAPTER.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
93
+ console.log(target);
94
+
95
+ function buildCodex(root) {
96
+ const sourceDir = path.join(skillRoot, "assets", "codex-agents");
97
+ const configRoot = path.join(root, ".codex");
98
+ const agentsRoot = path.join(configRoot, "agents");
99
+ const contractsRoot = path.join(configRoot, "role-contracts");
100
+ mkdirSync(agentsRoot, { recursive: true });
101
+ mkdirSync(contractsRoot, { recursive: true });
102
+ writeRendered(path.join(sourceDir, "config.toml"), path.join(configRoot, "config.toml"), {
103
+ __ODAI_CONTROLLER_MODEL_LINE__: `model = ${JSON.stringify(models.controller)}`,
104
+ __ODAI_CONTROLLER_EFFORT_LINE__: tomlEffortLine(efforts.controller),
105
+ __ODAI_CONTROLLER_BODY__: roleBody("controller", "codex"),
106
+ __ODAI_AGENT_SECTIONS__: codexAgentSections(),
107
+ });
108
+ for (const role of roles.slice(1)) {
109
+ writeRendered(path.join(sourceDir, "role.toml"), path.join(agentsRoot, `odai-${role}.toml`), {
110
+ __ODAI_ROLE_MODEL__: JSON.stringify(models[role]),
111
+ __ODAI_ROLE_EFFORT_LINE__: tomlEffortLine(efforts[role]),
112
+ __ODAI_ROLE_BODY__: roleBody(role, "codex"),
113
+ });
114
+ }
115
+ copyScript("verify-routing.mjs", path.join(configRoot, "odai-verify-routing.mjs"));
116
+ copyScript("run-role.mjs", path.join(configRoot, "odai-run-role.mjs"));
117
+ if (policy === "stage") {
118
+ copyScript("run-routing.mjs", path.join(configRoot, "odai-run-routing.mjs"));
119
+ }
120
+ for (const role of roles) writeFileSync(path.join(contractsRoot, `odai-${role}.md`), roleBody(role, "codex"), "utf8");
121
+ }
122
+
123
+ function buildClaude(root) {
124
+ const source = path.join(skillRoot, "assets", "claude-agents", "agent.md");
125
+ const agentsRoot = path.join(root, ".claude", "agents");
126
+ mkdirSync(agentsRoot, { recursive: true });
127
+ for (const role of roles) {
128
+ writeRendered(source, path.join(agentsRoot, `odai-${role}.md`), {
129
+ __ODAI_ROLE__: role,
130
+ __ODAI_ROLE_DESCRIPTION__: descriptions[role],
131
+ __ODAI_ROLE_MODEL__: models[role],
132
+ __ODAI_ROLE_EFFORT_LINE__: yamlEffortLine(efforts[role]),
133
+ __ODAI_PERMISSION_MODE__: role === "planner" || role === "reviewer" ? "plan" : "default",
134
+ __ODAI_TOOLS_LINE__: "",
135
+ __ODAI_ROLE_BODY__: roleBody(role, "claude"),
136
+ });
137
+ }
138
+ writeFileSync(path.join(root, ".claude", "settings.patch.json"), `${JSON.stringify({ agent: "odai-controller" }, null, 2)}\n`, "utf8");
139
+ }
140
+
141
+ function buildCopilot(root) {
142
+ const source = path.join(skillRoot, "assets", "copilot-agents", "agent.md");
143
+ const agentsRoot = path.join(root, ".github", "agents");
144
+ mkdirSync(agentsRoot, { recursive: true });
145
+ for (const role of roles) {
146
+ writeRendered(source, path.join(agentsRoot, `odai-${role}.agent.md`), {
147
+ __ODAI_ROLE__: role,
148
+ __ODAI_ROLE_DESCRIPTION__: descriptions[role],
149
+ __ODAI_ROLE_MODEL__: models[role],
150
+ __ODAI_DISABLE_MODEL_INVOCATION__: role === "controller" ? "true" : "false",
151
+ __ODAI_USER_INVOCABLE__: role === "controller" ? "true" : "false",
152
+ __ODAI_TOOLS__: role === "planner" || role === "reviewer" ? '["view", "glob", "grep", "shell"]' : '["*"]',
153
+ __ODAI_ROLE_BODY__: roleBody(role, "copilot"),
154
+ });
155
+ }
156
+ writeFileSync(path.join(root, "LAUNCH.json"), `${JSON.stringify({
157
+ command: ["copilot", `--model=${models.controller}`, "--agent=odai-controller"],
158
+ reason: "Copilot has no portable project setting that makes a custom agent the default main controller.",
159
+ autoModelUnsupported: true,
160
+ }, null, 2)}\n`, "utf8");
161
+ }
162
+
163
+ function roleBody(role, hostName) {
164
+ const source = path.join(skillRoot, "assets", "routing-roles", `${role}.md`);
165
+ if (!existsSync(source)) throw new Error(`Missing canonical routing role body: ${source}`);
166
+ const names = hostName === "codex"
167
+ ? { planner: "odai_planner", executor: "odai_executor", reviewer: "odai_reviewer" }
168
+ : { planner: "odai-planner", executor: "odai-executor", reviewer: "odai-reviewer" };
169
+ const rendered = renderText(readFileSync(source, "utf8"), {
170
+ __ODAI_POLICY__: policy,
171
+ __ODAI_PLANNER_ROLE__: names.planner,
172
+ __ODAI_EXECUTOR_ROLE__: names.executor,
173
+ __ODAI_REVIEWER_ROLE__: names.reviewer,
174
+ __ODAI_RUNTIME_VERIFICATION__: hostName === "codex"
175
+ ? "实际路由必须用宿主返回的线程、角色、模型与用量证据核对,不从调用请求推断成功。"
176
+ : "只有宿主原生运行证据能识别实际角色与模型时,路由才算已核实。",
177
+ __ODAI_HOST_NOTE__: hostName === "copilot" ? "Copilot Auto 会覆盖角色模型选择;需要区分角色时不使用 Auto。" : "",
178
+ }, source);
179
+ if (hostName !== "codex") return rendered;
180
+ const canonical = readFileSync(path.join(skillRoot, "SKILL.md"), "utf8")
181
+ .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
182
+ const responsibility = role === "controller"
183
+ ? "你是唯一总控,持有完整目标、全局状态、修正回路与最终交付。"
184
+ : `你只承担 ${role} 责任,不是第二个总控。`;
185
+ return `以下 canonical odai 是所有责任共享的内核;宿主角色契约只限制本责任,不得另建流程。${responsibility}\n\n${canonical}\n\n## 宿主角色契约\n\n${rendered}`;
186
+ }
187
+
188
+ function codexAgentSections() {
189
+ return roles.slice(1).flatMap((role) => [
190
+ `[agents.odai_${role}]`,
191
+ `description = ${JSON.stringify(descriptions[role])}`,
192
+ `config_file = ${JSON.stringify(`agents/odai-${role}.toml`)}`,
193
+ "",
194
+ ]).join("\n").trimEnd();
195
+ }
196
+
197
+ function activation(value) {
198
+ if (value === "claude") return { main: "新会话由托管设置选择 odai-controller。", reload: "重启 Claude Code 或重新加载配置。" };
199
+ if (value === "copilot") return {
200
+ main: `使用 copilot --model=${models.controller} --agent=odai-controller 启动。`,
201
+ reload: "安装或更新后重启 Copilot CLI。",
202
+ limitation: "Copilot Auto 会覆盖角色模型选择。",
203
+ };
204
+ return {
205
+ main: policy === "stage"
206
+ ? "通过生成的 odai-run-routing.mjs 从任务起点运行一次显式 stage 实验;日常会话不由隐藏 Hook 接管。"
207
+ : "普通会话由托管配置选择单一总控,其余责任按真实缺口调用。",
208
+ reload: "修改后开启新的 Codex 会话。",
209
+ };
210
+ }
211
+
212
+ function copyScript(name, targetFile) {
213
+ const source = path.join(skillRoot, "scripts", name);
214
+ if (!existsSync(source)) throw new Error(`Missing canonical routing script: ${source}`);
215
+ writeFileSync(targetFile, readFileSync(source), "utf8");
216
+ }
217
+
218
+ function option(name) {
219
+ const index = argv.indexOf(name);
220
+ if (index < 0) return "";
221
+ const value = argv[index + 1] || "";
222
+ if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
223
+ return value;
224
+ }
225
+
226
+ function tomlEffortLine(value) { return value ? `model_reasoning_effort = ${JSON.stringify(value)}` : ""; }
227
+ function yamlEffortLine(value) { return value ? `effort: ${value}\n` : ""; }
228
+ function writeRendered(source, targetFile, replacements) {
229
+ if (!existsSync(source)) throw new Error(`Missing canonical routing source: ${source}`);
230
+ mkdirSync(path.dirname(targetFile), { recursive: true });
231
+ writeFileSync(targetFile, renderText(readFileSync(source, "utf8"), replacements, source), "utf8");
232
+ }
233
+ function renderText(input, replacements, source = "generated routing text") {
234
+ let text = input;
235
+ for (const [key, value] of Object.entries(replacements)) text = text.replaceAll(key, value);
236
+ const unresolved = text.match(/__ODAI_[A-Z0-9_]+__/g);
237
+ if (unresolved) throw new Error(`Unresolved routing template fields in ${source}: ${[...new Set(unresolved)].join(", ")}`);
238
+ return text;
239
+ }