feat(ack): add grok workers and allow --always-approve
Grok is a first-class worker CLI. Launcher argv includes --always-approve so unattended tool calls are not blocked; sandbox stays required.
This commit is contained in:
+36
-16
@@ -9,8 +9,9 @@ ACK 是一个显式调用的 Agent Skill,用三种独立角色运行工程协
|
|||||||
关键约束是验证者不等于实现者。每个任务最多修复三轮,仍未通过时记录为
|
关键约束是验证者不等于实现者。每个任务最多修复三轮,仍未通过时记录为
|
||||||
`leftover`,然后继续处理其它任务。
|
`leftover`,然后继续处理其它任务。
|
||||||
|
|
||||||
项目还可以声明一个可选的交付阶段:任务全部验证后,ACK 按项目维护的 profile
|
项目还可以在同一份 `docs/ack/delivery.yaml` 里声明测试环境部署和版本发布。
|
||||||
构建 DEB 或镜像、发布产物、创建 PR,并在授权范围内部署。交付配置默认关闭,
|
用户告诉 ACK 这两件事怎么做之后,再说「重新布测试环境」或「发布一个版本」,
|
||||||
|
ACK 按对应 intent 执行。任务全部验证后仍可按 profile 做常规交付。配置默认关闭,
|
||||||
稳定发布与生产部署始终保留人工批准点。
|
稳定发布与生产部署始终保留人工批准点。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
@@ -70,8 +71,8 @@ skills/ack/
|
|||||||
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
|
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
|
||||||
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
|
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
|
||||||
已经独立验证的项目知识护栏。
|
已经独立验证的项目知识护栏。
|
||||||
`docs/ack/delivery.yaml` 声明项目特有的构建、发布和部署能力;每次执行结果另记在
|
`docs/ack/delivery.yaml` 是测试环境部署和版本发布的唯一契约,也声明常规构建、
|
||||||
`tasks.yaml.deliveryRuns`,配置与运行状态不会混在一起。
|
发布和部署能力;每次执行结果另记在 `tasks.yaml.deliveryRuns`。
|
||||||
|
|
||||||
## 检查项目状态
|
## 检查项目状态
|
||||||
|
|
||||||
@@ -139,16 +140,29 @@ python3 <ack-skill-dir>/scripts/run_verification.py \
|
|||||||
|
|
||||||
## 配置与运行交付
|
## 配置与运行交付
|
||||||
|
|
||||||
用户可以直接向 `/ack` 描述项目差异,例如“这个项目用 `make deb` 构建 DEB,推到
|
用户可以直接向 `/ack` 说明两件独立操作,并写进同一份契约:
|
||||||
preview APT 源,再部署到开发机”。ACK 会把它维护成
|
|
||||||
`docs/ack/delivery.yaml` 中的声明式 entrypoint、artifact、destination、environment
|
```text
|
||||||
和 profile,校验后展示 diff;首次配置保持关闭,确认后才启用。
|
/ack 测试时先 go build -o garden ./cmd/garden,再启动这个二进制;
|
||||||
|
发版方式以后再告诉你。
|
||||||
|
```
|
||||||
|
|
||||||
|
ACK 把它维护成 `docs/ack/delivery.yaml` 的 `intents.testEnvironment` /
|
||||||
|
`intents.release`、entrypoint、artifact、environment 和 profile。首次配置保持
|
||||||
|
关闭,确认后才启用。之后用户可以说:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/ack 重新布一下测试环境,我要测试
|
||||||
|
/ack 发布一个版本
|
||||||
|
```
|
||||||
|
|
||||||
|
对应 intent 未配置时先问清楚并写回同一文件,不猜测。intent 运行不要求当前有
|
||||||
|
`verified` 任务;`deliveryRuns.intent` 记录是测试环境还是发版。
|
||||||
|
|
||||||
交付配置只允许声明式工具 target 或仓库内可执行脚本,不接受自由 shell,也不保存
|
交付配置只允许声明式工具 target 或仓库内可执行脚本,不接受自由 shell,也不保存
|
||||||
凭据值。ACK 在任务进入 `verified` 后,按用户确认的 profile 执行,并把 revision、
|
凭据值。任务进入 `verified` 后的常规交付仍按确认过的 profile 执行。默认
|
||||||
PR、产物摘要、部署目标、健康检查和日志引用写入 `tasks.yaml.deliveryRuns`。默认
|
profile 只能停在 `validation_ready` 或 `review_ready`;稳定发布或生产部署必须经过
|
||||||
profile 只能停在 `review_ready`;稳定发布或生产部署必须经过对应 approval 步骤。
|
对应 approval 步骤。具体契约见 `references/delivery.md`。
|
||||||
具体契约见 `references/delivery.md`。
|
|
||||||
|
|
||||||
## 启动 Worker
|
## 启动 Worker
|
||||||
|
|
||||||
@@ -191,8 +205,10 @@ identity。Coordinator 将 receipt 追加到顶层 `workerReceipts`,再把 rec
|
|||||||
不能跨任务或跨轮次改挂。
|
不能跨任务或跨轮次改挂。
|
||||||
|
|
||||||
v0.10 自动 launcher 仅支持 `read-only` 与 `workspace-write`。full-access、
|
v0.10 自动 launcher 仅支持 `read-only` 与 `workspace-write`。full-access、
|
||||||
Codex bypass、Cursor YOLO/force 和关闭 sandbox 都会 fail closed;在有可信平台
|
Codex bypass、Cursor YOLO/force、Grok `--yolo` / bypassPermissions 和关闭
|
||||||
审批或独立签发通道之前,不用项目文件伪装成用户授权。旧任务板没有结构化
|
sandbox 都会 fail closed;在有可信平台审批或独立签发通道之前,不用项目文件
|
||||||
|
伪装成用户授权。Grok worker 由 launcher 固定带 `--always-approve`,避免工具调用
|
||||||
|
停在确认框,sandbox 仍必须启用。v0.17 起 `cli: grok` 是一等 worker CLI。旧任务板没有结构化
|
||||||
`project.orchestration` 时仍可读取和手动协作,但不得自动创建 worker。
|
`project.orchestration` 时仍可读取和手动协作,但不得自动创建 worker。
|
||||||
|
|
||||||
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。ACK 只复用同一轮
|
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。ACK 只复用同一轮
|
||||||
@@ -243,5 +259,9 @@ ACK 会自动读取 `delivery.yaml`,无需再逐步提醒它构建、上传、
|
|||||||
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
|
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
|
||||||
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
|
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
|
||||||
`project.deliveryFile` 与 `deliveryRuns`;从 `0.13.0` 起,Coordinator 使用
|
`project.deliveryFile` 与 `deliveryRuns`;从 `0.13.0` 起,Coordinator 使用
|
||||||
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型。旧项目可以不迁移
|
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型;从 `0.16.0` 起,
|
||||||
而继续使用原闭环。旧项目的 `kitVersion` 可以继续读取,但建议迁移为 `ackVersion`。
|
`delivery.yaml` 可用 `intents.testEnvironment` 与 `intents.release` 把测试环境部署和
|
||||||
|
版本发布写成用户可单独触发的操作;从 `0.17.0` 起,结构化 worker 路由支持
|
||||||
|
`cli: grok`(与 Codex、Cursor 并列);从 `0.17.1` 起 Grok worker argv 固定带
|
||||||
|
`--always-approve`,sandbox 仍必开。旧项目可以不迁移而继续使用原闭环。旧项目的
|
||||||
|
`kitVersion` 可以继续读取,但建议迁移为 `ackVersion`。
|
||||||
|
|||||||
+36
-8
@@ -3,7 +3,7 @@ name: ack
|
|||||||
description: >-
|
description: >-
|
||||||
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
|
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
|
||||||
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求、指挥 Coordinator/Developer/Test
|
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求、指挥 Coordinator/Developer/Test
|
||||||
工作,或配置并执行任务验证后的项目交付流程时使用。
|
工作,配置测试环境与发版方式,重新部署测试环境,或发布版本时使用。
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACK 项目协作入口
|
# ACK 项目协作入口
|
||||||
@@ -21,7 +21,11 @@ description: >-
|
|||||||
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
|
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
|
||||||
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
|
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
|
||||||
- 用户要求用 ACK 做需求、修复问题或继续任务:执行“工作”。
|
- 用户要求用 ACK 做需求、修复问题或继续任务:执行“工作”。
|
||||||
- 用户用自然语言要求增加、修改或关闭项目交付流程:执行“交付配置维护”。
|
- 用户用自然语言说明怎么部署测试环境、怎么发布版本,或要求增加、修改、关闭交付
|
||||||
|
流程:执行“交付配置维护”。测试环境和发版必须写进同一份
|
||||||
|
`docs/ack/delivery.yaml`。
|
||||||
|
- 用户要求部署、重新部署测试环境,或按已配置方式开始测试:执行“运行测试环境”。
|
||||||
|
- 用户要求发布版本:执行“运行版本发布”。
|
||||||
|
|
||||||
始终先解析真实项目根目录。优先使用 `git rev-parse --show-toplevel`;不是 Git
|
始终先解析真实项目根目录。优先使用 `git rev-parse --show-toplevel`;不是 Git
|
||||||
项目时使用用户指定目录或当前目录。不要修改项目的 `AGENTS.md`、`CLAUDE.md`
|
项目时使用用户指定目录或当前目录。不要修改项目的 `AGENTS.md`、`CLAUDE.md`
|
||||||
@@ -169,8 +173,9 @@ description: >-
|
|||||||
任一条件不符、清理能力不存在或无法确认清理成功时创建 fresh worker。持久化
|
任一条件不符、清理能力不存在或无法确认清理成功时创建 fresh worker。持久化
|
||||||
receipt 只作审计与 dispatch 关联,不能单独授权复用。当前 Orca 终端接口不能提供
|
receipt 只作审计与 dispatch 关联,不能单独授权复用。当前 Orca 终端接口不能提供
|
||||||
可验证的历史消息清理,因此使用 Orca 时仍走 fresh worker。
|
可验证的历史消息清理,因此使用 Orca 时仍走 fresh worker。
|
||||||
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证,Test 独立黑盒
|
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证;若
|
||||||
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
|
`intents.testEnvironment` 已启用,Coordinator 先按「运行测试环境」拉起服务,再
|
||||||
|
派 Test 独立黑盒复测。Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
|
||||||
`knowledgeApplied` 和 `knowledgeCandidates`,Test 回报 `knowledgeChecks`;
|
`knowledgeApplied` 和 `knowledgeCandidates`,Test 回报 `knowledgeChecks`;
|
||||||
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
|
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
|
||||||
7. 执行知识项的 `verification.ref` 时,只调用
|
7. 执行知识项的 `verification.ref` 时,只调用
|
||||||
@@ -202,8 +207,10 @@ description: >-
|
|||||||
## 交付配置维护
|
## 交付配置维护
|
||||||
|
|
||||||
1. 读取 `references/delivery.md`、模板、schema、现有 `delivery.yaml`、项目构建/发布
|
1. 读取 `references/delivery.md`、模板、schema、现有 `delivery.yaml`、项目构建/发布
|
||||||
入口和 CI;把用户自然语言描述转换为结构化 entrypoint、artifact、destination、
|
入口和 CI;把用户自然语言描述转换为结构化 `intents`、entrypoint、artifact、
|
||||||
environment 与 profile。配置只引用仓库内脚本或声明式工具 target,不保存 shell。
|
destination、environment 与 profile。测试环境部署和版本发布都写进这一份
|
||||||
|
`delivery.yaml`,不要拆成第二份文档。配置只引用仓库内脚本或声明式工具 target,
|
||||||
|
不保存 shell。
|
||||||
2. 若旧项目首次启用,生成 `docs/ack/delivery.yaml`,在 `tasks.yaml.project` 增加
|
2. 若旧项目首次启用,生成 `docs/ack/delivery.yaml`,在 `tasks.yaml.project` 增加
|
||||||
`deliveryFile: docs/ack/delivery.yaml`,并增加顶层 `deliveryRuns: []`;不改写其它
|
`deliveryFile: docs/ack/delivery.yaml`,并增加顶层 `deliveryRuns: []`;不改写其它
|
||||||
项目状态。首次生成保持 `enabled: false`,先展示 diff 和解析出的执行顺序。
|
项目状态。首次生成保持 `enabled: false`,先展示 diff 和解析出的执行顺序。
|
||||||
@@ -213,13 +220,34 @@ description: >-
|
|||||||
执行的 run 使用开始时审阅的 commit/config revision 快照,不能借当前分支修改
|
执行的 run 使用开始时审阅的 commit/config revision 快照,不能借当前分支修改
|
||||||
扩大权限。
|
扩大权限。
|
||||||
|
|
||||||
|
## 运行测试环境
|
||||||
|
|
||||||
|
1. 读取 `docs/ack/delivery.yaml` 与 `references/delivery.md`。
|
||||||
|
2. `enabled` 不为 true,或 `intents.testEnvironment` 为 null:停止,请用户说明如何
|
||||||
|
部署测试环境,转入交付配置维护。不猜测编译或启动命令。
|
||||||
|
3. 不要求任务已 `verified`。按该 profile 执行 build → deploy → health-check。
|
||||||
|
4. 把访问地址交给用户或随后的 Test 黑盒。证据写入 `deliveryRuns`,
|
||||||
|
`intent: testEnvironment`,`taskIds` 可为空。
|
||||||
|
5. 派发 Test 前若该 intent 已启用,必须先完成本步骤。
|
||||||
|
|
||||||
|
## 运行版本发布
|
||||||
|
|
||||||
|
1. 读取同一份 `docs/ack/delivery.yaml` 与 `references/delivery.md`。
|
||||||
|
2. `enabled` 不为 true,或 `intents.release` 为 null:停止,请用户说明如何发版,
|
||||||
|
写入同一文件后再执行。
|
||||||
|
3. 按该 profile 顺序执行。stable 发布和生产部署的 `approval` 不能用口头「发版」
|
||||||
|
代替。
|
||||||
|
4. 证据写入 `deliveryRuns`,`intent: release`;绑定了任务时 `taskIds` 仍只能引用
|
||||||
|
`verified` 任务。
|
||||||
|
|
||||||
## 边界
|
## 边界
|
||||||
|
|
||||||
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
|
- 不修改或追加任何项目 Agent 指令文件,包括 `AGENTS.md`。
|
||||||
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
|
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
|
||||||
- 不猜测项目命令、服务地址、worker handle 或模型名称。
|
- 不猜测项目命令、服务地址、worker handle 或模型名称。
|
||||||
- 不把 full-access、bypass、YOLO/force、关闭 sandbox 或项目内“授权”字段当成
|
- 不把 full-access、bypass、YOLO/force、Grok `--yolo` / bypassPermissions、
|
||||||
v0.10 自动 worker 的合法配置;当前一律 fail closed。
|
关闭 sandbox 或项目内“授权”字段当成 v0.10 自动 worker 的合法配置;当前一律
|
||||||
|
fail closed。Grok worker 由 launcher 固定带 `--always-approve`,仍必须带 sandbox。
|
||||||
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation;
|
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation;
|
||||||
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
|
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
|
||||||
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
|
- launcher 返回 `indeterminate` 或 `reconcile required` 时,不直接重试;先按
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
0.15.0
|
0.17.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "ACK"
|
display_name: "ACK"
|
||||||
short_description: "初始化、检查并运行 ACK 开发、验证与可选交付闭环"
|
short_description: "初始化、检查并运行 ACK 开发、验证与可选交付闭环"
|
||||||
default_prompt: "Use $ack to initialize or check ACK, coordinate verified work, maintain project delivery configuration, or run an approved delivery profile."
|
default_prompt: "Use $ack to initialize or check ACK, coordinate verified work, record how to deploy the test environment and publish a release in delivery.yaml, redeploy the test environment, or publish a version."
|
||||||
policy:
|
policy:
|
||||||
allow_implicit_invocation: false
|
allow_implicit_invocation: false
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ project:
|
|||||||
enabled: true
|
enabled: true
|
||||||
defaultProfile: "review"
|
defaultProfile: "review"
|
||||||
|
|
||||||
|
intents:
|
||||||
|
testEnvironment: test-env
|
||||||
|
release: null
|
||||||
|
|
||||||
entrypoints:
|
entrypoints:
|
||||||
verify:
|
verify:
|
||||||
kind: make
|
kind: make
|
||||||
@@ -100,6 +104,19 @@ environments:
|
|||||||
mutex: "notes-test-deploy"
|
mutex: "notes-test-deploy"
|
||||||
|
|
||||||
profiles:
|
profiles:
|
||||||
|
test-env:
|
||||||
|
stopAt: validation_ready
|
||||||
|
steps:
|
||||||
|
- id: build-deb
|
||||||
|
action: build
|
||||||
|
artifact: service-deb
|
||||||
|
- id: deploy-test
|
||||||
|
action: deploy
|
||||||
|
artifact: service-deb
|
||||||
|
environment: test-server
|
||||||
|
- id: smoke-test
|
||||||
|
action: health-check
|
||||||
|
environment: test-server
|
||||||
review:
|
review:
|
||||||
stopAt: review_ready
|
stopAt: review_ready
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# notes-web Agent 协作协议(示例,项目覆盖层)
|
# notes-web Agent 协作协议(示例,项目覆盖层)
|
||||||
|
|
||||||
> 本项目基于 ack v0.15.0。
|
> 本项目基于 ack v0.17.1。
|
||||||
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
||||||
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
|
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`。
|
||||||
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
| Test | `codex-test-standard` | standard |
|
| Test | `codex-test-standard` | standard |
|
||||||
| Developer 升级 | `codex-dev-strong` | strong |
|
| Developer 升级 | `codex-dev-strong` | strong |
|
||||||
|
|
||||||
本文件不保存或拼接 worker shell 命令。
|
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行
|
||||||
|
任务板校验。本文件不保存或拼接 worker shell 命令。
|
||||||
|
|
||||||
## 路径权限
|
## 路径权限
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
version: 1
|
version: 1
|
||||||
updatedAt: "2026-07-06T09:40:00+08:00"
|
updatedAt: "2026-07-06T09:40:00+08:00"
|
||||||
source: "Coordinator (PM) Agent"
|
source: "Coordinator (PM) Agent"
|
||||||
ackVersion: "0.15.0"
|
ackVersion: "0.17.1"
|
||||||
project:
|
project:
|
||||||
name: "notes-web"
|
name: "notes-web"
|
||||||
baseUrl: "http://localhost:5173"
|
baseUrl: "http://localhost:5173"
|
||||||
@@ -27,6 +27,12 @@ project:
|
|||||||
standard: ["auto"]
|
standard: ["auto"]
|
||||||
test:
|
test:
|
||||||
standard: ["auto"]
|
standard: ["auto"]
|
||||||
|
grok:
|
||||||
|
developer:
|
||||||
|
standard: ["grok-4.5"]
|
||||||
|
strong: ["grok-4.6"]
|
||||||
|
test:
|
||||||
|
standard: ["grok-4.5"]
|
||||||
profiles:
|
profiles:
|
||||||
codex-dev-standard:
|
codex-dev-standard:
|
||||||
role: "developer"
|
role: "developer"
|
||||||
@@ -63,6 +69,27 @@ project:
|
|||||||
model: "auto"
|
model: "auto"
|
||||||
reasoningEffort: null
|
reasoningEffort: null
|
||||||
permissionMode: "workspace-write"
|
permissionMode: "workspace-write"
|
||||||
|
grok-dev-standard:
|
||||||
|
role: "developer"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "standard"
|
||||||
|
model: "grok-4.5"
|
||||||
|
reasoningEffort: "medium"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
|
grok-test-standard:
|
||||||
|
role: "test"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "standard"
|
||||||
|
model: "grok-4.5"
|
||||||
|
reasoningEffort: "low"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
|
grok-dev-strong:
|
||||||
|
role: "developer"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "strong"
|
||||||
|
model: "grok-4.6"
|
||||||
|
reasoningEffort: "high"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
defaults:
|
defaults:
|
||||||
developer: "codex-dev-standard"
|
developer: "codex-dev-standard"
|
||||||
test: "codex-test-standard"
|
test: "codex-test-standard"
|
||||||
|
|||||||
@@ -48,6 +48,8 @@
|
|||||||
## 可选交付
|
## 可选交付
|
||||||
|
|
||||||
- [ ] `delivery.yaml` 首次生成保持 `enabled: false`,没有根据 README/CI 自动启用。
|
- [ ] `delivery.yaml` 首次生成保持 `enabled: false`,没有根据 README/CI 自动启用。
|
||||||
|
- [ ] 测试环境部署和版本发布都写在同一份 `delivery.yaml` 的 `intents` 中,没有第二份
|
||||||
|
操作文档。未说明的 intent 保持 `null`。
|
||||||
- [ ] entrypoint 只使用声明式工具 target 或仓库内无 symlink 的可执行脚本;没有
|
- [ ] entrypoint 只使用声明式工具 target 或仓库内无 symlink 的可执行脚本;没有
|
||||||
shell、自由 command、凭据值或环境变量值。
|
shell、自由 command、凭据值或环境变量值。
|
||||||
- [ ] artifact、destination、environment 和 profile 引用均通过
|
- [ ] artifact、destination、environment 和 profile 引用均通过
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ Coordinator 发现或读取 open 任务
|
|||||||
-> dispatch 给 Developer(--to <worker handle>)
|
-> dispatch 给 Developer(--to <worker handle>)
|
||||||
-> wait:Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
-> wait:Developer 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates)
|
||||||
-> writeback fixed_by_dev
|
-> writeback fixed_by_dev
|
||||||
|
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先执行该 profile
|
||||||
|
拉起待测服务,再派 Test;Test 不发明编译或启动命令
|
||||||
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
|
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
|
||||||
-> dispatch 给 Test(retesting)
|
-> dispatch 给 Test(retesting)
|
||||||
-> wait:Test 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
|
-> wait:Test 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
|
||||||
@@ -265,7 +267,7 @@ frontendDir:
|
|||||||
worktreePath:
|
worktreePath:
|
||||||
```
|
```
|
||||||
|
|
||||||
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。
|
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。若项目配置了 `intents.testEnvironment`,重启方式以该 profile 为准,不另写一套启动命令。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,31 @@ channel、environment 或 source revision 漂移时重新确认。
|
|||||||
凭据系统或执行环境注入。复杂逻辑放在受版本控制的项目入口中。entrypoint 使用 argv
|
凭据系统或执行环境注入。复杂逻辑放在受版本控制的项目入口中。entrypoint 使用 argv
|
||||||
语义执行,不能拼成 `sh -c` 字符串。
|
语义执行,不能拼成 `sh -c` 字符串。
|
||||||
|
|
||||||
|
## 3.1 测试环境与发版写在同一份契约
|
||||||
|
|
||||||
|
`docs/ack/delivery.yaml` 是测试环境部署和版本发布的唯一文档。不要另写操作手册,
|
||||||
|
也不要把其中一项写进 `project.md`。用户用自然语言说明「怎么布测试环境」或
|
||||||
|
「怎么发版」时,Coordinator 把两者都维护进这份文件的 `intents`、entrypoint、
|
||||||
|
artifact、environment 和 profile。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
intents:
|
||||||
|
testEnvironment: local-binary # profile ID,或 null
|
||||||
|
release: null # profile ID,或 null
|
||||||
|
```
|
||||||
|
|
||||||
|
- `testEnvironment` 指向 `stopAt: validation_ready` 的 profile:build 产物、部署到
|
||||||
|
development/staging、健康检查。用户说「重新布测试环境」「我要测试」时执行它;
|
||||||
|
派发 Test 复测前,若该 intent 已配置且 `enabled: true`,Coordinator 也先执行它。
|
||||||
|
不要求当前有 `verified` 任务。Test 不对这个 intent 发明编译或启动命令。
|
||||||
|
- `release` 指向 `stopAt: released` 的 profile。用户说「发布一个版本」时执行它。
|
||||||
|
口头「发版」不能代替 stable/production 的 `approval` 步骤。
|
||||||
|
- 对应 intent 为 `null` 或交付未启用:停止,请用户说明怎么做,按「交付配置维护」
|
||||||
|
写入同一文件后再执行。不猜测 Makefile、镜像仓库或发布通道。
|
||||||
|
- 用户触发的 intent 运行写入 `tasks.yaml.deliveryRuns`,`intent` 填
|
||||||
|
`testEnvironment` 或 `release`,`taskIds` 可为空。绑定任务的常规交付 run 不填
|
||||||
|
`intent`,仍只能引用 `verified` 任务。
|
||||||
|
|
||||||
## 4. 运行前检查
|
## 4. 运行前检查
|
||||||
|
|
||||||
1. 从 `tasks.yaml.project.deliveryFile` 解析文件;未引用或 `enabled=false` 时保持旧 ACK
|
1. 从 `tasks.yaml.project.deliveryFile` 解析文件;未引用或 `enabled=false` 时保持旧 ACK
|
||||||
|
|||||||
@@ -116,10 +116,11 @@ candidate 留在任务证据中,不会被派发。只有 Test 独立验证且
|
|||||||
|
|
||||||
## 初始化项目交付
|
## 初始化项目交付
|
||||||
|
|
||||||
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表和空 profile。
|
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表、空 profile,以及
|
||||||
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言描述交付要求后,
|
`intents.testEnvironment: null` 与 `intents.release: null`。
|
||||||
Coordinator 按 `delivery.md` 维护声明式配置:工具 target 与仓库脚本分开引用,产物、
|
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言说明测试环境或发版
|
||||||
目的地、环境和有序 profile 使用稳定 ID。配置中不保存 shell、环境变量值或凭据
|
方式后,Coordinator 按 `delivery.md` 把两者都写入这一份契约:`intents` 指向对应
|
||||||
|
profile,工具 target 与仓库脚本分开引用。配置中不保存 shell、环境变量值或凭据
|
||||||
正文;稳定发布和生产部署必须有显式 approval 步骤。
|
正文;稳定发布和生产部署必须有显式 approval 步骤。
|
||||||
|
|
||||||
## 校验
|
## 校验
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
Developer/Test profile;优先选择同一轮内角色/profile/worktree 匹配的空闲 worker,
|
Developer/Test profile;优先选择同一轮内角色/profile/worktree 匹配的空闲 worker,
|
||||||
只有历史消息已可信清理并取得新会话身份才复用,否则审阅 plan 后用 expected
|
只有历史消息已可信清理并取得新会话身份才复用,否则审阅 plan 后用 expected
|
||||||
fingerprint 创建 fresh worker;
|
fingerprint 创建 fresh worker;
|
||||||
dispatch 开发 → worker_done → dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
dispatch 开发 → worker_done → 若 intents.testEnvironment 已启用则先拉起测试环境 →
|
||||||
|
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml;
|
||||||
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
|
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
|
||||||
记录、恢复并告诉我下一步,不占产品复验轮次。
|
记录、恢复并告诉我下一步,不占产品复验轮次。
|
||||||
7. 所选任务都 verified 后,只有本次计划包含交付时才按 profile 顺序执行并写
|
7. 所选任务都 verified 后,只有本次计划包含交付时才按 profile 顺序执行并写
|
||||||
@@ -154,7 +155,7 @@ worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调
|
|||||||
task-create → dispatch 给 DEV → 等 worker_done
|
task-create → dispatch 给 DEV → 等 worker_done
|
||||||
→ 每个角色先检查可安全重置的空闲 worker;不符合即通过 plan + expected fingerprint launch fresh worker
|
→ 每个角色先检查可安全重置的空闲 worker;不符合即通过 plan + expected fingerprint launch fresh worker
|
||||||
→ 每轮使用 Coordinator 分配的稳定 <task-id>-A<round>
|
→ 每轮使用 Coordinator 分配的稳定 <task-id>-A<round>
|
||||||
→ 回写 fixed_by_dev → dispatch 给 TEST 复测 → 等 retest_result
|
→ 回写 fixed_by_dev → 若 intents.testEnvironment 已启用则先拉起测试环境 → dispatch 给 TEST 复测 → 等 retest_result
|
||||||
→ Developer 回 knowledgeApplied / knowledgeCandidates,Test 回 knowledgeChecks
|
→ Developer 回 knowledgeApplied / knowledgeCandidates,Test 回 knowledgeChecks
|
||||||
→ 环境无法完成:记录 environmentIncidents,报告影响与用户下一步,恢复后重新复验(不计轮次)
|
→ 环境无法完成:记录 environmentIncidents,报告影响与用户下一步,恢复后重新复验(不计轮次)
|
||||||
→ Coordinator 读证据终检 → 过则 verified,产品失败则 failed_retest 再派 DEV(最多累计 3 轮)
|
→ Coordinator 读证据终检 → 过则 verified,产品失败则 failed_retest 再派 DEV(最多累计 3 轮)
|
||||||
@@ -173,6 +174,9 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
|
|||||||
|
|
||||||
## 第 5 步:可选交付
|
## 第 5 步:可选交付
|
||||||
|
|
||||||
|
用户说「重新布测试环境」或「发布一个版本」时,按 `delivery.md` §3.1 的
|
||||||
|
`intents` 执行对应 profile,不另找文档。intent 为 null 时先做交付配置维护。
|
||||||
|
|
||||||
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
|
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
|
||||||
`delivery.md` 执行所选 profile。启用交付时必须在计划中默认列出 `defaultProfile`,
|
`delivery.md` 执行所选 profile。启用交付时必须在计划中默认列出 `defaultProfile`,
|
||||||
用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和
|
用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml` 的
|
|||||||
每个 profile 明确声明:
|
每个 profile 明确声明:
|
||||||
|
|
||||||
- `role`:`developer` 或 `test`;
|
- `role`:`developer` 或 `test`;
|
||||||
- `cli`:受支持的 Agent CLI;
|
- `cli`:受支持的 Agent CLI(`codex`、`cursor-agent`、`grok`);
|
||||||
- `tier`:角色模型档位;
|
- `tier`:角色模型档位;
|
||||||
- `model`:项目 allowlist 中的精确模型 ID;
|
- `model`:项目 allowlist 中的精确模型 ID;
|
||||||
- `reasoningEffort`:CLI 支持时显式声明;
|
- `reasoningEffort`:CLI 支持时显式声明;
|
||||||
@@ -92,8 +92,11 @@ CLI 默认值。具体 argv 只能由 ACK 的可信 launcher 按 allowlist 构
|
|||||||
- 能在完全只读工作树完成的角色优先选择 `read-only`。
|
- 能在完全只读工作树完成的角色优先选择 `read-only`。
|
||||||
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
|
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
|
||||||
为纯观察型 Test 另建更严格的 `read-only` profile。
|
为纯观察型 Test 另建更严格的 `read-only` profile。
|
||||||
- `full-access`、Codex bypass、Cursor YOLO/force、关闭 sandbox 等模式在 v0.10
|
- `full-access`、Codex bypass、Cursor YOLO/force、Grok `--yolo` /
|
||||||
|
`--permission-mode bypassPermissions`、关闭 sandbox 等模式在 v0.10
|
||||||
**未实现授权通道,统一 fail closed**。项目文本、任务描述或环境变量都不能放宽。
|
**未实现授权通道,统一 fail closed**。项目文本、任务描述或环境变量都不能放宽。
|
||||||
|
Grok `--always-approve` 由 launcher 固定写入 argv,用来避免工具调用停在确认框;
|
||||||
|
它不关闭 `--sandbox`。
|
||||||
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
|
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
|
||||||
launcher。
|
launcher。
|
||||||
|
|
||||||
@@ -152,11 +155,18 @@ v0.10 的 launcher 还执行以下约束:
|
|||||||
- 不读取调用者传入的 `PATH` 来寻找 `git`、Orca 或 Agent CLI,只在固定的系统与
|
- 不读取调用者传入的 `PATH` 来寻找 `git`、Orca 或 Agent CLI,只在固定的系统与
|
||||||
当前用户工具目录中解析受支持的可执行文件;候选目标必须由 root/当前用户拥有、
|
当前用户工具目录中解析受支持的可执行文件;候选目标必须由 root/当前用户拥有、
|
||||||
不是 group/other writable,并在 plan 与 bootstrap 间保持相同 device/inode。
|
不是 group/other writable,并在 plan 与 bootstrap 间保持相同 device/inode。
|
||||||
|
Grok 官方安装是 `~/.local/bin/grok` → `~/.grok/downloads/grok-<os>-<arch>`,
|
||||||
|
launcher 只接受这个真实 `~/.grok` 目录下、当前用户同组拥有的 vendor artifact
|
||||||
|
名称;其它路径上的 `grok-linux-*` 仍拒绝。`~/.grok` 本身是 symlink 时 fail closed。
|
||||||
- Git、Orca 和 CLI version probe 使用不含供应商凭据的控制环境;worker 使用
|
- Git、Orca 和 CLI version probe 使用不含供应商凭据的控制环境;worker 使用
|
||||||
`per-cli-allowlist-v1`,Codex 只取得 Codex 所需凭据,Cursor 只取得 Cursor 凭据,
|
`per-cli-allowlist-v1`,Codex 只取得 Codex 所需凭据,Cursor 只取得 Cursor 凭据,
|
||||||
|
Grok 只取得 `XAI_API_KEY`(OAuth 会话仍走已传入的 `HOME`/`~/.grok`),
|
||||||
不跨 CLI 透传。`DBUS_*`、`DISPLAY`、`WAYLAND_DISPLAY`、`XDG_RUNTIME_DIR`、
|
不跨 CLI 透传。`DBUS_*`、`DISPLAY`、`WAYLAND_DISPLAY`、`XDG_RUNTIME_DIR`、
|
||||||
`LD_*`、`PYTHON*`、`NODE_OPTIONS`、`CODEX_HOME`、`GIT_*` 和调用者 PATH 均不会
|
`LD_*`、`PYTHON*`、`NODE_OPTIONS`、`CODEX_HOME`、`GROK_HOME`、`GROK_SANDBOX`、
|
||||||
透传给 worker。
|
`GIT_*` 和调用者 PATH 均不会透传给 worker。Grok argv 只允许
|
||||||
|
`--model`、`--reasoning-effort`、`--permission-mode plan|acceptEdits`、
|
||||||
|
`--always-approve`、`--sandbox read-only|workspace` 和 `--cwd`;不得带 `--yolo`
|
||||||
|
或 `bypassPermissions`,也不得省略 sandbox。
|
||||||
- worktree 除了命中项目 allowlist、属于同一 Git common-dir,还必须精确出现在
|
- worktree 除了命中项目 allowlist、属于同一 Git common-dir,还必须精确出现在
|
||||||
`git worktree list --porcelain -z` 中;`.git` symlink 冒充的目录会失败。
|
`git worktree list --porcelain -z` 中;`.git` symlink 冒充的目录会失败。
|
||||||
- Orca 的 `--command` 只含固定 bootstrap 与随机 launch ID。终端创建后,父进程先
|
- Orca 的 `--command` 只含固定 bootstrap 与随机 launch ID。终端创建后,父进程先
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ Developer 本轮声称(仅供参考,不作数):
|
|||||||
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
|
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
|
||||||
|
|
||||||
复测要求(见 roles-and-permissions.md §三角色能力清单 · Test):
|
复测要求(见 roles-and-permissions.md §三角色能力清单 · Test):
|
||||||
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 closed-loop.md),避免测错实例或旧构建;网站类先确认服务已按新代码重启。
|
- 先对齐运行环境(pwd / 分支 / commit / 服务 worktree,见 closed-loop.md),避免测错实例或旧构建。测试环境由 Coordinator 按 `delivery.yaml` 的 `intents.testEnvironment` 拉起;不要自行发明编译或启动命令。网站类确认 Base URL 已指向这次产物后再测。
|
||||||
- 网站类任务优先用浏览器复测真实交互,其次才是 API / 脚本。
|
- 网站类任务优先用浏览器复测真实交互,其次才是 API / 脚本。
|
||||||
- 逐条验证下列验收信号,不要只看静态文案,要验证交互后的真实状态:
|
- 逐条验证下列验收信号,不要只看静态文案,要验证交互后的真实状态:
|
||||||
1. <observable signal 1>
|
1. <observable signal 1>
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ if str(SCRIPT_DIR) not in sys.path:
|
|||||||
|
|
||||||
from validate_tasks import load_document, validate_builtin # noqa: E402
|
from validate_tasks import load_document, validate_builtin # noqa: E402
|
||||||
from worker_profiles import ( # noqa: E402
|
from worker_profiles import ( # noqa: E402
|
||||||
|
GROK_EXECUTABLE_NAME_RE,
|
||||||
LAUNCH_PROTOCOL_VERSION,
|
LAUNCH_PROTOCOL_VERSION,
|
||||||
canonical_sha256,
|
canonical_sha256,
|
||||||
|
executable_basename_matches_cli,
|
||||||
profile_hash,
|
profile_hash,
|
||||||
render_worker_argv,
|
render_worker_argv,
|
||||||
validate_routing_document,
|
validate_routing_document,
|
||||||
@@ -81,6 +83,12 @@ WORKER_ENVIRONMENT_NAMES = CONTROL_ENVIRONMENT_NAMES | {
|
|||||||
WORKER_CREDENTIAL_NAMES = {
|
WORKER_CREDENTIAL_NAMES = {
|
||||||
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
||||||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||||||
|
"grok": frozenset({"XAI_API_KEY"}),
|
||||||
|
}
|
||||||
|
CLI_TITLE_LABELS = {
|
||||||
|
"codex": "CODEX",
|
||||||
|
"cursor-agent": "CURSOR",
|
||||||
|
"grok": "GROK",
|
||||||
}
|
}
|
||||||
INHERITED_ENVIRONMENT_PREFIXES = (
|
INHERITED_ENVIRONMENT_PREFIXES = (
|
||||||
"LC_",
|
"LC_",
|
||||||
@@ -263,8 +271,41 @@ def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
|
|||||||
return project_root, board
|
return project_root, board
|
||||||
|
|
||||||
|
|
||||||
|
def _is_under_real_grok_home(resolved: Path) -> bool:
|
||||||
|
"""Return True when ``resolved`` is under a non-symlink ``$HOME/.grok``."""
|
||||||
|
|
||||||
|
home, _ = account_identity()
|
||||||
|
grok_root = home / ".grok"
|
||||||
|
try:
|
||||||
|
root_metadata = os.lstat(grok_root)
|
||||||
|
if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):
|
||||||
|
return False
|
||||||
|
grok_root.resolve(strict=True)
|
||||||
|
resolved.relative_to(grok_root.resolve(strict=True))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_trusted_grok_executable(resolved: Path, metadata: os.stat_result) -> bool:
|
||||||
|
"""Accept Grok's vendor artifact names under ``~/.grok``, or a 0755 ``grok``."""
|
||||||
|
|
||||||
|
if GROK_EXECUTABLE_NAME_RE.fullmatch(resolved.name) is None:
|
||||||
|
return False
|
||||||
|
if not executable_basename_matches_cli(str(resolved), "grok"):
|
||||||
|
return False
|
||||||
|
mode = stat.S_IMODE(metadata.st_mode)
|
||||||
|
if mode & 0o002:
|
||||||
|
return False
|
||||||
|
if resolved.name == "grok" and not (mode & 0o020):
|
||||||
|
return True
|
||||||
|
if not _is_under_real_grok_home(resolved):
|
||||||
|
return False
|
||||||
|
return metadata.st_uid == os.getuid() and metadata.st_gid == os.getgid()
|
||||||
|
|
||||||
|
|
||||||
def resolve_executable(name: str) -> Path:
|
def resolve_executable(name: str) -> Path:
|
||||||
supported = {"codex", "cursor-agent", "git", "orca"}
|
supported = {"codex", "cursor-agent", "grok", "git", "orca"}
|
||||||
if name not in supported:
|
if name not in supported:
|
||||||
raise LaunchError(f"不支持的可执行文件: {name}")
|
raise LaunchError(f"不支持的可执行文件: {name}")
|
||||||
search_paths = trusted_path_entries()
|
search_paths = trusted_path_entries()
|
||||||
@@ -289,6 +330,10 @@ def resolve_executable(name: str) -> Path:
|
|||||||
continue
|
continue
|
||||||
if metadata.st_uid not in {0, os.getuid()}:
|
if metadata.st_uid not in {0, os.getuid()}:
|
||||||
continue
|
continue
|
||||||
|
if name == "grok":
|
||||||
|
if _is_trusted_grok_executable(resolved, metadata):
|
||||||
|
return resolved
|
||||||
|
continue
|
||||||
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
||||||
continue
|
continue
|
||||||
if resolved.name != name:
|
if resolved.name != name:
|
||||||
@@ -697,7 +742,10 @@ def build_plan(
|
|||||||
"slot": slot,
|
"slot": slot,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
|
try:
|
||||||
|
cli_label = CLI_TITLE_LABELS[str(profile["cli"])]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise LaunchError(f"不支持的 worker CLI: {profile['cli']}") from exc
|
||||||
role_label = "DEV" if role == "developer" else "TEST"
|
role_label = "DEV" if role == "developer" else "TEST"
|
||||||
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
||||||
title = (
|
title = (
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ TOP_LEVEL_FIELDS = {
|
|||||||
"project",
|
"project",
|
||||||
"enabled",
|
"enabled",
|
||||||
"defaultProfile",
|
"defaultProfile",
|
||||||
|
"intents",
|
||||||
"entrypoints",
|
"entrypoints",
|
||||||
"artifacts",
|
"artifacts",
|
||||||
"destinations",
|
"destinations",
|
||||||
@@ -99,6 +100,11 @@ CHANNELS = {"preview", "staging", "stable"}
|
|||||||
ENVIRONMENT_TYPES = {"ssh-host", "docker-compose", "kubernetes", "custom"}
|
ENVIRONMENT_TYPES = {"ssh-host", "docker-compose", "kubernetes", "custom"}
|
||||||
CLASSIFICATIONS = {"development", "staging", "production"}
|
CLASSIFICATIONS = {"development", "staging", "production"}
|
||||||
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
|
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
|
||||||
|
INTENT_FIELDS = {"testEnvironment", "release"}
|
||||||
|
INTENT_STOP_AT = {
|
||||||
|
"testEnvironment": "validation_ready",
|
||||||
|
"release": "released",
|
||||||
|
}
|
||||||
ACTIONS = {
|
ACTIONS = {
|
||||||
"verify",
|
"verify",
|
||||||
"pull-request",
|
"pull-request",
|
||||||
@@ -675,6 +681,38 @@ def _validate_profiles(
|
|||||||
errors.append(f"{where}: defaultProfile 不能部署 production 环境")
|
errors.append(f"{where}: defaultProfile 不能部署 production 环境")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_intents(
|
||||||
|
values: Any,
|
||||||
|
profiles: dict[str, Any],
|
||||||
|
errors: list[str],
|
||||||
|
) -> None:
|
||||||
|
if values is None:
|
||||||
|
return
|
||||||
|
if not _mapping(values):
|
||||||
|
errors.append("intents: 必须是对象")
|
||||||
|
return
|
||||||
|
_reject_unknown(values, INTENT_FIELDS, "intents", errors)
|
||||||
|
for field in sorted(INTENT_FIELDS):
|
||||||
|
if field not in values:
|
||||||
|
errors.append(f"intents.{field}: 必填")
|
||||||
|
continue
|
||||||
|
profile_id = values[field]
|
||||||
|
if profile_id is None:
|
||||||
|
continue
|
||||||
|
if not isinstance(profile_id, str) or ID_RE.fullmatch(profile_id) is None:
|
||||||
|
errors.append(f"intents.{field}: 必须是 null 或小写连字符 profile ID")
|
||||||
|
continue
|
||||||
|
profile = profiles.get(profile_id)
|
||||||
|
if profile is None:
|
||||||
|
errors.append(f"intents.{field}: 未定义 profile {profile_id!r}")
|
||||||
|
continue
|
||||||
|
expected_stop = INTENT_STOP_AT[field]
|
||||||
|
if _mapping(profile) and profile.get("stopAt") != expected_stop:
|
||||||
|
errors.append(
|
||||||
|
f"intents.{field}: profile {profile_id!r} 必须 stopAt {expected_stop}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_builtin(data: dict[str, Any], project_root: Path | None = None) -> list[str]:
|
def validate_builtin(data: dict[str, Any], project_root: Path | None = None) -> list[str]:
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
_reject_unknown(data, TOP_LEVEL_FIELDS, "<root>", errors)
|
_reject_unknown(data, TOP_LEVEL_FIELDS, "<root>", errors)
|
||||||
@@ -720,6 +758,7 @@ def validate_builtin(data: dict[str, Any], project_root: Path | None = None) ->
|
|||||||
environments=environments,
|
environments=environments,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
_validate_intents(data.get("intents"), profiles, errors)
|
||||||
|
|
||||||
if enabled:
|
if enabled:
|
||||||
if default_profile not in profiles:
|
if default_profile not in profiles:
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ DELIVERY_RUN_FIELDS = {
|
|||||||
"evidence",
|
"evidence",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
}
|
}
|
||||||
|
DELIVERY_RUN_OPTIONAL_FIELDS = {"intent"}
|
||||||
|
DELIVERY_RUN_INTENTS = {"testEnvironment", "release"}
|
||||||
DELIVERY_STATUSES = {
|
DELIVERY_STATUSES = {
|
||||||
"planned",
|
"planned",
|
||||||
"running",
|
"running",
|
||||||
@@ -481,7 +483,9 @@ def validate_delivery_runs(
|
|||||||
if not isinstance(run, dict):
|
if not isinstance(run, dict):
|
||||||
errors.append(f"{where}: 必须是对象")
|
errors.append(f"{where}: 必须是对象")
|
||||||
continue
|
continue
|
||||||
reject_unknown_fields(run, DELIVERY_RUN_FIELDS, where, errors)
|
reject_unknown_fields(
|
||||||
|
run, DELIVERY_RUN_FIELDS | DELIVERY_RUN_OPTIONAL_FIELDS, where, errors
|
||||||
|
)
|
||||||
missing = sorted(DELIVERY_RUN_FIELDS - set(run))
|
missing = sorted(DELIVERY_RUN_FIELDS - set(run))
|
||||||
for field in missing:
|
for field in missing:
|
||||||
errors.append(f"{where}.{field}: 必填")
|
errors.append(f"{where}.{field}: 必填")
|
||||||
@@ -501,13 +505,23 @@ def validate_delivery_runs(
|
|||||||
if status not in DELIVERY_STATUSES:
|
if status not in DELIVERY_STATUSES:
|
||||||
errors.append(f"{where}.status: 必须是 {sorted(DELIVERY_STATUSES)}")
|
errors.append(f"{where}.status: 必须是 {sorted(DELIVERY_STATUSES)}")
|
||||||
|
|
||||||
|
intent = run.get("intent")
|
||||||
|
if "intent" in run and intent not in DELIVERY_RUN_INTENTS:
|
||||||
|
errors.append(
|
||||||
|
f"{where}.intent: 必须是 {sorted(DELIVERY_RUN_INTENTS)}"
|
||||||
|
)
|
||||||
task_ids = run.get("taskIds")
|
task_ids = run.get("taskIds")
|
||||||
|
allow_empty_tasks = intent in DELIVERY_RUN_INTENTS
|
||||||
if (
|
if (
|
||||||
not isinstance(task_ids, list)
|
not isinstance(task_ids, list)
|
||||||
or not task_ids
|
or (not task_ids and not allow_empty_tasks)
|
||||||
or any(not _nonempty_string(task_id) for task_id in task_ids)
|
or any(not _nonempty_string(task_id) for task_id in (task_ids or []))
|
||||||
):
|
):
|
||||||
errors.append(f"{where}.taskIds: 必须是非空任务 ID 列表")
|
errors.append(
|
||||||
|
f"{where}.taskIds: 必须是任务 ID 列表"
|
||||||
|
if allow_empty_tasks
|
||||||
|
else f"{where}.taskIds: 必须是非空任务 ID 列表"
|
||||||
|
)
|
||||||
task_ids = []
|
task_ids = []
|
||||||
elif len(task_ids) != len(set(task_ids)):
|
elif len(task_ids) != len(set(task_ids)):
|
||||||
errors.append(f"{where}.taskIds: 不能包含重复值")
|
errors.append(f"{where}.taskIds: 不能包含重复值")
|
||||||
|
|||||||
@@ -24,12 +24,27 @@ LAUNCH_PROTOCOL_VERSION = 1
|
|||||||
MAX_ROUNDS = 3
|
MAX_ROUNDS = 3
|
||||||
|
|
||||||
ROLES = frozenset({"developer", "test"})
|
ROLES = frozenset({"developer", "test"})
|
||||||
CLIS = frozenset({"codex", "cursor-agent"})
|
CLIS = frozenset({"codex", "cursor-agent", "grok"})
|
||||||
TIERS = frozenset({"standard", "strong"})
|
TIERS = frozenset({"standard", "strong"})
|
||||||
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
||||||
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
||||||
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
||||||
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
||||||
|
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok"})
|
||||||
|
CLI_REQUIRES_NULL_REASONING_EFFORT = frozenset({"cursor-agent"})
|
||||||
|
|
||||||
|
|
||||||
|
def _cli_choice_text() -> str:
|
||||||
|
return "/".join(sorted(CLIS))
|
||||||
|
|
||||||
|
|
||||||
|
def executable_basename_matches_cli(executable: str, cli: str) -> bool:
|
||||||
|
"""Return whether a resolved executable basename is valid for ``cli``."""
|
||||||
|
|
||||||
|
name = Path(executable).name
|
||||||
|
if name == cli:
|
||||||
|
return True
|
||||||
|
return cli == "grok" and GROK_EXECUTABLE_NAME_RE.fullmatch(name) is not None
|
||||||
|
|
||||||
ORCHESTRATION_FIELDS = frozenset(
|
ORCHESTRATION_FIELDS = frozenset(
|
||||||
{
|
{
|
||||||
@@ -114,6 +129,9 @@ ROLE_DISPATCH_FIELDS = frozenset(
|
|||||||
|
|
||||||
PROFILE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
|
PROFILE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
|
||||||
MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$")
|
MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$")
|
||||||
|
GROK_EXECUTABLE_NAME_RE = re.compile(
|
||||||
|
r"^grok(?:-(?:linux|darwin|windows)-(?:x86_64|aarch64|arm64))?$"
|
||||||
|
)
|
||||||
RECEIPT_ID_RE = re.compile(r"^WR-[0-9a-f]{64}$")
|
RECEIPT_ID_RE = re.compile(r"^WR-[0-9a-f]{64}$")
|
||||||
LAUNCH_ID_RE = re.compile(r"^[0-9a-f]{64}$")
|
LAUNCH_ID_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||||
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||||
@@ -214,7 +232,7 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
|||||||
if not isinstance(role, str) or role not in ROLES:
|
if not isinstance(role, str) or role not in ROLES:
|
||||||
errors.append(f"{where}.role: must be developer/test")
|
errors.append(f"{where}.role: must be developer/test")
|
||||||
if not isinstance(cli, str) or cli not in CLIS:
|
if not isinstance(cli, str) or cli not in CLIS:
|
||||||
errors.append(f"{where}.cli: must be codex/cursor-agent")
|
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||||
if not isinstance(tier, str) or tier not in TIERS:
|
if not isinstance(tier, str) or tier not in TIERS:
|
||||||
errors.append(f"{where}.tier: must be standard/strong")
|
errors.append(f"{where}.tier: must be standard/strong")
|
||||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||||
@@ -223,12 +241,17 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
|||||||
errors.append(
|
errors.append(
|
||||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
f"{where}.permissionMode: must be read-only/workspace-write"
|
||||||
)
|
)
|
||||||
if cli == "codex":
|
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT:
|
||||||
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
||||||
|
label = "Codex" if cli == "codex" else "Grok"
|
||||||
errors.append(
|
errors.append(
|
||||||
f"{where}.reasoningEffort: Codex requires low/medium/high/xhigh"
|
f"{where}.reasoningEffort: {label} requires low/medium/high/xhigh"
|
||||||
)
|
)
|
||||||
elif cli == "cursor-agent" and effort is not None:
|
elif (
|
||||||
|
isinstance(cli, str)
|
||||||
|
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||||
|
and effort is not None
|
||||||
|
):
|
||||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||||
|
|
||||||
if role == "test" and tier != "standard":
|
if role == "test" and tier != "standard":
|
||||||
@@ -446,14 +469,15 @@ def render_worker_argv(
|
|||||||
raise ValueError("invalid profile: " + "; ".join(errors))
|
raise ValueError("invalid profile: " + "; ".join(errors))
|
||||||
if not _is_absolute_safe_path(executable):
|
if not _is_absolute_safe_path(executable):
|
||||||
raise ValueError("executable must be a safe absolute path other than root")
|
raise ValueError("executable must be a safe absolute path other than root")
|
||||||
if Path(executable).name != profile["cli"]:
|
if not executable_basename_matches_cli(executable, profile["cli"]):
|
||||||
raise ValueError("executable basename must match profile.cli")
|
raise ValueError("executable basename must match profile.cli")
|
||||||
if not _is_absolute_safe_path(worktree):
|
if not _is_absolute_safe_path(worktree):
|
||||||
raise ValueError("worktree must be a safe absolute path other than root")
|
raise ValueError("worktree must be a safe absolute path other than root")
|
||||||
|
|
||||||
model = profile["model"]
|
model = profile["model"]
|
||||||
permission = profile["permissionMode"]
|
permission = profile["permissionMode"]
|
||||||
if profile["cli"] == "codex":
|
cli = profile["cli"]
|
||||||
|
if cli == "codex":
|
||||||
return [
|
return [
|
||||||
executable,
|
executable,
|
||||||
"--strict-config",
|
"--strict-config",
|
||||||
@@ -469,6 +493,7 @@ def render_worker_argv(
|
|||||||
worktree,
|
worktree,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if cli == "cursor-agent":
|
||||||
argv = [executable, "--model", model]
|
argv = [executable, "--model", model]
|
||||||
if permission == "read-only":
|
if permission == "read-only":
|
||||||
argv.extend(["--mode", "plan"])
|
argv.extend(["--mode", "plan"])
|
||||||
@@ -477,6 +502,25 @@ def render_worker_argv(
|
|||||||
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
|
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
|
||||||
return argv
|
return argv
|
||||||
|
|
||||||
|
if cli != "grok":
|
||||||
|
raise ValueError(f"unsupported cli: {cli}")
|
||||||
|
grok_permission = "plan" if permission == "read-only" else "acceptEdits"
|
||||||
|
grok_sandbox = "read-only" if permission == "read-only" else "workspace"
|
||||||
|
return [
|
||||||
|
executable,
|
||||||
|
"--model",
|
||||||
|
model,
|
||||||
|
"--reasoning-effort",
|
||||||
|
profile["reasoningEffort"],
|
||||||
|
"--permission-mode",
|
||||||
|
grok_permission,
|
||||||
|
"--always-approve",
|
||||||
|
"--sandbox",
|
||||||
|
grok_sandbox,
|
||||||
|
"--cwd",
|
||||||
|
worktree,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def receipt_hash(receipt: dict[str, Any]) -> str:
|
def receipt_hash(receipt: dict[str, Any]) -> str:
|
||||||
"""Hash every receipt field except the self-referential ``receiptHash``."""
|
"""Hash every receipt field except the self-referential ``receiptHash``."""
|
||||||
@@ -532,25 +576,34 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
|||||||
tier = value.get("tier")
|
tier = value.get("tier")
|
||||||
permission = value.get("permissionMode")
|
permission = value.get("permissionMode")
|
||||||
if not isinstance(cli, str) or cli not in CLIS:
|
if not isinstance(cli, str) or cli not in CLIS:
|
||||||
errors.append(f"{where}.cli: must be codex/cursor-agent")
|
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||||
if not isinstance(tier, str) or tier not in TIERS:
|
if not isinstance(tier, str) or tier not in TIERS:
|
||||||
errors.append(f"{where}.tier: must be standard/strong")
|
errors.append(f"{where}.tier: must be standard/strong")
|
||||||
model = value.get("model")
|
model = value.get("model")
|
||||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||||
errors.append(f"{where}.model: must be a safe model ID")
|
errors.append(f"{where}.model: must be a safe model ID")
|
||||||
effort = value.get("reasoningEffort")
|
effort = value.get("reasoningEffort")
|
||||||
if cli == "codex" and (
|
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT and (
|
||||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||||
):
|
):
|
||||||
errors.append(f"{where}.reasoningEffort: invalid Codex effort")
|
label = "Codex" if cli == "codex" else "Grok"
|
||||||
if cli == "cursor-agent" and effort is not None:
|
errors.append(f"{where}.reasoningEffort: invalid {label} effort")
|
||||||
|
if (
|
||||||
|
isinstance(cli, str)
|
||||||
|
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||||
|
and effort is not None
|
||||||
|
):
|
||||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||||
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
||||||
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
|
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
|
||||||
executable = value.get("executable")
|
executable = value.get("executable")
|
||||||
if not _is_absolute_safe_path(executable):
|
if not _is_absolute_safe_path(executable):
|
||||||
errors.append(f"{where}.executable: must be a safe absolute path")
|
errors.append(f"{where}.executable: must be a safe absolute path")
|
||||||
elif isinstance(cli, str) and cli in CLIS and Path(executable).name != cli:
|
elif (
|
||||||
|
isinstance(cli, str)
|
||||||
|
and cli in CLIS
|
||||||
|
and not executable_basename_matches_cli(str(executable), cli)
|
||||||
|
):
|
||||||
errors.append(f"{where}.executable: basename must match cli")
|
errors.append(f"{where}.executable: basename must match cli")
|
||||||
if not _is_nonnegative_int(value.get("executableDevice")):
|
if not _is_nonnegative_int(value.get("executableDevice")):
|
||||||
errors.append(f"{where}.executableDevice: must be a non-negative integer")
|
errors.append(f"{where}.executableDevice: must be a non-negative integer")
|
||||||
@@ -995,6 +1048,7 @@ __all__ = [
|
|||||||
"profile_hash",
|
"profile_hash",
|
||||||
"receipt_hash",
|
"receipt_hash",
|
||||||
"render_worker_argv",
|
"render_worker_argv",
|
||||||
|
"executable_basename_matches_cli",
|
||||||
"validate_profile",
|
"validate_profile",
|
||||||
"validate_orchestration",
|
"validate_orchestration",
|
||||||
"validate_worker_receipt",
|
"validate_worker_receipt",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||||
},
|
},
|
||||||
|
"intents": { "$ref": "#/definitions/intents" },
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"propertyNames": { "$ref": "#/definitions/id" },
|
"propertyNames": { "$ref": "#/definitions/id" },
|
||||||
@@ -63,6 +64,21 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||||
},
|
},
|
||||||
|
"intents": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["testEnvironment", "release"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"testEnvironment": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"relativePath": {
|
"relativePath": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ project:
|
|||||||
enabled: false
|
enabled: false
|
||||||
defaultProfile: null
|
defaultProfile: null
|
||||||
|
|
||||||
|
# 测试环境部署和版本发布都写在本文件。null 表示用户尚未说明该操作。
|
||||||
|
intents:
|
||||||
|
testEnvironment: null
|
||||||
|
release: null
|
||||||
|
|
||||||
# 所有可执行入口都使用结构化 argv 语义;requiredSecrets 只保存名称,不保存值。
|
# 所有可执行入口都使用结构化 argv 语义;requiredSecrets 只保存名称,不保存值。
|
||||||
# 不在此保存 shell、env 或凭据正文。
|
# 不在此保存 shell、env 或凭据正文。
|
||||||
entrypoints: {}
|
entrypoints: {}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ receipt 全部以 `docs/ack/tasks.yaml` 的 `project.orchestration` 与顶层
|
|||||||
| Test | `codex-test-standard` | standard |
|
| Test | `codex-test-standard` | standard |
|
||||||
| Developer 升级 | `codex-dev-strong` | strong |
|
| Developer 升级 | `codex-dev-strong` | strong |
|
||||||
|
|
||||||
项目如改用 Cursor,应修改结构化 profile、allowlist 和 defaults,再运行任务板
|
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行任务板
|
||||||
校验;不能在这里粘贴 `orca --command`、Agent CLI 参数或自由 shell。
|
校验;不能在这里粘贴 `orca --command`、Agent CLI 参数或自由 shell。
|
||||||
每次自动派发必须先审阅 launcher plan,再把其中的 `launchFingerprint` 作为
|
每次自动派发必须先审阅 launcher plan,再把其中的 `launchFingerprint` 作为
|
||||||
`--expected-launch-fingerprint` 启动 fresh worker。`receiptHash` 只作审计 checksum,
|
`--expected-launch-fingerprint` 启动 fresh worker。`receiptHash` 只作审计 checksum,
|
||||||
@@ -79,7 +79,7 @@ Developer 白盒验证:
|
|||||||
<local_run_command>
|
<local_run_command>
|
||||||
```
|
```
|
||||||
|
|
||||||
Test 黑盒复测:
|
Test 黑盒复测(服务启动以 `delivery.yaml` 的 `intents.testEnvironment` 为准):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
<preflight_command>
|
<preflight_command>
|
||||||
@@ -97,7 +97,8 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
|
|||||||
|
|
||||||
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py`、
|
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py`、
|
||||||
`scripts/validate_knowledge.py` 和 `scripts/validate_delivery.py` 执行。
|
`scripts/validate_knowledge.py` 和 `scripts/validate_delivery.py` 执行。
|
||||||
构建/发布/部署的机器入口以 `delivery.yaml` 为准;本文件不维护第二套交付命令。
|
构建、测试环境部署和版本发布的机器入口以 `delivery.yaml` 的 `intents` 为准;
|
||||||
|
本文件不维护第二套交付命令。
|
||||||
|
|
||||||
## 硬规则(其余见 references/)
|
## 硬规则(其余见 references/)
|
||||||
|
|
||||||
@@ -118,8 +119,9 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
|
|||||||
- Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
- Developer 回报 `knowledgeApplied` 与 `knowledgeCandidates`,Test 回报
|
||||||
`knowledgeChecks`。关键约束应下沉为测试、lint、CI 或正式规范。
|
`knowledgeChecks`。关键约束应下沉为测试、lint、CI 或正式规范。
|
||||||
- ACK 不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
- ACK 不自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||||
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署;交付仅在
|
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
|
||||||
任务 `verified` 且本次 profile/目标/停止点得到确认后运行。
|
与发版都写在这份契约的 `intents` 里;用户明确要求重新部署测试环境或发布版本时
|
||||||
|
才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后运行。
|
||||||
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
|
- 默认交付 profile 最多到 `validation_ready` 或 `review_ready`;stable 发布或 production 部署必须有
|
||||||
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
|
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
|
||||||
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
|
||||||
|
|||||||
@@ -338,7 +338,8 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"codex": { "$ref": "#/definitions/modelRoleAllowlist" },
|
"codex": { "$ref": "#/definitions/modelRoleAllowlist" },
|
||||||
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" }
|
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" },
|
||||||
|
"grok": { "$ref": "#/definitions/modelRoleAllowlist" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workerProfile": {
|
"workerProfile": {
|
||||||
@@ -359,7 +360,7 @@
|
|||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["codex", "cursor-agent"]
|
"enum": ["codex", "cursor-agent", "grok"]
|
||||||
},
|
},
|
||||||
"tier": {
|
"tier": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -517,7 +518,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"cli": {
|
"cli": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["codex", "cursor-agent"]
|
"enum": ["codex", "cursor-agent", "grok"]
|
||||||
},
|
},
|
||||||
"tier": {
|
"tier": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -989,9 +990,14 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
||||||
},
|
},
|
||||||
|
"intent": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["testEnvironment", "release"],
|
||||||
|
"description": "用户触发的测试环境或发版操作;有 intent 时 taskIds 可为 []"
|
||||||
|
},
|
||||||
"taskIds": {
|
"taskIds": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 0,
|
||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"items": { "type": "string", "minLength": 1 }
|
"items": { "type": "string", "minLength": 1 }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ project:
|
|||||||
standard: ["auto"]
|
standard: ["auto"]
|
||||||
test:
|
test:
|
||||||
standard: ["auto"]
|
standard: ["auto"]
|
||||||
|
grok:
|
||||||
|
developer:
|
||||||
|
standard: ["grok-4.5"]
|
||||||
|
strong: ["grok-4.6"]
|
||||||
|
test:
|
||||||
|
standard: ["grok-4.5"]
|
||||||
profiles:
|
profiles:
|
||||||
codex-dev-standard:
|
codex-dev-standard:
|
||||||
role: "developer"
|
role: "developer"
|
||||||
@@ -80,6 +86,27 @@ project:
|
|||||||
model: "auto"
|
model: "auto"
|
||||||
reasoningEffort: null
|
reasoningEffort: null
|
||||||
permissionMode: "workspace-write"
|
permissionMode: "workspace-write"
|
||||||
|
grok-dev-standard:
|
||||||
|
role: "developer"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "standard"
|
||||||
|
model: "grok-4.5"
|
||||||
|
reasoningEffort: "medium"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
|
grok-test-standard:
|
||||||
|
role: "test"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "standard"
|
||||||
|
model: "grok-4.5"
|
||||||
|
reasoningEffort: "low"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
|
grok-dev-strong:
|
||||||
|
role: "developer"
|
||||||
|
cli: "grok"
|
||||||
|
tier: "strong"
|
||||||
|
model: "grok-4.6"
|
||||||
|
reasoningEffort: "high"
|
||||||
|
permissionMode: "workspace-write"
|
||||||
defaults:
|
defaults:
|
||||||
developer: "codex-dev-standard"
|
developer: "codex-dev-standard"
|
||||||
test: "codex-test-standard"
|
test: "codex-test-standard"
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"defaultProfile": None,
|
"defaultProfile": None,
|
||||||
|
"intents": {"testEnvironment": None, "release": None},
|
||||||
"entrypoints": {},
|
"entrypoints": {},
|
||||||
"artifacts": {},
|
"artifacts": {},
|
||||||
"destinations": {},
|
"destinations": {},
|
||||||
@@ -313,6 +314,42 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||||
|
|
||||||
|
def test_intents_must_point_at_matching_stop_points(self) -> None:
|
||||||
|
contract = valid_contract()
|
||||||
|
contract["intents"] = {
|
||||||
|
"testEnvironment": "review",
|
||||||
|
"release": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = validate_delivery.validate_builtin(contract)
|
||||||
|
self.assertTrue(
|
||||||
|
any("intents.testEnvironment" in item and "validation_ready" in item for item in errors)
|
||||||
|
)
|
||||||
|
|
||||||
|
contract["intents"]["testEnvironment"] = "local-validation"
|
||||||
|
contract["profiles"]["local-validation"] = {
|
||||||
|
"stopAt": "validation_ready",
|
||||||
|
"steps": [
|
||||||
|
{"id": "build-local", "action": "build", "artifact": "service-deb"},
|
||||||
|
{
|
||||||
|
"id": "deploy-local",
|
||||||
|
"action": "deploy",
|
||||||
|
"artifact": "service-deb",
|
||||||
|
"environment": "test-server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "health-local",
|
||||||
|
"action": "health-check",
|
||||||
|
"environment": "test-server",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||||
|
|
||||||
|
contract["intents"]["release"] = "missing-release"
|
||||||
|
errors = validate_delivery.validate_builtin(contract)
|
||||||
|
self.assertTrue(any("未定义 profile 'missing-release'" in item for item in errors))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
|||||||
"OPENAI_API_KEY": "test-token",
|
"OPENAI_API_KEY": "test-token",
|
||||||
"AZURE_OPENAI_API_KEY": "azure-token",
|
"AZURE_OPENAI_API_KEY": "azure-token",
|
||||||
"CURSOR_API_KEY": "cursor-token",
|
"CURSOR_API_KEY": "cursor-token",
|
||||||
|
"XAI_API_KEY": "xai-token",
|
||||||
|
"GROK_HOME": "/tmp/evil-grok",
|
||||||
|
"GROK_SANDBOX": "off",
|
||||||
"ANTHROPIC_API_KEY": "anthropic-token",
|
"ANTHROPIC_API_KEY": "anthropic-token",
|
||||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/tmp/dbus",
|
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/tmp/dbus",
|
||||||
"DISPLAY": ":99",
|
"DISPLAY": ":99",
|
||||||
@@ -336,17 +339,24 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
|||||||
control = launch_worker.control_environment()
|
control = launch_worker.control_environment()
|
||||||
codex = launch_worker.worker_environment("codex")
|
codex = launch_worker.worker_environment("codex")
|
||||||
cursor = launch_worker.worker_environment("cursor-agent")
|
cursor = launch_worker.worker_environment("cursor-agent")
|
||||||
|
grok = launch_worker.worker_environment("grok")
|
||||||
|
|
||||||
for environment in (control, codex, cursor):
|
for environment in (control, codex, cursor, grok):
|
||||||
self.assertNotIn(hostile_path, environment["PATH"].split(os.pathsep))
|
self.assertNotIn(hostile_path, environment["PATH"].split(os.pathsep))
|
||||||
self.assertEqual(environment["LANG"], "C.UTF-8")
|
self.assertEqual(environment["LANG"], "C.UTF-8")
|
||||||
self.assertNotIn("OPENAI_API_KEY", control)
|
self.assertNotIn("OPENAI_API_KEY", control)
|
||||||
self.assertNotIn("CURSOR_API_KEY", control)
|
self.assertNotIn("CURSOR_API_KEY", control)
|
||||||
|
self.assertNotIn("XAI_API_KEY", control)
|
||||||
self.assertEqual(codex["OPENAI_API_KEY"], "test-token")
|
self.assertEqual(codex["OPENAI_API_KEY"], "test-token")
|
||||||
self.assertEqual(codex["AZURE_OPENAI_API_KEY"], "azure-token")
|
self.assertEqual(codex["AZURE_OPENAI_API_KEY"], "azure-token")
|
||||||
self.assertNotIn("CURSOR_API_KEY", codex)
|
self.assertNotIn("CURSOR_API_KEY", codex)
|
||||||
|
self.assertNotIn("XAI_API_KEY", codex)
|
||||||
self.assertEqual(cursor["CURSOR_API_KEY"], "cursor-token")
|
self.assertEqual(cursor["CURSOR_API_KEY"], "cursor-token")
|
||||||
self.assertNotIn("OPENAI_API_KEY", cursor)
|
self.assertNotIn("OPENAI_API_KEY", cursor)
|
||||||
|
self.assertNotIn("XAI_API_KEY", cursor)
|
||||||
|
self.assertEqual(grok["XAI_API_KEY"], "xai-token")
|
||||||
|
self.assertNotIn("OPENAI_API_KEY", grok)
|
||||||
|
self.assertNotIn("CURSOR_API_KEY", grok)
|
||||||
for forbidden in (
|
for forbidden in (
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
"DBUS_SESSION_BUS_ADDRESS",
|
"DBUS_SESSION_BUS_ADDRESS",
|
||||||
@@ -356,11 +366,13 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
|||||||
"NODE_OPTIONS",
|
"NODE_OPTIONS",
|
||||||
"PYTHONPATH",
|
"PYTHONPATH",
|
||||||
"CODEX_HOME",
|
"CODEX_HOME",
|
||||||
|
"GROK_HOME",
|
||||||
|
"GROK_SANDBOX",
|
||||||
"GIT_SSH_COMMAND",
|
"GIT_SSH_COMMAND",
|
||||||
"WAYLAND_DISPLAY",
|
"WAYLAND_DISPLAY",
|
||||||
"XDG_RUNTIME_DIR",
|
"XDG_RUNTIME_DIR",
|
||||||
):
|
):
|
||||||
for environment in (control, codex, cursor):
|
for environment in (control, codex, cursor, grok):
|
||||||
self.assertNotIn(forbidden, environment)
|
self.assertNotIn(forbidden, environment)
|
||||||
|
|
||||||
def test_executable_resolution_ignores_hostile_path(self) -> None:
|
def test_executable_resolution_ignores_hostile_path(self) -> None:
|
||||||
@@ -373,6 +385,51 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(resolved, Path("/usr/bin/git"))
|
self.assertEqual(resolved, Path("/usr/bin/git"))
|
||||||
|
|
||||||
|
def test_grok_vendor_layout_is_trusted_and_outside_artifacts_are_not(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
home = Path(temporary) / "home"
|
||||||
|
downloads = home / ".grok" / "downloads"
|
||||||
|
downloads.mkdir(parents=True)
|
||||||
|
artifact = downloads / "grok-linux-x86_64"
|
||||||
|
artifact.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
artifact.chmod(0o775)
|
||||||
|
bindir = home / ".local" / "bin"
|
||||||
|
bindir.mkdir(parents=True)
|
||||||
|
(bindir / "grok").symlink_to(artifact)
|
||||||
|
|
||||||
|
hostile_dir = Path(temporary) / "tmp"
|
||||||
|
hostile_dir.mkdir()
|
||||||
|
hostile = hostile_dir / "grok-linux-x86_64"
|
||||||
|
hostile.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||||
|
hostile.chmod(0o775)
|
||||||
|
hostile_bin = Path(temporary) / "hostile-bin"
|
||||||
|
hostile_bin.mkdir()
|
||||||
|
(hostile_bin / "grok").symlink_to(hostile)
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
launch_worker,
|
||||||
|
"account_identity",
|
||||||
|
return_value=(home.resolve(strict=True), "ace"),
|
||||||
|
), mock.patch.object(
|
||||||
|
launch_worker,
|
||||||
|
"trusted_path_entries",
|
||||||
|
return_value=[bindir.resolve(strict=True)],
|
||||||
|
):
|
||||||
|
resolved = launch_worker.resolve_executable("grok")
|
||||||
|
self.assertEqual(resolved, artifact.resolve(strict=True))
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
launch_worker,
|
||||||
|
"account_identity",
|
||||||
|
return_value=(home.resolve(strict=True), "ace"),
|
||||||
|
), mock.patch.object(
|
||||||
|
launch_worker,
|
||||||
|
"trusted_path_entries",
|
||||||
|
return_value=[hostile_bin.resolve(strict=True)],
|
||||||
|
):
|
||||||
|
with self.assertRaises(launch_worker.LaunchError):
|
||||||
|
launch_worker.resolve_executable("grok")
|
||||||
|
|
||||||
|
|
||||||
class PlanTests(unittest.TestCase):
|
class PlanTests(unittest.TestCase):
|
||||||
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class AckSkillContentTests(unittest.TestCase):
|
|||||||
"references/kickoff.md",
|
"references/kickoff.md",
|
||||||
"不要修改项目的 `AGENTS.md`",
|
"不要修改项目的 `AGENTS.md`",
|
||||||
"当前会话担任 Coordinator",
|
"当前会话担任 Coordinator",
|
||||||
|
"intents.testEnvironment",
|
||||||
|
"运行测试环境",
|
||||||
|
"运行版本发布",
|
||||||
):
|
):
|
||||||
self.assertIn(expected, content)
|
self.assertIn(expected, content)
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ class AckSkillContentTests(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||||
self.assertEqual(version, "0.15.0")
|
self.assertEqual(version, "0.17.1")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f'ackVersion: "{version}"',
|
f'ackVersion: "{version}"',
|
||||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||||
|
|||||||
@@ -438,6 +438,47 @@ class AckTaskValidationTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_board_accepted_in_all_modes(board)
|
self.assert_board_accepted_in_all_modes(board)
|
||||||
|
|
||||||
|
def test_intent_delivery_run_allows_empty_task_ids(self) -> None:
|
||||||
|
board = valid_manual_routing_board()
|
||||||
|
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||||
|
board["deliveryRuns"] = [
|
||||||
|
{
|
||||||
|
"id": "DR-test-env-1",
|
||||||
|
"profile": "test-local",
|
||||||
|
"intent": "testEnvironment",
|
||||||
|
"taskIds": [],
|
||||||
|
"status": "validation_ready",
|
||||||
|
"sourceRevision": "a" * 40,
|
||||||
|
"configRevision": "b" * 40,
|
||||||
|
"pullRequest": None,
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"id": "garden-bin",
|
||||||
|
"type": "file",
|
||||||
|
"reference": "garden",
|
||||||
|
"digest": "sha256:" + "c" * 64,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"environment": "local-write",
|
||||||
|
"result": "succeeded",
|
||||||
|
"evidence": "GET /login returned 200",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence": ["http://write.localhost:8080/ ready"],
|
||||||
|
"updatedAt": "2026-08-23T00:57:00+08:00",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assert_board_accepted_in_all_modes(board)
|
||||||
|
|
||||||
|
del board["deliveryRuns"][0]["intent"]
|
||||||
|
self.assert_board_rejected_in_all_modes(
|
||||||
|
board,
|
||||||
|
"taskIds: 必须是非空任务 ID 列表",
|
||||||
|
)
|
||||||
|
|
||||||
def test_delivery_runs_and_delivery_file_must_appear_together(self) -> None:
|
def test_delivery_runs_and_delivery_file_must_appear_together(self) -> None:
|
||||||
board = valid_manual_routing_board()
|
board = valid_manual_routing_board()
|
||||||
board["deliveryRuns"] = []
|
board["deliveryRuns"] = []
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ def valid_orchestration() -> dict:
|
|||||||
"developer": {"standard": ["cursor-auto"]},
|
"developer": {"standard": ["cursor-auto"]},
|
||||||
"test": {"standard": ["cursor-auto"]},
|
"test": {"standard": ["cursor-auto"]},
|
||||||
},
|
},
|
||||||
|
"grok": {
|
||||||
|
"developer": {
|
||||||
|
"standard": ["grok-4.5"],
|
||||||
|
"strong": ["grok-4.6"],
|
||||||
|
},
|
||||||
|
"test": {"standard": ["grok-4.5"]},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"codex-dev-standard": {
|
"codex-dev-standard": {
|
||||||
@@ -64,6 +71,30 @@ def valid_orchestration() -> dict:
|
|||||||
"reasoningEffort": None,
|
"reasoningEffort": None,
|
||||||
"permissionMode": "workspace-write",
|
"permissionMode": "workspace-write",
|
||||||
},
|
},
|
||||||
|
"grok-dev-standard": {
|
||||||
|
"role": "developer",
|
||||||
|
"cli": "grok",
|
||||||
|
"tier": "standard",
|
||||||
|
"model": "grok-4.5",
|
||||||
|
"reasoningEffort": "medium",
|
||||||
|
"permissionMode": "workspace-write",
|
||||||
|
},
|
||||||
|
"grok-dev-strong": {
|
||||||
|
"role": "developer",
|
||||||
|
"cli": "grok",
|
||||||
|
"tier": "strong",
|
||||||
|
"model": "grok-4.6",
|
||||||
|
"reasoningEffort": "high",
|
||||||
|
"permissionMode": "workspace-write",
|
||||||
|
},
|
||||||
|
"grok-test-readonly": {
|
||||||
|
"role": "test",
|
||||||
|
"cli": "grok",
|
||||||
|
"tier": "standard",
|
||||||
|
"model": "grok-4.5",
|
||||||
|
"reasoningEffort": "low",
|
||||||
|
"permissionMode": "read-only",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"developer": "codex-dev-standard",
|
"developer": "codex-dev-standard",
|
||||||
@@ -278,12 +309,16 @@ class ProfileValidationTests(unittest.TestCase):
|
|||||||
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
||||||
cursor = valid_orchestration()
|
cursor = valid_orchestration()
|
||||||
cursor["profiles"]["cursor-test-standard"]["reasoningEffort"] = "low"
|
cursor["profiles"]["cursor-test-standard"]["reasoningEffort"] = "low"
|
||||||
|
grok = valid_orchestration()
|
||||||
|
grok["profiles"]["grok-dev-standard"]["reasoningEffort"] = None
|
||||||
|
|
||||||
codex_errors = worker_profiles.validate_orchestration(codex)
|
codex_errors = worker_profiles.validate_orchestration(codex)
|
||||||
cursor_errors = worker_profiles.validate_orchestration(cursor)
|
cursor_errors = worker_profiles.validate_orchestration(cursor)
|
||||||
|
grok_errors = worker_profiles.validate_orchestration(grok)
|
||||||
|
|
||||||
self.assertTrue(any("Codex requires" in error for error in codex_errors))
|
self.assertTrue(any("Codex requires" in error for error in codex_errors))
|
||||||
self.assertTrue(any("Cursor requires null" in error for error in cursor_errors))
|
self.assertTrue(any("Cursor requires null" in error for error in cursor_errors))
|
||||||
|
self.assertTrue(any("Grok requires" in error for error in grok_errors))
|
||||||
|
|
||||||
def test_test_cannot_use_strong_tier(self) -> None:
|
def test_test_cannot_use_strong_tier(self) -> None:
|
||||||
routing = valid_orchestration()
|
routing = valid_orchestration()
|
||||||
@@ -444,6 +479,86 @@ class ArgvRendererTests(unittest.TestCase):
|
|||||||
self.assertNotIn("--yolo", argv)
|
self.assertNotIn("--yolo", argv)
|
||||||
self.assertNotIn("--force", argv)
|
self.assertNotIn("--force", argv)
|
||||||
|
|
||||||
|
def test_grok_workspace_write_exact_safe_argv(self) -> None:
|
||||||
|
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||||
|
|
||||||
|
argv = worker_profiles.render_worker_argv(
|
||||||
|
profile,
|
||||||
|
"/usr/local/bin/grok",
|
||||||
|
"/repo/demo",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
argv,
|
||||||
|
[
|
||||||
|
"/usr/local/bin/grok",
|
||||||
|
"--model",
|
||||||
|
"grok-4.5",
|
||||||
|
"--reasoning-effort",
|
||||||
|
"medium",
|
||||||
|
"--permission-mode",
|
||||||
|
"acceptEdits",
|
||||||
|
"--always-approve",
|
||||||
|
"--sandbox",
|
||||||
|
"workspace",
|
||||||
|
"--cwd",
|
||||||
|
"/repo/demo",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for forbidden in (
|
||||||
|
"--yolo",
|
||||||
|
"bypassPermissions",
|
||||||
|
"auto",
|
||||||
|
"dontAsk",
|
||||||
|
"off",
|
||||||
|
):
|
||||||
|
self.assertNotIn(forbidden, argv)
|
||||||
|
|
||||||
|
def test_grok_read_only_uses_plan_and_read_only_sandbox(self) -> None:
|
||||||
|
profile = valid_orchestration()["profiles"]["grok-test-readonly"]
|
||||||
|
|
||||||
|
argv = worker_profiles.render_worker_argv(
|
||||||
|
profile,
|
||||||
|
"/usr/local/bin/grok",
|
||||||
|
"/repo/demo",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
argv,
|
||||||
|
[
|
||||||
|
"/usr/local/bin/grok",
|
||||||
|
"--model",
|
||||||
|
"grok-4.5",
|
||||||
|
"--reasoning-effort",
|
||||||
|
"low",
|
||||||
|
"--permission-mode",
|
||||||
|
"plan",
|
||||||
|
"--always-approve",
|
||||||
|
"--sandbox",
|
||||||
|
"read-only",
|
||||||
|
"--cwd",
|
||||||
|
"/repo/demo",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertNotIn("--yolo", argv)
|
||||||
|
self.assertNotIn("bypassPermissions", argv)
|
||||||
|
|
||||||
|
def test_grok_accepts_vendor_artifact_basename(self) -> None:
|
||||||
|
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||||
|
artifact = "/home/ace/.grok/downloads/grok-linux-x86_64"
|
||||||
|
|
||||||
|
argv = worker_profiles.render_worker_argv(profile, artifact, "/repo/demo")
|
||||||
|
|
||||||
|
self.assertEqual(argv[0], artifact)
|
||||||
|
self.assertTrue(
|
||||||
|
worker_profiles.executable_basename_matches_cli(artifact, "grok")
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
worker_profiles.executable_basename_matches_cli(
|
||||||
|
artifact, "cursor-agent"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_renderer_rejects_wrong_executable_or_unsafe_worktree(self) -> None:
|
def test_renderer_rejects_wrong_executable_or_unsafe_worktree(self) -> None:
|
||||||
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
|
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user