odai-dsh-agent 0.2.56 → 0.2.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/client/client.js +12 -5
- package/package.json +1 -1
- package/preset/odai/package.json +2 -1
- package/preset/odai/runtime/context-activation.mjs +1 -1
- package/preset/odai/runtime/context-activation.mjs.map +1 -1
- package/preset/odai/runtime/index.d.mts.map +1 -1
- package/preset/odai/runtime/index.mjs +51 -2
- package/preset/odai/runtime/index.mjs.map +1 -1
- package/preset/odai/runtime/lead-mode.d.mts +23 -0
- package/preset/odai/runtime/lead-mode.d.mts.map +1 -0
- package/preset/odai/runtime/lead-mode.mjs +17 -0
- package/preset/odai/runtime/lead-mode.mjs.map +1 -0
- package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/lifecycle-runtime.mjs +196 -13
- package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts +4 -1
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-gap.mjs +1 -0
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
- package/preset/odai/runtime/role-overlays.d.mts +1 -1
- package/preset/odai/runtime/role-overlays.d.mts.map +1 -1
- package/preset/odai/runtime/role-overlays.mjs +4 -1
- package/preset/odai/runtime/role-overlays.mjs.map +1 -1
- package/preset/odai/runtime/route-session.d.mts +20 -0
- package/preset/odai/runtime/route-session.d.mts.map +1 -0
- package/preset/odai/runtime/route-session.mjs +44 -0
- package/preset/odai/runtime/route-session.mjs.map +1 -0
- package/preset/odai/runtime/router.d.mts +2 -1
- package/preset/odai/runtime/router.d.mts.map +1 -1
- package/preset/odai/runtime/router.mjs +52 -1
- package/preset/odai/runtime/router.mjs.map +1 -1
- package/preset/odai/runtime/routing-config.d.mts +5 -2
- package/preset/odai/runtime/routing-config.d.mts.map +1 -1
- package/preset/odai/runtime/routing-config.mjs +25 -14
- package/preset/odai/runtime/routing-config.mjs.map +1 -1
- package/preset/odai/runtime/runtime-config.d.mts +1 -1
- package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.d.mts +6 -1
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.mjs +13 -7
- package/preset/odai/runtime/runtime-support.mjs.map +1 -1
- package/preset/odai/runtime/runtime-types.d.mts +2 -0
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
- 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 +6 -0
- package/preset/odai/runtime/session-evidence.mjs.map +1 -1
- package/preset/odai/runtime/skill-bundle.d.mts +1 -1
- package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.mjs +28 -0
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
- package/preset/odai/skills/odai-orchestration/SKILL.md +1 -1
- package/preset/odai/skills/odai-orchestration/assets/routing-roles/controller.md +1 -1
- package/preset/odai/skills/odai-orchestration/assets/routing-roles/writer.md +9 -0
- package/preset/odai/skills/odai-orchestration/manifest.json +10 -4
- package/preset/odai/skills/odai-orchestration/references/install.md +66 -4
- package/preset/odai/skills/odai-orchestration/references/orchestration.md +2 -1
- package/preset/odai/skills/odai-orchestration/scripts/build-routing.mjs +78 -24
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +1 -1
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +6 -5
- package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +66 -15
- package/preset/odai/skills/odai-orchestration/scripts/local-cli.d.mts +8 -0
- package/preset/odai/skills/odai-orchestration/scripts/local-cli.mjs +25 -9
- package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.d.mts +6 -0
- package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.mjs +93 -0
- package/preset/odai/skills/odai-orchestration/scripts/route-hook.mjs +243 -0
- package/preset/odai/skills/odai-orchestration/scripts/run-role.mjs +71 -16
- package/preset/odai/skills/odai-orchestration/scripts/verify-routing.mjs +2 -2
|
@@ -20,13 +20,14 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
20
20
|
const builder = path.join(scriptDir, "build-routing.mjs");
|
|
21
21
|
const manifestName = "odai-routing.json";
|
|
22
22
|
const requiredRoles = ["controller", "planner", "reviewer"];
|
|
23
|
-
const configurableRoles = [...requiredRoles, "researcher", "frontend", "creative"];
|
|
23
|
+
const configurableRoles = [...requiredRoles, "researcher", "frontend", "creative", "writer"];
|
|
24
24
|
const args = parseArgs(process.argv.slice(2));
|
|
25
25
|
const roles = Object.freeze([
|
|
26
26
|
...requiredRoles,
|
|
27
27
|
...(args.researcherModel ? ["researcher"] : []),
|
|
28
28
|
...(args.frontendModel ? ["frontend"] : []),
|
|
29
29
|
...(args.creativeModel ? ["creative"] : []),
|
|
30
|
+
...(args.writerModel ? ["writer"] : []),
|
|
30
31
|
]);
|
|
31
32
|
const retiredByHost = {
|
|
32
33
|
codex: [
|
|
@@ -41,11 +42,12 @@ const retiredByHost = {
|
|
|
41
42
|
|
|
42
43
|
if (args.help) { printHelp(); process.exit(0); }
|
|
43
44
|
if (!new Set(["codex", "claude", "copilot"]).has(args.host)) fail(`不支持的 host:${args.host || "(missing)"}`);
|
|
45
|
+
if ((args.namedDispatch || args.leadMode) && !["claude", "codex"].includes(args.host)) fail("--named-dispatch / --lead-mode requires claude or codex");
|
|
44
46
|
if (!new Set(["project", "user"]).has(args.scope)) fail(`不支持的 scope:${args.scope}`);
|
|
45
47
|
for (const role of configurableRoles) {
|
|
46
48
|
if (!args[`${role}Cli`]) continue;
|
|
47
|
-
if (args.host
|
|
48
|
-
if (!["codex", "antigravity", "grok"].includes(args[`${role}Cli`])) fail(`--${role}-cli must be codex, antigravity, or grok`);
|
|
49
|
+
if (!["claude", "codex"].includes(args.host)) fail(`--${role}-cli requires claude or codex`);
|
|
50
|
+
if (!(args.host === "codex" ? ["claude", "antigravity", "grok"] : ["codex", "antigravity", "grok"]).includes(args[`${role}Cli`])) fail(`--${role}-cli must be ${args.host === "codex" ? "claude" : "codex"}, antigravity, or grok`);
|
|
49
51
|
if (!args[`${role}Model`]) fail(`--${role}-cli 需要同时提供 --${role}-model`);
|
|
50
52
|
}
|
|
51
53
|
if (!args.yes) fail("安装、更新或卸载会持久化宿主配置;取得用户授权后传入 --yes");
|
|
@@ -72,6 +74,7 @@ function main() {
|
|
|
72
74
|
for (const role of requiredRoles) if (!args[`${role}Model`]) fail(`缺少 --${role}-model`);
|
|
73
75
|
if (args.researcherEffort && !args.researcherModel) fail("--researcher-effort 需要同时提供 --researcher-model");
|
|
74
76
|
if (args.frontendEffort && !args.frontendModel) fail("--frontend-effort 需要同时提供 --frontend-model");
|
|
77
|
+
if (args.writerEffort && !args.writerModel) fail("--writer-effort 需要同时提供 --writer-model");
|
|
75
78
|
if (args.creativeEffort && !args.creativeModel) fail("--creative-effort 需要同时提供 --creative-model");
|
|
76
79
|
if (!existsSync(builder)) fail(`缺少路由生成器:${builder}`);
|
|
77
80
|
|
|
@@ -128,10 +131,13 @@ function main() {
|
|
|
128
131
|
|
|
129
132
|
function buildAdapter(outputRoot, root) {
|
|
130
133
|
const command = [builder, "--host", args.host, "--out", outputRoot];
|
|
134
|
+
if (args.namedDispatch) command.push("--named-dispatch");
|
|
135
|
+
if (args.leadMode) command.push("--lead-mode");
|
|
131
136
|
if (args.governanceRoot) command.push("--governance-root", args.governanceRoot);
|
|
132
137
|
for (const role of roles) {
|
|
133
138
|
command.push(`--${role}-model`, args[`${role}Model`]);
|
|
134
139
|
if (args[`${role}Effort`]) command.push(`--${role}-effort`, args[`${role}Effort`]);
|
|
140
|
+
if (args[`${role}Session`]) command.push(`--${role}-session`, args[`${role}Session`]);
|
|
135
141
|
if (args[`${role}Cli`]) command.push(`--${role}-cli`, args[`${role}Cli`]);
|
|
136
142
|
}
|
|
137
143
|
if (args.host === "codex") command.push("--verifier-command", `node ${JSON.stringify(path.join(root, "odai-verify-routing.mjs"))}`);
|
|
@@ -151,6 +157,11 @@ function collectGeneratedFiles(root, layoutValue) {
|
|
|
151
157
|
if (args.host === "claude" && args.scope === "user" && targetRelative === "agents/odai-controller.md") {
|
|
152
158
|
content = Buffer.from(content.toString("utf8").replaceAll("node .claude/odai-run-role.mjs", claudeRunnerCommand()));
|
|
153
159
|
}
|
|
160
|
+
if (args.host === "codex" && targetRelative === "named-dispatch-plugin/hooks/hooks.json") {
|
|
161
|
+
const value = JSON.parse(content);
|
|
162
|
+
for (const entries of Object.values(value.hooks)) for (const entry of entries) for (const hook of entry.hooks) hook.command = `node '${path.join(configRoot, "odai-route-hook.mjs").replaceAll("'", "'\\''")}'`;
|
|
163
|
+
content = Buffer.from(JSON.stringify(value, null, 2) + "\n");
|
|
164
|
+
}
|
|
154
165
|
result.set(targetRelative, content);
|
|
155
166
|
}
|
|
156
167
|
return result;
|
|
@@ -176,19 +187,19 @@ function originalFileRecord(file) {
|
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
function mergeCodexConfig(original, generated) {
|
|
179
|
-
if (/^\s*\[agents\.odai_(?:researcher|planner|executor|reviewer|frontend|creative)\]\s*$/m.test(original)) {
|
|
190
|
+
if (/^\s*\[agents\.odai_(?:researcher|planner|executor|reviewer|frontend|creative|writer)\]\s*$/m.test(original)) {
|
|
180
191
|
fail("既有 Codex 配置已声明 odai 角色但不受当前安装清单管理");
|
|
181
192
|
}
|
|
182
193
|
const generatedFeatures = generated.search(/^\s*\[features\]\s*$/m);
|
|
183
194
|
if (generatedFeatures < 0) fail("生成的 Codex 配置缺少 [features]");
|
|
184
195
|
const generatedAgents = generated.search(/^\s*\[agents\.odai_/m);
|
|
185
|
-
|
|
196
|
+
|
|
186
197
|
const existingInstructions = topLevelTomlString(original, "developer_instructions");
|
|
187
198
|
const generatedController = generated.slice(0, generatedFeatures).trim();
|
|
188
199
|
const controller = existingInstructions
|
|
189
200
|
? appendDeveloperInstructions(generatedController, existingInstructions)
|
|
190
201
|
: generatedController;
|
|
191
|
-
const agents = generated.slice(generatedAgents).trim();
|
|
202
|
+
const agents = generatedAgents < 0 ? "" : generated.slice(generatedAgents).trim();
|
|
192
203
|
let preserved = removeTopLevelCodexKeys(original, new Set(["model", "model_reasoning_effort", "developer_instructions"]));
|
|
193
204
|
preserved = setFeatureFlag(preserved, "multi_agent", "true");
|
|
194
205
|
return [controller, preserved.trim(), agents].filter(Boolean).join("\n\n") + "\n";
|
|
@@ -246,6 +257,7 @@ function setFeatureFlag(source, name, value) {
|
|
|
246
257
|
return lines.join("\n");
|
|
247
258
|
}
|
|
248
259
|
|
|
260
|
+
|
|
249
261
|
function hostLayout(host, scope) {
|
|
250
262
|
if (host === "codex") {
|
|
251
263
|
const researcherFiles = [
|
|
@@ -258,6 +270,8 @@ function hostLayout(host, scope) {
|
|
|
258
270
|
[".codex/role-contracts/odai-frontend.md", "role-contracts/odai-frontend.md"],
|
|
259
271
|
];
|
|
260
272
|
const frontend = roles.includes("frontend") ? frontendFiles : [];
|
|
273
|
+
const writerFiles = [[".codex/agents/odai-writer.toml", "agents/odai-writer.toml"], [".codex/role-contracts/odai-writer.md", "role-contracts/odai-writer.md"]];
|
|
274
|
+
const writer = roles.includes("writer") ? writerFiles : [];
|
|
261
275
|
const creativeFiles = [
|
|
262
276
|
[".codex/agents/odai-creative.toml", "agents/odai-creative.toml"],
|
|
263
277
|
[".codex/role-contracts/odai-creative.md", "role-contracts/odai-creative.md"],
|
|
@@ -274,10 +288,13 @@ function hostLayout(host, scope) {
|
|
|
274
288
|
...researcher,
|
|
275
289
|
...frontend,
|
|
276
290
|
...creative,
|
|
291
|
+
...writer,
|
|
277
292
|
];
|
|
278
|
-
|
|
293
|
+
common.push(...namedFiles(host).map(file => [`.codex/${file}`, file]));
|
|
294
|
+
const generatedFiles = common.filter(([source]) => !roles.some(role => args[`${role}Cli`] && source === `.codex/agents/odai-${role}.toml`));
|
|
295
|
+
if (roles.some(role => args[`${role}Cli`])) generatedFiles.push([".codex/odai-local-cli.mjs", "odai-local-cli.mjs"]);
|
|
279
296
|
return {
|
|
280
|
-
knownFiles: [...new Set([...common, ...researcherFiles, ...frontendFiles, ...creativeFiles].map(([, target]) => target)), "hooks.json", "odai-route-hook.mjs", "odai-run-routing.mjs", "agents/odai-executor.toml", "role-contracts/odai-executor.md"],
|
|
297
|
+
knownFiles: [...namedFiles(host, true), "odai-local-cli.mjs", ...new Set([...common, ...researcherFiles, ...frontendFiles, ...creativeFiles, ...writerFiles].map(([, target]) => target)), "hooks.json", "odai-route-hook.mjs", "odai-run-routing.mjs", "agents/odai-executor.toml", "role-contracts/odai-executor.md"],
|
|
281
298
|
managedFiles: generatedFiles.map(([, target]) => target),
|
|
282
299
|
generatedFiles,
|
|
283
300
|
settings: null,
|
|
@@ -288,9 +305,10 @@ function hostLayout(host, scope) {
|
|
|
288
305
|
const local = roles.filter(role => args[`${role}Cli`]);
|
|
289
306
|
const managedFiles = roles.map(role => `${args[`${role}Cli`] ? "role-contracts" : "agents"}/odai-${role}.md`);
|
|
290
307
|
if (local.length) managedFiles.push(...executors);
|
|
308
|
+
managedFiles.push(...namedFiles(host));
|
|
291
309
|
return {
|
|
292
310
|
knownFiles: [...configurableRoles.map(role => `agents/odai-${role}.md`),
|
|
293
|
-
...configurableRoles.filter(role => role !== "controller").map(role => `role-contracts/odai-${role}.md`), ...executors],
|
|
311
|
+
...configurableRoles.filter(role => role !== "controller").map(role => `role-contracts/odai-${role}.md`), ...executors, ...namedFiles(host, true)],
|
|
294
312
|
managedFiles,
|
|
295
313
|
generatedFiles: managedFiles.map(file => [`.claude/${file}`, file]),
|
|
296
314
|
settings: { file: scope === "project" ? "settings.local.json" : "settings.json", key: "agent", value: "odai-controller",
|
|
@@ -314,6 +332,7 @@ function planSettings(root, layoutValue, manifest) {
|
|
|
314
332
|
if (manifest) {
|
|
315
333
|
if (!installed || installed.file !== spec.file || installed.key !== spec.key || installed.installed !== spec.value) fail("旧路由缺少可恢复的宿主设置记录");
|
|
316
334
|
if (current[spec.key] !== spec.value) fail(`宿主设置 ${spec.key} 已被外部修改`);
|
|
335
|
+
restoreNamedHooks(current, installed.namedHooks);
|
|
317
336
|
restorePermission(current, installed.permission);
|
|
318
337
|
} else {
|
|
319
338
|
const present = Object.hasOwn(current, spec.key);
|
|
@@ -321,6 +340,14 @@ function planSettings(root, layoutValue, manifest) {
|
|
|
321
340
|
installed = { file: spec.file, key: spec.key, installed: spec.value, fileExistedBefore: existsSync(file), previous: { present, value: present ? current[spec.key] : null } };
|
|
322
341
|
}
|
|
323
342
|
const record = { ...installed };
|
|
343
|
+
delete record.namedHooks;
|
|
344
|
+
if (args.namedDispatch || args.leadMode) {
|
|
345
|
+
const command = `node '${path.join(root, "odai-route-hook.mjs").replaceAll("'", "'\\''")}'`;
|
|
346
|
+
const entries = Object.fromEntries(["UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"].map(event => [event, { matcher: "*", hooks: [{ type: "command", command, timeout: 10 }] }]));
|
|
347
|
+
record.namedHooks = entries;
|
|
348
|
+
current.hooks ||= {};
|
|
349
|
+
for (const [event, entry] of Object.entries(entries)) current.hooks[event] = [...(current.hooks[event] || []), entry];
|
|
350
|
+
}
|
|
324
351
|
delete record.permission;
|
|
325
352
|
if (spec.permission) {
|
|
326
353
|
const permissionsPresent = Object.hasOwn(current, "permissions");
|
|
@@ -388,7 +415,9 @@ function assertManagedState(root, layoutValue, manifest) {
|
|
|
388
415
|
|| installedSetting.installed !== expectedSetting.value) {
|
|
389
416
|
fail("旧路由缺少可恢复的宿主设置记录");
|
|
390
417
|
}
|
|
391
|
-
|
|
418
|
+
const currentSettings = readJsonObject(path.join(root, installedSetting.file));
|
|
419
|
+
assertNamedHooks(currentSettings, installedSetting.namedHooks);
|
|
420
|
+
assertPermission(currentSettings, installedSetting.permission);
|
|
392
421
|
} else if (installedSetting) {
|
|
393
422
|
fail("旧路由包含当前宿主不支持的设置记录");
|
|
394
423
|
}
|
|
@@ -415,7 +444,7 @@ function removeObsolete(root, manifest, desiredFiles) {
|
|
|
415
444
|
if (!manifest) return;
|
|
416
445
|
const desired = new Set(desiredFiles.keys());
|
|
417
446
|
const obsolete = new Set([
|
|
418
|
-
...layout.retiredFiles.filter((relative) => manifest.files?.[relative]),
|
|
447
|
+
...layout.retiredFiles.filter((relative) => manifest.files?.[relative] && !desired.has(relative)),
|
|
419
448
|
...Object.keys(manifest.files || {}).filter((relative) => !desired.has(relative)),
|
|
420
449
|
]);
|
|
421
450
|
for (const relative of obsolete) if (existsSync(path.join(root, relative))) unlinkSync(path.join(root, relative));
|
|
@@ -461,6 +490,7 @@ function uninstall(root, manifestPathValue, manifest) {
|
|
|
461
490
|
function planSettingRestore(root, spec) {
|
|
462
491
|
const current = readJsonObject(path.join(root, spec.file));
|
|
463
492
|
if (current[spec.key] !== spec.installed) fail(`宿主设置 ${spec.key} 已被外部修改,拒绝删除`);
|
|
493
|
+
restoreNamedHooks(current, spec.namedHooks);
|
|
464
494
|
restorePermission(current, spec.permission);
|
|
465
495
|
if (spec.previous?.present) current[spec.key] = spec.previous.value; else delete current[spec.key];
|
|
466
496
|
return {
|
|
@@ -542,12 +572,15 @@ function parseArgs(values) {
|
|
|
542
572
|
const fields = new Map([["--host", "host"], ["--scope", "scope"], ["--target", "target"], ["--governance-root", "governanceRoot"]]);
|
|
543
573
|
for (const role of configurableRoles) {
|
|
544
574
|
fields.set(`--${role}-model`, `${role}Model`); fields.set(`--${role}-effort`, `${role}Effort`);
|
|
575
|
+
if (role !== "controller") fields.set(`--${role}-session`, `${role}Session`);
|
|
545
576
|
if (role !== "controller") fields.set(`--${role}-cli`, `${role}Cli`);
|
|
546
577
|
}
|
|
547
578
|
for (let index = 0; index < values.length; index += 1) {
|
|
548
579
|
const value = values[index];
|
|
549
580
|
if (value === "--help" || value === "-h") result.help = true;
|
|
550
581
|
else if (value === "--uninstall") result.uninstall = true;
|
|
582
|
+
else if (value === "--lead-mode") result.leadMode = true;
|
|
583
|
+
else if (value === "--named-dispatch") result.namedDispatch = true;
|
|
551
584
|
else if (value === "--yes") result.yes = true;
|
|
552
585
|
else if (fields.has(value)) { const next = values[++index] || ""; if (!next || next.startsWith("--")) fail(`${value} 需要一个值`); result[fields.get(value)] = next; }
|
|
553
586
|
else fail(`未知参数:${value}`);
|
|
@@ -555,20 +588,38 @@ function parseArgs(values) {
|
|
|
555
588
|
return result;
|
|
556
589
|
}
|
|
557
590
|
|
|
558
|
-
function hasMappingArgs(value) { return configurableRoles.some((role) => value[`${role}Model`] || value[`${role}Effort`] || value[`${role}Cli`]); }
|
|
591
|
+
function hasMappingArgs(value) { return configurableRoles.some((role) => value[`${role}Model`] || value[`${role}Effort`] || value[`${role}Cli`] || value[`${role}Session`]); }
|
|
559
592
|
function printHelp() {
|
|
560
593
|
console.log(`由 odai 在取得用户授权后安装、更新或卸载宿主路由。
|
|
561
594
|
|
|
562
595
|
Usage:
|
|
563
596
|
node skills/odai-orchestration/scripts/install-routing.mjs --host <codex|claude|copilot> --scope <project|user> [--target <path>] \\
|
|
564
597
|
--controller-model <model> --planner-model <model> --reviewer-model <model> \\
|
|
565
|
-
[--researcher-model <model>] [--frontend-model <model>] [--creative-model <model>] [--<role>-effort <effort>] [--governance-root <path>] --yes
|
|
566
|
-
Claude
|
|
598
|
+
[--researcher-model <model>] [--frontend-model <model>] [--creative-model <model>] [--writer-model <model>] [--<role>-effort <effort>] [--governance-root <path>] --yes
|
|
599
|
+
Claude: [--<role>-cli <codex|antigravity|grok>]; Codex: [--<role>-cli <claude|antigravity|grok>]\n [--<role>-session <fresh|continue>] (non-controller roles)\n [--named-dispatch] [--lead-mode] (Claude / Codex only)
|
|
567
600
|
|
|
568
601
|
node skills/odai-orchestration/scripts/install-routing.mjs --host <codex|claude|copilot> --scope <project|user> [--target <path>] --uninstall --yes
|
|
569
602
|
|
|
570
|
-
安装后用户只需正常使用 odai。总控是唯一持续任务线程并负责实施整合;planner、reviewer 以及可选 researcher、frontend、creative 只在能改变结果时启动。researcher、frontend 与
|
|
603
|
+
安装后用户只需正常使用 odai。总控是唯一持续任务线程并负责实施整合;planner、reviewer 以及可选 researcher、frontend、creative、writer 只在能改变结果时启动。researcher、frontend、creative 与 writer 映射默认不配置。更新会安全移除旧版 advisor、implementer、worker、executor 和 stage runner 托管文件。`);
|
|
571
604
|
}
|
|
572
605
|
|
|
573
606
|
function returnResult(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); }
|
|
574
607
|
function fail(message) { throw new Error(message); }
|
|
608
|
+
|
|
609
|
+
function namedFiles(host, all = false) {
|
|
610
|
+
if (!all && !args.namedDispatch && !args.leadMode) return [];
|
|
611
|
+
return ["odai-route-hook.mjs", "recognize-routing.mjs", "odai-named-dispatch.json", ...(host === "codex" ? [".agents/plugins/marketplace.json", "named-dispatch-plugin/.codex-plugin/plugin.json", "named-dispatch-plugin/hooks/hooks.json"] : [])];
|
|
612
|
+
}
|
|
613
|
+
function assertNamedHooks(current, entries) {
|
|
614
|
+
for (const [event, entry] of Object.entries(entries || {})) {
|
|
615
|
+
if (!(current.hooks?.[event] || []).some(value => JSON.stringify(value) === JSON.stringify(entry))) fail(`路由 Hook 已漂移:${event}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function restoreNamedHooks(current, entries) {
|
|
619
|
+
assertNamedHooks(current, entries);
|
|
620
|
+
for (const [event, entry] of Object.entries(entries || {})) {
|
|
621
|
+
current.hooks[event] = current.hooks[event].filter(value => JSON.stringify(value) !== JSON.stringify(entry));
|
|
622
|
+
if (!current.hooks[event].length) delete current.hooks[event];
|
|
623
|
+
}
|
|
624
|
+
if (current.hooks && !Object.keys(current.hooks).length) delete current.hooks;
|
|
625
|
+
}
|
|
@@ -7,6 +7,7 @@ export interface LocalCliRoute {
|
|
|
7
7
|
/** Validated by the selected CLI adapter. */
|
|
8
8
|
reasoningEffort?: string;
|
|
9
9
|
timeoutSeconds?: number;
|
|
10
|
+
session?: "fresh" | "continue";
|
|
10
11
|
provider?: never;
|
|
11
12
|
maxTokens?: never;
|
|
12
13
|
}
|
|
@@ -31,6 +32,8 @@ export interface LocalCliParsedResult {
|
|
|
31
32
|
actualReasoningEffort?: string;
|
|
32
33
|
sessionId?: string;
|
|
33
34
|
deniedActionsCount?: number;
|
|
35
|
+
boundaryStatus?: "boundary-violation";
|
|
36
|
+
boundaryTools?: string[];
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export interface LocalCliExecutionResult extends Omit<LocalCliParsedResult, "status"> {
|
|
@@ -45,6 +48,7 @@ export interface ExecuteLocalCliOptions {
|
|
|
45
48
|
prompt: string;
|
|
46
49
|
cwd: string;
|
|
47
50
|
signal?: AbortSignal;
|
|
51
|
+
resumeSessionId?: string;
|
|
48
52
|
env?: NodeJS.ProcessEnv;
|
|
49
53
|
/** Test seam. Injected spawns use child.kill unless killProcess is also supplied. */
|
|
50
54
|
spawnProcess?: typeof spawn;
|
|
@@ -57,6 +61,8 @@ export interface ExecuteLocalCliOptions {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export interface LocalCliContext {
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
resumeSessionId?: string;
|
|
60
66
|
cwd?: string;
|
|
61
67
|
prompt?: string;
|
|
62
68
|
promptFile?: string;
|
|
@@ -73,3 +79,5 @@ export function parseLocalCliResult(stdout: string, requestedRoute: string | Loc
|
|
|
73
79
|
* the direct child; process-tree cleanup on Windows is not verified.
|
|
74
80
|
*/
|
|
75
81
|
export function executeLocalCli(options: ExecuteLocalCliOptions): Promise<LocalCliExecutionResult>;
|
|
82
|
+
|
|
83
|
+
export const AGY_READ_ONLY_TOOLS: readonly string[];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { chmodSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { homedir, tmpdir } from "node:os";
|
|
4
5
|
import { join, resolve } from "node:path";
|
|
5
6
|
const ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
|
|
6
|
-
const ROUTE_FIELDS = new Set(["backend", "cli", "model", "reasoningEffort", "timeoutSeconds"]);
|
|
7
|
+
const ROUTE_FIELDS = new Set(["backend", "cli", "model", "reasoningEffort", "timeoutSeconds", "session"]);
|
|
7
8
|
const DEFAULT_TIMEOUT_SECONDS = 1200;
|
|
8
9
|
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
9
10
|
const STDERR_BYTES = 8192;
|
|
@@ -29,28 +30,36 @@ export function validateLocalCliRoute(value) {
|
|
|
29
30
|
if (value.reasoningEffort !== undefined && (typeof value.reasoningEffort !== "string" || /[^a-z]/.test(value.reasoningEffort) || !ADAPTERS[value.cli].effort.test(value.reasoningEffort))) {
|
|
30
31
|
throw new TypeError(`Invalid ${value.cli} reasoningEffort`);
|
|
31
32
|
}
|
|
33
|
+
if (value.session !== undefined && !["fresh", "continue"].includes(value.session)) throw new TypeError("session must be fresh or continue");
|
|
34
|
+
if (value.session === "continue" && !ADAPTERS[value.cli].continuation) throw new TypeError(`${value.cli} 暂不支持会话续接`);
|
|
32
35
|
const timeoutSeconds = value.timeoutSeconds;
|
|
33
36
|
if (timeoutSeconds !== undefined && (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0 || timeoutSeconds > 7200)) {
|
|
34
37
|
throw new TypeError("local-cli timeoutSeconds must be an integer from 1 to 7200");
|
|
35
38
|
}
|
|
36
39
|
return {
|
|
37
40
|
backend: "local-cli", cli: value.cli, model: value.model,
|
|
41
|
+
...(value.session !== undefined ? { session: value.session } : {}),
|
|
38
42
|
...(value.reasoningEffort !== undefined ? { reasoningEffort: value.reasoningEffort } : {}),
|
|
39
43
|
...(timeoutSeconds !== undefined ? { timeoutSeconds } : {}),
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
const
|
|
47
|
+
export const AGY_READ_ONLY_TOOLS = Object.freeze(["read_file", "search_web", "ask_question", "manage_task", "view_file", "grep_search", "list_dir", "find_by_name"]);
|
|
48
|
+
const AGY_READ_BOUNDARY = "\n\n只用只读工具读取任务包中已给出路径的文件(例如 read_file,旧版本叫 view_file)。不要列目录、搜索文件、执行命令、写入或编辑文件;这些动作会被拒绝,并使本次调用作废。需要但未给出的文件,请在回交中列明所需文件,不要自行尝试。";
|
|
44
49
|
const effortArgs = (route) => route.reasoningEffort ? ["--effort", route.reasoningEffort] : [];
|
|
45
50
|
const ADAPTERS = Object.freeze({
|
|
46
51
|
claude: {
|
|
52
|
+
continuation: true,
|
|
47
53
|
effort: /^(low|medium|high|xhigh|max)$/,
|
|
48
54
|
command: "claude", override: "ODAI_CLAUDE_COMMAND", input: "stdin",
|
|
49
|
-
args: (route) => ["-p", "--output-format", "json", "--model", route.model, ...effortArgs(route),
|
|
50
|
-
"--tools", "Read,Glob,Grep", "--permission-mode", "dontAsk",
|
|
55
|
+
args: (route, { sessionId, resumeSessionId }) => ["-p", "--output-format", "json", "--model", route.model, ...effortArgs(route),
|
|
56
|
+
"--tools", "Read,Glob,Grep", "--permission-mode", "dontAsk", ...(route.session === "continue"
|
|
57
|
+
? [resumeSessionId ? "--resume" : "--session-id", resumeSessionId || sessionId]
|
|
58
|
+
: ["--no-session-persistence"])],
|
|
51
59
|
parse: (stdout, route) => parseClaudeResult(stdout, route.model),
|
|
52
60
|
},
|
|
53
61
|
codex: {
|
|
62
|
+
continuation: false,
|
|
54
63
|
effort: /^[a-z]+$/,
|
|
55
64
|
command: "codex", override: "ODAI_CODEX_COMMAND", input: "stdin",
|
|
56
65
|
args: (route, { cwd }) => ["exec", "--json", "--model", route.model,
|
|
@@ -59,18 +68,20 @@ const ADAPTERS = Object.freeze({
|
|
|
59
68
|
parse: parseCodexResult,
|
|
60
69
|
},
|
|
61
70
|
antigravity: {
|
|
71
|
+
continuation: true,
|
|
62
72
|
effort: /^(low|medium|high)$/,
|
|
63
73
|
command: "agy", override: "ODAI_AGY_COMMAND", input: "argv",
|
|
64
|
-
args: (route, { prompt }) => {
|
|
74
|
+
args: (route, { prompt, resumeSessionId }) => {
|
|
65
75
|
if (typeof prompt !== "string") throw new TypeError("antigravity requires a prompt");
|
|
66
76
|
const input = prompt + AGY_READ_BOUNDARY;
|
|
67
77
|
if (Buffer.byteLength(input, "utf8") > 256 * 1024) throw new TypeError("antigravity prompt exceeds 256 KiB (including read-only instructions)");
|
|
68
|
-
return ["-p", input, "--output-format", "stream-json", "--mode", "plan", "--disable-slash-commands", "--model", route.model,
|
|
78
|
+
return [...(resumeSessionId ? ["--conversation", resumeSessionId] : []), "-p", input, "--output-format", "stream-json", "--mode", "plan", "--disable-slash-commands", "--model", route.model,
|
|
69
79
|
...effortArgs(route), "--print-timeout", `${route.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS}s`];
|
|
70
80
|
},
|
|
71
81
|
parse: parseAntigravityResult,
|
|
72
82
|
},
|
|
73
83
|
grok: {
|
|
84
|
+
continuation: false,
|
|
74
85
|
effort: /^(none|minimal|low|medium|high|xhigh|max)$/,
|
|
75
86
|
command: "grok", override: "ODAI_GROK_COMMAND", input: "file",
|
|
76
87
|
args: (route, { cwd, promptFile }) => {
|
|
@@ -89,6 +100,8 @@ export function buildLocalCliInvocation(route, env = process.env, context = {})
|
|
|
89
100
|
throw new TypeError(`${normalized.cli} requires cwd`);
|
|
90
101
|
}
|
|
91
102
|
const adapter = ADAPTERS[normalized.cli];
|
|
103
|
+
if (context.resumeSessionId !== undefined && (normalized.session !== "continue" || typeof context.resumeSessionId !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(context.resumeSessionId))) throw new TypeError("Invalid resume session ID");
|
|
104
|
+
if (normalized.cli === "claude" && normalized.session === "continue" && !context.resumeSessionId && !context.sessionId) throw new TypeError("Claude continuation requires a new session ID");
|
|
92
105
|
return { command: env[adapter.override] || adapter.command, args: adapter.args(normalized, context) };
|
|
93
106
|
}
|
|
94
107
|
|
|
@@ -214,6 +227,8 @@ function parseCodexResult(stdout, route, { cwd, env }) {
|
|
|
214
227
|
|
|
215
228
|
function parseAntigravityResult(stdout, route) {
|
|
216
229
|
const events = jsonLines(stdout);
|
|
230
|
+
const violations = [...new Set(events.filter(event => event.event === "step_update" && event.step_update?.step_type === "tool" && event.step_update?.state === "DONE" && !AGY_READ_ONLY_TOOLS.includes(event.step_update.tool_name)).map(event => String(event.step_update.tool_name ?? "<missing>")))];
|
|
231
|
+
if (violations.length) return { ...emptyResult("error", `boundary-violation: ${violations.join(", ")};agy 的权限设置允许了写类工具,请检查 agy 设置里的 permissions.allow。执行后检查不能撤销已发生的动作。`), boundaryStatus: "boundary-violation", boundaryTools: violations };
|
|
217
232
|
const init = events.find((event) => event.event === "init");
|
|
218
233
|
const payload = events.findLast((event) => event.event === "result")?.result;
|
|
219
234
|
if (!isRecord(payload)) return emptyResult("error", "Antigravity returned no result event");
|
|
@@ -285,10 +300,11 @@ export function parseLocalCliResult(stdout, requestedRoute, context = {}) {
|
|
|
285
300
|
|
|
286
301
|
/** Executes only the target CLI. Host routing and fallback policy belong to callers. */
|
|
287
302
|
export async function executeLocalCli({
|
|
288
|
-
route, prompt, cwd, signal, env = process.env, spawnProcess = spawn, killProcess,
|
|
303
|
+
route, prompt, cwd, signal, resumeSessionId, env = process.env, spawnProcess = spawn, killProcess,
|
|
289
304
|
killGraceMs = 1000, maxOutputBytes = MAX_OUTPUT_BYTES,
|
|
290
305
|
}) {
|
|
291
306
|
const normalized = validateLocalCliRoute(route);
|
|
307
|
+
const sessionId = normalized.session === "continue" && normalized.cli === "claude" ? resumeSessionId || randomUUID() : undefined;
|
|
292
308
|
if (typeof prompt !== "string") throw new TypeError("local-cli prompt must be a string");
|
|
293
309
|
if (typeof cwd !== "string" || !cwd) throw new TypeError("local-cli cwd must be a non-empty string");
|
|
294
310
|
if (!Number.isSafeInteger(killGraceMs) || killGraceMs < 0) throw new TypeError("killGraceMs must be a non-negative safe integer");
|
|
@@ -313,7 +329,7 @@ export async function executeLocalCli({
|
|
|
313
329
|
promptFile = join(temporary, "prompt.txt");
|
|
314
330
|
writeFileSync(promptFile, prompt, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
315
331
|
}
|
|
316
|
-
const invocation = buildLocalCliInvocation(normalized, env, { prompt, cwd, promptFile });
|
|
332
|
+
const invocation = buildLocalCliInvocation(normalized, env, { prompt, cwd, promptFile, sessionId, resumeSessionId });
|
|
317
333
|
return await new Promise((resolve) => {
|
|
318
334
|
let child;
|
|
319
335
|
let settled = false;
|
|
@@ -396,7 +412,7 @@ export async function executeLocalCli({
|
|
|
396
412
|
finish(emptyResult("error", `${normalized.cli} exited ${terminationSignal ? `with signal ${terminationSignal}` : `with code ${code}`}${details ? `: ${details}` : ""}`));
|
|
397
413
|
return;
|
|
398
414
|
}
|
|
399
|
-
finish(parseLocalCliResult(Buffer.concat(stdout).toString("utf8"), normalized, { cwd, env }));
|
|
415
|
+
finish({ ...parseLocalCliResult(Buffer.concat(stdout).toString("utf8"), normalized, { cwd, env }), ...(sessionId ? { sessionId } : {}) });
|
|
400
416
|
});
|
|
401
417
|
child.stdin.on("error", (error) => {
|
|
402
418
|
// Early CLI failure often closes stdin first; close still supplies its stderr and exit code.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Responsibility = 'controller' | 'researcher' | 'planner' | 'reviewer' | 'frontend' | 'creative' | 'writer' | 'user';
|
|
2
|
+
export interface ModelRoute { backend?: string; cli?: string; provider?: string; model: string }
|
|
3
|
+
export interface NamedRequest { responsibility: Responsibility; timing: 'start' | 'completion'; excerpt: string; name: string; canceled?: boolean; issue?: 'unconfigured' | 'mapping-mismatch' }
|
|
4
|
+
export function recognizeNamedRequests(text: string, routes: Partial<Record<Responsibility, ModelRoute>>): NamedRequest[];
|
|
5
|
+
export function recognizeLeadSwitch(text: string, routes: Partial<Record<'planner' | 'reviewer', ModelRoute>>): { enabled: boolean; roles: string[]; issue?: string } | undefined;
|
|
6
|
+
export function parseLeadVerdict(text: string): "pass" | "changes-requested" | "invalid";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Deliberately bounded grammar: an imperative delegation, never a bare role word.
|
|
13
|
+
export function recognizeNamedRequests(text , routes ) {
|
|
14
|
+
const clean = text.replace(/```[\s\S]*?```|`[^`]*`|“[^”]*”|‘[^’]*’|「[^」]*」|『[^』]*』|"[^"\n]*"|'[^'\n]*'/gu, " ").replace(/^\s*>.*$/gmu, " ");
|
|
15
|
+
const dynamicNames = Object.values(routes).flatMap(route => route ? [
|
|
16
|
+
route.backend === "local-cli" ? route.cli : route.provider,
|
|
17
|
+
/^[a-z]+/iu.exec(route.model)?.[0],
|
|
18
|
+
].filter((name) => !!name && /^[a-z][a-z0-9_-]*$/iu.test(name)) : []);
|
|
19
|
+
const escape = (name ) => name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
20
|
+
const aliases = "Claude|克劳德|opus|sonnet|Gemini|反重力|agy|antigravity|GPT|Codex|Grok|planner|reviewer|researcher|creative|writer|frontend|规划职责|审查职责|研究职责|创意职责|写作职责|前端职责";
|
|
21
|
+
const names = [...new Set([...aliases.split("|"), ...dynamicNames])].sort((a, b) => b.length - a.length).map(escape).join("|");
|
|
22
|
+
const directive = new RegExp(`(?:让|交给|请|叫|找|用|have\\s+|let\\s+|ask\\s+|use\\s+)\\s*(${names})(?![a-z0-9_-])|交给\\s*(规划|调查|审查|创意|写作|前端)(?!职责)`, "iu");
|
|
23
|
+
const bare = new RegExp(`^\\s*(${names})(?![a-z0-9_-])\\s*(?:来|去)?\\s*(?:写|起草|改写|润色|翻译|定方向|审|规划|plan|write|review)`, "iu");
|
|
24
|
+
const result = [];
|
|
25
|
+
let creativeContext = false;
|
|
26
|
+
let delegated = false;
|
|
27
|
+
for (const clause of clean.split(/[,,;;。\n]|\bthen\b/iu)) {
|
|
28
|
+
if (/(?:以后|今后|from now on).*?(?:都|always)|^\s*(?:configure|配置|设置|映射)\b|(?:规划|审查|写作|前端|创意|调查)\s*用本机|推理档/iu.test(clause)) continue;
|
|
29
|
+
const match = directive.exec(clause) ?? (delegated ? bare.exec(clause) : null);
|
|
30
|
+
if (!match || /(?:了吗|过了|不错|怎么样|是否|要不要|能不能|吗|?|\?|last time|already)/iu.test(clause)) continue;
|
|
31
|
+
const prefix = clause.slice(0, match.index).trim();
|
|
32
|
+
// A bounded topic is allowed; an enclosing action ("写一下让…的测试") is not.
|
|
33
|
+
if (!/^(?:(?:先|再|然后|最后|做完|写完|改完|完成后|之后|交付前|麻烦|请你|你|不用|不要|别|无需|不必)\s*)*(?:文案|台词|对白|宣传语|商店页|规划|代码|界面)?\s*$/u.test(prefix)) continue;
|
|
34
|
+
delegated = true;
|
|
35
|
+
const name = match[1] ?? match[2] ;
|
|
36
|
+
const creativeObject = /文案|台词|对白|画面|美术|视觉|宣传|商店页|稿|copy|dialogue|prose/iu.test(clause);
|
|
37
|
+
let candidates = [];
|
|
38
|
+
if (/规划|计划|出方案|定方案|拆任务|\bplan\b/iu.test(clause)) candidates = ["planner"];
|
|
39
|
+
else if (/调查|调研|查资料|核实来源|\bresearch\b/iu.test(clause)) candidates = ["researcher"];
|
|
40
|
+
else if (/定方向|构思|起名|出点子/iu.test(clause)) { candidates = ["creative"]; creativeContext = true; }
|
|
41
|
+
else if (/界面|前端实现|做\s*UI|frontend/iu.test(clause)) candidates = ["frontend"];
|
|
42
|
+
else if (/(?:写(?!完|进|到|入|在)|起草|改写|润色|翻译|本地化|\bwrite\b)/iu.test(clause) && creativeObject) candidates = routes.writer ? ["writer", "creative"] : ["creative"];
|
|
43
|
+
else if (/审|验收|复核|检查|把关|评估|看看|\breview\b/iu.test(clause)) candidates = creativeObject || (creativeContext && !/验收|复核|检查/iu.test(clause)) ? ["creative"] : ["reviewer"];
|
|
44
|
+
if (!candidates.length && match[2]) candidates = [{ 规划: "planner", 调查: "researcher", 审查: "reviewer", 创意: "creative", 写作: "writer", 前端: "frontend" }[name] ];
|
|
45
|
+
if (!candidates.length) continue;
|
|
46
|
+
const explicitRole = { planner: "planner", reviewer: "reviewer", researcher: "researcher", creative: "creative", writer: "writer", frontend: "frontend", 规划职责: "planner", 审查职责: "reviewer", 研究职责: "researcher", 创意职责: "creative", 写作职责: "writer", 前端职责: "frontend", 规划: "planner", 调查: "researcher", 审查: "reviewer", 创意: "creative", 写作: "writer", 前端: "frontend" };
|
|
47
|
+
const role = explicitRole[name.toLowerCase()];
|
|
48
|
+
const matches = (route ) => {
|
|
49
|
+
if (!route) return false;
|
|
50
|
+
const value = `${route.backend === "local-cli" ? route.cli : route.provider} ${route.model}`;
|
|
51
|
+
if (/^(Claude|克劳德|opus|sonnet)$/iu.test(name)) return /claude|anthropic|opus|sonnet/iu.test(value);
|
|
52
|
+
if (/^(Gemini|反重力|agy|antigravity)$/iu.test(name)) return /antigravity|gemini/iu.test(value);
|
|
53
|
+
if (/^(GPT|Codex)$/iu.test(name)) return /openai|codex|gpt/iu.test(value);
|
|
54
|
+
if (/^Grok$/iu.test(name)) return route.backend === "local-cli" && route.cli === "grok";
|
|
55
|
+
return [route.backend === "local-cli" ? route.cli : route.provider, /^[a-z]+/iu.exec(route.model)?.[0]].some(value => value?.toLowerCase() === name.toLowerCase());
|
|
56
|
+
};
|
|
57
|
+
const responsibility = role ?? candidates.find(candidate => matches(routes[candidate])) ?? candidates[0] ;
|
|
58
|
+
const timing = /先(?:让|请|用|交给|叫|找|规划|审|验收|调研|调查|写|做|定)|\bfirst\b/iu.test(clause) ? "start" : /做完|写完|改完|完成后|之后|最后|交付前|\bafter\b|once done|when done|finally|before delivering/iu.test(clause) ? "completion" : responsibility === "reviewer" ? "completion" : "start";
|
|
59
|
+
const delegationPrefix = clause.slice(0, match.index + match[0].indexOf(name));
|
|
60
|
+
const canceled = /(?:不用|不要|别|无需|不必)\s*(?:再)?\s*(?:让|交给|请|叫|找|用)?\s*$|(?:do not|don't)\s+(?:let|have|ask|use)\s*$/iu.test(delegationPrefix);
|
|
61
|
+
const issue = !routes[responsibility] ? "unconfigured" : !role && !matches(routes[responsibility]) ? "mapping-mismatch" : undefined;
|
|
62
|
+
if (!result.some(item => item.responsibility === responsibility && item.timing === timing && item.canceled === canceled)) result.push({ responsibility, timing, excerpt: clause.trim(), name, ...(canceled ? { canceled: true } : {}), ...(issue ? { issue } : {}) });
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
export function recognizeLeadSwitch(text , routes ) {
|
|
69
|
+
const clean = text.replace(/```[\s\S]*?```|`[^`]*`|“[^”]*”|「[^」]*」|『[^』]*』|‘[^’]*’|"[^"\n]*"|'[^'\n]*'/gu, " ").replace(/^\s*>.*$/gmu, " ");
|
|
70
|
+
let result;
|
|
71
|
+
for (const raw of clean.split(/[,,;;。\n]/u)) {
|
|
72
|
+
const clause = raw.trim();
|
|
73
|
+
if (/吗|?|\?|是否|能不能|配置|设置|以后|默认|开关|测试/iu.test(clause)) continue;
|
|
74
|
+
if (/^(?:关闭主导模式|退出主导模式|恢复正常模式|disable lead mode)[!!]?$/iu.test(clause)) result = { enabled: false, roles: [] };
|
|
75
|
+
else {
|
|
76
|
+
const direct = /^(?:(?:这个|当前)会话\s*)?(?:开启|打开|启用|开)主导模式[!!]?$/u.test(clause) || /^enable lead mode[!!]?$/iu.test(clause);
|
|
77
|
+
const named = /^(?:这个|当前)会话由\s*(\S+)\s*主导[!!]?$/u.exec(clause);
|
|
78
|
+
if (!direct && !named) continue;
|
|
79
|
+
let roles = Object.keys(routes).filter(role => routes[role ]);
|
|
80
|
+
if (named) roles = roles.filter(role => !recognizeNamedRequests(`让 ${named[1]} ${role === "planner" ? "规划" : "验收"}`, routes).every(request => request.issue || request.responsibility !== role));
|
|
81
|
+
result = { enabled: roles.length > 0, roles, ...(roles.length ? {} : { issue: named ? "mapping-mismatch" : "缺少 planner、reviewer 插槽;请用 odai_routing_config 配置" }) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseLeadVerdict(text) {
|
|
88
|
+
const normalize = (line) => line.trim().replace(/^[*_`"'“”‘’「」『』]+|[*_`"'“”‘’「」『』。.]+$/gu, "").trim();
|
|
89
|
+
const lines = text.split(/\r?\n/u).filter(line => line.trim());
|
|
90
|
+
const verdicts = [...text.matchAll(/\bVERDICT\s*[::]\s*[*_`"'“‘「『]*(pass|changes-requested)\b/giu)].map(match => match[1].toLowerCase());
|
|
91
|
+
if (new Set(verdicts).size > 1) return "invalid";
|
|
92
|
+
return /^VERDICT\s*[::]\s*(pass|changes-requested)$/iu.exec(normalize(lines.at(-1) ?? ""))?.[1]?.toLowerCase() ?? "invalid";
|
|
93
|
+
}
|