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:
2026-08-23 19:08:54 +08:00
parent c113f68bf4
commit 7dfdf80e9e
28 changed files with 679 additions and 78 deletions
+36 -16
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
0.15.0 0.17.1
+1 -1
View File
@@ -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
+17
View File
@@ -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:
+3 -2
View File
@@ -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 命令。
## 路径权限 ## 路径权限
+28 -1
View File
@@ -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 引用均通过
+3 -1
View File
@@ -41,6 +41,8 @@ Coordinator 发现或读取 open 任务
-> dispatch 给 Developer--to <worker handle> -> dispatch 给 Developer--to <worker handle>
-> waitDeveloper 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates -> waitDeveloper 的 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 给 Testretesting -> dispatch 给 Testretesting
-> waitTest 的 retest_result(含 knowledgeChecks 和 candidate 独立证据) -> waitTest 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
@@ -265,7 +267,7 @@ frontendDir:
worktreePath: worktreePath:
``` ```
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。 如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。若项目配置了 `intents.testEnvironment`,重启方式以该 profile 为准,不另写一套启动命令。
--- ---
+25
View File
@@ -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` 的 profilebuild 产物、部署到
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
+5 -4
View File
@@ -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 步骤。
## 校验 ## 校验
+6 -2
View File
@@ -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 / knowledgeCandidatesTest 回 knowledgeChecks → Developer 回 knowledgeApplied / knowledgeCandidatesTest 回 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 和
+14 -4
View File
@@ -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。终端创建后,父进程先
+1 -1
View File
@@ -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>
+50 -2
View File
@@ -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 = (
+39
View File
@@ -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:
+18 -4
View File
@@ -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: 不能包含重复值")
+66 -12
View File
@@ -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",
+16
View File
@@ -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: {}
+7 -5
View File
@@ -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` 并继续下一个。
+10 -4
View File
@@ -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 }
}, },
+27
View File
@@ -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"
+37
View File
@@ -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()
+59 -2
View File
@@ -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:
+4 -1
View File
@@ -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"),
+41
View File
@@ -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"] = []
+115
View File
@@ -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"]