odai-dsh-agent 0.2.55 → 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 +14 -7
- 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 +197 -14
- 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/review-evidence.d.mts +2 -0
- package/preset/odai/runtime/review-evidence.d.mts.map +1 -1
- package/preset/odai/runtime/review-evidence.mjs +45 -0
- package/preset/odai/runtime/review-evidence.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 -20
- 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 +21 -8
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
你是当前编排的总控,持有完整目标、授权、全局状态、实施整合、修正回路与最终交付;需求覆盖和最终验收责任不能随委派转移。
|
|
2
2
|
|
|
3
|
-
当前能力足够就直接完成。可选职责:__ODAI_RESEARCHER_ROLE__、__ODAI_PLANNER_ROLE__、__ODAI_REVIEWER_ROLE__、__ODAI_FRONTEND_ROLE__、__ODAI_CREATIVE_ROLE__。按随合同提供的 orchestration reference 选择职责、核实权限并准备有界交接;任务复杂、高风险或角色已配置不自动触发。
|
|
3
|
+
当前能力足够就直接完成。可选职责:__ODAI_RESEARCHER_ROLE__、__ODAI_PLANNER_ROLE__、__ODAI_REVIEWER_ROLE__、__ODAI_FRONTEND_ROLE__、__ODAI_CREATIVE_ROLE__、__ODAI_WRITER_ROLE__。按随合同提供的 orchestration reference 选择职责、核实权限并准备有界交接;任务复杂、高风险或角色已配置不自动触发。
|
|
4
4
|
|
|
5
5
|
回交只证明其实际覆盖。局部判断和审查不要求重建全任务合同,若干局部通过也不能替代组合结果;核实 finding 对既有要求与安全边界的影响,既不让它产生新目标或写域,也不忽略真实失败。实施偏差组织修正;路线失效只重判受影响决定;缺用户取舍或外部条件只阻断依赖项。
|
|
6
6
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## 写作专业责任
|
|
2
|
+
|
|
3
|
+
只承接方向已定的写作制作,不限领域:按给定说明起草、改写、扩写或压缩面向用户或读者的文字,或把已定稿内容翻译、本地化成目标语言。不是第二个总控,也不替代创意判断;受众、目的、语气、篇幅、必须保留和禁用项缺到会改变成稿时,回交准确缺口,不自行定方向、改设定或扩大范围。
|
|
4
|
+
|
|
5
|
+
动笔前读清给定说明、项目已有设定与术语、文风样本和既有译名;写作方法遵循随合同提供的 治理 craft reference。成稿写给具体读者,用他们会用的词;具体胜过空泛,可以加入贴合语境、不与已有内容冲突的细节,不堆砌修饰、套话和模板句式。翻译忠实原意、语气和信息量,按目标语言读者的习惯表达,不逐字硬译;术语、专名、占位符和格式标记保持一致,长度受限的文字按限制写。
|
|
6
|
+
|
|
7
|
+
要候选时,候选要实质有别并各写明取向;只要一稿时直接给成稿。
|
|
8
|
+
|
|
9
|
+
只读:交回成稿或候选,附必要的简短说明(依据的说明条目、刻意的取舍、未能满足的限制、待确认的设定或术语),不编辑文件,不把成稿写成已采纳或已落盘;改写已有文字时给出原文位置与替换文本,由总控核对后落盘。不扩大到相邻内容的重写,顺带发现的问题单列为建议。模型、预算及执行权限由宿主选择,不写入本通用责任合同。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "odai-orchestration",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"governanceContract": 9,
|
|
6
6
|
"delegationFile": "contracts/delegation.md",
|
|
7
7
|
"roleFiles": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"planner": "assets/routing-roles/planner.md",
|
|
11
11
|
"reviewer": "assets/routing-roles/reviewer.md",
|
|
12
12
|
"frontend": "assets/routing-roles/frontend.md",
|
|
13
|
-
"creative": "assets/routing-roles/creative.md"
|
|
13
|
+
"creative": "assets/routing-roles/creative.md",
|
|
14
|
+
"writer": "assets/routing-roles/writer.md"
|
|
14
15
|
},
|
|
15
16
|
"rolePresets": {
|
|
16
17
|
"controller": { "references": ["orchestration"] },
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
"planner": { "references": ["planning"] },
|
|
19
20
|
"reviewer": { "references": ["verification"] },
|
|
20
21
|
"frontend": { "references": ["craft"] },
|
|
21
|
-
"creative": { "references": ["craft"] }
|
|
22
|
+
"creative": { "references": ["craft"] },
|
|
23
|
+
"writer": { "references": ["craft"] }
|
|
22
24
|
},
|
|
23
25
|
"referenceFiles": { "orchestration": "references/orchestration.md" },
|
|
24
26
|
"requiredFiles": [
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
"assets/routing-roles/reviewer.md",
|
|
32
34
|
"assets/routing-roles/frontend.md",
|
|
33
35
|
"assets/routing-roles/creative.md",
|
|
36
|
+
"assets/routing-roles/writer.md",
|
|
34
37
|
"assets/codex-agents/config.toml",
|
|
35
38
|
"assets/codex-agents/role.toml",
|
|
36
39
|
"assets/claude-agents/agent.md",
|
|
@@ -44,6 +47,9 @@
|
|
|
44
47
|
"scripts/compose-contracts.mjs",
|
|
45
48
|
"scripts/compose-contracts.d.mts",
|
|
46
49
|
"scripts/local-cli.mjs",
|
|
47
|
-
"scripts/local-cli.d.mts"
|
|
50
|
+
"scripts/local-cli.d.mts",
|
|
51
|
+
"scripts/route-hook.mjs",
|
|
52
|
+
"scripts/recognize-routing.mjs",
|
|
53
|
+
"scripts/recognize-routing.d.mts"
|
|
48
54
|
]
|
|
49
55
|
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
用户要求为 Codex、Claude Code 或 Copilot 安装、更新、卸载或修复职责路由时读取。安装本技能和 `odai` 只使规则与脚本可发现,不自动修改宿主配置;DSH 已内置编排时使用其现有能力,不运行这里的安装器。
|
|
4
4
|
|
|
5
|
-
1. 从宿主确认实际模型、能力与作用域。controller、planner、reviewer 显式映射,researcher、frontend 和
|
|
5
|
+
1. 从宿主确认实际模型、能力与作用域。controller、planner、reviewer 显式映射,researcher、frontend、creative 和 writer 按需选择,默认不生成,也不从其他职责继承映射;creative 与 writer 只读,交回内容由总控核对后落盘;从真实模型目录核对,不虚构模型 ID。默认当前项目,跨项目作用域须明确授权。
|
|
6
6
|
2. 先用 `scripts/build-routing.mjs --help` 查看生成接口;治理安装位置不相邻时显式传入 `--governance-root`。
|
|
7
7
|
3. 需要持久安装、更新或卸载时使用 `scripts/install-routing.mjs --help` 对应入口,说明映射、写入范围与新会话生效条件。已有授权覆盖本次动作时直接执行;否则完成准备后取得授权。
|
|
8
8
|
4. 按安装结果在新会话核对真实角色和模型的实际调用。生成文件和配置成功只证明文件生成,不证明路由已调用。托管清单负责更新、漂移检查、卸载与恢复,遇到外部修改不覆盖。
|
|
9
9
|
|
|
10
10
|
## Claude Code 总控调用本机 CLI
|
|
11
11
|
|
|
12
|
-
`build-routing.mjs` 与 `install-routing.mjs` 在 `--host claude` 下接受非 controller 职责的 `--<role>-cli <codex|antigravity|grok>`,必须同时显式提供该职责的 `--<role>-model`;`--<role>-effort` 可选。未指定 CLI 的职责仍使用 Claude Code 原生子代理,controller
|
|
12
|
+
`build-routing.mjs` 与 `install-routing.mjs` 在 `--host claude` 下接受非 controller 职责的 `--<role>-cli <codex|antigravity|grok>`,必须同时显式提供该职责的 `--<role>-model`;`--<role>-effort` 可选。未指定 CLI 的职责仍使用 Claude Code 原生子代理,controller 始终保持原生。Codex 宿主接受 `--<role>-cli <claude|antigravity|grok>`,controller 保持原生;Copilot 不接受 CLI 选项。
|
|
13
13
|
|
|
14
14
|
例如,先在临时输出目录生成 Claude Code 总控与 planner、由 Codex 承担 reviewer 的配置(模型名替换为已确认可用的 ID):
|
|
15
15
|
|
|
@@ -33,8 +33,70 @@ node .claude/odai-run-role.mjs --role reviewer --input <任务包文件> \
|
|
|
33
33
|
|
|
34
34
|
执行器把角色合同和任务包交给目标 CLI,所有职责均只读;制作任务只能交回未应用补丁,由总控核对落盘。需要先安装并登录目标 CLI;子进程继承 Claude Code 的环境变量,需要代理时应在启动 Claude Code 的环境中配置 `HTTPS_PROXY` 等变量。可执行文件分别由 `ODAI_CODEX_COMMAND`、`ODAI_AGY_COMMAND`、`ODAI_GROK_COMMAND` 覆盖。默认超时 1200 秒,`--timeout` 接受 1–7200 整数秒。
|
|
35
35
|
|
|
36
|
-
执行器输出 version 1 证据;省略 `--evidence`
|
|
36
|
+
执行器输出 version 1 证据;省略 `--evidence` 时,Claude 宿主持久保存在执行器同目录 `role-evidence/`,Codex 本机 CLI 保存到当前项目 `.odai/role-evidence/`,路径写到 stderr。证据是运行产物,卸载保留。只有调用完成、正文非空且实际模型核实时才输出正文并退出 0;能取得推理档证据的 CLI 出现推理档不一致时同样失败。失败退出非 0,不输出正文、不覆盖已有正文文件。Codex 通过本次会话的模型与推理档取证;Antigravity 和 Grok 目前只能把推理档记为 `requested-only`,证据如实记录、不阻断回交,与 DSH 的规则一致。
|
|
37
37
|
|
|
38
|
-
Antigravity
|
|
38
|
+
Antigravity 1.4.7 下禁用斜杠展开使计划模式失效;只读依赖无界面授权拒绝和 DONE 工具的执行后检查(不能撤销已发生的动作)。不要给 odai 使用的 agy 配置写类 permissions.allow;被拒动作可能使正文为空。Grok 适配继续标注**按官方文档及既有评测证据实现,当前适配未实测**:优先取 JSON `modelUsage`,缺失时回退会话 summary;格式不符或证据缺失就失败。各 CLI 自身的会话或计划目录可能产生运行记录,只读边界针对被委托项目。
|
|
39
39
|
|
|
40
40
|
安装只启用按缺口选择的分工,不注入每轮必走流程。没有宿主适配或无法取得所需能力时,治理仍独立工作:继续安全可完成的部分,准确交付剩余缺口;不要把技能已安装、角色已配置或一次自报当成调用和收益证明。
|
|
41
|
+
|
|
42
|
+
writer 用于方向已定的起草、改写、翻译与本地化,creative 负责方向、构思与品味把关;未配置 writer 时 creative 照旧承接写作。可选参数为 `--writer-model`、`--writer-effort`,Claude 宿主另有 `--writer-cli`;默认不生成映射。
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Codex 的本机 CLI 与权限
|
|
46
|
+
|
|
47
|
+
Codex 使用 `.codex/odai-run-role.mjs`、`odai-local-cli.mjs`、`role-contracts/` 和 `odai-routing.json`;本机映射不生成同名原生代理。构建和安装接口相同,例如(仅一种配置,模型须自行确认):
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
node skills/odai-orchestration/scripts/build-routing.mjs --host codex --out /tmp/odai-preview --controller-model gpt-6.1-sol --planner-model gpt-6.1-sol --reviewer-cli claude --reviewer-model haiku --reviewer-session continue
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
总控在 codex-cli 0.163.0 的默认 workspace-write 沙箱中实测:本机 CLI 职责因无法联网而失败,claude 返回零 token、退出码 1;CODEX_THREAD_ID 已确认注入命令环境。安装器不修改沙箱、联网或审批策略。
|
|
54
|
+
|
|
55
|
+
运行具体执行器命令(例如 `node .codex/odai-run-role.mjs --role reviewer --input task.md`)时,需要申请越出沙箱,理由是需要联网,并写入本机 CLI 自己的目录;由用户按自己的审批策略批准该命令。用户拒绝或当前审批策略不允许时,如实报告该职责不可用,不改用别的模型。越出后 CLI 可联网、访问凭据并写自身会话目录;撤回时拒绝后续批准,恢复用户自行修改的配置并重开会话。
|
|
56
|
+
|
|
57
|
+
依据:本机 `codex --help` 列出 `--ask-for-approval on-request`,`codex exec --help` 列出沙箱选项;已安装 `@openai/codex/README.md` 仅提供入门及文档链接,未说明逐命令越出沙箱的申请接口。因此这里不指定未经核实的申请参数。
|
|
58
|
+
|
|
59
|
+
Codex 本机 CLI 的证据和续接状态分别位于当前项目 `.odai/role-evidence/`、`.odai/role-sessions/`,用户级安装也按执行器的 `--cwd`(省略时为当前目录)确定项目。建议将这两个目录加入项目 `.gitignore`;安装器不自动修改 `.gitignore`。
|
|
60
|
+
|
|
61
|
+
## 显式会话续接
|
|
62
|
+
|
|
63
|
+
构建/安装可用 `--<role>-session fresh|continue`;清单对应 `session`,省略为 fresh。Claude Code 原生职责开启 continue 时,用 SendMessage 继续上次派出的同一个子代理,可按名字或 agentId 续发,已完成的子代理也可以续发,不新派(Claude Code 2.1.307 本机实测)。Codex 原生角色续接证据不足,显式 continue 报错。
|
|
64
|
+
|
|
65
|
+
本机 CLI 复用共用适配器:claude、antigravity 支持,codex、grok 不支持并报错。宿主键按清单 host 优先取对应变量(Claude:`CLAUDE_CODE_SESSION_ID`;Codex:`CODEX_THREAD_ID`),另一变量后备,再取显式 `--session-key`,均缺失时报错。显式键须在同一宿主会话内稳定、跨会话唯一。路由标识为 backend + cli + model,各职责可共享。
|
|
66
|
+
|
|
67
|
+
Claude 状态位于执行器旁 `role-sessions/<键SHA256>.json`,Codex 本机 CLI 状态位于当前项目 `.odai/role-sessions/<键SHA256>.json`,与 `role-evidence/` 同级;锁目录 `<状态文件>.lock` 保证跨进程串行,等待超时明确失败。异常强杀遗留锁时,确认进程已结束后由用户移除该具体锁。状态原子替换,卸载保留状态和证据。证据 `observed.session.mode` 使用 `created`、`resumed`、`resume-failed-created`;未开启续接时省略该项。续接错误仅新开一次并在 stderr 和证据注明;超时、取消、边界越界不重开,不换模型。每次仍发送完整合同和任务包并核实模型。后端会话由各 CLI 自行存放管理,odai 不删除,只读模型证据所需部分;上下文逐次增长,由后端压缩。
|
|
68
|
+
|
|
69
|
+
默认映射、合同与宿主配置保持既有行为;由于编排 requiredFiles 增加及执行器升级,ADAPTER/安装清单的真实 bundle digest 和复制的执行器字节会变化,不能宣称全部生成文件逐字节不变。
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## 可选点名与主导模式路由 Hook
|
|
73
|
+
|
|
74
|
+
默认只注册角色,不装路由 Hook。只有 Claude Code / Codex 显式传 `--named-dispatch` 或 `--lead-mode` 才生成和安装声明式路由 Hook;Copilot 拒绝这两个参数。两者可组合,共用脚本、状态与托管清单;只传 `--named-dispatch` 保持点名行为。点名识别复用 `recognize-routing.mjs`,不依赖项目护栏的 `.odai/hooks.json`。新消息取代旧待办,“继续”保留,否定取消;未配置或名字不符只注入说明,不拦工具。
|
|
75
|
+
|
|
76
|
+
Claude 合并 UserPromptSubmit、PreToolUse、PostToolUse、Stop 条目到项目 `settings.local.json`(用户级为 `settings.json`);更新/卸载只移除托管条目,外部漂移拒绝覆盖。Hook 和共用识别脚本在执行器同目录,`odai-named-dispatch.json` 保存独立映射,路由安装清单记录文件摘要和设置恢复信息,与护栏清单分开。状态在 `.claude/named-dispatch/<session_id SHA256>.json`;Codex 状态在当前项目 `.odai/named-dispatch/`,建议加入 `.gitignore`,安装器不修改忽略规则。状态原子替换且锁定,卸载保留。
|
|
77
|
+
|
|
78
|
+
UserPromptSubmit 的 `hookSpecificOutput.additionalContext` 向总控说明职责、时机和调用方式。PreToolUse 拦截尚有开始时点名的写类工具:Claude 的 Edit/Write/MultiEdit/NotebookEdit/Bash,Codex 的 apply_patch、shell/shell_command/exec_command 以及未知工具均按写类处理。只读工具放行,当前职责本身的原生委派或独立执行器命令放行。PostToolUse 才记录满足:本机 CLI 要有本次任务之后的执行器证据、模型核实和退出 0;Claude 同步子代理仅在 tool_response.status 为 completed 时满足,其他状态不算完成,Codex spawn_agent 只记 agent_id,wait 收到 completed 才满足。Claude 的结构化 tool_response.isAsync === true 或 status === async_launched(文本 `Async agent launched successfully.` 保留作后备)只记进行中、agentId 和 tool_use_id。用户角色的 `<task-notification>`(会话 origin 为 `{kind: "task-notification", producer: "session-task"}`)、`<system-reminder>` 及 Codex `<subagent_notification>` 都按继续处理,不重新识别或清空待办。Claude 完成通知优先按 `<tool-use-id>` 匹配,其次按 `<task-id>` 等于 agentId 匹配,只有 `<status>completed</status>` 才满足(钩子输入无需 origin);failed/killed/cancelled/canceled 记失败,缺失或未知状态继续等待,不以 summary 或 output-file 推测完成。Hook 将 tool_response.resolvedModel 记入状态,并提示总控最终答复采用该宿主字段,不采用子代理自报;与配置模型明显不符时提示,不拦截、不重试。状态标签及 completed 值已从本机 Claude Code 2.1.307 通知序列化实现核实。失败或未核实记录原因,不重试、不替换模型。开始时点名未结束或任何后台职责进行中时 Stop 放行、不消耗催办;前序结束后才催完成时点名,每项最多一次,stop_hook_active 时直接放行,防止循环。
|
|
79
|
+
|
|
80
|
+
Codex 沿用护栏的插件形式,生成 `.codex/named-dispatch-plugin/.codex-plugin/plugin.json` 和 `hooks/hooks.json`,并生成本地来源清单 `.codex/.agents/plugins/marketplace.json`:`{"name":"odai-local-routing","plugins":[{"name":"odai-named-dispatch","source":"./named-dispatch-plugin"}]}`。source 相对于来源根目录 `.codex/`。本机 codex-cli 0.163.0 在隔离 CODEX_HOME 下已成功登记来源并安装生成的插件,不调用模型。安装后的插件命令指向安装目录内脚本的绝对路径,插件复制到缓存后仍使用原清单;移动安装目录需要重新安装路由和插件。安装器不执行登记、安装或信任操作,也不放宽沙箱。
|
|
81
|
+
|
|
82
|
+
用户在安装路由后执行(项目路径须为真实绝对路径;用户级安装把来源改成对应配置目录):
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
export CODEX_HOME="$(mktemp -d)"
|
|
86
|
+
codex plugin marketplace add /绝对路径/项目/.codex
|
|
87
|
+
codex plugin add odai-named-dispatch@odai-local-routing
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
保持同一 CODEX_HOME,在该临时项目中启动交互式 `codex`。如提示文件夹信任,审阅后选择 `Trust and continue`;在 `Hooks need review` 中选择 `Review hooks`,核对四个事件的命令确实指向已审阅的 `odai-route-hook.mjs`,再信任(`Trust` 或 `Trust all and continue`)。选择 `Continue without trusting` 时 Hook 不运行。以上选项来自本机二进制 UI 文本;本次没有执行交互式信任或模型请求。Hook 在信任后可在沙箱外运行,更新命令后须重新审阅。
|
|
91
|
+
|
|
92
|
+
本机依据:codex --help、codex plugin add --help、codex plugin marketplace add --help;codex-cli 二进制内的 user-prompt-submit/pre-tool-use/post-tool-use/stop.command.input/output schema。Claude Code 2.1.307 二进制的输入 schema、Hook 输出帮助与退出码说明明确 additionalContext 注入上下文、PreToolUse 退出 2 阻断工具,以及 Stop decision:block。agent_type 可出现在 --agent 主线程,只以 agent_id 区分子代理。仓库 integrations/hooks 提供两宿主的 Stop/PreToolUse 和插件格式先例。以上为静态契约依据,不代表真实宿主调用已验证。
|
|
93
|
+
|
|
94
|
+
卸载:`node skills/odai-orchestration/scripts/install-routing.mjs --host <claude|codex> --scope project --target <项目> --uninstall --yes`。也可用原映射参数更新且省略 `--named-dispatch` 和 `--lead-mode`,撤回路由 Hook,保留角色;Codex 另由用户卸载插件。
|
|
95
|
+
|
|
96
|
+
`--lead-mode` 安装主导能力后,说“开启主导模式”或“这个会话由 <已映射模型名> 主导”,用“关闭主导模式”退出;默认未开启。按宿主会话保存 `lead`(enabled、roles),新任务保留开关、清空任务进度,宿主通知只续作。只使用匹配的 planner/reviewer 插槽;缺失或不匹配时说明,不换模型。现有安装器仍要求 controller/planner/reviewer 映射;运行时兼容只含一个或缺少两个主导插槽的清单。
|
|
97
|
+
|
|
98
|
+
主导模式开启期间,每次 UserPromptSubmit 的直接用户任务都注入简短流程(开启时也注入):第一次改动前先派规划,有改动时做完派审查,审查回交最后单独一行写 `VERDICT: pass` 或 `VERDICT: changes-requested`,最终答复按宿主记录说明实际模型。只讲已启用插槽对应的部分;宿主通知不注入流程。规划拦截在启用审查插槽时也带上结论格式,供总控提前派审查使用。
|
|
99
|
+
|
|
100
|
+
第一次写类工具前才生成 planner 请求,与同职责同开始时机的点名合并;后台进行中禁止抢写,Stop 不消耗审查催办。成功的非职责写类 PostToolUse 累计 `writes`,没有写入不自动审查;任意命令与未知工具仍保守视作写类。Stop 生成 reviewer 请求,与完成时点名合并,要求回交末行 VERDICT。同步取 content;后台只读取启动回执记录的 outputFile(支持文本或 assistant JSONL);Codex 取 wait.completed;本机执行器取已核实证据的 output 文件或本次标准输出。缺少输出或结论冲突/无效时不追加轮次;下一次 Stop 拦一次,要求最终答复如实说明审查结论未按格式给出,以 invalidNudged 记录已提示。stop_hook_active 时放行且不消耗这次提示。共用解析器接受 Markdown、引号、全角冒号与句号,关键字不区分大小写,仍要求最后非空行是结论。
|
|
101
|
+
|
|
102
|
+
请求复用 done/result、inProgress、agentId、toolUseId、resolvedModel、nudged,审查另记 leadReview、round(0–2)、reviewWrites、handback、verdict、repairNudged。changes-requested 后下一次 Stop 催修正一次;有新的成功写入才再审,最多额外两轮。stop_hook_active 放行,不消耗催办;调用失败或主导审查模型不符只说明,不重试。用 `node --test scripts/lead-dispatch.test.mjs scripts/named-dispatch.test.mjs` 验证隔离夹具,不调用真实模型。
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## 选择职责
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
按会改变结果的缺口选择,不按角色名称预拆任务。调用前说明所缺判断或产物,以及预期贡献为何足以支持启动、交接和复验负担;任务复杂、高后果或已有配置均不自动触发。用户明确点名某项已配置职责或其映射的模型时照办,并按用户说的时机调用;未配置、所点模型与映射不符或调用失败时如实说明,不改用其他职责或模型,也不擅自改配置。
|
|
8
8
|
|
|
9
9
|
- **researcher**:事实问题的材料规模、专业性或压缩价值足以支持分工时,返回有界来源账本;来源数量不作门槛。直接可答的普通查看和实现调试直接处理,只有冲突或关键事实需要交叉核实时才补独立来源。账本只是索引,总控或 planner 抽查会改路的原始来源;不因为模型便宜而制造调查。
|
|
10
10
|
- **planner**:关键前提、路线或验收仍有实质分支,且独立上下文能补当前判断时使用。局部决定只回交该决定;明确委托正式计划才覆盖该计划范围,不预做实施。接回后依原有效授权继续,不额外等待“继续”。
|
|
11
11
|
- **reviewer**:独立判断能改变尚未放行的具体属性,或用户明确要求独立复核时使用。点名属性与必要依赖,完整需求覆盖须明确委托;不自设验收或扩围,不重跑仍有效的成功检查。按绑定本次范围与版本的可追溯原件判断,读取方式遵循宿主权限和本次授权,缺失原件保留未判定。
|
|
12
12
|
- **frontend**:界面设计或前端制作存在当前能力不能可靠闭合的专业缺口时使用。局部样式与文案通常直接完成;目标或范围未定先回交决定缺口。宿主可在当前线程或独立上下文取得该能力,权限分别核实。
|
|
13
13
|
- **creative**:发散构思、面向用户或读者的写作与表达、视觉与表现方向或品味把关存在当前能力不能可靠闭合的专业缺口时使用,不限领域。普通措辞调整、已定稿内容的机械替换和局部文字修正直接完成;目标、受众或风格约定未定先回交决定缺口。只读,回交构思、候选、取舍依据或审查意见,由总控核对后落盘。
|
|
14
|
+
- **writer**:方向已定、需要起草、改写或翻译面向用户或读者的文字,且交给专门的写作能力能改善结果时使用,不限领域。方向、构思与品味把关仍归 creative;未配置 writer 时由 creative 照旧承接写作。说明不足以写出可用成稿时先回交缺口。只读,回交成稿或候选,由总控核对后落盘。
|
|
14
15
|
|
|
15
16
|
这些是预设而非能力上限。其他工具、技能或原生子代理可承接有界工作,但不得靠改名绕过只读、授权或证据边界。实际模型、权限、上下文与独立性分别核实;共用原件的独立推理可以提供复核,同源转述不增加原始证据。
|
|
16
17
|
|
|
@@ -43,17 +43,21 @@ function printHelp() {
|
|
|
43
43
|
console.log(`Usage:
|
|
44
44
|
node skills/odai-orchestration/scripts/build-routing.mjs --host <codex|claude|copilot> --out <directory> [--governance-root <path>] \\
|
|
45
45
|
--controller-model <model> --planner-model <model> --reviewer-model <model> \\
|
|
46
|
-
[--researcher-model <model>] [--frontend-model <model>] [--creative-model <model>] [--controller-effort <effort>] \\
|
|
46
|
+
[--researcher-model <model>] [--frontend-model <model>] [--creative-model <model>] [--writer-model <model>] [--controller-effort <effort>] \\
|
|
47
47
|
[--researcher-effort <effort>] [--planner-effort <effort>] [--reviewer-effort <effort>] \\
|
|
48
|
-
[--frontend-effort <effort>] [--creative-effort <effort>] [--verifier-command <command>]
|
|
49
|
-
Claude
|
|
48
|
+
[--frontend-effort <effort>] [--creative-effort <effort>] [--writer-effort <effort>] [--verifier-command <command>]
|
|
49
|
+
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)
|
|
50
50
|
|
|
51
|
-
生成 odai 的可选宿主 auto 路由适配器。一个持续总控负责实施整合与最终交付,只在证据压缩、独立规划、独立验收、前端专业制作或创意专业判断能改变结果时调用相应责任;researcher、frontend 与
|
|
51
|
+
生成 odai 的可选宿主 auto 路由适配器。一个持续总控负责实施整合与最终交付,只在证据压缩、独立规划、独立验收、前端专业制作或创意专业判断能改变结果时调用相应责任;researcher、frontend、creative 与 writer 映射默认不生成。Claude Code 可把非 controller 职责显式映射到只读本机 CLI;不增加第二总控或隐藏的每轮前置流程。`);
|
|
52
52
|
process.exit(0);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
assertKnownArgs();
|
|
56
56
|
const host = option("--host");
|
|
57
|
+
const leadMode = argv.includes("--lead-mode");
|
|
58
|
+
if (leadMode && !["claude", "codex"].includes(host)) throw new Error("--lead-mode requires claude or codex");
|
|
59
|
+
const namedDispatch = argv.includes("--named-dispatch");
|
|
60
|
+
if (namedDispatch && !["claude", "codex"].includes(host)) throw new Error("--named-dispatch requires claude or codex");
|
|
57
61
|
const out = option("--out");
|
|
58
62
|
const models = {
|
|
59
63
|
controller: option("--controller-model"),
|
|
@@ -62,6 +66,7 @@ const models = {
|
|
|
62
66
|
reviewer: option("--reviewer-model"),
|
|
63
67
|
frontend: option("--frontend-model"),
|
|
64
68
|
creative: option("--creative-model"),
|
|
69
|
+
writer: option("--writer-model"),
|
|
65
70
|
};
|
|
66
71
|
const efforts = {
|
|
67
72
|
controller: option("--controller-effort"),
|
|
@@ -70,20 +75,31 @@ const efforts = {
|
|
|
70
75
|
reviewer: option("--reviewer-effort"),
|
|
71
76
|
frontend: option("--frontend-effort"),
|
|
72
77
|
creative: option("--creative-effort"),
|
|
78
|
+
writer: option("--writer-effort"),
|
|
73
79
|
};
|
|
74
80
|
const localRoutes = {};
|
|
75
|
-
for (const role of ["researcher", "planner", "reviewer", "frontend", "creative"]) {
|
|
81
|
+
for (const role of ["researcher", "planner", "reviewer", "frontend", "creative", "writer"]) {
|
|
76
82
|
const cli = option(`--${role}-cli`);
|
|
77
83
|
if (!cli) continue;
|
|
78
|
-
if (
|
|
79
|
-
if (!["codex", "antigravity", "grok"].includes(cli)) throw new Error(`--${role}-cli must be codex, antigravity, or grok`);
|
|
84
|
+
if (!["claude", "codex"].includes(host)) throw new Error(`--${role}-cli requires claude or codex`);
|
|
85
|
+
if (!(host === "codex" ? ["claude", "antigravity", "grok"] : ["codex", "antigravity", "grok"]).includes(cli)) throw new Error(`--${role}-cli must be ${host === "codex" ? "claude" : "codex"}, antigravity, or grok`);
|
|
80
86
|
localRoutes[role] = validateLocalCliRoute({ backend: "local-cli", cli, model: models[role],
|
|
81
87
|
...(efforts[role] ? { reasoningEffort: efforts[role] } : {}) });
|
|
82
88
|
}
|
|
89
|
+
const sessions = {};
|
|
90
|
+
for (const role of Object.keys(models)) {
|
|
91
|
+
const session = option(`--${role}-session`);
|
|
92
|
+
if (!session) continue;
|
|
93
|
+
if (role === "controller" || host === "copilot" || !models[role] || !["fresh", "continue"].includes(session)) throw new Error("Invalid role session mapping");
|
|
94
|
+
if (session === "continue" && localRoutes[role]) validateLocalCliRoute({ ...localRoutes[role], session });
|
|
95
|
+
if (session === "continue" && host === "codex" && !localRoutes[role]) throw new Error("Codex 原生角色暂不支持会话续接:证据不足");
|
|
96
|
+
sessions[role] = session;
|
|
97
|
+
}
|
|
83
98
|
const hasLocalRoutes = Object.keys(localRoutes).length > 0;
|
|
84
99
|
const roleMapping = (role) => ({
|
|
85
100
|
...(localRoutes[role] ? { backend: "local-cli", cli: localRoutes[role].cli } : { provider: host }),
|
|
86
101
|
model: models[role], reasoning_effort: efforts[role] || null,
|
|
102
|
+
...(sessions[role] ? { session: sessions[role] } : {}),
|
|
87
103
|
});
|
|
88
104
|
const verifierCommand = option("--verifier-command") || "node .codex/odai-verify-routing.mjs";
|
|
89
105
|
const requiredRoles = ["controller", "planner", "reviewer"];
|
|
@@ -92,8 +108,10 @@ const roles = Object.freeze([
|
|
|
92
108
|
...(models.researcher ? ["researcher"] : []),
|
|
93
109
|
...(models.frontend ? ["frontend"] : []),
|
|
94
110
|
...(models.creative ? ["creative"] : []),
|
|
111
|
+
...(models.writer ? ["writer"] : []),
|
|
95
112
|
]);
|
|
96
113
|
const descriptions = {
|
|
114
|
+
writer: "只在方向已定、需要起草、改写或翻译面向用户或读者的文字时提供成稿或候选。",
|
|
97
115
|
controller: "持续持有用户目标、全局状态、修正回路与最终交付。",
|
|
98
116
|
researcher: "只为会改变决定的事实缺口返回有界来源账本,调用前有收益依据,回交后验贡献。",
|
|
99
117
|
planner: "只在独立判断能改变路线时形成有界的证据化规划。",
|
|
@@ -107,6 +125,7 @@ if (!out) throw new Error("--out requires a directory");
|
|
|
107
125
|
for (const role of requiredRoles) if (!models[role]) throw new Error(`--${role}-model is required`);
|
|
108
126
|
if (efforts.researcher && !models.researcher) throw new Error("--researcher-effort requires --researcher-model");
|
|
109
127
|
if (efforts.frontend && !models.frontend) throw new Error("--frontend-effort requires --frontend-model");
|
|
128
|
+
if (efforts.writer && !models.writer) throw new Error("--writer-effort requires --writer-model");
|
|
110
129
|
if (efforts.creative && !models.creative) throw new Error("--creative-effort requires --creative-model");
|
|
111
130
|
if (host === "copilot" && Object.values(efforts).some(Boolean)) {
|
|
112
131
|
throw new Error("Copilot custom-agent profiles do not provide a portable reasoning-effort field");
|
|
@@ -142,21 +161,23 @@ const metadata = {
|
|
|
142
161
|
creative_activation: models.creative
|
|
143
162
|
? "only-when-player-or-user-facing-content-needs-specialist-creative-judgment"
|
|
144
163
|
: "unconfigured",
|
|
164
|
+
writer_activation: models.writer ? "only-when-reader-facing-writing-needs-specialist-production" : "unconfigured",
|
|
145
165
|
controller_owns_implementation: true,
|
|
146
166
|
sufficient_controller_defaults_to_single_pass: true,
|
|
147
167
|
},
|
|
148
|
-
runtime_verification: host === "codex"
|
|
168
|
+
runtime_verification: host === "codex" && !hasLocalRoutes
|
|
149
169
|
? { mode: "post-run-executable", command: verifierCommand }
|
|
150
170
|
: hasLocalRoutes ? {
|
|
151
171
|
mode: "per-role-evidence-required",
|
|
152
172
|
roles: Object.fromEntries(roles.map(role => [role, localRoutes[role]
|
|
153
|
-
? { mode: "role-executor-evidence", command: `node
|
|
173
|
+
? { mode: "role-executor-evidence", command: `node .${host}/odai-run-role.mjs --role ${role} --input <file>` }
|
|
154
174
|
: { mode: "host-native-evidence-required", command: null }])),
|
|
155
175
|
} : { mode: "host-native-evidence-required", command: null },
|
|
156
176
|
activation: activation(host),
|
|
157
177
|
profiles: roles.map((role) => ({ name: `odai-${role}`, purpose: descriptions[role] })),
|
|
158
178
|
};
|
|
159
179
|
writeFileSync(path.join(target, "ADAPTER.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
180
|
+
if (namedDispatch || leadMode) buildNamedDispatch();
|
|
160
181
|
console.log(target);
|
|
161
182
|
|
|
162
183
|
function buildCodex(root) {
|
|
@@ -172,14 +193,18 @@ function buildCodex(root) {
|
|
|
172
193
|
__ODAI_CONTROLLER_BODY__: roleBody("controller", "codex"),
|
|
173
194
|
__ODAI_AGENT_SECTIONS__: codexAgentSections(),
|
|
174
195
|
});
|
|
175
|
-
for (const role of roles.slice(1)) {
|
|
196
|
+
for (const role of roles.slice(1).filter(role => !localRoutes[role])) {
|
|
176
197
|
writeRendered(path.join(sourceDir, "role.toml"), path.join(agentsRoot, `odai-${role}.toml`), {
|
|
177
198
|
__ODAI_ROLE_MODEL__: JSON.stringify(models[role]),
|
|
178
199
|
__ODAI_ROLE_EFFORT_LINE__: tomlEffortLine(efforts[role]),
|
|
179
|
-
__ODAI_ROLE_SANDBOX_LINE__:
|
|
200
|
+
__ODAI_ROLE_SANDBOX_LINE__: ["creative", "writer"].includes(role) ? 'sandbox_mode = "read-only"' : "",
|
|
180
201
|
__ODAI_ROLE_BODY__: roleBody(role, "codex"),
|
|
181
202
|
});
|
|
182
203
|
}
|
|
204
|
+
if (hasLocalRoutes) {
|
|
205
|
+
copyScript("local-cli.mjs", path.join(configRoot, "odai-local-cli.mjs"));
|
|
206
|
+
writeFileSync(path.join(configRoot, "odai-routing.json"), `${JSON.stringify({ id: "odai-routing-installation", host: "codex", mapping: Object.fromEntries(roles.map(role => [role, roleMapping(role)])) }, null, 2)}\n`);
|
|
207
|
+
}
|
|
183
208
|
copyScript("verify-routing.mjs", path.join(configRoot, "odai-verify-routing.mjs"));
|
|
184
209
|
copyScript("run-role.mjs", path.join(configRoot, "odai-run-role.mjs"));
|
|
185
210
|
for (const role of roles) writeFileSync(path.join(contractsRoot, `odai-${role}.md`), roleBody(role, "codex"), "utf8");
|
|
@@ -190,7 +215,7 @@ function buildClaude(root) {
|
|
|
190
215
|
const agentsRoot = path.join(root, ".claude", "agents");
|
|
191
216
|
// Never leave a native proxy beside a local route, or reuse a stale local manifest.
|
|
192
217
|
// Build output has no installation ownership record: reject instead of deleting files.
|
|
193
|
-
for (const role of ["researcher", "planner", "reviewer", "frontend", "creative"]) {
|
|
218
|
+
for (const role of ["researcher", "planner", "reviewer", "frontend", "creative", "writer"]) {
|
|
194
219
|
const opposite = localRoutes[role] ? `agents/odai-${role}.md` : `role-contracts/odai-${role}.md`;
|
|
195
220
|
if (existsSync(path.join(root, ".claude", opposite))) throw new Error(`Conflicting previous routing output: ${opposite}; use a fresh --out directory`);
|
|
196
221
|
}
|
|
@@ -210,7 +235,7 @@ function buildClaude(root) {
|
|
|
210
235
|
__ODAI_ROLE_DESCRIPTION__: descriptions[role],
|
|
211
236
|
__ODAI_ROLE_MODEL__: models[role],
|
|
212
237
|
__ODAI_ROLE_EFFORT_LINE__: yamlEffortLine(efforts[role]),
|
|
213
|
-
__ODAI_PERMISSION_MODE__: ["researcher", "planner", "reviewer", "creative"].includes(role) ? "plan" : "default",
|
|
238
|
+
__ODAI_PERMISSION_MODE__: ["researcher", "planner", "reviewer", "creative", "writer"].includes(role) ? "plan" : "default",
|
|
214
239
|
__ODAI_TOOLS_LINE__: "",
|
|
215
240
|
__ODAI_ROLE_BODY__: roleBody(role, "claude"),
|
|
216
241
|
});
|
|
@@ -240,7 +265,7 @@ function buildCopilot(root) {
|
|
|
240
265
|
__ODAI_ROLE_MODEL__: models[role],
|
|
241
266
|
__ODAI_DISABLE_MODEL_INVOCATION__: role === "controller" ? "true" : "false",
|
|
242
267
|
__ODAI_USER_INVOCABLE__: role === "controller" ? "true" : "false",
|
|
243
|
-
__ODAI_TOOLS__: ["researcher", "creative"].includes(role)
|
|
268
|
+
__ODAI_TOOLS__: ["researcher", "creative", "writer"].includes(role)
|
|
244
269
|
? '["view", "glob", "grep"]'
|
|
245
270
|
: ["planner", "reviewer"].includes(role) ? '["view", "glob", "grep", "shell"]' : '["*"]',
|
|
246
271
|
__ODAI_ROLE_BODY__: roleBody(role, "copilot"),
|
|
@@ -257,11 +282,11 @@ function roleBody(role, hostName) {
|
|
|
257
282
|
const source = path.join(skillRoot, manifest.roleFiles[role]);
|
|
258
283
|
if (!existsSync(source)) throw new Error(`Missing canonical routing role body: ${source}`);
|
|
259
284
|
const names = hostName === "codex"
|
|
260
|
-
? { researcher: "odai_researcher", planner: "odai_planner", reviewer: "odai_reviewer", frontend: "odai_frontend", creative: "odai_creative" }
|
|
261
|
-
: { researcher: "odai-researcher", planner: "odai-planner", reviewer: "odai-reviewer", frontend: "odai-frontend", creative: "odai-creative" };
|
|
262
|
-
if (
|
|
285
|
+
? { researcher: "odai_researcher", planner: "odai_planner", reviewer: "odai_reviewer", frontend: "odai_frontend", creative: "odai_creative", writer: "odai_writer" }
|
|
286
|
+
: { researcher: "odai-researcher", planner: "odai-planner", reviewer: "odai-reviewer", frontend: "odai-frontend", creative: "odai-creative", writer: "odai-writer" };
|
|
287
|
+
if (["claude", "codex"].includes(hostName)) {
|
|
263
288
|
for (const [localRole, route] of Object.entries(localRoutes)) {
|
|
264
|
-
names[localRole] += `(本机 ${route.cli},只读;把有界任务包写入文件后运行 node
|
|
289
|
+
names[localRole] += `(本机 ${route.cli},只读;把有界任务包写入文件后运行 node .${hostName}/odai-run-role.mjs --role ${localRole} --input <文件>)`;
|
|
265
290
|
}
|
|
266
291
|
}
|
|
267
292
|
const rendered = renderText(composeRoleContract(role, governance, manifest, contractContents), {
|
|
@@ -270,19 +295,24 @@ function roleBody(role, hostName) {
|
|
|
270
295
|
__ODAI_PLANNER_ROLE__: names.planner,
|
|
271
296
|
__ODAI_REVIEWER_ROLE__: names.reviewer,
|
|
272
297
|
__ODAI_FRONTEND_ROLE__: models.frontend ? names.frontend : "frontend(当前适配器未配置映射,不能调用)",
|
|
298
|
+
__ODAI_WRITER_ROLE__: models.writer ? names.writer : "writer(当前适配器未配置映射,不能调用)",
|
|
273
299
|
__ODAI_CREATIVE_ROLE__: models.creative ? names.creative : "creative(当前适配器未配置映射,不能调用)",
|
|
274
300
|
__ODAI_RUNTIME_VERIFICATION__: hostName === "codex"
|
|
275
|
-
? "实际路由必须用宿主返回的线程、角色、模型与用量证据核对,不从调用请求推断成功。"
|
|
301
|
+
? (hasLocalRoutes ? "原生角色用宿主运行证据核对;本机 CLI 只接受执行器退出 0 且模型已核实的 version 1 证据,证据路径见 stderr;非 0 退出时不能接收正文为成功。" : "实际路由必须用宿主返回的线程、角色、模型与用量证据核对,不从调用请求推断成功。")
|
|
276
302
|
: hasLocalRoutes
|
|
277
303
|
? "原生子代理用宿主运行证据核实角色与模型;本机 CLI 用角色执行器的 version 1 证据核实,证据路径见 stderr。执行器非 0 退出时不能接收正文为成功;显式请求的推理档也必须核实。"
|
|
278
304
|
: "只有宿主原生运行证据能识别实际角色与模型时,路由才算已核实。",
|
|
279
305
|
__ODAI_HOST_NOTE__: hostName === "copilot" ? "Copilot Auto 会覆盖角色模型选择;需要区分角色时不使用 Auto。" : "",
|
|
280
306
|
}, source);
|
|
281
|
-
return rendered
|
|
307
|
+
return rendered + (role === "controller" ? [
|
|
308
|
+
...(hostName === "codex" && hasLocalRoutes ? ["\n\n默认 workspace-write 沙箱中,本机 CLI 职责实测因无法联网而失败(claude 零 token、退出码 1)。运行上述具体执行器命令时,需要申请越出沙箱,理由是需要联网,并写入本机 CLI 自己的目录;由用户按自己的审批策略批准该命令。用户拒绝或当前审批策略不允许时,如实报告该职责不可用,不改用别的模型。安装器不修改沙箱或联网设置。越出后 CLI 可联网并写用户目录;撤回时拒绝后续批准,恢复用户自行修改的配置。"] : []),
|
|
309
|
+
...Object.keys(sessions).filter(r => sessions[r] === "continue" && !localRoutes[r] && hostName === "claude").map(r => `\n\n${r} 开启会话续接时,用 SendMessage 继续上次派出的同一个 odai-${r} 子代理(可按名字或 agentId 续发,已完成的子代理也可以续发),不新派。`),
|
|
310
|
+
...(Object.values(sessions).includes("continue") && hasLocalRoutes ? ["\n\n本机 CLI 续接键按清单宿主优先取对应环境变量(Claude: CLAUDE_CODE_SESSION_ID;Codex: CODEX_THREAD_ID),另一变量作为后备;缺失时调用执行器须传 --session-key <宿主会话唯一键>。"] : []),
|
|
311
|
+
].join("") : "");
|
|
282
312
|
}
|
|
283
313
|
|
|
284
314
|
function codexAgentSections() {
|
|
285
|
-
return roles.slice(1).flatMap((role) => [
|
|
315
|
+
return roles.slice(1).filter(role => !localRoutes[role]).flatMap((role) => [
|
|
286
316
|
`[agents.odai_${role}]`,
|
|
287
317
|
`description = ${JSON.stringify(descriptions[role])}`,
|
|
288
318
|
`config_file = ${JSON.stringify(`agents/odai-${role}.toml`)}`,
|
|
@@ -311,13 +341,15 @@ function copyScript(name, targetFile) {
|
|
|
311
341
|
|
|
312
342
|
function assertKnownArgs() {
|
|
313
343
|
const known = new Set([
|
|
344
|
+
...["researcher", "planner", "reviewer", "frontend", "creative", "writer"].map(role => `--${role}-session`),
|
|
314
345
|
"--host", "--out", "--governance-root", "--controller-model", "--researcher-model", "--planner-model",
|
|
315
|
-
"--reviewer-model", "--frontend-model", "--creative-model", "--controller-effort", "--researcher-effort",
|
|
316
|
-
"--planner-effort", "--reviewer-effort", "--frontend-effort", "--creative-effort", "--verifier-command",
|
|
317
|
-
"--researcher-cli", "--planner-cli", "--reviewer-cli", "--frontend-cli", "--creative-cli",
|
|
346
|
+
"--reviewer-model", "--frontend-model", "--creative-model", "--writer-model", "--controller-effort", "--researcher-effort",
|
|
347
|
+
"--planner-effort", "--reviewer-effort", "--frontend-effort", "--creative-effort", "--writer-effort", "--verifier-command",
|
|
348
|
+
"--researcher-cli", "--planner-cli", "--reviewer-cli", "--frontend-cli", "--creative-cli", "--writer-cli",
|
|
318
349
|
]);
|
|
319
350
|
for (let index = 0; index < argv.length; index += 2) {
|
|
320
351
|
const name = argv[index];
|
|
352
|
+
if (["--named-dispatch", "--lead-mode"].includes(name)) { index -= 1; continue; }
|
|
321
353
|
if (!known.has(name)) throw new Error(`Unknown option: ${name}`);
|
|
322
354
|
const value = argv[index + 1] || "";
|
|
323
355
|
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
@@ -346,3 +378,25 @@ function renderText(input, replacements, source = "generated routing text") {
|
|
|
346
378
|
if (unresolved) throw new Error(`Unresolved routing template fields in ${source}: ${[...new Set(unresolved)].join(", ")}`);
|
|
347
379
|
return text;
|
|
348
380
|
}
|
|
381
|
+
|
|
382
|
+
function buildNamedDispatch() {
|
|
383
|
+
const config = path.join(target, `.${host}`);
|
|
384
|
+
const hooks = Object.fromEntries(["UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"].map(event => [event, [{ matcher: "*", hooks: [{ type: "command", command: `node "${host === "codex" ? "${PLUGIN_ROOT}/../.." : ".claude"}/odai-route-hook.mjs"`, timeout: 10 }] }]]));
|
|
385
|
+
copyScript("route-hook.mjs", path.join(config, "odai-route-hook.mjs"));
|
|
386
|
+
copyScript("recognize-routing.mjs", path.join(config, "recognize-routing.mjs"));
|
|
387
|
+
writeFileSync(path.join(config, "odai-named-dispatch.json"), JSON.stringify({ id: "odai-named-dispatch", host, ...(leadMode ? { leadMode: true, namedDispatch } : {}), mapping: metadata.mapping }, null, 2) + "\n");
|
|
388
|
+
if (host === "claude") {
|
|
389
|
+
const file = path.join(config, "settings.patch.json");
|
|
390
|
+
writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file, "utf8")), hooks }, null, 2) + "\n");
|
|
391
|
+
} else {
|
|
392
|
+
const plugin = path.join(config, "named-dispatch-plugin");
|
|
393
|
+
mkdirSync(path.join(plugin, ".codex-plugin"), { recursive: true });
|
|
394
|
+
mkdirSync(path.join(plugin, "hooks"), { recursive: true });
|
|
395
|
+
mkdirSync(path.join(config, ".agents/plugins"), { recursive: true });
|
|
396
|
+
writeFileSync(path.join(config, ".agents/plugins/marketplace.json"), JSON.stringify({ name: "odai-local-routing", plugins: [{ name: "odai-named-dispatch", source: "./named-dispatch-plugin" }] }, null, 2) + "\n");
|
|
397
|
+
// PLUGIN_ROOT is the plugin directory, whose parent contains the executor.
|
|
398
|
+
for (const groups of Object.values(hooks)) groups[0].hooks[0].command = 'node "${PLUGIN_ROOT}/../odai-route-hook.mjs"';
|
|
399
|
+
writeFileSync(path.join(plugin, ".codex-plugin/plugin.json"), JSON.stringify({ id: "odai-named-dispatch", name: "odai-named-dispatch", version: "0.2.3", description: "Optional named responsibility routing" }));
|
|
400
|
+
writeFileSync(path.join(plugin, "hooks/hooks.json"), JSON.stringify({ hooks }, null, 2));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const RUNTIME_CONTRACT: 9;
|
|
2
2
|
export const MANIFEST_SCHEMA: 1;
|
|
3
3
|
export const ORCHESTRATION_URL: URL;
|
|
4
|
-
export const ROLE_NAMES: readonly ["controller", "researcher", "planner", "reviewer", "frontend", "creative"];
|
|
4
|
+
export const ROLE_NAMES: readonly ["controller", "researcher", "planner", "reviewer", "frontend", "creative", "writer"];
|
|
5
5
|
export const REFERENCE_NAMES: readonly ["dao", "planning", "craft", "verification", "memory", "leverage", "care", "human-safety", "orchestration"];
|
|
6
6
|
export type RoleName = typeof ROLE_NAMES[number];
|
|
7
7
|
export type ReferenceName = typeof REFERENCE_NAMES[number];
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
export const RUNTIME_CONTRACT = 9;
|
|
3
3
|
export const MANIFEST_SCHEMA = 1;
|
|
4
4
|
export const ORCHESTRATION_URL = new URL("../", import.meta.url);
|
|
5
|
-
export const ROLE_NAMES = Object.freeze(["controller", "researcher", "planner", "reviewer", "frontend", "creative"]);
|
|
5
|
+
export const ROLE_NAMES = Object.freeze(["controller", "researcher", "planner", "reviewer", "frontend", "creative", "writer"]);
|
|
6
6
|
export const REFERENCE_NAMES = Object.freeze(["dao", "planning", "craft", "verification", "memory", "leverage", "care", "human-safety", "orchestration"]);
|
|
7
|
-
const REQUIRED_REFERENCES = { controller: ["orchestration"], researcher: [], planner: ["planning"], reviewer: ["verification"], frontend: ["craft"], creative: ["craft"] };
|
|
7
|
+
const REQUIRED_REFERENCES = { controller: ["orchestration"], researcher: [], planner: ["planning"], reviewer: ["verification"], frontend: ["craft"], creative: ["craft"], writer: ["craft"] };
|
|
8
8
|
function object(value, label) {
|
|
9
9
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object`);
|
|
10
10
|
return value;
|
|
@@ -24,13 +24,14 @@ export function validateCompositionManifest(manifest) {
|
|
|
24
24
|
if (typeof manifest.version !== "string" || !manifest.version) throw new TypeError("orchestration version is required");
|
|
25
25
|
if (!Array.isArray(manifest.requiredFiles) || manifest.requiredFiles.some(file => !safePath(file))
|
|
26
26
|
|| new Set(manifest.requiredFiles).size !== manifest.requiredFiles.length) throw new TypeError("invalid orchestration requiredFiles");
|
|
27
|
-
|
|
28
|
-
exactKeys(manifest.
|
|
27
|
+
const roles = ROLE_NAMES;
|
|
28
|
+
exactKeys(manifest.roleFiles, roles, "roleFiles");
|
|
29
|
+
exactKeys(manifest.rolePresets, roles, "rolePresets");
|
|
29
30
|
exactKeys(manifest.referenceFiles, ["orchestration"], "referenceFiles");
|
|
30
31
|
const owned = [manifest.delegationFile, ...Object.values(manifest.roleFiles), ...Object.values(manifest.referenceFiles)];
|
|
31
32
|
if (new Set(owned).size !== owned.length || owned.some(file => !safePath(file) || !manifest.requiredFiles.includes(file))) throw new TypeError("orchestration owners must use distinct declared files");
|
|
32
33
|
const presets = {};
|
|
33
|
-
for (const role of
|
|
34
|
+
for (const role of roles) {
|
|
34
35
|
const preset = manifest.rolePresets[role];
|
|
35
36
|
exactKeys(preset, ["references"], `rolePresets.${role}`);
|
|
36
37
|
if (!Array.isArray(preset.references) || new Set(preset.references).size !== preset.references.length
|