feat(orc): centralize host-aware routing
This commit is contained in:
+22
-14
@@ -1,7 +1,7 @@
|
||||
# orc
|
||||
|
||||
ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Docker 任务拆成阶段,
|
||||
交给对应 Skill,并为每个执行 Agent 选择 `low`、`mid` 或 `high` 档位。
|
||||
ORC 是显式调用的薄路由器:只把开发、版本发布、DEB 和 Docker 意图映射成阶段,
|
||||
按静态配置选择 `low`、`mid` 或 `high` worker,再交给对应 Skill。
|
||||
|
||||
## 什么时候使用
|
||||
|
||||
@@ -16,13 +16,14 @@ ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Do
|
||||
- Orca 正在运行并启用了 orchestration。
|
||||
- 安装本次需要的 `$ack`、`$manage-release`、`$deb-publisher` 或
|
||||
`$publish-docker-image`。
|
||||
- 运行 `$orc 初始化` 生成 `docs/orc/config.yaml`,确认三档对应的精确模型。
|
||||
- `/usr/bin/python3`;ORC v1 配置必须保持为内置模板使用的 JSON-compatible YAML。
|
||||
- ORC 直接使用 skill 内共享的 `config.yaml`,不需要在每个项目初始化配置。修改这份
|
||||
配置会影响所有项目;旧的项目级 `docs/orc/config.yaml` 不再参与解析。
|
||||
- `/usr/bin/python3`;ORC v2 配置必须保持为 JSON-compatible YAML。
|
||||
resolver 以 `-I -S` 隔离模式运行,不加载项目模块、用户 site-packages 或第三方解析器。
|
||||
- ORC v1 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息;
|
||||
- ORC v2 worker 使用 `workspace-write` sandbox,以便发送 Orca lifecycle 消息;
|
||||
只读任务会在阶段 prompt 中禁止文件修改。
|
||||
|
||||
## Agent 档位
|
||||
## Worker 档位
|
||||
|
||||
| 档位 | 典型任务 |
|
||||
|------|----------|
|
||||
@@ -31,27 +32,34 @@ ORC 是显式调用的工程编排入口:把开发、版本发布、DEB 和 Do
|
||||
| `high` | 需求理解、跨系统改动、异常恢复和高风险裁决 |
|
||||
|
||||
档位不是权限。三个档位仍受各自 profile 和下游 Skill 的授权边界约束。
|
||||
当前 ORC v1 的结构化启动适配器支持 Codex worker;其它 Agent CLI 需要独立适配器,
|
||||
不会通过自由命令接入。
|
||||
这些档位只选择下游 worker,不会切换当前 ORC Coordinator 已经使用的模型。
|
||||
结构化启动适配器支持 Codex 与 Cursor worker,并固定跟随当前宿主:从 Codex 调用就使用
|
||||
Codex,从 Cursor 调用就使用 Cursor。共享配置为两个 CLI 分别维护三档模型,不设置跨宿主
|
||||
默认值。
|
||||
|
||||
## 使用示例
|
||||
|
||||
```text
|
||||
$orc high 修复登录问题,验证通过后发布新版本。
|
||||
$orc code=high release=mid docker=low,完成修复、发版并推送镜像。
|
||||
$orc low,检查并准备下一个版本,不执行远端写操作。
|
||||
$orc mid 继续上次中断的 v1.4.0 发布流程。
|
||||
```
|
||||
|
||||
阶段级档位优先于全局档位。用户显式指定后,ORC 不会静默改档;能力不足时会暂停并
|
||||
请求确认。
|
||||
阶段级档位优先于全局档位;未指定时只读取 `stageDefaults` / `defaultLevel`。ORC 不根据
|
||||
任务复杂度动态升降档;worker 报告能力不足时,ORC 只转发请求。
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
1. 识别开发、源码发布、DEB 与 Docker 阶段及其依赖。
|
||||
2. 解析每个阶段的 Agent profile,把配置快照、可信 Codex executable 和真实 Git
|
||||
1. 识别开发、源码发布、DEB 与 Docker 意图,并按固定表建立阶段依赖。
|
||||
2. 从当前 Agent 的系统身份锁定宿主 CLI,再解析每个阶段的 profile,把共享配置快照、
|
||||
可信 Agent CLI executable 和真实 Git
|
||||
worktree 绑定成 launch fingerprint,并展示计划和外部写入边界。
|
||||
3. 通过 Orca 分发给对应 Skill,监督完成消息、异常和决策门。
|
||||
4. 汇总每个阶段的实际状态、证据和安全恢复入口。
|
||||
3. 通过 Orca 分发给对应 Skill,转发完成消息、异常和决策门。
|
||||
4. 按下游 Skill 声明的证据字段汇总状态和恢复入口,不重新做领域判断。
|
||||
|
||||
ORC 不分析实现、不建议版本、不评估发布风险、不选择合并或构建方案;这些工作全部属于
|
||||
下游 Skill。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
|
||||
+101
-67
@@ -1,95 +1,128 @@
|
||||
---
|
||||
name: orc
|
||||
description: >-
|
||||
显式编排开发、源码版本发布、DEB 和 Docker 产物任务,把阶段分发给对应 Skill,
|
||||
并用 low、mid、high 选择执行 Agent 档位。仅在用户显式调用 $orc 或 /orc,要求
|
||||
跨阶段协调、指定 Agent 级别、监督多个 worker 或继续 ORC 编排时使用。
|
||||
作为薄路由器显式识别开发、源码版本发布、DEB 和 Docker 意图,把阶段分发给对应
|
||||
Skill,并按静态规则选择 low、mid、high 执行档位。仅在用户显式调用 $orc 或 /orc,
|
||||
要求跨阶段协调、指定 Agent 级别、监督多个 worker 或继续 ORC 编排时使用。
|
||||
---
|
||||
|
||||
# ORC 工程编排入口
|
||||
|
||||
当前会话担任 Coordinator:拆分阶段、选择档位、派发 worker、监督依赖与决策门,
|
||||
但不替代下游 Skill 执行其领域流程。
|
||||
当前会话担任薄路由器(Thin Coordinator):只识别阶段、宿主 CLI、静态档位、依赖和
|
||||
授权边界,然后派发 worker、转发决策门并汇总结构化状态。ORC 不做领域判断,不替代
|
||||
下游 Skill 执行、分析或复核其领域流程。
|
||||
|
||||
开始时解析当前 `SKILL.md` 所在目录,记为 `<orc-skill-dir>`;解析真实项目根目录,
|
||||
优先使用 `git rev-parse --show-toplevel`。项目配置固定为
|
||||
`<project-root>/docs/orc/config.yaml`。
|
||||
优先使用 `git rev-parse --show-toplevel`。所有项目共享唯一配置
|
||||
`<orc-skill-dir>/config.yaml`,不得在项目中创建 `docs/orc/config.yaml` 或其它配置副本。
|
||||
|
||||
## 选择模式
|
||||
|
||||
- 用户要求初始化 ORC:执行“初始化”。
|
||||
- 用户要求检查 ORC、档位或运行环境:执行“检查”。
|
||||
- 用户要求初始化 ORC:说明 ORC 已改为共享配置、不需要项目初始化,然后执行“检查”。
|
||||
- 用户要求检查 ORC、CLI、档位或运行环境:执行“检查”。
|
||||
- 用户明确要求修改 ORC 的共享模型或档位默认值:执行“修改共享配置”。
|
||||
- 用户要求用 ORC 完成任务:执行“编排”。
|
||||
|
||||
不要静默初始化,也不要在配置缺失或无效时退回裸命令或当前会话直接执行。
|
||||
配置或运行时无效时 fail closed,不退回裸命令或当前会话直接执行。
|
||||
|
||||
## 初始化
|
||||
## 薄路由器边界
|
||||
|
||||
1. 若 `docs/orc/config.yaml` 已存在,停止创建并转入“检查”,不得覆盖。
|
||||
2. 确认 `docs/orc/` 和目标文件都不是 symlink,再从
|
||||
`<orc-skill-dir>/templates/config.template.yaml` 以 create-only 方式创建配置,不覆盖
|
||||
或跟随既有路径。ORC v1 配置必须保持 JSON-compatible YAML,只由隔离的 Python
|
||||
标准库解析;模板中的 `.` 表示当前项目根。模型 ID 必须由用户或项目的可信配置确认,
|
||||
不能从任务文本猜测。
|
||||
3. 运行:
|
||||
ORC 只执行以下机械步骤:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py validate \
|
||||
<project-root>/docs/orc/config.yaml
|
||||
```
|
||||
1. 把用户明确表达的意图映射到 `code`、`release`、`deb`、`docker`。
|
||||
2. 从系统身份映射宿主 CLI,并按显式覆盖或共享配置解析档位。
|
||||
3. 根据固定路由表建立任务依赖,生成满足契约的 worker prompt 并派发。
|
||||
4. 转发 `decision_gate` / `escalation`,按下游 Skill 的完成证据汇总状态。
|
||||
|
||||
4. 报告三个档位的 CLI、模型、reasoning、权限和阶段默认值。除非用户明确要求,
|
||||
不安装下游 Skill、不创建终端、不修改 Agent 全局配置。
|
||||
不得阅读项目实现来形成技术判断,不得决定版本号、实现方案、测试策略、发布风险、
|
||||
合并方式或产物策略,不得代替 worker 执行命令。意图无法映射时报告范围外;缺少派发所需
|
||||
的关键输入时建立 decision gate。领域问题原样交给对应下游 Skill,不由 ORC 推理补全。
|
||||
|
||||
## 共享配置
|
||||
|
||||
`<orc-skill-dir>/config.yaml` 是 ORC profile 的 SSOT,对所有项目生效。配置使用
|
||||
JSON-compatible YAML,并由隔离的 Python 标准库解析。它固定包含:
|
||||
|
||||
- `cliPolicy: current-host`:Codex 宿主只启动 Codex worker,Cursor 宿主只启动 Cursor worker。
|
||||
- `defaultLevel` 与 `stageDefaults`:只用于下游 worker 的 `low`、`mid`、`high` 默认选择,
|
||||
不表示 ORC Coordinator 自身的模型档位。
|
||||
- `worktreePolicy: registered-same-repository`:允许当前仓库中已注册且身份一致的 worktree。
|
||||
- `profiles.codex` 与 `profiles.cursor-agent`:两个 CLI 各自完整的三档 profile。
|
||||
|
||||
不要把项目路径、命令、argv、环境变量、secret、hook 或 shell 片段写入共享配置。
|
||||
旧项目若残留 `docs/orc/config.yaml`,resolver 会忽略它;没有用户明确清理授权时不要删除。
|
||||
|
||||
## 修改共享配置
|
||||
|
||||
只有用户明确要求修改 ORC 的全局档位或模型时才编辑
|
||||
`<orc-skill-dir>/config.yaml`。修改前说明它会影响所有项目;模型 ID 必须来自用户输入、
|
||||
目标 CLI 的模型列表或其它可信配置,不能从任务文本猜测。修改后运行:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py validate
|
||||
```
|
||||
|
||||
报告两个 CLI 的三个档位、reasoning、权限策略、阶段默认值和宿主 CLI 策略。不要创建
|
||||
项目配置。
|
||||
|
||||
## 检查
|
||||
|
||||
1. 校验 `docs/orc/config.yaml`,确认只存在 `low`、`mid`、`high` 三档。
|
||||
1. 运行共享配置校验,确认 `codex` 和 `cursor-agent` 都完整配置 `low`、`mid`、`high`。
|
||||
2. 检查 `orca status --json`,并确认 orchestration 命令可用。
|
||||
3. 确认本次所需下游 Skill 已安装:`ack`、`manage-release`、`deb-publisher`、
|
||||
3. 从当前 Agent 的系统身份确定宿主 CLI:Codex 映射为 `codex`,Cursor 映射为
|
||||
`cursor-agent`。身份不明确时停止,不从用户任务文本、默认值或已安装 executable 猜测。
|
||||
确认宿主 CLI 可用;在 Cursor 中还要用 `cursor-agent --list-models`
|
||||
核对精确模型 ID 对当前账号可见。
|
||||
4. 确认本次所需下游 Skill 已安装:`ack`、`manage-release`、`deb-publisher`、
|
||||
`publish-docker-image`。只检查实际会用到的项。
|
||||
4. 解析 profile 时把项目根和目标 worktree 一并交给 resolver;只有 resolver 验证目标
|
||||
命中 `allowedWorktrees`、属于当前 Git 仓库且身份稳定后才可创建终端。不得只做文本
|
||||
比较或跳过机器校验。
|
||||
5. 任何 profile、Skill、运行时或 worktree 不可用时 fail closed;不得选择相邻档位、
|
||||
复用身份不明的终端或手写替代流程。
|
||||
5. 解析 profile 时把项目根和目标 worktree 一并交给 resolver;只有 resolver 验证目标是
|
||||
当前 Git 仓库已注册的 worktree 且身份稳定后才可创建终端。不得只做文本比较或跳过
|
||||
机器校验。
|
||||
6. 任何 profile、CLI、Skill、运行时或 worktree 不可用时 fail closed;不得切换其它 CLI、
|
||||
相邻档位、复用身份不明的终端或手写替代流程。
|
||||
|
||||
## 编排
|
||||
|
||||
1. 读取 [routing.md](references/routing.md),把请求拆成 `code`、`release`、`deb`、
|
||||
`docker` 阶段。没有匹配下游 Skill 的工作留在范围外并明确报告。
|
||||
2. 锁定用户授权的最远动作、目标版本、产物目标、源 commit/tag 与停止点。ORC 的调用
|
||||
本身不扩大 push、合并、打 tag、上传或部署权限;每个下游 Skill 的授权边界继续生效。
|
||||
3. 解析档位:阶段级指定 > 全局指定 > `stageDefaults` > `defaultLevel`。只接受
|
||||
`low`、`mid`、`high`;用户显式指定后不得静默升降级。若该档位不足以安全完成,
|
||||
建立 decision gate,等待用户改档或缩小范围。
|
||||
4. 用户未指定档位时采用以下判断:清晰、机械的构建或上传可用 `low`;常规版本流程
|
||||
用 `mid`;需求理解、跨系统改动、恢复中断流程、目标含糊或高风险裁决用 `high`。
|
||||
5. 对每个阶段运行 profile resolver。全局档位用 `--global-level`,阶段档位用
|
||||
`--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。模型认证默认复用
|
||||
`codex-login`,只有明确使用对应环境凭据时才选 `openai` 或 `azure-openai`。远端认证
|
||||
默认 `none`;只有目标 provider 与 transport 已确认时,才选择一个精确的
|
||||
`github-token`、`gitlab-token`、`gitea-token`、`forgejo-token`、`ssh-agent` 或
|
||||
`deb-token`。不得把多个 provider 凭据一起交给 worker。resolver 不存在静默
|
||||
fallback:
|
||||
2. 原样提取用户明确给出的授权最远动作、目标版本、产物目标、源 commit/tag 与停止点;
|
||||
未给出的领域参数保持未指定并交给下游 Skill。ORC 的调用本身不扩大 push、合并、
|
||||
打 tag、上传或部署权限。
|
||||
3. 从系统身份锁定当前宿主 CLI。运行在 Codex 时传 `--host-cli codex`;运行在 Cursor 时传
|
||||
`--host-cli cursor-agent`。宿主 CLI 不是任务选项,即使用户文本点名另一个 CLI 也不得
|
||||
跨宿主启动;身份不明确时 fail closed。
|
||||
4. 解析档位:阶段级指定 > 全局指定 > `stageDefaults` > `defaultLevel`。只接受
|
||||
`low`、`mid`、`high`;用户显式指定后不得静默升降级。ORC 不根据任务复杂度动态判断
|
||||
档位;worker 报告能力不足时,只转发 escalation 或 decision gate。
|
||||
5. 对每个阶段运行 profile resolver。宿主用必填的 `--host-cli`,全局档位用
|
||||
`--global-level`,阶段
|
||||
档位用 `--stage-level`;项目根与目标 worktree 必须使用规范绝对路径。Codex 默认复用
|
||||
`codex-login`,Cursor 默认复用 `cursor-login`;只有明确使用环境凭据时才分别选择
|
||||
`openai`、`azure-openai` 或 `cursor-api-key`。远端认证默认 `none`;只有目标 provider
|
||||
与 transport 已确认时,才选择一个精确的 `github-token`、`gitlab-token`、
|
||||
`gitea-token`、`forgejo-token`、`ssh-agent` 或 `deb-token`。不得把多个 provider 凭据
|
||||
一起交给 worker。resolver 不存在静默 fallback:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 -I -S <orc-skill-dir>/scripts/resolve_profile.py resolve \
|
||||
<project-root>/docs/orc/config.yaml --stage <stage> \
|
||||
--stage <stage> \
|
||||
--project-root <absolute-project-root> \
|
||||
--worktree <absolute-target-worktree> \
|
||||
--host-cli <codex|cursor-agent> \
|
||||
[--global-level <low|mid|high>] [--stage-level <low|mid|high>] \
|
||||
[--model-auth <codex-login|openai|azure-openai>] \
|
||||
[--model-auth <codex-login|openai|azure-openai|cursor-login|cursor-api-key>] \
|
||||
[--remote-auth <exact-provider-or-transport>]
|
||||
```
|
||||
|
||||
6. 核对 resolver 返回的 `launchFingerprint`、绝对 executable、worktree identity 和选择
|
||||
来源,再读取 [orca-adapter.md](references/orca-adapter.md),把阶段组织为 Orca task DAG。
|
||||
worker prompt 必须显式写出对应 `$skill`、阶段范围、输入 revision、用户授权边界、
|
||||
验收证据和依赖结果;下游 Skill 无需知道 ORC。
|
||||
6. 核对 resolver 返回的 `launchFingerprint`、绝对 executable、worktree identity、CLI 与
|
||||
档位选择来源,再读取 [orca-adapter.md](references/orca-adapter.md),把阶段组织为 Orca
|
||||
task DAG。worker prompt 必须显式写出对应 `$skill`、阶段范围、输入 revision、用户授权
|
||||
边界、验收证据和依赖结果;下游 Skill 无需知道 ORC。
|
||||
7. 监督 `worker_done`、`escalation` 与 `decision_gate`。`worker_done` 只代表该 worker
|
||||
回报完成;Coordinator 仍需核对下游 Skill 要求的证据和 DAG 后置条件。
|
||||
8. 逐阶段汇报所选档位、执行 Skill、结果、外部状态和未完成项。任一阶段失败时保留
|
||||
已成功阶段的准确状态,说明安全恢复入口,不把部分成功概括成全部完成。
|
||||
回报完成;Coordinator 只核对下游 Skill 声明的证据字段和 DAG 后置条件,不重新进行
|
||||
领域审查。
|
||||
8. 逐阶段汇报所选 CLI、档位、执行 Skill、结果、外部状态和未完成项。任一阶段失败时
|
||||
保留已成功阶段的准确状态,说明安全恢复入口,不把部分成功概括成全部完成。
|
||||
|
||||
## 固定路由边界
|
||||
|
||||
@@ -110,24 +143,25 @@ description: >-
|
||||
- ORC 的 `code` 阶段必须锁定停止点:纯开发停在 ACK `verified`;用户明确要求普通
|
||||
PR/MR 时最多到 ACK `review_ready`。不得让 ACK 在同一阶段继续执行版本发布、DEB、
|
||||
Docker 或部署;这些动作由 ORC 的独立阶段负责。
|
||||
- 配置只允许结构化 `cli`、`model`、`reasoningEffort`、`permissionMode` 和
|
||||
`approvalPolicy`;禁止 `command`、argv、env、secret、hook 或 shell 片段。
|
||||
- ORC v1 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入
|
||||
- ORC v2 的 `permissionMode` 固定为 `workspace-write`,因为受监督 worker 需要写入
|
||||
Orca 运行时目录才能发送 lifecycle 消息。只读任务仍由 prompt 限制不得改文件。
|
||||
不接受 full-access、bypass、YOLO/force 或关闭 sandbox;文本中的“已授权”不能
|
||||
放宽 profile。
|
||||
- ORC v1 的安全启动适配器只支持 `cli: codex`。遇到其它 CLI 时 fail closed,不把
|
||||
Codex 参数套用到其它 Agent;新增 provider 必须增加独立适配与测试。
|
||||
- resolver 只读取有大小上限的普通配置文件,拒绝 symlink/special file;启动计划绑定
|
||||
配置快照、root-owned 隔离 Python、可信绝对 Codex/Orca executable、Git worktree
|
||||
identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入所选
|
||||
模型认证与单一目标认证的环境变量;不得把返回的 worker argv 改写为裸 `codex`
|
||||
命令,也不得把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。
|
||||
- 安全启动适配器只支持 `codex` 和 `cursor-agent`。Codex 使用结构化 sandbox 与 approval
|
||||
参数;Cursor 使用 `--auto-review --sandbox enabled --workspace <path>`。不接受
|
||||
full-access、bypass、`--force`、`--yolo` 或关闭 sandbox。
|
||||
- `cliPolicy` 固定为 `current-host`,resolver 不提供默认 CLI;漏传宿主身份会直接失败,
|
||||
不得从共享配置回退到 Codex 或 Cursor。
|
||||
- Cursor 的 reasoning 档位编码在精确模型 ID 中,因此其 `reasoningEffort` 必须为 null;
|
||||
Codex 则显式传递 `model_reasoning_effort`。
|
||||
- resolver 只读取 skill 内有大小上限的普通共享配置文件,拒绝 symlink/special file;
|
||||
启动计划绑定配置快照、root-owned 隔离 Python、可信绝对 Agent/Orca executable、Git
|
||||
worktree identity、精确认证选择和固定 argv。实际启动会重新校验 fingerprint,并只注入
|
||||
所选模型认证与单一目标认证的环境变量;不得改写 resolver 返回的 worker argv,也不得
|
||||
把终端创建 argv 的绝对 Orca 路径换成项目 `PATH` 解析。
|
||||
- 不把模型档位当作权限。`high` 不自动获得更多文件、凭据、网络或远端写权限。
|
||||
- 不执行 `orca orchestration reset`,除非用户明确要求放弃全部相关运行时状态。
|
||||
|
||||
## 完成标准
|
||||
|
||||
每个计划阶段都有明确下游 Skill、Agent 档位、输入 revision、授权边界和可核对结果;
|
||||
每个计划阶段都有明确下游 Skill、Agent CLI、档位、输入 revision、授权边界和可核对结果;
|
||||
DAG 中所有必要阶段完成,或失败阶段具有准确状态与恢复入口。ACK 和其它下游 Skill
|
||||
保持独立且不存在对 ORC 的反向引用。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
interface:
|
||||
display_name: "ORC"
|
||||
short_description: "按阶段编排开发、版本发布与产物构建,并选择 Agent 档位"
|
||||
default_prompt: "Use $orc to coordinate this engineering task with explicit low, mid, or high worker levels."
|
||||
short_description: "用 Codex 或 Cursor 分档编排开发、发布与产物任务"
|
||||
default_prompt: "Use $orc to coordinate this engineering task on the current host CLI with a low, mid, or high worker level."
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": 2,
|
||||
"cliPolicy": "current-host",
|
||||
"defaultLevel": "mid",
|
||||
"stageDefaults": {
|
||||
"code": "high",
|
||||
"release": "mid",
|
||||
"deb": "low",
|
||||
"docker": "low"
|
||||
},
|
||||
"worktreePolicy": "registered-same-repository",
|
||||
"profiles": {
|
||||
"codex": {
|
||||
"low": {
|
||||
"model": "gpt-5.6-luna",
|
||||
"reasoningEffort": "low",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
},
|
||||
"mid": {
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
},
|
||||
"high": {
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoningEffort": "high",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
}
|
||||
},
|
||||
"cursor-agent": {
|
||||
"low": {
|
||||
"model": "auto",
|
||||
"reasoningEffort": null,
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "auto-review"
|
||||
},
|
||||
"mid": {
|
||||
"model": "gpt-5.6-terra-medium",
|
||||
"reasoningEffort": null,
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "auto-review"
|
||||
},
|
||||
"high": {
|
||||
"model": "gpt-5.6-sol-high",
|
||||
"reasoningEffort": null,
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "auto-review"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,12 @@ orca orchestration inbox --limit 20 --json
|
||||
|
||||
## 解析安全启动计划
|
||||
|
||||
对每个阶段调用 `resolve_profile.py resolve`,同时传入规范绝对 `--project-root` 和
|
||||
`--worktree`,以及本阶段精确的 `--model-auth`、`--remote-auth`。JSON 结果包含选择来源、
|
||||
结构化 profile、认证选择、配置与 worktree identity、可信 Python/Codex/Orca executable、
|
||||
对每个阶段调用 `resolve_profile.py resolve`,同时传入规范绝对 `--project-root`、
|
||||
`--worktree` 和从当前 Agent 系统身份确定的必填 `--host-cli`,以及本阶段精确的
|
||||
`--model-auth`、`--remote-auth`。
|
||||
resolver 始终读取 skill 内共享的 `config.yaml`,不接受项目配置路径。JSON 结果包含
|
||||
选择来源、结构化 profile、认证选择、配置与 worktree identity、可信 Python/Agent
|
||||
CLI/Orca executable、
|
||||
`launchFingerprint`、固定 worker/launcher argv,以及完整的 `terminalCreateArgv` 和
|
||||
`terminalCreateShellCommand`。
|
||||
|
||||
@@ -31,11 +34,16 @@ orca orchestration inbox --limit 20 --json
|
||||
同时暴露多个 token 或 SSH agent。需要代理、额外凭据或 Docker registry env secret 时
|
||||
fail closed,由用户确认新的最小权限适配,不继承 ambient proxy/credential 变量。
|
||||
- resolver 失败时停止;不改用默认模型、邻近档位或已有未知终端。
|
||||
- 不重复手写 worktree 检查;resolver 已拒绝 root、不存在、非规范、含 symlink、未注册、
|
||||
跨仓库或不在 allowlist 的目标,并把身份写入 fingerprint。
|
||||
- ORC v1 profile 必须使用 `workspace-write`。`read-only` sandbox 可能阻止 Orca CLI
|
||||
- 不从任务文本、可执行文件存在性或共享配置猜宿主;Codex 传 `--host-cli codex`,Cursor
|
||||
传 `--host-cli cursor-agent`。宿主身份不明确时停止。
|
||||
- 不重复手写 worktree 检查;resolver 已拒绝 root、不存在、非规范、含 symlink、未注册
|
||||
或跨仓库的目标,并把身份写入 fingerprint。
|
||||
- ORC v2 profile 必须使用 `workspace-write`。`read-only` sandbox 可能阻止 Orca CLI
|
||||
写入自身运行时或挂载目录,导致 worker 无法发送 `worker_done`;只读工作通过 task
|
||||
spec 限制,而不是换成无法完成 lifecycle 的 sandbox。
|
||||
- Codex 与 Cursor 使用不同的固定 argv 适配器。Cursor 只允许 `--auto-review`、启用的
|
||||
sandbox 和精确 workspace;不得把 Codex 参数套给 Cursor,也不得给 Cursor 追加
|
||||
`--force` 或 `--yolo`。
|
||||
|
||||
## 创建与派发
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ORC 阶段路由
|
||||
|
||||
ORC 只负责拆分、依赖、档位和监督。领域步骤、授权检查和完成标准由下游 Skill 自己
|
||||
决定。
|
||||
ORC 是薄路由器,只负责意图映射、依赖、静态档位和结构化状态汇总。它不判断版本号、
|
||||
实现方案、发布风险或产物策略;领域步骤、授权检查和完成标准由下游 Skill 自己决定。
|
||||
|
||||
## 路由表
|
||||
|
||||
@@ -15,6 +15,10 @@ ORC 只负责拆分、依赖、档位和监督。领域步骤、授权检查和
|
||||
没有匹配项时不要临时扩写某个 Skill 的职责,也不要让 ORC 自己模仿领域流程。报告缺少
|
||||
的能力,由用户决定直接执行、安装新 Skill 或另行设计。
|
||||
|
||||
路由只依据用户明确表达的目标。缺少目标版本、revision、产物目标或授权停止点时,保留
|
||||
为未指定并交给下游 Skill;只有缺少创建 task 所必需的项目或阶段身份时才建立
|
||||
decision gate。不得为了填满 worker prompt 而分析代码、推断版本或设计执行方案。
|
||||
|
||||
## 拆分规则
|
||||
|
||||
1. 先从用户请求提取最终结果,再拆出真正需要的阶段;不要因为安装了某个 Skill 就
|
||||
@@ -52,7 +56,7 @@ deb ($deb-publisher) || docker ($publish-docker-image)
|
||||
release ($manage-release)
|
||||
```
|
||||
|
||||
依赖不是固定模板,但 ORC v1 不拆分一个 `manage-release` 生命周期。若项目要求在打开
|
||||
依赖不是固定模板,但 ORC v2 不拆分一个 `manage-release` 生命周期。若项目要求在打开
|
||||
release PR 与合并之间插入 DEB/Docker gate,当前 task 粒度无法安全表达该中间里程碑;
|
||||
在打开 PR 前建立 decision gate 并报告该流程暂不支持,不得用循环依赖或两个 release
|
||||
worker 临时拼接。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate ORC config and build a worktree-bound Codex launch plan."""
|
||||
"""Validate shared ORC config and build a worktree-bound Agent launch plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -18,16 +18,17 @@ from typing import Any
|
||||
|
||||
LEVELS = ("low", "mid", "high")
|
||||
STAGES = ("code", "release", "deb", "docker")
|
||||
CLIS = ("codex", "cursor-agent")
|
||||
EXPECTED_EFFORT = {"low": "low", "mid": "medium", "high": "high"}
|
||||
TOP_LEVEL_KEYS = {
|
||||
"version",
|
||||
"cliPolicy",
|
||||
"defaultLevel",
|
||||
"stageDefaults",
|
||||
"allowedWorktrees",
|
||||
"worktreePolicy",
|
||||
"profiles",
|
||||
}
|
||||
PROFILE_KEYS = {
|
||||
"cli",
|
||||
"model",
|
||||
"reasoningEffort",
|
||||
"permissionMode",
|
||||
@@ -37,7 +38,7 @@ MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}\Z")
|
||||
MAX_CONFIG_SIZE = 64 * 1024
|
||||
MAX_CONTROL_OUTPUT = 64 * 1024
|
||||
CONTROL_TIMEOUT_SECONDS = 15
|
||||
CONFIG_RELATIVE_PATH = Path("docs/orc/config.yaml")
|
||||
SHARED_CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.yaml"
|
||||
COMMON_ENVIRONMENT_NAMES = {
|
||||
"COLORTERM",
|
||||
"LANG",
|
||||
@@ -50,6 +51,16 @@ MODEL_AUTH_ENVIRONMENT = {
|
||||
"codex-login": frozenset(),
|
||||
"openai": frozenset({"OPENAI_API_KEY"}),
|
||||
"azure-openai": frozenset({"AZURE_OPENAI_API_KEY"}),
|
||||
"cursor-login": frozenset(),
|
||||
"cursor-api-key": frozenset({"CURSOR_API_KEY"}),
|
||||
}
|
||||
CLI_MODEL_AUTH = {
|
||||
"codex": frozenset({"codex-login", "openai", "azure-openai"}),
|
||||
"cursor-agent": frozenset({"cursor-login", "cursor-api-key"}),
|
||||
}
|
||||
DEFAULT_MODEL_AUTH = {
|
||||
"codex": "codex-login",
|
||||
"cursor-agent": "cursor-login",
|
||||
}
|
||||
REMOTE_AUTH_ENVIRONMENT = {
|
||||
"none": frozenset(),
|
||||
@@ -169,9 +180,11 @@ def validate_config(document: Any) -> dict[str, Any]:
|
||||
_exact_keys(config, required=TOP_LEVEL_KEYS, path="config")
|
||||
|
||||
version = config["version"]
|
||||
if isinstance(version, bool) or version != 1:
|
||||
raise ConfigError("version must be integer 1")
|
||||
if isinstance(version, bool) or version != 2:
|
||||
raise ConfigError("version must be integer 2")
|
||||
|
||||
if config["cliPolicy"] != "current-host":
|
||||
raise ConfigError("cliPolicy must be current-host in ORC v2")
|
||||
_enum(config["defaultLevel"], LEVELS, "defaultLevel")
|
||||
|
||||
stage_defaults = _mapping(config["stageDefaults"], "stageDefaults")
|
||||
@@ -183,53 +196,51 @@ def validate_config(document: Any) -> dict[str, Any]:
|
||||
for stage, level in stage_defaults.items():
|
||||
_enum(level, LEVELS, f"stageDefaults.{stage}")
|
||||
|
||||
allowed = config["allowedWorktrees"]
|
||||
if not isinstance(allowed, list) or not allowed:
|
||||
raise ConfigError("allowedWorktrees must be a non-empty list")
|
||||
seen_worktrees: set[str] = set()
|
||||
for index, entry in enumerate(allowed):
|
||||
if (
|
||||
not isinstance(entry, str)
|
||||
or not entry
|
||||
or entry != entry.strip()
|
||||
or any(character in entry for character in ("\x00", "\n", "\r"))
|
||||
):
|
||||
raise ConfigError(
|
||||
f"allowedWorktrees[{index}] must be a safe non-empty path"
|
||||
)
|
||||
if entry != "." and not Path(entry).is_absolute():
|
||||
raise ConfigError(
|
||||
f"allowedWorktrees[{index}] must be '.' or an absolute path"
|
||||
)
|
||||
normalized = entry if entry == "." else str(Path(entry).absolute())
|
||||
if normalized in seen_worktrees:
|
||||
raise ConfigError(f"allowedWorktrees contains duplicate path: {entry}")
|
||||
seen_worktrees.add(normalized)
|
||||
if config["worktreePolicy"] != "registered-same-repository":
|
||||
raise ConfigError(
|
||||
"worktreePolicy must be registered-same-repository in ORC v2"
|
||||
)
|
||||
|
||||
profiles = _mapping(config["profiles"], "profiles")
|
||||
_exact_keys(profiles, required=set(LEVELS), path="profiles")
|
||||
for level in LEVELS:
|
||||
profile = _mapping(profiles[level], f"profiles.{level}")
|
||||
_exact_keys(profile, required=PROFILE_KEYS, path=f"profiles.{level}")
|
||||
if profile["cli"] != "codex":
|
||||
raise ConfigError(f"profiles.{level}.cli must be codex in ORC v1")
|
||||
model = profile["model"]
|
||||
if not isinstance(model, str) or not MODEL_RE.fullmatch(model):
|
||||
raise ConfigError(f"profiles.{level}.model is not a safe exact model ID")
|
||||
expected_effort = EXPECTED_EFFORT[level]
|
||||
if profile["reasoningEffort"] != expected_effort:
|
||||
raise ConfigError(
|
||||
f"profiles.{level}.reasoningEffort must be {expected_effort}"
|
||||
)
|
||||
if profile["permissionMode"] != "workspace-write":
|
||||
raise ConfigError(
|
||||
f"profiles.{level}.permissionMode must be workspace-write in ORC v1"
|
||||
)
|
||||
_enum(
|
||||
profile["approvalPolicy"],
|
||||
{"untrusted", "on-request", "never"},
|
||||
f"profiles.{level}.approvalPolicy",
|
||||
)
|
||||
_exact_keys(profiles, required=set(CLIS), path="profiles")
|
||||
for cli in CLIS:
|
||||
cli_profiles = _mapping(profiles[cli], f"profiles.{cli}")
|
||||
_exact_keys(cli_profiles, required=set(LEVELS), path=f"profiles.{cli}")
|
||||
for level in LEVELS:
|
||||
path = f"profiles.{cli}.{level}"
|
||||
profile = _mapping(cli_profiles[level], path)
|
||||
_exact_keys(profile, required=PROFILE_KEYS, path=path)
|
||||
model = profile["model"]
|
||||
if not isinstance(model, str) or not MODEL_RE.fullmatch(model):
|
||||
raise ConfigError(f"{path}.model is not a safe exact model ID")
|
||||
if cli == "codex":
|
||||
expected_effort = EXPECTED_EFFORT[level]
|
||||
if profile["reasoningEffort"] != expected_effort:
|
||||
raise ConfigError(
|
||||
f"{path}.reasoningEffort must be {expected_effort}"
|
||||
)
|
||||
_enum(
|
||||
profile["approvalPolicy"],
|
||||
{"untrusted", "on-request", "never"},
|
||||
f"{path}.approvalPolicy",
|
||||
)
|
||||
else:
|
||||
if profile["reasoningEffort"] is not None:
|
||||
raise ConfigError(f"{path}.reasoningEffort: Cursor requires null")
|
||||
model_tokens = set(re.split(r"[^a-z0-9]+", model.lower()))
|
||||
expected_token = "medium" if level == "mid" else level
|
||||
if model != "auto" and expected_token not in model_tokens:
|
||||
raise ConfigError(
|
||||
f"{path}.model must encode the {expected_token} effort level"
|
||||
)
|
||||
if profile["approvalPolicy"] != "auto-review":
|
||||
raise ConfigError(
|
||||
f"{path}.approvalPolicy must be auto-review for Cursor"
|
||||
)
|
||||
if profile["permissionMode"] != "workspace-write":
|
||||
raise ConfigError(
|
||||
f"{path}.permissionMode must be workspace-write in ORC v2"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -260,7 +271,8 @@ def resolve_profile(
|
||||
stage: str,
|
||||
global_level: str | None = None,
|
||||
stage_level: str | None = None,
|
||||
model_auth: str = "codex-login",
|
||||
host_cli: str | None = None,
|
||||
model_auth: str | None = None,
|
||||
remote_auth: str = "none",
|
||||
) -> dict[str, Any]:
|
||||
_enum(stage, STAGES, "stage")
|
||||
@@ -268,9 +280,19 @@ def resolve_profile(
|
||||
_enum(global_level, LEVELS, "global level")
|
||||
if stage_level is not None:
|
||||
_enum(stage_level, LEVELS, "stage level")
|
||||
_enum(model_auth, set(MODEL_AUTH_ENVIRONMENT), "model auth")
|
||||
_enum(remote_auth, STAGE_REMOTE_AUTH[stage], f"{stage} remote auth")
|
||||
|
||||
if host_cli is None:
|
||||
raise ConfigError("host CLI is required; resolve it from the current Agent")
|
||||
selected_cli = _enum(host_cli, CLIS, "host CLI")
|
||||
cli_source = "runtime.host"
|
||||
selected_auth = model_auth or DEFAULT_MODEL_AUTH[selected_cli]
|
||||
_enum(selected_auth, set(MODEL_AUTH_ENVIRONMENT), "model auth")
|
||||
if selected_auth not in CLI_MODEL_AUTH[selected_cli]:
|
||||
raise ConfigError(
|
||||
f"model auth {selected_auth} is not valid for {selected_cli}"
|
||||
)
|
||||
|
||||
if stage_level is not None:
|
||||
level, source = stage_level, "request.stage"
|
||||
elif global_level is not None:
|
||||
@@ -280,25 +302,36 @@ def resolve_profile(
|
||||
else:
|
||||
level, source = config["defaultLevel"], "config.defaultLevel"
|
||||
|
||||
profile = config["profiles"].get(level)
|
||||
profile = config["profiles"][selected_cli].get(level)
|
||||
if profile is None:
|
||||
raise ConfigError(f"requested profile does not exist: {level}")
|
||||
worker_args = [
|
||||
"--model",
|
||||
profile["model"],
|
||||
"-c",
|
||||
f'model_reasoning_effort="{profile["reasoningEffort"]}"',
|
||||
"--sandbox",
|
||||
profile["permissionMode"],
|
||||
"--ask-for-approval",
|
||||
profile["approvalPolicy"],
|
||||
"--strict-config",
|
||||
]
|
||||
raise ConfigError(f"requested profile does not exist: {selected_cli}/{level}")
|
||||
if selected_cli == "codex":
|
||||
worker_args = [
|
||||
"--model",
|
||||
profile["model"],
|
||||
"-c",
|
||||
f'model_reasoning_effort="{profile["reasoningEffort"]}"',
|
||||
"--sandbox",
|
||||
profile["permissionMode"],
|
||||
"--ask-for-approval",
|
||||
profile["approvalPolicy"],
|
||||
"--strict-config",
|
||||
]
|
||||
else:
|
||||
worker_args = [
|
||||
"--model",
|
||||
profile["model"],
|
||||
"--auto-review",
|
||||
"--sandbox",
|
||||
"enabled",
|
||||
]
|
||||
return {
|
||||
"stage": stage,
|
||||
"cli": selected_cli,
|
||||
"cliSelectionSource": cli_source,
|
||||
"level": level,
|
||||
"selectionSource": source,
|
||||
"modelAuth": model_auth,
|
||||
"modelAuth": selected_auth,
|
||||
"remoteAuth": remote_auth,
|
||||
"profile": dict(profile),
|
||||
"workerArgs": worker_args,
|
||||
@@ -400,7 +433,7 @@ def _trusted_executable(path: Path, expected_name: str) -> Path | None:
|
||||
|
||||
|
||||
def resolve_trusted_executable(name: str) -> Path:
|
||||
if name not in {"codex", "git", "orca"}:
|
||||
if name not in {"codex", "cursor-agent", "git", "orca"}:
|
||||
raise ConfigError(f"unsupported executable: {name}")
|
||||
search_paths = trusted_path_entries()
|
||||
if name == "git":
|
||||
@@ -572,17 +605,10 @@ def validate_worktree(
|
||||
except ConfigError:
|
||||
continue
|
||||
|
||||
allowed: set[Path] = set()
|
||||
for entry in config["allowedWorktrees"]:
|
||||
allowed.add(
|
||||
project_root
|
||||
if entry == "."
|
||||
else canonical_directory(entry, "allowed worktree")
|
||||
)
|
||||
if not allowed <= registered:
|
||||
raise ConfigError("allowedWorktrees contains an unregistered Git worktree")
|
||||
if worktree not in allowed:
|
||||
raise ConfigError("target worktree is not in allowedWorktrees")
|
||||
if config["worktreePolicy"] != "registered-same-repository":
|
||||
raise ConfigError("unsupported worktree policy")
|
||||
if worktree not in registered:
|
||||
raise ConfigError("target worktree is not registered in the project repository")
|
||||
|
||||
target_top = canonical_directory(
|
||||
_run_control(
|
||||
@@ -631,7 +657,7 @@ def _file_facts(path: Path) -> dict[str, Any]:
|
||||
def _executable_facts(path: Path) -> dict[str, Any]:
|
||||
return {
|
||||
**_file_facts(path),
|
||||
"version": _run_control([str(path), "--version"], "Codex version check"),
|
||||
"version": _run_control([str(path), "--version"], "Agent CLI version check"),
|
||||
}
|
||||
|
||||
|
||||
@@ -663,22 +689,19 @@ def _fingerprint(value: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def build_launch_plan(
|
||||
config_path: Path,
|
||||
*,
|
||||
project_root: str | Path,
|
||||
worktree: str | Path,
|
||||
stage: str,
|
||||
host_cli: str,
|
||||
global_level: str | None = None,
|
||||
stage_level: str | None = None,
|
||||
model_auth: str = "codex-login",
|
||||
model_auth: str | None = None,
|
||||
remote_auth: str = "none",
|
||||
) -> dict[str, Any]:
|
||||
project = canonical_directory(project_root, "project root")
|
||||
expected_config = project / CONFIG_RELATIVE_PATH
|
||||
if config_path.absolute() != expected_config:
|
||||
raise ConfigError(f"config path must be {expected_config}")
|
||||
_assert_no_symlink_components(expected_config, "config path")
|
||||
config, config_snapshot = load_config_snapshot(expected_config)
|
||||
_assert_no_symlink_components(SHARED_CONFIG_PATH, "shared config path")
|
||||
config, config_snapshot = load_config_snapshot(SHARED_CONFIG_PATH)
|
||||
worktree_facts = validate_worktree(
|
||||
config,
|
||||
project_root_value=project,
|
||||
@@ -689,16 +712,17 @@ def build_launch_plan(
|
||||
stage=stage,
|
||||
global_level=global_level,
|
||||
stage_level=stage_level,
|
||||
host_cli=host_cli,
|
||||
model_auth=model_auth,
|
||||
remote_auth=remote_auth,
|
||||
)
|
||||
selected_credential_names(
|
||||
stage,
|
||||
model_auth=model_auth,
|
||||
model_auth=profile["modelAuth"],
|
||||
remote_auth=remote_auth,
|
||||
)
|
||||
codex = resolve_trusted_executable("codex")
|
||||
executable = _executable_facts(codex)
|
||||
agent_cli = resolve_trusted_executable(profile["cli"])
|
||||
executable = _executable_facts(agent_cli)
|
||||
orca = resolve_trusted_executable("orca")
|
||||
orca_executable = _file_facts(orca)
|
||||
python = resolve_trusted_python()
|
||||
@@ -710,7 +734,7 @@ def build_launch_plan(
|
||||
**profile,
|
||||
**worktree_facts,
|
||||
"config": {
|
||||
"path": str(expected_config),
|
||||
"path": str(SHARED_CONFIG_PATH),
|
||||
**config_snapshot,
|
||||
},
|
||||
"executable": executable,
|
||||
@@ -726,14 +750,15 @@ def build_launch_plan(
|
||||
},
|
||||
}
|
||||
fingerprint = _fingerprint(launch_facts)
|
||||
worker_argv = [str(codex), *profile["workerArgs"]]
|
||||
worker_argv = [str(agent_cli), *profile["workerArgs"]]
|
||||
if profile["cli"] == "cursor-agent":
|
||||
worker_argv.extend(["--workspace", str(worktree_facts["worktree"])])
|
||||
launcher_argv = [
|
||||
str(python),
|
||||
"-I",
|
||||
"-S",
|
||||
str(script),
|
||||
"_launch",
|
||||
str(expected_config),
|
||||
"--project-root",
|
||||
str(project),
|
||||
"--worktree",
|
||||
@@ -747,13 +772,17 @@ def build_launch_plan(
|
||||
launcher_argv.extend(["--global-level", global_level])
|
||||
if stage_level is not None:
|
||||
launcher_argv.extend(["--stage-level", stage_level])
|
||||
if model_auth != "codex-login":
|
||||
launcher_argv.extend(["--host-cli", host_cli])
|
||||
if model_auth is not None:
|
||||
launcher_argv.extend(["--model-auth", model_auth])
|
||||
if remote_auth != "none":
|
||||
launcher_argv.extend(["--remote-auth", remote_auth])
|
||||
terminal_command = shlex.join(launcher_argv)
|
||||
worktree_selector = "path:" + str(worktree_facts["worktree"])
|
||||
terminal_title = f"ORC-{stage}-{profile['level']}-{fingerprint[7:15]}"
|
||||
cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
|
||||
terminal_title = (
|
||||
f"ORC-{stage}-{cli_label}-{profile['level']}-{fingerprint[7:15]}"
|
||||
)
|
||||
terminal_create_argv = [
|
||||
str(orca),
|
||||
"terminal",
|
||||
@@ -781,10 +810,10 @@ def build_launch_plan(
|
||||
|
||||
def execute_launch(args: argparse.Namespace) -> int:
|
||||
plan = build_launch_plan(
|
||||
args.config,
|
||||
project_root=args.project_root,
|
||||
worktree=args.worktree,
|
||||
stage=args.stage,
|
||||
host_cli=args.host_cli,
|
||||
global_level=args.global_level,
|
||||
stage_level=args.stage_level,
|
||||
model_auth=args.model_auth,
|
||||
@@ -802,7 +831,7 @@ def execute_launch(args: argparse.Namespace) -> int:
|
||||
raise ConfigError("Python executable changed before launch")
|
||||
executable = Path(plan["executable"]["path"])
|
||||
if _executable_facts(executable) != plan["executable"]:
|
||||
raise ConfigError("Codex executable changed before launch")
|
||||
raise ConfigError("Agent CLI executable changed before launch")
|
||||
try:
|
||||
os.execve(
|
||||
executable,
|
||||
@@ -814,21 +843,20 @@ def execute_launch(args: argparse.Namespace) -> int:
|
||||
),
|
||||
)
|
||||
except OSError as exc:
|
||||
raise ConfigError("Codex worker could not be launched") from exc
|
||||
raise ConfigError("Agent worker could not be launched") from exc
|
||||
return 1 # pragma: no cover - os.execve does not return on success
|
||||
|
||||
|
||||
def _add_resolution_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("config", type=Path)
|
||||
parser.add_argument("--project-root", required=True)
|
||||
parser.add_argument("--worktree", required=True)
|
||||
parser.add_argument("--stage", required=True, choices=STAGES)
|
||||
parser.add_argument("--global-level", choices=LEVELS)
|
||||
parser.add_argument("--stage-level", choices=LEVELS)
|
||||
parser.add_argument("--host-cli", required=True, choices=CLIS)
|
||||
parser.add_argument(
|
||||
"--model-auth",
|
||||
choices=tuple(MODEL_AUTH_ENVIRONMENT),
|
||||
default="codex-login",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remote-auth",
|
||||
@@ -842,8 +870,6 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
validate = subparsers.add_parser("validate", help="validate config only")
|
||||
validate.add_argument("config", type=Path)
|
||||
|
||||
resolve = subparsers.add_parser("resolve", help="resolve a bound launch plan")
|
||||
_add_resolution_arguments(resolve)
|
||||
|
||||
@@ -857,20 +883,22 @@ def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
if args.command == "validate":
|
||||
config = load_config(args.config)
|
||||
config = load_config(SHARED_CONFIG_PATH)
|
||||
result: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"config": str(args.config.absolute()),
|
||||
"config": str(SHARED_CONFIG_PATH),
|
||||
"clis": list(CLIS),
|
||||
"levels": list(LEVELS),
|
||||
"stages": list(STAGES),
|
||||
"cliPolicy": config["cliPolicy"],
|
||||
"defaultLevel": config["defaultLevel"],
|
||||
}
|
||||
elif args.command == "resolve":
|
||||
result = build_launch_plan(
|
||||
args.config,
|
||||
project_root=args.project_root,
|
||||
worktree=args.worktree,
|
||||
stage=args.stage,
|
||||
host_cli=args.host_cli,
|
||||
global_level=args.global_level,
|
||||
stage_level=args.stage_level,
|
||||
model_auth=args.model_auth,
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"defaultLevel": "mid",
|
||||
"stageDefaults": {
|
||||
"code": "high",
|
||||
"release": "mid",
|
||||
"deb": "low",
|
||||
"docker": "low"
|
||||
},
|
||||
"allowedWorktrees": [
|
||||
"."
|
||||
],
|
||||
"profiles": {
|
||||
"low": {
|
||||
"cli": "codex",
|
||||
"model": "gpt-5.6-luna",
|
||||
"reasoningEffort": "low",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
},
|
||||
"mid": {
|
||||
"cli": "codex",
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
},
|
||||
"high": {
|
||||
"cli": "codex",
|
||||
"model": "gpt-5.6-sol",
|
||||
"reasoningEffort": "high",
|
||||
"permissionMode": "workspace-write",
|
||||
"approvalPolicy": "never"
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
-42
@@ -18,7 +18,7 @@ from unittest import mock
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
ORC_DIR = REPO_ROOT / "skills" / "orc"
|
||||
SCRIPT = ORC_DIR / "scripts" / "resolve_profile.py"
|
||||
CONFIG = ORC_DIR / "templates" / "config.template.yaml"
|
||||
CONFIG = ORC_DIR / "config.yaml"
|
||||
|
||||
spec = importlib.util.spec_from_file_location("orc_resolve_profile", SCRIPT)
|
||||
assert spec is not None and spec.loader is not None
|
||||
@@ -36,7 +36,13 @@ class OrcSkillTests(unittest.TestCase):
|
||||
self.assertIn("low", skill)
|
||||
self.assertIn("mid", skill)
|
||||
self.assertIn("high", skill)
|
||||
self.assertIn("只支持 `cli: codex`", skill)
|
||||
self.assertIn("`codex` 和 `cursor-agent`", skill)
|
||||
self.assertIn("<orc-skill-dir>/config.yaml", skill)
|
||||
self.assertIn("不得在项目中创建 `docs/orc/config.yaml`", skill)
|
||||
self.assertIn("薄路由器", skill)
|
||||
self.assertIn("不做领域判断", skill)
|
||||
self.assertNotIn("用户未指定档位时采用以下判断", skill)
|
||||
self.assertNotIn("若该档位不足以安全完成", skill)
|
||||
for child in ("$ack", "$manage-release", "$deb-publisher", "$publish-docker-image"):
|
||||
self.assertIn(child, routing)
|
||||
|
||||
@@ -64,12 +70,20 @@ class OrcSkillTests(unittest.TestCase):
|
||||
self.assertIn("最多到", routing)
|
||||
self.assertIn("review_ready", routing)
|
||||
self.assertIn("当前 task 粒度无法安全表达", routing)
|
||||
self.assertRegex(
|
||||
routing,
|
||||
r"不判断版本号、\s*实现方案、发布风险或产物策略",
|
||||
)
|
||||
|
||||
def test_template_and_cli_validate(self) -> None:
|
||||
def test_shared_config_and_cli_validate(self) -> None:
|
||||
config = orc_profiles.load_config(CONFIG)
|
||||
self.assertEqual(set(config["profiles"]), {"low", "mid", "high"})
|
||||
self.assertEqual(config["version"], 2)
|
||||
self.assertEqual(config["cliPolicy"], "current-host")
|
||||
self.assertEqual(set(config["profiles"]), {"codex", "cursor-agent"})
|
||||
for cli in ("codex", "cursor-agent"):
|
||||
self.assertEqual(set(config["profiles"][cli]), {"low", "mid", "high"})
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "validate", str(CONFIG)],
|
||||
[sys.executable, str(SCRIPT), "validate"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
@@ -77,8 +91,16 @@ class OrcSkillTests(unittest.TestCase):
|
||||
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||
self.assertIn('"ok": true', completed.stdout)
|
||||
|
||||
project_config_argument = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "validate", str(CONFIG)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(project_config_argument.returncode, 2)
|
||||
|
||||
clean_python = subprocess.run(
|
||||
[sys.executable, "-I", "-S", str(SCRIPT), "validate", str(CONFIG)],
|
||||
[sys.executable, "-I", "-S", str(SCRIPT), "validate"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
@@ -88,24 +110,39 @@ class OrcSkillTests(unittest.TestCase):
|
||||
def test_level_precedence_is_stage_then_global_then_config(self) -> None:
|
||||
config = orc_profiles.load_config(CONFIG)
|
||||
stage = orc_profiles.resolve_profile(
|
||||
config, stage="docker", global_level="mid", stage_level="high"
|
||||
config,
|
||||
stage="docker",
|
||||
host_cli="codex",
|
||||
global_level="mid",
|
||||
stage_level="high",
|
||||
)
|
||||
global_choice = orc_profiles.resolve_profile(
|
||||
config, stage="docker", global_level="mid"
|
||||
config, stage="docker", host_cli="codex", global_level="mid"
|
||||
)
|
||||
configured = orc_profiles.resolve_profile(
|
||||
config, stage="docker", host_cli="codex"
|
||||
)
|
||||
configured = orc_profiles.resolve_profile(config, stage="docker")
|
||||
fallback_config = copy.deepcopy(config)
|
||||
del fallback_config["stageDefaults"]["docker"]
|
||||
fallback = orc_profiles.resolve_profile(fallback_config, stage="docker")
|
||||
fallback = orc_profiles.resolve_profile(
|
||||
fallback_config, stage="docker", host_cli="codex"
|
||||
)
|
||||
|
||||
self.assertEqual((stage["level"], stage["selectionSource"]), ("high", "request.stage"))
|
||||
self.assertEqual((global_choice["level"], global_choice["selectionSource"]), ("mid", "request.global"))
|
||||
self.assertEqual((configured["level"], configured["selectionSource"]), ("low", "config.stageDefaults.docker"))
|
||||
self.assertEqual((fallback["level"], fallback["selectionSource"]), ("mid", "config.defaultLevel"))
|
||||
self.assertEqual(configured["cli"], "codex")
|
||||
self.assertEqual(configured["cliSelectionSource"], "runtime.host")
|
||||
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "host CLI is required"):
|
||||
orc_profiles.resolve_profile(config, stage="docker")
|
||||
|
||||
def test_resolver_builds_fixed_worker_args_without_free_command_fields(self) -> None:
|
||||
config = orc_profiles.load_config(CONFIG)
|
||||
plan = orc_profiles.resolve_profile(config, stage="release")
|
||||
plan = orc_profiles.resolve_profile(
|
||||
config, stage="release", host_cli="codex"
|
||||
)
|
||||
|
||||
self.assertEqual(plan["workerArgs"][:2], ["--model", "gpt-5.6-terra"])
|
||||
self.assertIn("--strict-config", plan["workerArgs"])
|
||||
@@ -113,6 +150,40 @@ class OrcSkillTests(unittest.TestCase):
|
||||
self.assertNotIn("env", plan["profile"])
|
||||
self.assertNotIn("command", plan["profile"])
|
||||
|
||||
cursor = orc_profiles.resolve_profile(
|
||||
config,
|
||||
stage="release",
|
||||
host_cli="cursor-agent",
|
||||
stage_level="low",
|
||||
)
|
||||
self.assertEqual(cursor["cli"], "cursor-agent")
|
||||
self.assertEqual(cursor["cliSelectionSource"], "runtime.host")
|
||||
self.assertEqual(cursor["modelAuth"], "cursor-login")
|
||||
self.assertEqual(
|
||||
cursor["workerArgs"],
|
||||
[
|
||||
"--model",
|
||||
"auto",
|
||||
"--auto-review",
|
||||
"--sandbox",
|
||||
"enabled",
|
||||
],
|
||||
)
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "not valid for cursor-agent"):
|
||||
orc_profiles.resolve_profile(
|
||||
config,
|
||||
stage="release",
|
||||
host_cli="cursor-agent",
|
||||
model_auth="openai",
|
||||
)
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "not valid for codex"):
|
||||
orc_profiles.resolve_profile(
|
||||
config,
|
||||
stage="release",
|
||||
host_cli="codex",
|
||||
model_auth="cursor-api-key",
|
||||
)
|
||||
|
||||
def test_worker_environment_selects_one_model_and_remote_auth(self) -> None:
|
||||
ambient = {
|
||||
"OPENAI_API_KEY": "openai-secret",
|
||||
@@ -145,6 +216,19 @@ class OrcSkillTests(unittest.TestCase):
|
||||
):
|
||||
self.assertNotIn(rejected, environment)
|
||||
|
||||
cursor_ambient = {
|
||||
"CURSOR_API_KEY": "cursor-secret",
|
||||
"OPENAI_API_KEY": "openai-secret",
|
||||
}
|
||||
with mock.patch.dict(os.environ, cursor_ambient, clear=True):
|
||||
cursor_environment = orc_profiles.worker_environment(
|
||||
"release",
|
||||
model_auth="cursor-api-key",
|
||||
remote_auth="none",
|
||||
)
|
||||
self.assertEqual(cursor_environment["CURSOR_API_KEY"], "cursor-secret")
|
||||
self.assertNotIn("OPENAI_API_KEY", cursor_environment)
|
||||
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "deb remote auth"):
|
||||
orc_profiles.worker_environment(
|
||||
"deb",
|
||||
@@ -240,35 +324,60 @@ class OrcSkillTests(unittest.TestCase):
|
||||
base = orc_profiles.load_config(CONFIG)
|
||||
|
||||
free_command = copy.deepcopy(base)
|
||||
free_command["profiles"]["low"]["command"] = "codex --dangerously-bypass"
|
||||
free_command["profiles"]["codex"]["low"]["command"] = "codex --dangerously-bypass"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "unknown fields"):
|
||||
orc_profiles.validate_config(free_command)
|
||||
|
||||
full_access = copy.deepcopy(base)
|
||||
full_access["profiles"]["high"]["permissionMode"] = "danger-full-access"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"):
|
||||
full_access["profiles"]["codex"]["high"]["permissionMode"] = "danger-full-access"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v2"):
|
||||
orc_profiles.validate_config(full_access)
|
||||
|
||||
read_only = copy.deepcopy(base)
|
||||
read_only["profiles"]["low"]["permissionMode"] = "read-only"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v1"):
|
||||
read_only["profiles"]["codex"]["low"]["permissionMode"] = "read-only"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "workspace-write in ORC v2"):
|
||||
orc_profiles.validate_config(read_only)
|
||||
|
||||
missing_level = copy.deepcopy(base)
|
||||
del missing_level["profiles"]["mid"]
|
||||
del missing_level["profiles"]["cursor-agent"]["mid"]
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "missing fields: mid"):
|
||||
orc_profiles.validate_config(missing_level)
|
||||
|
||||
option_model = copy.deepcopy(base)
|
||||
option_model["profiles"]["low"]["model"] = "--model"
|
||||
option_model["profiles"]["codex"]["low"]["model"] = "--model"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "safe exact model ID"):
|
||||
orc_profiles.validate_config(option_model)
|
||||
|
||||
list_policy = copy.deepcopy(base)
|
||||
list_policy["profiles"]["low"]["approvalPolicy"] = ["never"]
|
||||
list_policy["profiles"]["codex"]["low"]["approvalPolicy"] = ["never"]
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "approvalPolicy"):
|
||||
orc_profiles.validate_config(list_policy)
|
||||
|
||||
cursor_effort = copy.deepcopy(base)
|
||||
cursor_effort["profiles"]["cursor-agent"]["low"]["reasoningEffort"] = "low"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "Cursor requires null"):
|
||||
orc_profiles.validate_config(cursor_effort)
|
||||
|
||||
mislabeled_cursor = copy.deepcopy(base)
|
||||
mislabeled_cursor["profiles"]["cursor-agent"]["low"]["model"] = (
|
||||
"gpt-5.6-sol-high"
|
||||
)
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "encode the low"):
|
||||
orc_profiles.validate_config(mislabeled_cursor)
|
||||
|
||||
project_specific = copy.deepcopy(base)
|
||||
project_specific["worktreePolicy"] = ["."]
|
||||
with self.assertRaisesRegex(
|
||||
orc_profiles.ConfigError,
|
||||
"registered-same-repository",
|
||||
):
|
||||
orc_profiles.validate_config(project_specific)
|
||||
|
||||
default_cli = copy.deepcopy(base)
|
||||
default_cli["cliPolicy"] = "default-codex"
|
||||
with self.assertRaisesRegex(orc_profiles.ConfigError, "current-host"):
|
||||
orc_profiles.validate_config(default_cli)
|
||||
|
||||
def test_config_reader_rejects_symlinks_and_redacts_parser_input(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
@@ -282,16 +391,11 @@ class OrcSkillTests(unittest.TestCase):
|
||||
malformed = root / "malformed.yaml"
|
||||
secret_marker = "PRIVATE_MATERIAL_MUST_NOT_APPEAR"
|
||||
malformed.write_text(f"{secret_marker}: [unterminated\n", encoding="utf-8")
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "validate", str(malformed)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(completed.returncode, 1)
|
||||
self.assertNotIn(secret_marker, completed.stderr)
|
||||
with self.assertRaises(orc_profiles.ConfigError) as error:
|
||||
orc_profiles.load_config(malformed)
|
||||
self.assertNotIn(secret_marker, str(error.exception))
|
||||
|
||||
def test_bound_plan_enforces_registered_allowlisted_worktree(self) -> None:
|
||||
def test_bound_plan_enforces_registered_same_repository_worktree(self) -> None:
|
||||
git = shutil.which("git")
|
||||
self.assertIsNotNone(git)
|
||||
assert git is not None
|
||||
@@ -306,13 +410,14 @@ class OrcSkillTests(unittest.TestCase):
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
config_path = root / "docs" / "orc" / "config.yaml"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text(CONFIG.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
fake_codex = root / "trusted-codex" / "codex"
|
||||
fake_codex.parent.mkdir()
|
||||
fake_codex.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
fake_codex.chmod(0o700)
|
||||
fake_cursor = root / "trusted-cursor" / "cursor-agent"
|
||||
fake_cursor.parent.mkdir()
|
||||
fake_cursor.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
fake_cursor.chmod(0o700)
|
||||
fake_orca = root / "trusted-orca" / "orca"
|
||||
fake_orca.parent.mkdir()
|
||||
fake_orca.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
@@ -322,9 +427,20 @@ class OrcSkillTests(unittest.TestCase):
|
||||
return {
|
||||
"git": git_path,
|
||||
"codex": fake_codex,
|
||||
"cursor-agent": fake_cursor,
|
||||
"orca": fake_orca,
|
||||
}[name]
|
||||
|
||||
def executable_facts(path: Path) -> dict[str, object]:
|
||||
return {
|
||||
"path": str(path),
|
||||
"device": 1,
|
||||
"inode": 2,
|
||||
"size": 3,
|
||||
"mtimeNs": 4,
|
||||
"version": f"{path.name}-test 1",
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
orc_profiles,
|
||||
@@ -339,25 +455,33 @@ class OrcSkillTests(unittest.TestCase):
|
||||
mock.patch.object(
|
||||
orc_profiles,
|
||||
"_executable_facts",
|
||||
return_value={
|
||||
"path": str(fake_codex),
|
||||
"device": 1,
|
||||
"inode": 2,
|
||||
"size": 3,
|
||||
"mtimeNs": 4,
|
||||
"version": "codex-test 1",
|
||||
},
|
||||
side_effect=executable_facts,
|
||||
),
|
||||
):
|
||||
plan = orc_profiles.build_launch_plan(
|
||||
config_path,
|
||||
project_root=root,
|
||||
worktree=root,
|
||||
stage="code",
|
||||
host_cli="codex",
|
||||
stage_level="high",
|
||||
)
|
||||
|
||||
cursor_plan = orc_profiles.build_launch_plan(
|
||||
project_root=root,
|
||||
worktree=root,
|
||||
stage="release",
|
||||
host_cli="cursor-agent",
|
||||
stage_level="low",
|
||||
)
|
||||
|
||||
self.assertEqual(plan["argv"][0], str(fake_codex))
|
||||
self.assertEqual(plan["config"]["path"], str(CONFIG))
|
||||
self.assertEqual(cursor_plan["argv"][0], str(fake_cursor))
|
||||
self.assertEqual(cursor_plan["executable"]["path"], str(fake_cursor))
|
||||
self.assertEqual(cursor_plan["argv"][1:3], ["--model", "auto"])
|
||||
self.assertIn("--auto-review", cursor_plan["argv"])
|
||||
self.assertEqual(cursor_plan["modelAuth"], "cursor-login")
|
||||
self.assertIn("--host-cli", cursor_plan["launcherArgv"])
|
||||
self.assertEqual(plan["terminalCreateArgv"][0], str(fake_orca))
|
||||
self.assertEqual(plan["worktree"], str(root))
|
||||
self.assertEqual(plan["selectionSource"], "request.stage")
|
||||
@@ -392,10 +516,10 @@ class OrcSkillTests(unittest.TestCase):
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
orc_profiles.ConfigError,
|
||||
"not in allowedWorktrees",
|
||||
"not registered in the project repository",
|
||||
):
|
||||
orc_profiles.validate_worktree(
|
||||
orc_profiles.load_config(config_path),
|
||||
orc_profiles.load_config(CONFIG),
|
||||
project_root_value=root,
|
||||
worktree_value=outside,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user