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.
Files changed (70) hide show
  1. package/README.md +11 -9
  2. package/client/client.js +12 -5
  3. package/package.json +1 -1
  4. package/preset/odai/package.json +2 -1
  5. package/preset/odai/runtime/context-activation.mjs +1 -1
  6. package/preset/odai/runtime/context-activation.mjs.map +1 -1
  7. package/preset/odai/runtime/index.d.mts.map +1 -1
  8. package/preset/odai/runtime/index.mjs +51 -2
  9. package/preset/odai/runtime/index.mjs.map +1 -1
  10. package/preset/odai/runtime/lead-mode.d.mts +23 -0
  11. package/preset/odai/runtime/lead-mode.d.mts.map +1 -0
  12. package/preset/odai/runtime/lead-mode.mjs +17 -0
  13. package/preset/odai/runtime/lead-mode.mjs.map +1 -0
  14. package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
  15. package/preset/odai/runtime/lifecycle-runtime.mjs +196 -13
  16. package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
  17. package/preset/odai/runtime/responsibility-gap.d.mts +4 -1
  18. package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
  19. package/preset/odai/runtime/responsibility-gap.mjs +1 -0
  20. package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
  21. package/preset/odai/runtime/role-overlays.d.mts +1 -1
  22. package/preset/odai/runtime/role-overlays.d.mts.map +1 -1
  23. package/preset/odai/runtime/role-overlays.mjs +4 -1
  24. package/preset/odai/runtime/role-overlays.mjs.map +1 -1
  25. package/preset/odai/runtime/route-session.d.mts +20 -0
  26. package/preset/odai/runtime/route-session.d.mts.map +1 -0
  27. package/preset/odai/runtime/route-session.mjs +44 -0
  28. package/preset/odai/runtime/route-session.mjs.map +1 -0
  29. package/preset/odai/runtime/router.d.mts +2 -1
  30. package/preset/odai/runtime/router.d.mts.map +1 -1
  31. package/preset/odai/runtime/router.mjs +52 -1
  32. package/preset/odai/runtime/router.mjs.map +1 -1
  33. package/preset/odai/runtime/routing-config.d.mts +5 -2
  34. package/preset/odai/runtime/routing-config.d.mts.map +1 -1
  35. package/preset/odai/runtime/routing-config.mjs +25 -14
  36. package/preset/odai/runtime/routing-config.mjs.map +1 -1
  37. package/preset/odai/runtime/runtime-config.d.mts +1 -1
  38. package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
  39. package/preset/odai/runtime/runtime-support.d.mts +6 -1
  40. package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
  41. package/preset/odai/runtime/runtime-support.mjs +13 -7
  42. package/preset/odai/runtime/runtime-support.mjs.map +1 -1
  43. package/preset/odai/runtime/runtime-types.d.mts +2 -0
  44. package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
  45. package/preset/odai/runtime/runtime-types.mjs.map +1 -1
  46. package/preset/odai/runtime/session-evidence.d.mts.map +1 -1
  47. package/preset/odai/runtime/session-evidence.mjs +6 -0
  48. package/preset/odai/runtime/session-evidence.mjs.map +1 -1
  49. package/preset/odai/runtime/skill-bundle.d.mts +1 -1
  50. package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
  51. package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
  52. package/preset/odai/runtime/tool-runtime.mjs +28 -0
  53. package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
  54. package/preset/odai/skills/odai-orchestration/SKILL.md +1 -1
  55. package/preset/odai/skills/odai-orchestration/assets/routing-roles/controller.md +1 -1
  56. package/preset/odai/skills/odai-orchestration/assets/routing-roles/writer.md +9 -0
  57. package/preset/odai/skills/odai-orchestration/manifest.json +10 -4
  58. package/preset/odai/skills/odai-orchestration/references/install.md +66 -4
  59. package/preset/odai/skills/odai-orchestration/references/orchestration.md +2 -1
  60. package/preset/odai/skills/odai-orchestration/scripts/build-routing.mjs +78 -24
  61. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +1 -1
  62. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +6 -5
  63. package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +66 -15
  64. package/preset/odai/skills/odai-orchestration/scripts/local-cli.d.mts +8 -0
  65. package/preset/odai/skills/odai-orchestration/scripts/local-cli.mjs +25 -9
  66. package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.d.mts +6 -0
  67. package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.mjs +93 -0
  68. package/preset/odai/skills/odai-orchestration/scripts/route-hook.mjs +243 -0
  69. package/preset/odai/skills/odai-orchestration/scripts/run-role.mjs +71 -16
  70. package/preset/odai/skills/odai-orchestration/scripts/verify-routing.mjs +2 -2
@@ -2,14 +2,14 @@
2
2
 
3
3
  用户要求为 Codex、Claude Code 或 Copilot 安装、更新、卸载或修复职责路由时读取。安装本技能和 `odai` 只使规则与脚本可发现,不自动修改宿主配置;DSH 已内置编排时使用其现有能力,不运行这里的安装器。
4
4
 
5
- 1. 从宿主确认实际模型、能力与作用域。controller、planner、reviewer 显式映射,researcher、frontend 和 creative 按需选择,默认不生成,也不从其他职责继承映射;creative 只读,交回内容由总控核对后落盘;从真实模型目录核对,不虚构模型 ID。默认当前项目,跨项目作用域须明确授权。
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 始终保持原生。其他宿主不接受这些 CLI 选项,`claude` 不作为本机目标重复配置。
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` 时持久保存在执行器同目录 `role-evidence/`,路径写到 stderr。证据是运行产物,卸载保留。只有调用完成、正文非空且实际模型核实时才输出正文并退出 0;能取得推理档证据的 CLI 出现推理档不一致时同样失败。失败退出非 0,不输出正文、不覆盖已有正文文件。Codex 通过本次会话的模型与推理档取证;Antigravity 和 Grok 目前只能把推理档记为 `requested-only`,证据如实记录、不阻断回交,与 DSH 的规则一致。
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 使用计划模式并禁用斜杠命令展开,制作请求可能返回实施计划;其浏览器、联网和生成工具是否全部受只读限制尚未逐项实测。Grok 适配继续标注**按官方文档及既有评测证据实现,当前适配未实测**:优先取 JSON `modelUsage`,缺失时回退会话 summary;格式不符或证据缺失就失败。各 CLI 自身的会话或计划目录可能产生运行记录,只读边界针对被委托项目。
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 Code only: [--<role>-cli <codex|antigravity|grok>] (non-controller roles)
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 与 creative 映射默认不生成。Claude Code 可把非 controller 职责显式映射到只读本机 CLI;不增加第二总控或隐藏的每轮前置流程。`);
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 (host !== "claude") throw new Error(`--${role}-cli is only supported with --host claude`);
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 .claude/odai-run-role.mjs --role ${role} --input <file>` }
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__: role === "creative" ? 'sandbox_mode = "read-only"' : "",
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 (hostName === "claude") {
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 .claude/odai-run-role.mjs --role ${localRole} --input <文件>)`;
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
- exactKeys(manifest.roleFiles, ROLE_NAMES, "roleFiles");
28
- exactKeys(manifest.rolePresets, ROLE_NAMES, "rolePresets");
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 ROLE_NAMES) {
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