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,285 +1,285 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import process from "node:process";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
process.on("uncaughtException", (error) => {
|
|
11
|
-
console.error(error?.message || String(error));
|
|
12
|
-
process.exitCode = 1;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const args = parseArgs(process.argv.slice(2));
|
|
17
|
-
const supportedRoles = new Set(["controller", "researcher", "planner", "reviewer", "frontend"]);
|
|
18
|
-
|
|
19
|
-
if (args.help) {
|
|
20
|
-
console.log(`Usage:
|
|
21
|
-
node .codex/odai-run-role.mjs --role <controller|researcher|planner|reviewer|frontend> \\
|
|
22
|
-
[--input FILE] [--output FILE] [--evidence FILE] [--cwd PATH] [--manifest FILE] \\
|
|
23
|
-
[--codex-bin FILE] [--sandbox MODE] [--session-id ID] [--closeout-ids A1,A2]
|
|
24
|
-
|
|
25
|
-
从 odai 托管清单取得当前 Codex 宿主的角色、模型与推理档,运行一次实际责任并记录可核对证据。`);
|
|
26
|
-
process.exit(0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!supportedRoles.has(args.role)) fail(`不支持的角色:${args.role || "(missing)"}`);
|
|
30
|
-
const manifestFile = path.resolve(args.manifest || path.join(scriptDir, "odai-routing.json"));
|
|
31
|
-
const manifest = readJson(manifestFile, "路由清单");
|
|
32
|
-
if (manifest.id !== "odai-routing-installation" || manifest.host !== "codex") fail("路由清单不是 Codex odai 托管安装");
|
|
33
|
-
const mapping = manifest.mapping?.[args.role];
|
|
34
|
-
if (!mapping?.model) fail(`路由清单缺少 ${args.role} 映射`);
|
|
35
|
-
|
|
36
|
-
const workdir = path.resolve(args.cwd || process.cwd());
|
|
37
|
-
const input = args.input ? readFileSync(path.resolve(args.input), "utf8").trim() : readFileSync(0, "utf8").trim();
|
|
38
|
-
if (!input) fail("角色输入为空");
|
|
39
|
-
const contractFile = path.join(scriptDir, "role-contracts", `odai-${args.role}.md`);
|
|
40
|
-
if (!existsSync(contractFile)) fail(`缺少角色契约:${contractFile}`);
|
|
41
|
-
const contract = readFileSync(contractFile, "utf8");
|
|
42
|
-
const temporary = mkdtempSync(path.join(tmpdir(), "odai-role-"));
|
|
43
|
-
const outputFile = path.resolve(args.output || path.join(temporary, "result.txt"));
|
|
44
|
-
const evidenceFile = args.evidence ? path.resolve(args.evidence) : "";
|
|
45
|
-
const startedAt = new Date().toISOString();
|
|
46
|
-
const started = Date.now();
|
|
47
|
-
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const readOnly = args.role === "researcher" || args.role === "planner" || args.role === "reviewer";
|
|
51
|
-
const closeoutIds = parseCloseoutIds(args.closeoutIds);
|
|
52
|
-
const schemaFile = closeoutIds.length > 0 ? writeCloseoutSchema(temporary, closeoutIds) : "";
|
|
53
|
-
const hostOwnsRouting = process.env.ODAI_ROUTING_ACTIVE === "1";
|
|
54
|
-
const shared = [
|
|
55
|
-
"--json", "--model", mapping.model,
|
|
56
|
-
...effortArgs(mapping.reasoning_effort),
|
|
57
|
-
...(hostOwnsRouting ? ["-c", "features.multi_agent=false"] : []),
|
|
58
|
-
...(args.sessionId ? ["-c", `sandbox_mode=${JSON.stringify(readOnly ? "read-only" : args.sandbox)}`] : []),
|
|
59
|
-
"-c", `developer_instructions=${JSON.stringify(contract)}`,
|
|
60
|
-
...(schemaFile ? ["--output-schema", schemaFile] : []),
|
|
61
|
-
"-o", outputFile,
|
|
62
|
-
];
|
|
63
|
-
const command = args.sessionId
|
|
64
|
-
? ["exec", "resume", ...shared, args.sessionId, "-"]
|
|
65
|
-
: ["exec", ...shared, "--sandbox", readOnly ? "read-only" : args.sandbox, "-C", workdir, "-"];
|
|
66
|
-
const child = spawnSync(args.codexBin || process.env.ODAI_CODEX_COMMAND || "codex", command, {
|
|
67
|
-
cwd: workdir,
|
|
68
|
-
input,
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
windowsHide: true,
|
|
71
|
-
timeout: args.timeout * 1000,
|
|
72
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
73
|
-
});
|
|
74
|
-
if (child.error) fail(child.error.message);
|
|
75
|
-
const raw = `${child.stdout || ""}${child.stderr || ""}`;
|
|
76
|
-
if (child.status !== 0) fail(`Codex ${args.role} 调用失败:${tail(raw)}`);
|
|
77
|
-
if (!existsSync(outputFile) || !readFileSync(outputFile, "utf8").trim()) fail("角色没有形成输出");
|
|
78
|
-
if (closeoutIds.length > 0) writeFileSync(outputFile, renderCloseout(readFileSync(outputFile, "utf8"), closeoutIds), "utf8");
|
|
79
|
-
const events = parseJsonLines(raw);
|
|
80
|
-
const threadId = String(events.find((event) => event.type === "thread.started")?.thread_id || args.sessionId || "");
|
|
81
|
-
if (args.sessionId && threadId !== args.sessionId) fail(`Codex 恢复了错误线程:期望 ${args.sessionId},实际 ${threadId || "unknown"}`);
|
|
82
|
-
const eventModels = collectStrings(events, "model");
|
|
83
|
-
const runtime = eventModels.length > 0 ? null : queryRuntime(threadId, Date.parse(startedAt));
|
|
84
|
-
const models = runtime?.model ? [runtime.model] : eventModels;
|
|
85
|
-
const evidence = {
|
|
86
|
-
version: 1,
|
|
87
|
-
role: args.role,
|
|
88
|
-
requested: { provider: "codex", model: mapping.model, reasoning_effort: mapping.reasoning_effort || null },
|
|
89
|
-
observed: {
|
|
90
|
-
provider: "codex",
|
|
91
|
-
thread_id: threadId || null,
|
|
92
|
-
models,
|
|
93
|
-
model_verified: models.length > 0,
|
|
94
|
-
reasoning_efforts: runtime?.reasoning_effort ? [runtime.reasoning_effort] : [],
|
|
95
|
-
tool_use_detected: containsToolUse(events),
|
|
96
|
-
tool_evidence: collectToolEvidence(events),
|
|
97
|
-
usage: lastDefined(events.map((event) => event.usage || event.payload?.usage)) || null,
|
|
98
|
-
cost_usd: null,
|
|
99
|
-
duration_ms: Date.now() - started,
|
|
100
|
-
started_at: startedAt,
|
|
101
|
-
completed_at: new Date().toISOString(),
|
|
102
|
-
exit_code: child.status,
|
|
103
|
-
},
|
|
104
|
-
output: outputFile,
|
|
105
|
-
};
|
|
106
|
-
writeEvidence(evidence);
|
|
107
|
-
const response = readFileSync(outputFile, "utf8");
|
|
108
|
-
process.stdout.write(response);
|
|
109
|
-
if (!response.endsWith("\n")) process.stdout.write("\n");
|
|
110
|
-
} catch (error) {
|
|
111
|
-
writeEvidence({
|
|
112
|
-
version: 1,
|
|
113
|
-
role: args.role,
|
|
114
|
-
requested: { provider: "codex", model: mapping.model, reasoning_effort: mapping.reasoning_effort || null },
|
|
115
|
-
observed: {
|
|
116
|
-
provider: "codex", thread_id: null, models: [], model_verified: false,
|
|
117
|
-
usage: null, cost_usd: null, duration_ms: Date.now() - started,
|
|
118
|
-
started_at: startedAt, completed_at: new Date().toISOString(), exit_code: null,
|
|
119
|
-
error: error?.message || String(error),
|
|
120
|
-
},
|
|
121
|
-
output: null,
|
|
122
|
-
});
|
|
123
|
-
throw error;
|
|
124
|
-
} finally {
|
|
125
|
-
rmSync(temporary, { recursive: true, force: true });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function writeEvidence(value) {
|
|
129
|
-
if (!evidenceFile) return;
|
|
130
|
-
mkdirSync(path.dirname(evidenceFile), { recursive: true });
|
|
131
|
-
writeFileSync(evidenceFile, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function queryRuntime(threadId, startedAt) {
|
|
135
|
-
if (!threadId) return null;
|
|
136
|
-
const root = path.join(process.env.CODEX_HOME || path.join(homedir(), ".codex"), "sessions");
|
|
137
|
-
if (!existsSync(root)) return null;
|
|
138
|
-
const threshold = Number.isFinite(startedAt) ? startedAt - 15000 : Date.now() - 60000;
|
|
139
|
-
const pending = [root];
|
|
140
|
-
while (pending.length > 0) {
|
|
141
|
-
const directory = pending.pop();
|
|
142
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
143
|
-
const full = path.join(directory, entry.name);
|
|
144
|
-
if (entry.isDirectory()) { pending.push(full); continue; }
|
|
145
|
-
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || statSync(full).mtimeMs < threshold) continue;
|
|
146
|
-
const events = parseJsonLines(readFileSync(full, "utf8"));
|
|
147
|
-
const session = events.find((event) => event.type === "session_meta")?.payload;
|
|
148
|
-
if (session?.id !== threadId || path.resolve(session.cwd || "") !== workdir) continue;
|
|
149
|
-
const context = [...events].reverse().find((event) => event.type === "turn_context")?.payload || {};
|
|
150
|
-
return { model: String(context.model || ""), reasoning_effort: String(context.effort || context.collaboration_mode?.settings?.reasoning_effort || "") };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function parseJsonLines(value) {
|
|
157
|
-
const result = [];
|
|
158
|
-
for (const line of String(value || "").split(/\r?\n/)) {
|
|
159
|
-
if (!line.trim()) continue;
|
|
160
|
-
try { result.push(JSON.parse(line)); } catch { /* warnings are not evidence */ }
|
|
161
|
-
}
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function collectStrings(value, key) {
|
|
166
|
-
const found = new Set();
|
|
167
|
-
const walk = (item) => {
|
|
168
|
-
if (!item || typeof item !== "object") return;
|
|
169
|
-
if (typeof item[key] === "string" && item[key]) found.add(item[key]);
|
|
170
|
-
for (const child of Object.values(item)) {
|
|
171
|
-
if (Array.isArray(child)) child.forEach(walk);
|
|
172
|
-
else if (child && typeof child === "object") walk(child);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
value.forEach(walk);
|
|
176
|
-
return [...found];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function containsToolUse(events) {
|
|
180
|
-
const types = new Set(["command_execution", "collab_tool_call", "mcp_tool_call", "file_change", "web_search"]);
|
|
181
|
-
return events.some((event) => (event.type === "item.started" || event.type === "item.completed") && types.has(event.item?.type));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function collectToolEvidence(events) {
|
|
185
|
-
const supported = new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
|
|
186
|
-
const result = [];
|
|
187
|
-
let chars = 0;
|
|
188
|
-
for (const event of events) {
|
|
189
|
-
if (event.type !== "item.completed" || !supported.has(event.item?.type)) continue;
|
|
190
|
-
const item = event.item;
|
|
191
|
-
const evidence = {
|
|
192
|
-
type: item.type,
|
|
193
|
-
command: clip(item.command || item.name || "", 4000),
|
|
194
|
-
status: item.status || "",
|
|
195
|
-
exit_code: Number.isInteger(item.exit_code) ? item.exit_code : null,
|
|
196
|
-
output: clip(item.aggregated_output || item.output || item.text || "", 16000),
|
|
197
|
-
};
|
|
198
|
-
chars += JSON.stringify(evidence).length;
|
|
199
|
-
if (chars > 80000) break;
|
|
200
|
-
result.push(evidence);
|
|
201
|
-
}
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function effortArgs(value) { return value ? ["-c", `model_reasoning_effort=${JSON.stringify(value)}`] : []; }
|
|
206
|
-
function clip(value, limit) { const text = String(value || ""); return text.length <= limit ? text : `${text.slice(0, limit)}\n[truncated]`; }
|
|
207
|
-
function lastDefined(values) { return [...values].reverse().find((value) => value != null); }
|
|
208
|
-
function readJson(file, label) { if (!existsSync(file)) fail(`缺少${label}:${file}`); return JSON.parse(readFileSync(file, "utf8")); }
|
|
209
|
-
|
|
210
|
-
function parseArgs(values) {
|
|
211
|
-
const result = { role: "", input: "", output: "", evidence: "", cwd: "", manifest: "", codexBin: "", sandbox: "workspace-write", sessionId: "", closeoutIds: "", timeout: 900, help: false };
|
|
212
|
-
const fields = new Map([
|
|
213
|
-
["--role", "role"], ["--input", "input"], ["--output", "output"], ["--evidence", "evidence"], ["--cwd", "cwd"],
|
|
214
|
-
["--manifest", "manifest"], ["--codex-bin", "codexBin"], ["--sandbox", "sandbox"], ["--session-id", "sessionId"], ["--closeout-ids", "closeoutIds"], ["--timeout", "timeout"],
|
|
215
|
-
]);
|
|
216
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
217
|
-
const value = values[index];
|
|
218
|
-
if (value === "--help" || value === "-h") result.help = true;
|
|
219
|
-
else if (fields.has(value)) {
|
|
220
|
-
const next = values[++index] || "";
|
|
221
|
-
if (!next || next.startsWith("--")) fail(`${value} 需要一个值`);
|
|
222
|
-
result[fields.get(value)] = fields.get(value) === "timeout" ? Number(next) : next;
|
|
223
|
-
} else fail(`未知参数:${value}`);
|
|
224
|
-
}
|
|
225
|
-
if (!Number.isFinite(result.timeout) || result.timeout <= 0) fail("--timeout 必须是正数秒数");
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function writeCloseoutSchema(directory, ids) {
|
|
230
|
-
const file = path.join(directory, "closeout.schema.json");
|
|
231
|
-
const report = {
|
|
232
|
-
type: "object",
|
|
233
|
-
additionalProperties: false,
|
|
234
|
-
properties: {
|
|
235
|
-
status: { type: "string", enum: ["verified", "unresolved", "failed"] },
|
|
236
|
-
evidence: { type: "string" },
|
|
237
|
-
next: { type: "string" },
|
|
238
|
-
},
|
|
239
|
-
required: ["status", "evidence", "next"],
|
|
240
|
-
};
|
|
241
|
-
const schema = {
|
|
242
|
-
type: "object",
|
|
243
|
-
additionalProperties: false,
|
|
244
|
-
properties: {
|
|
245
|
-
delivery: { type: "string" },
|
|
246
|
-
acceptance: {
|
|
247
|
-
type: "object",
|
|
248
|
-
additionalProperties: false,
|
|
249
|
-
properties: Object.fromEntries(ids.map((id) => [id, report])),
|
|
250
|
-
required: ids,
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
required: ["delivery", "acceptance"],
|
|
254
|
-
};
|
|
255
|
-
writeFileSync(file, `${JSON.stringify(schema, null, 2)}\n`, "utf8");
|
|
256
|
-
return file;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function parseCloseoutIds(value) {
|
|
260
|
-
if (!String(value || "").trim()) return [];
|
|
261
|
-
const ids = String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
262
|
-
if (ids.length === 0 || new Set(ids).size !== ids.length || ids.some((id) => !/^A[1-9][0-9]{0,2}$/.test(id))) {
|
|
263
|
-
fail("--closeout-ids 必须是互不重复的 A1,A2 形式");
|
|
264
|
-
}
|
|
265
|
-
return ids;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function renderCloseout(source, ids) {
|
|
269
|
-
let value;
|
|
270
|
-
try { value = JSON.parse(String(source || "").trim()); }
|
|
271
|
-
catch { fail("结构化执行回交不是有效 JSON"); }
|
|
272
|
-
const delivery = String(value?.delivery || "").trim();
|
|
273
|
-
if (!delivery) fail("结构化执行回交缺少面向用户的 delivery");
|
|
274
|
-
for (const id of ids) {
|
|
275
|
-
const report = value?.acceptance?.[id];
|
|
276
|
-
if (!report || !String(report.evidence || "").trim()) fail(`结构化执行回交缺少 ${id} 的实际证据`);
|
|
277
|
-
const next = String(report.next || "").trim();
|
|
278
|
-
if (report.status === "verified" && next) fail(`结构化执行回交中 ${id} 已 verified 却仍有继续条件`);
|
|
279
|
-
if (report.status !== "verified" && !next) fail(`结构化执行回交中 ${id} 未闭合却缺少继续条件`);
|
|
280
|
-
}
|
|
281
|
-
return `${delivery}\n\n<odai_closeout>${JSON.stringify(value.acceptance)}</odai_closeout>\n`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function tail(value) { return String(value || "").trim().slice(-2000); }
|
|
285
|
-
function fail(message) { throw new Error(message); }
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
process.on("uncaughtException", (error) => {
|
|
11
|
+
console.error(error?.message || String(error));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const args = parseArgs(process.argv.slice(2));
|
|
17
|
+
const supportedRoles = new Set(["controller", "researcher", "planner", "reviewer", "frontend"]);
|
|
18
|
+
|
|
19
|
+
if (args.help) {
|
|
20
|
+
console.log(`Usage:
|
|
21
|
+
node .codex/odai-run-role.mjs --role <controller|researcher|planner|reviewer|frontend> \\
|
|
22
|
+
[--input FILE] [--output FILE] [--evidence FILE] [--cwd PATH] [--manifest FILE] \\
|
|
23
|
+
[--codex-bin FILE] [--sandbox MODE] [--session-id ID] [--closeout-ids A1,A2]
|
|
24
|
+
|
|
25
|
+
从 odai 托管清单取得当前 Codex 宿主的角色、模型与推理档,运行一次实际责任并记录可核对证据。`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!supportedRoles.has(args.role)) fail(`不支持的角色:${args.role || "(missing)"}`);
|
|
30
|
+
const manifestFile = path.resolve(args.manifest || path.join(scriptDir, "odai-routing.json"));
|
|
31
|
+
const manifest = readJson(manifestFile, "路由清单");
|
|
32
|
+
if (manifest.id !== "odai-routing-installation" || manifest.host !== "codex") fail("路由清单不是 Codex odai 托管安装");
|
|
33
|
+
const mapping = manifest.mapping?.[args.role];
|
|
34
|
+
if (!mapping?.model) fail(`路由清单缺少 ${args.role} 映射`);
|
|
35
|
+
|
|
36
|
+
const workdir = path.resolve(args.cwd || process.cwd());
|
|
37
|
+
const input = args.input ? readFileSync(path.resolve(args.input), "utf8").trim() : readFileSync(0, "utf8").trim();
|
|
38
|
+
if (!input) fail("角色输入为空");
|
|
39
|
+
const contractFile = path.join(scriptDir, "role-contracts", `odai-${args.role}.md`);
|
|
40
|
+
if (!existsSync(contractFile)) fail(`缺少角色契约:${contractFile}`);
|
|
41
|
+
const contract = readFileSync(contractFile, "utf8");
|
|
42
|
+
const temporary = mkdtempSync(path.join(tmpdir(), "odai-role-"));
|
|
43
|
+
const outputFile = path.resolve(args.output || path.join(temporary, "result.txt"));
|
|
44
|
+
const evidenceFile = args.evidence ? path.resolve(args.evidence) : "";
|
|
45
|
+
const startedAt = new Date().toISOString();
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const readOnly = args.role === "researcher" || args.role === "planner" || args.role === "reviewer";
|
|
51
|
+
const closeoutIds = parseCloseoutIds(args.closeoutIds);
|
|
52
|
+
const schemaFile = closeoutIds.length > 0 ? writeCloseoutSchema(temporary, closeoutIds) : "";
|
|
53
|
+
const hostOwnsRouting = process.env.ODAI_ROUTING_ACTIVE === "1";
|
|
54
|
+
const shared = [
|
|
55
|
+
"--json", "--model", mapping.model,
|
|
56
|
+
...effortArgs(mapping.reasoning_effort),
|
|
57
|
+
...(hostOwnsRouting ? ["-c", "features.multi_agent=false"] : []),
|
|
58
|
+
...(args.sessionId ? ["-c", `sandbox_mode=${JSON.stringify(readOnly ? "read-only" : args.sandbox)}`] : []),
|
|
59
|
+
"-c", `developer_instructions=${JSON.stringify(contract)}`,
|
|
60
|
+
...(schemaFile ? ["--output-schema", schemaFile] : []),
|
|
61
|
+
"-o", outputFile,
|
|
62
|
+
];
|
|
63
|
+
const command = args.sessionId
|
|
64
|
+
? ["exec", "resume", ...shared, args.sessionId, "-"]
|
|
65
|
+
: ["exec", ...shared, "--sandbox", readOnly ? "read-only" : args.sandbox, "-C", workdir, "-"];
|
|
66
|
+
const child = spawnSync(args.codexBin || process.env.ODAI_CODEX_COMMAND || "codex", command, {
|
|
67
|
+
cwd: workdir,
|
|
68
|
+
input,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
timeout: args.timeout * 1000,
|
|
72
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
73
|
+
});
|
|
74
|
+
if (child.error) fail(child.error.message);
|
|
75
|
+
const raw = `${child.stdout || ""}${child.stderr || ""}`;
|
|
76
|
+
if (child.status !== 0) fail(`Codex ${args.role} 调用失败:${tail(raw)}`);
|
|
77
|
+
if (!existsSync(outputFile) || !readFileSync(outputFile, "utf8").trim()) fail("角色没有形成输出");
|
|
78
|
+
if (closeoutIds.length > 0) writeFileSync(outputFile, renderCloseout(readFileSync(outputFile, "utf8"), closeoutIds), "utf8");
|
|
79
|
+
const events = parseJsonLines(raw);
|
|
80
|
+
const threadId = String(events.find((event) => event.type === "thread.started")?.thread_id || args.sessionId || "");
|
|
81
|
+
if (args.sessionId && threadId !== args.sessionId) fail(`Codex 恢复了错误线程:期望 ${args.sessionId},实际 ${threadId || "unknown"}`);
|
|
82
|
+
const eventModels = collectStrings(events, "model");
|
|
83
|
+
const runtime = eventModels.length > 0 ? null : queryRuntime(threadId, Date.parse(startedAt));
|
|
84
|
+
const models = runtime?.model ? [runtime.model] : eventModels;
|
|
85
|
+
const evidence = {
|
|
86
|
+
version: 1,
|
|
87
|
+
role: args.role,
|
|
88
|
+
requested: { provider: "codex", model: mapping.model, reasoning_effort: mapping.reasoning_effort || null },
|
|
89
|
+
observed: {
|
|
90
|
+
provider: "codex",
|
|
91
|
+
thread_id: threadId || null,
|
|
92
|
+
models,
|
|
93
|
+
model_verified: models.length > 0,
|
|
94
|
+
reasoning_efforts: runtime?.reasoning_effort ? [runtime.reasoning_effort] : [],
|
|
95
|
+
tool_use_detected: containsToolUse(events),
|
|
96
|
+
tool_evidence: collectToolEvidence(events),
|
|
97
|
+
usage: lastDefined(events.map((event) => event.usage || event.payload?.usage)) || null,
|
|
98
|
+
cost_usd: null,
|
|
99
|
+
duration_ms: Date.now() - started,
|
|
100
|
+
started_at: startedAt,
|
|
101
|
+
completed_at: new Date().toISOString(),
|
|
102
|
+
exit_code: child.status,
|
|
103
|
+
},
|
|
104
|
+
output: outputFile,
|
|
105
|
+
};
|
|
106
|
+
writeEvidence(evidence);
|
|
107
|
+
const response = readFileSync(outputFile, "utf8");
|
|
108
|
+
process.stdout.write(response);
|
|
109
|
+
if (!response.endsWith("\n")) process.stdout.write("\n");
|
|
110
|
+
} catch (error) {
|
|
111
|
+
writeEvidence({
|
|
112
|
+
version: 1,
|
|
113
|
+
role: args.role,
|
|
114
|
+
requested: { provider: "codex", model: mapping.model, reasoning_effort: mapping.reasoning_effort || null },
|
|
115
|
+
observed: {
|
|
116
|
+
provider: "codex", thread_id: null, models: [], model_verified: false,
|
|
117
|
+
usage: null, cost_usd: null, duration_ms: Date.now() - started,
|
|
118
|
+
started_at: startedAt, completed_at: new Date().toISOString(), exit_code: null,
|
|
119
|
+
error: error?.message || String(error),
|
|
120
|
+
},
|
|
121
|
+
output: null,
|
|
122
|
+
});
|
|
123
|
+
throw error;
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeEvidence(value) {
|
|
129
|
+
if (!evidenceFile) return;
|
|
130
|
+
mkdirSync(path.dirname(evidenceFile), { recursive: true });
|
|
131
|
+
writeFileSync(evidenceFile, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function queryRuntime(threadId, startedAt) {
|
|
135
|
+
if (!threadId) return null;
|
|
136
|
+
const root = path.join(process.env.CODEX_HOME || path.join(homedir(), ".codex"), "sessions");
|
|
137
|
+
if (!existsSync(root)) return null;
|
|
138
|
+
const threshold = Number.isFinite(startedAt) ? startedAt - 15000 : Date.now() - 60000;
|
|
139
|
+
const pending = [root];
|
|
140
|
+
while (pending.length > 0) {
|
|
141
|
+
const directory = pending.pop();
|
|
142
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
143
|
+
const full = path.join(directory, entry.name);
|
|
144
|
+
if (entry.isDirectory()) { pending.push(full); continue; }
|
|
145
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || statSync(full).mtimeMs < threshold) continue;
|
|
146
|
+
const events = parseJsonLines(readFileSync(full, "utf8"));
|
|
147
|
+
const session = events.find((event) => event.type === "session_meta")?.payload;
|
|
148
|
+
if (session?.id !== threadId || path.resolve(session.cwd || "") !== workdir) continue;
|
|
149
|
+
const context = [...events].reverse().find((event) => event.type === "turn_context")?.payload || {};
|
|
150
|
+
return { model: String(context.model || ""), reasoning_effort: String(context.effort || context.collaboration_mode?.settings?.reasoning_effort || "") };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseJsonLines(value) {
|
|
157
|
+
const result = [];
|
|
158
|
+
for (const line of String(value || "").split(/\r?\n/)) {
|
|
159
|
+
if (!line.trim()) continue;
|
|
160
|
+
try { result.push(JSON.parse(line)); } catch { /* warnings are not evidence */ }
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectStrings(value, key) {
|
|
166
|
+
const found = new Set();
|
|
167
|
+
const walk = (item) => {
|
|
168
|
+
if (!item || typeof item !== "object") return;
|
|
169
|
+
if (typeof item[key] === "string" && item[key]) found.add(item[key]);
|
|
170
|
+
for (const child of Object.values(item)) {
|
|
171
|
+
if (Array.isArray(child)) child.forEach(walk);
|
|
172
|
+
else if (child && typeof child === "object") walk(child);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
value.forEach(walk);
|
|
176
|
+
return [...found];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function containsToolUse(events) {
|
|
180
|
+
const types = new Set(["command_execution", "collab_tool_call", "mcp_tool_call", "file_change", "web_search"]);
|
|
181
|
+
return events.some((event) => (event.type === "item.started" || event.type === "item.completed") && types.has(event.item?.type));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function collectToolEvidence(events) {
|
|
185
|
+
const supported = new Set(["command_execution", "file_change", "mcp_tool_call", "web_search"]);
|
|
186
|
+
const result = [];
|
|
187
|
+
let chars = 0;
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
if (event.type !== "item.completed" || !supported.has(event.item?.type)) continue;
|
|
190
|
+
const item = event.item;
|
|
191
|
+
const evidence = {
|
|
192
|
+
type: item.type,
|
|
193
|
+
command: clip(item.command || item.name || "", 4000),
|
|
194
|
+
status: item.status || "",
|
|
195
|
+
exit_code: Number.isInteger(item.exit_code) ? item.exit_code : null,
|
|
196
|
+
output: clip(item.aggregated_output || item.output || item.text || "", 16000),
|
|
197
|
+
};
|
|
198
|
+
chars += JSON.stringify(evidence).length;
|
|
199
|
+
if (chars > 80000) break;
|
|
200
|
+
result.push(evidence);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function effortArgs(value) { return value ? ["-c", `model_reasoning_effort=${JSON.stringify(value)}`] : []; }
|
|
206
|
+
function clip(value, limit) { const text = String(value || ""); return text.length <= limit ? text : `${text.slice(0, limit)}\n[truncated]`; }
|
|
207
|
+
function lastDefined(values) { return [...values].reverse().find((value) => value != null); }
|
|
208
|
+
function readJson(file, label) { if (!existsSync(file)) fail(`缺少${label}:${file}`); return JSON.parse(readFileSync(file, "utf8")); }
|
|
209
|
+
|
|
210
|
+
function parseArgs(values) {
|
|
211
|
+
const result = { role: "", input: "", output: "", evidence: "", cwd: "", manifest: "", codexBin: "", sandbox: "workspace-write", sessionId: "", closeoutIds: "", timeout: 900, help: false };
|
|
212
|
+
const fields = new Map([
|
|
213
|
+
["--role", "role"], ["--input", "input"], ["--output", "output"], ["--evidence", "evidence"], ["--cwd", "cwd"],
|
|
214
|
+
["--manifest", "manifest"], ["--codex-bin", "codexBin"], ["--sandbox", "sandbox"], ["--session-id", "sessionId"], ["--closeout-ids", "closeoutIds"], ["--timeout", "timeout"],
|
|
215
|
+
]);
|
|
216
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
217
|
+
const value = values[index];
|
|
218
|
+
if (value === "--help" || value === "-h") result.help = true;
|
|
219
|
+
else if (fields.has(value)) {
|
|
220
|
+
const next = values[++index] || "";
|
|
221
|
+
if (!next || next.startsWith("--")) fail(`${value} 需要一个值`);
|
|
222
|
+
result[fields.get(value)] = fields.get(value) === "timeout" ? Number(next) : next;
|
|
223
|
+
} else fail(`未知参数:${value}`);
|
|
224
|
+
}
|
|
225
|
+
if (!Number.isFinite(result.timeout) || result.timeout <= 0) fail("--timeout 必须是正数秒数");
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function writeCloseoutSchema(directory, ids) {
|
|
230
|
+
const file = path.join(directory, "closeout.schema.json");
|
|
231
|
+
const report = {
|
|
232
|
+
type: "object",
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
properties: {
|
|
235
|
+
status: { type: "string", enum: ["verified", "unresolved", "failed"] },
|
|
236
|
+
evidence: { type: "string" },
|
|
237
|
+
next: { type: "string" },
|
|
238
|
+
},
|
|
239
|
+
required: ["status", "evidence", "next"],
|
|
240
|
+
};
|
|
241
|
+
const schema = {
|
|
242
|
+
type: "object",
|
|
243
|
+
additionalProperties: false,
|
|
244
|
+
properties: {
|
|
245
|
+
delivery: { type: "string" },
|
|
246
|
+
acceptance: {
|
|
247
|
+
type: "object",
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
properties: Object.fromEntries(ids.map((id) => [id, report])),
|
|
250
|
+
required: ids,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
required: ["delivery", "acceptance"],
|
|
254
|
+
};
|
|
255
|
+
writeFileSync(file, `${JSON.stringify(schema, null, 2)}\n`, "utf8");
|
|
256
|
+
return file;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseCloseoutIds(value) {
|
|
260
|
+
if (!String(value || "").trim()) return [];
|
|
261
|
+
const ids = String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
262
|
+
if (ids.length === 0 || new Set(ids).size !== ids.length || ids.some((id) => !/^A[1-9][0-9]{0,2}$/.test(id))) {
|
|
263
|
+
fail("--closeout-ids 必须是互不重复的 A1,A2 形式");
|
|
264
|
+
}
|
|
265
|
+
return ids;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderCloseout(source, ids) {
|
|
269
|
+
let value;
|
|
270
|
+
try { value = JSON.parse(String(source || "").trim()); }
|
|
271
|
+
catch { fail("结构化执行回交不是有效 JSON"); }
|
|
272
|
+
const delivery = String(value?.delivery || "").trim();
|
|
273
|
+
if (!delivery) fail("结构化执行回交缺少面向用户的 delivery");
|
|
274
|
+
for (const id of ids) {
|
|
275
|
+
const report = value?.acceptance?.[id];
|
|
276
|
+
if (!report || !String(report.evidence || "").trim()) fail(`结构化执行回交缺少 ${id} 的实际证据`);
|
|
277
|
+
const next = String(report.next || "").trim();
|
|
278
|
+
if (report.status === "verified" && next) fail(`结构化执行回交中 ${id} 已 verified 却仍有继续条件`);
|
|
279
|
+
if (report.status !== "verified" && !next) fail(`结构化执行回交中 ${id} 未闭合却缺少继续条件`);
|
|
280
|
+
}
|
|
281
|
+
return `${delivery}\n\n<odai_closeout>${JSON.stringify(value.acceptance)}</odai_closeout>\n`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function tail(value) { return String(value || "").trim().slice(-2000); }
|
|
285
|
+
function fail(message) { throw new Error(message); }
|