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`,然后继续处理其它任务。
项目还可以声明一个可选的交付阶段:任务全部验证后,ACK 按项目维护的 profile
构建 DEB 或镜像、发布产物、创建 PR,并在授权范围内部署。交付配置默认关闭
项目还可以在同一份 `docs/ack/delivery.yaml` 里声明测试环境部署和版本发布。
用户告诉 ACK 这两件事怎么做之后,再说「重新布测试环境」或「发布一个版本」
ACK 按对应 intent 执行。任务全部验证后仍可按 profile 做常规交付。配置默认关闭,
稳定发布与生产部署始终保留人工批准点。
## 安装
@@ -70,8 +71,8 @@ skills/ack/
`docs/ack/project.md` 只保存当前项目的命令、路径和权限差异;
`docs/ack/tasks.yaml` 保存当前任务状态;`docs/ack/knowledge.yaml` 保存跨任务复用、
已经独立验证的项目知识护栏。
`docs/ack/delivery.yaml` 声明项目特有的构建、发布和部署能力;每次执行结果另记在
`tasks.yaml.deliveryRuns`,配置与运行状态不会混在一起
`docs/ack/delivery.yaml` 是测试环境部署和版本发布的唯一契约,也声明常规构建、
发布和部署能力;每次执行结果另记在 `tasks.yaml.deliveryRuns`
## 检查项目状态
@@ -139,16 +140,29 @@ python3 <ack-skill-dir>/scripts/run_verification.py \
## 配置与运行交付
用户可以直接向 `/ack` 描述项目差异,例如“这个项目用 `make deb` 构建 DEB,推到
preview APT 源,再部署到开发机”。ACK 会把它维护成
`docs/ack/delivery.yaml` 中的声明式 entrypoint、artifact、destination、environment
和 profile,校验后展示 diff;首次配置保持关闭,确认后才启用。
用户可以直接向 `/ack` 说明两件独立操作,并写进同一份契约:
```text
/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,也不保存
凭据值。ACK 在任务进入 `verified`,按用户确认的 profile 执行,并把 revision、
PR、产物摘要、部署目标、健康检查和日志引用写入 `tasks.yaml.deliveryRuns`。默认
profile 只能停在 `review_ready`;稳定发布或生产部署必须经过对应 approval 步骤
具体契约见 `references/delivery.md`
凭据值。任务进入 `verified`的常规交付仍按确认的 profile 执行。默认
profile 只能停在 `validation_ready``review_ready`;稳定发布或生产部署必须经过
对应 approval 步骤。具体契约见 `references/delivery.md`
## 启动 Worker
@@ -191,8 +205,10 @@ identity。Coordinator 将 receipt 追加到顶层 `workerReceipts`,再把 rec
不能跨任务或跨轮次改挂。
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。
持久化 `receiptHash` 是无密钥 checksum,不是 launcher 身份证明。ACK 只复用同一轮
@@ -243,5 +259,9 @@ ACK 会自动读取 `delivery.yaml`,无需再逐步提醒它构建、上传、
`ackVersion`。从 `0.10.0` 起,`project.orchestration` 与顶层 `workerReceipts` 必须
同时存在;从 `0.11.0` 起,新项目还会生成默认关闭的 `delivery.yaml`,并在任务板声明
`project.deliveryFile``deliveryRuns`;从 `0.13.0` 起,Coordinator 使用
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型。旧项目可以不迁移
而继续使用原闭环。旧项目的 `kitVersion` 可以继续读取,但建议迁移为 `ackVersion`
`select_tasks.py` 获取有预算的任务上下文,不再把完整任务板注入模型;从 `0.16.0` 起,
`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: >-
初始化、检查并运行 ACK 三角色协作闭环。仅在用户显式调用 /ack 或 $ack,并要求
初始化 ACK、检查 docs/ack 配置、按 ACK 规划需求、指挥 Coordinator/Developer/Test
工作,配置并执行任务验证后的项目交付流程时使用。
工作,配置测试环境与发版方式,重新部署测试环境,或发布版本时使用。
---
# ACK 项目协作入口
@@ -21,7 +21,11 @@ description: >-
- 用户要求初始化、接入或安装 ACK:执行“初始化”。
- 用户要求检查 ACK 是否可用、配置是否完整:执行“检查”。
- 用户要求用 ACK 做需求、修复问题或继续任务:执行“工作”。
- 用户用自然语言要求增加、修改关闭项目交付流程:执行“交付配置维护”。
- 用户用自然语言说明怎么部署测试环境、怎么发布版本,或要求增加、修改关闭交付
流程:执行“交付配置维护”。测试环境和发版必须写进同一份
`docs/ack/delivery.yaml`
- 用户要求部署、重新部署测试环境,或按已配置方式开始测试:执行“运行测试环境”。
- 用户要求发布版本:执行“运行版本发布”。
始终先解析真实项目根目录。优先使用 `git rev-parse --show-toplevel`;不是 Git
项目时使用用户指定目录或当前目录。不要修改项目的 `AGENTS.md``CLAUDE.md`
@@ -169,8 +173,9 @@ description: >-
任一条件不符、清理能力不存在或无法确认清理成功时创建 fresh worker。持久化
receipt 只作审计与 dispatch 关联,不能单独授权复用。当前 Orca 终端接口不能提供
可验证的历史消息清理,因此使用 Orca 时仍走 fresh worker。
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证Test 独立黑盒
复测,Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
6. 用户已确认的任务按 ACK 闭环执行:Developer 实现与白盒验证;若
`intents.testEnvironment` 已启用,Coordinator 先按「运行测试环境」拉起服务,再
派 Test 独立黑盒复测。Coordinator 读取证据终检并唯一写入 `tasks.yaml`。Developer 回报
`knowledgeApplied` 和 `knowledgeCandidates`Test 回报 `knowledgeChecks`
`candidate` 只有在独立验证和 gate 后才能由 Coordinator 写入或激活。
7. 执行知识项的 `verification.ref` 时,只调用
@@ -202,8 +207,10 @@ description: >-
## 交付配置维护
1. 读取 `references/delivery.md`、模板、schema、现有 `delivery.yaml`、项目构建/发布
入口和 CI;把用户自然语言描述转换为结构化 entrypoint、artifact、destination、
environment 与 profile。配置只引用仓库内脚本或声明式工具 target,不保存 shell。
入口和 CI;把用户自然语言描述转换为结构化 `intents`、entrypoint、artifact、
destination、environment 与 profile。测试环境部署和版本发布都写进这一份
`delivery.yaml`,不要拆成第二份文档。配置只引用仓库内脚本或声明式工具 target,
不保存 shell。
2. 若旧项目首次启用,生成 `docs/ack/delivery.yaml`,在 `tasks.yaml.project` 增加
`deliveryFile: docs/ack/delivery.yaml`,并增加顶层 `deliveryRuns: []`;不改写其它
项目状态。首次生成保持 `enabled: false`,先展示 diff 和解析出的执行顺序。
@@ -213,13 +220,34 @@ description: >-
执行的 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`。
- 不在项目中维护第二份 ACK 通用规范、模板或任务 schema。
- 不猜测项目命令、服务地址、worker handle 或模型名称。
- 不把 full-access、bypass、YOLO/force、关闭 sandbox 或项目内“授权”字段当成
v0.10 自动 worker 的合法配置;当前一律 fail closed。
- 不把 full-access、bypass、YOLO/force、Grok `--yolo` / bypassPermissions、
关闭 sandbox 或项目内“授权”字段当成 v0.10 自动 worker 的合法配置;当前一律
fail closed。Grok worker 由 launcher 固定带 `--always-approve`,仍必须带 sandbox。
- 不把无密钥 `receiptHash` 或 Orca live metadata 当作旧终端的启动 attestation
没有可信空闲状态、配置匹配和历史消息清理证明时不复用既有 worker。
- 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:
display_name: "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:
allow_implicit_invocation: false
+17
View File
@@ -6,6 +6,10 @@ project:
enabled: true
defaultProfile: "review"
intents:
testEnvironment: test-env
release: null
entrypoints:
verify:
kind: make
@@ -100,6 +104,19 @@ environments:
mutex: "notes-test-deploy"
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:
stopAt: review_ready
steps:
+3 -2
View File
@@ -1,6 +1,6 @@
# notes-web Agent 协作协议(示例,项目覆盖层)
> 本项目基于 ack v0.15.0
> 本项目基于 ack v0.17.1
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
> 覆盖层文件放在 `docs/ack/project.md`,不占用 `AGENTS.md`
> ACK 不会自动修改 `AGENTS.md``CLAUDE.md` 或其它 Agent 指令文件。
@@ -39,7 +39,8 @@
| Test | `codex-test-standard` | standard |
| Developer 升级 | `codex-dev-strong` | strong |
本文件不保存或拼接 worker shell 命令。
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行
任务板校验。本文件不保存或拼接 worker shell 命令。
## 路径权限
+28 -1
View File
@@ -3,7 +3,7 @@
version: 1
updatedAt: "2026-07-06T09:40:00+08:00"
source: "Coordinator (PM) Agent"
ackVersion: "0.15.0"
ackVersion: "0.17.1"
project:
name: "notes-web"
baseUrl: "http://localhost:5173"
@@ -27,6 +27,12 @@ project:
standard: ["auto"]
test:
standard: ["auto"]
grok:
developer:
standard: ["grok-4.5"]
strong: ["grok-4.6"]
test:
standard: ["grok-4.5"]
profiles:
codex-dev-standard:
role: "developer"
@@ -63,6 +69,27 @@ project:
model: "auto"
reasoningEffort: null
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:
developer: "codex-dev-standard"
test: "codex-test-standard"
@@ -48,6 +48,8 @@
## 可选交付
- [ ] `delivery.yaml` 首次生成保持 `enabled: false`,没有根据 README/CI 自动启用。
- [ ] 测试环境部署和版本发布都写在同一份 `delivery.yaml``intents` 中,没有第二份
操作文档。未说明的 intent 保持 `null`
- [ ] entrypoint 只使用声明式工具 target 或仓库内无 symlink 的可执行脚本;没有
shell、自由 command、凭据值或环境变量值。
- [ ] artifact、destination、environment 和 profile 引用均通过
+3 -1
View File
@@ -41,6 +41,8 @@ Coordinator 发现或读取 open 任务
-> dispatch 给 Developer--to <worker handle>
-> waitDeveloper 的 worker_done / escalation(含 knowledgeApplied / knowledgeCandidates
-> writeback fixed_by_dev
-> 若 delivery.yaml intents.testEnvironment 已启用:Coordinator 先执行该 profile
拉起待测服务,再派 Test;Test 不发明编译或启动命令
-> 为 Test 独立解析安全 profile;安全重置同角色空闲 worker,或重新 plan/launch fresh worker
-> dispatch 给 Testretesting
-> waitTest 的 retest_result(含 knowledgeChecks 和 candidate 独立证据)
@@ -265,7 +267,7 @@ frontendDir:
worktreePath:
```
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。
如果开发在 `<dev_worktree>` 修复,但服务跑的是另一个 worktree,必须**停止并重启正确服务**后再测。长跑服务或静态前端尤其要确认加载的是最新构建产物。若项目配置了 `intents.testEnvironment`,重启方式以该 profile 为准,不另写一套启动命令。
---
+25
View File
@@ -52,6 +52,31 @@ channel、environment 或 source revision 漂移时重新确认。
凭据系统或执行环境注入。复杂逻辑放在受版本控制的项目入口中。entrypoint 使用 argv
语义执行,不能拼成 `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. 运行前检查
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
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言描述交付要求后,
Coordinator 按 `delivery.md` 维护声明式配置:工具 target 与仓库脚本分开引用,产物、
目的地、环境和有序 profile 使用稳定 ID。配置中不保存 shell、环境变量值或凭据
新项目的 `docs/ack/delivery.yaml` 保持 `enabled: false`、空能力表空 profile,以及
`intents.testEnvironment: null``intents.release: null`
不要根据 README 或 CI 自动推断并启用发布/部署。用户用自然语言说明测试环境或发版
方式后,Coordinator 按 `delivery.md` 把两者都写入这一份契约:`intents` 指向对应
profile,工具 target 与仓库脚本分开引用。配置中不保存 shell、环境变量值或凭据
正文;稳定发布和生产部署必须有显式 approval 步骤。
## 校验
+6 -2
View File
@@ -37,7 +37,8 @@
Developer/Test profile;优先选择同一轮内角色/profile/worktree 匹配的空闲 worker
只有历史消息已可信清理并取得新会话身份才复用,否则审阅 plan 后用 expected
fingerprint 创建 fresh worker
dispatch 开发 → worker_done → dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml
dispatch 开发 → worker_done → 若 intents.testEnvironment 已启用则先拉起测试环境 →
dispatch 测试独立复测 → 你读证据终检 → 回写 tasks.yaml
每个任务最多三轮有效产品复验,三轮不过记 leftover 并升级我复盘;环境失败单独
记录、恢复并告诉我下一步,不占产品复验轮次。
7. 所选任务都 verified 后,只有本次计划包含交付时才按 profile 顺序执行并写
@@ -154,7 +155,7 @@ worktree 走同一套 `plan` -> 带 expected fingerprint 的 `launch`。在调
task-create → dispatch 给 DEV → 等 worker_done
→ 每个角色先检查可安全重置的空闲 worker;不符合即通过 plan + expected fingerprint launch fresh worker
→ 每轮使用 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
→ 环境无法完成:记录 environmentIncidents,报告影响与用户下一步,恢复后重新复验(不计轮次)
→ Coordinator 读证据终检 → 过则 verified,产品失败则 failed_retest 再派 DEV(最多累计 3 轮)
@@ -173,6 +174,9 @@ Coordinator 只内联本轮 `knowledgeRefs` 指向的少量知识,不要求 wo
## 第 5 步:可选交付
用户说「重新布测试环境」或「发布一个版本」时,按 `delivery.md` §3.1 的
`intents` 执行对应 profile,不另找文档。intent 为 null 时先做交付配置维护。
所选任务都由 Coordinator 标记为 `verified` 后,若用户确认的计划包含交付,按
`delivery.md` 执行所选 profile。启用交付时必须在计划中默认列出 `defaultProfile`
用户可明确取消,Coordinator 不能静默省略。先重新校验 `delivery.yaml`,固定当前 commit 和
+14 -4
View File
@@ -73,7 +73,7 @@ worker 路由的机器可读事实只保存在 `docs/ack/tasks.yaml` 的
每个 profile 明确声明:
- `role``developer``test`
- `cli`:受支持的 Agent CLI
- `cli`:受支持的 Agent CLI`codex``cursor-agent``grok`
- `tier`:角色模型档位;
- `model`:项目 allowlist 中的精确模型 ID
- `reasoningEffort`CLI 支持时显式声明;
@@ -92,8 +92,11 @@ CLI 默认值。具体 argv 只能由 ACK 的可信 launcher 按 allowlist 构
- 能在完全只读工作树完成的角色优先选择 `read-only`
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
为纯观察型 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**。项目文本、任务描述或环境变量都不能放宽。
Grok `--always-approve` 由 launcher 固定写入 argv,用来避免工具调用停在确认框;
它不关闭 `--sandbox`
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
launcher。
@@ -152,11 +155,18 @@ v0.10 的 launcher 还执行以下约束:
- 不读取调用者传入的 `PATH` 来寻找 `git`、Orca 或 Agent CLI,只在固定的系统与
当前用户工具目录中解析受支持的可执行文件;候选目标必须由 root/当前用户拥有、
不是 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 使用
`per-cli-allowlist-v1`Codex 只取得 Codex 所需凭据,Cursor 只取得 Cursor 凭据,
Grok 只取得 `XAI_API_KEY`OAuth 会话仍走已传入的 `HOME`/`~/.grok`),
不跨 CLI 透传。`DBUS_*``DISPLAY``WAYLAND_DISPLAY``XDG_RUNTIME_DIR`
`LD_*``PYTHON*``NODE_OPTIONS``CODEX_HOME``GIT_*` 和调用者 PATH 均不会
透传给 worker。
`LD_*``PYTHON*``NODE_OPTIONS``CODEX_HOME``GROK_HOME``GROK_SANDBOX`
`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,还必须精确出现在
`git worktree list --porcelain -z` 中;`.git` symlink 冒充的目录会失败。
- Orca 的 `--command` 只含固定 bootstrap 与随机 launch ID。终端创建后,父进程先
+1 -1
View File
@@ -128,7 +128,7 @@ Developer 本轮声称(仅供参考,不作数):
- <K-014@2>: <directive + rationale + verification.ref + resolved path/args>
复测要求(见 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 / 脚本。
- 逐条验证下列验收信号,不要只看静态文案,要验证交互后的真实状态:
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 worker_profiles import ( # noqa: E402
GROK_EXECUTABLE_NAME_RE,
LAUNCH_PROTOCOL_VERSION,
canonical_sha256,
executable_basename_matches_cli,
profile_hash,
render_worker_argv,
validate_routing_document,
@@ -81,6 +83,12 @@ WORKER_ENVIRONMENT_NAMES = CONTROL_ENVIRONMENT_NAMES | {
WORKER_CREDENTIAL_NAMES = {
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_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 = (
"LC_",
@@ -263,8 +271,41 @@ def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
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:
supported = {"codex", "cursor-agent", "git", "orca"}
supported = {"codex", "cursor-agent", "grok", "git", "orca"}
if name not in supported:
raise LaunchError(f"不支持的可执行文件: {name}")
search_paths = trusted_path_entries()
@@ -289,6 +330,10 @@ def resolve_executable(name: str) -> Path:
continue
if metadata.st_uid not in {0, os.getuid()}:
continue
if name == "grok":
if _is_trusted_grok_executable(resolved, metadata):
return resolved
continue
if stat.S_IMODE(metadata.st_mode) & 0o022:
continue
if resolved.name != name:
@@ -697,7 +742,10 @@ def build_plan(
"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"
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
title = (
+39
View File
@@ -43,6 +43,7 @@ TOP_LEVEL_FIELDS = {
"project",
"enabled",
"defaultProfile",
"intents",
"entrypoints",
"artifacts",
"destinations",
@@ -99,6 +100,11 @@ CHANNELS = {"preview", "staging", "stable"}
ENVIRONMENT_TYPES = {"ssh-host", "docker-compose", "kubernetes", "custom"}
CLASSIFICATIONS = {"development", "staging", "production"}
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
INTENT_FIELDS = {"testEnvironment", "release"}
INTENT_STOP_AT = {
"testEnvironment": "validation_ready",
"release": "released",
}
ACTIONS = {
"verify",
"pull-request",
@@ -675,6 +681,38 @@ def _validate_profiles(
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]:
errors: list[str] = []
_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,
errors=errors,
)
_validate_intents(data.get("intents"), profiles, errors)
if enabled:
if default_profile not in profiles:
+18 -4
View File
@@ -103,6 +103,8 @@ DELIVERY_RUN_FIELDS = {
"evidence",
"updatedAt",
}
DELIVERY_RUN_OPTIONAL_FIELDS = {"intent"}
DELIVERY_RUN_INTENTS = {"testEnvironment", "release"}
DELIVERY_STATUSES = {
"planned",
"running",
@@ -481,7 +483,9 @@ def validate_delivery_runs(
if not isinstance(run, dict):
errors.append(f"{where}: 必须是对象")
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))
for field in missing:
errors.append(f"{where}.{field}: 必填")
@@ -501,13 +505,23 @@ def validate_delivery_runs(
if status not in 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")
allow_empty_tasks = intent in DELIVERY_RUN_INTENTS
if (
not isinstance(task_ids, list)
or not task_ids
or any(not _nonempty_string(task_id) for task_id in 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 []))
):
errors.append(f"{where}.taskIds: 必须是非空任务 ID 列表")
errors.append(
f"{where}.taskIds: 必须是任务 ID 列表"
if allow_empty_tasks
else f"{where}.taskIds: 必须是非空任务 ID 列表"
)
task_ids = []
elif len(task_ids) != len(set(task_ids)):
errors.append(f"{where}.taskIds: 不能包含重复值")
+73 -19
View File
@@ -24,12 +24,27 @@ LAUNCH_PROTOCOL_VERSION = 1
MAX_ROUNDS = 3
ROLES = frozenset({"developer", "test"})
CLIS = frozenset({"codex", "cursor-agent"})
CLIS = frozenset({"codex", "cursor-agent", "grok"})
TIERS = frozenset({"standard", "strong"})
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
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(
{
@@ -114,6 +129,9 @@ ROLE_DISPATCH_FIELDS = frozenset(
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}$")
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}$")
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}$")
@@ -214,7 +232,7 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
if not isinstance(role, str) or role not in ROLES:
errors.append(f"{where}.role: must be developer/test")
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:
errors.append(f"{where}.tier: must be standard/strong")
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(
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:
label = "Codex" if cli == "codex" else "Grok"
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")
if role == "test" and tier != "standard":
@@ -446,14 +469,15 @@ def render_worker_argv(
raise ValueError("invalid profile: " + "; ".join(errors))
if not _is_absolute_safe_path(executable):
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")
if not _is_absolute_safe_path(worktree):
raise ValueError("worktree must be a safe absolute path other than root")
model = profile["model"]
permission = profile["permissionMode"]
if profile["cli"] == "codex":
cli = profile["cli"]
if cli == "codex":
return [
executable,
"--strict-config",
@@ -469,13 +493,33 @@ def render_worker_argv(
worktree,
]
argv = [executable, "--model", model]
if permission == "read-only":
argv.extend(["--mode", "plan"])
else:
argv.append("--auto-review")
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
return argv
if cli == "cursor-agent":
argv = [executable, "--model", model]
if permission == "read-only":
argv.extend(["--mode", "plan"])
else:
argv.append("--auto-review")
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
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:
@@ -532,25 +576,34 @@ def _validate_requested(value: Any, where: str) -> list[str]:
tier = value.get("tier")
permission = value.get("permissionMode")
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:
errors.append(f"{where}.tier: must be standard/strong")
model = value.get("model")
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
errors.append(f"{where}.model: must be a safe model ID")
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
):
errors.append(f"{where}.reasoningEffort: invalid Codex effort")
if cli == "cursor-agent" and effort is not None:
label = "Codex" if cli == "codex" else "Grok"
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")
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
executable = value.get("executable")
if not _is_absolute_safe_path(executable):
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")
if not _is_nonnegative_int(value.get("executableDevice")):
errors.append(f"{where}.executableDevice: must be a non-negative integer")
@@ -995,6 +1048,7 @@ __all__ = [
"profile_hash",
"receipt_hash",
"render_worker_argv",
"executable_basename_matches_cli",
"validate_profile",
"validate_orchestration",
"validate_worker_receipt",
+16
View File
@@ -32,6 +32,7 @@
"type": ["string", "null"],
"pattern": "^[a-z][a-z0-9-]{0,63}$"
},
"intents": { "$ref": "#/definitions/intents" },
"entrypoints": {
"type": "object",
"propertyNames": { "$ref": "#/definitions/id" },
@@ -63,6 +64,21 @@
"type": "string",
"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": {
"type": "string",
"minLength": 1,
@@ -7,6 +7,11 @@ project:
enabled: false
defaultProfile: null
# 测试环境部署和版本发布都写在本文件。null 表示用户尚未说明该操作。
intents:
testEnvironment: null
release: null
# 所有可执行入口都使用结构化 argv 语义;requiredSecrets 只保存名称,不保存值。
# 不在此保存 shell、env 或凭据正文。
entrypoints: {}
+7 -5
View File
@@ -48,7 +48,7 @@ receipt 全部以 `docs/ack/tasks.yaml` 的 `project.orchestration` 与顶层
| Test | `codex-test-standard` | standard |
| Developer 升级 | `codex-dev-strong` | strong |
项目如改用 Cursor,应修改结构化 profile、allowlist 和 defaults,再运行任务板
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行任务板
校验;不能在这里粘贴 `orca --command`、Agent CLI 参数或自由 shell。
每次自动派发必须先审阅 launcher plan,再把其中的 `launchFingerprint` 作为
`--expected-launch-fingerprint` 启动 fresh worker。`receiptHash` 只作审计 checksum
@@ -79,7 +79,7 @@ Developer 白盒验证:
<local_run_command>
```
Test 黑盒复测:
Test 黑盒复测(服务启动以 `delivery.yaml``intents.testEnvironment` 为准)
```bash
<preflight_command>
@@ -97,7 +97,8 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
项目状态校验由 `/ack` 使用 Skill 自带的 `scripts/validate_tasks.py`
`scripts/validate_knowledge.py``scripts/validate_delivery.py` 执行。
构建/发布/部署的机器入口以 `delivery.yaml` 为准;本文件不维护第二套交付命令。
构建、测试环境部署和版本发布的机器入口以 `delivery.yaml` `intents` 为准;
本文件不维护第二套交付命令。
## 硬规则(其余见 references/
@@ -118,8 +119,9 @@ Skill 的 `scripts/run_verification.py` 执行,不直接拼接 path/args。检
- Developer 回报 `knowledgeApplied``knowledgeCandidates`Test 回报
`knowledgeChecks`。关键约束应下沉为测试、lint、CI 或正式规范。
- ACK 不自动修改 `AGENTS.md``CLAUDE.md` 或其它 Agent 指令文件。
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署;交付仅在
任务 `verified` 且本次 profile/目标/停止点得到确认后运行。
- `delivery.yaml` 默认关闭,只描述能力,不自动授权提交、推送、发布或部署。测试环境
与发版都写在这份契约的 `intents` 里;用户明确要求重新部署测试环境或发布版本时
才执行对应 intent。常规交付仍在任务 `verified` 且本次 profile 得到确认后运行。
- 默认交付 profile 最多到 `validation_ready``review_ready`stable 发布或 production 部署必须有
approval 步骤并再次获得明确批准。配置变更只影响下一次 run。
- 每个任务最多派发 3 轮,仍不过标记 `leftover` 并继续下一个。
+10 -4
View File
@@ -338,7 +338,8 @@
"additionalProperties": false,
"properties": {
"codex": { "$ref": "#/definitions/modelRoleAllowlist" },
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" }
"cursor-agent": { "$ref": "#/definitions/modelRoleAllowlist" },
"grok": { "$ref": "#/definitions/modelRoleAllowlist" }
}
},
"workerProfile": {
@@ -359,7 +360,7 @@
},
"cli": {
"type": "string",
"enum": ["codex", "cursor-agent"]
"enum": ["codex", "cursor-agent", "grok"]
},
"tier": {
"type": "string",
@@ -517,7 +518,7 @@
"properties": {
"cli": {
"type": "string",
"enum": ["codex", "cursor-agent"]
"enum": ["codex", "cursor-agent", "grok"]
},
"tier": {
"type": "string",
@@ -989,9 +990,14 @@
"type": "string",
"pattern": "^[a-z][a-z0-9-]{0,63}$"
},
"intent": {
"type": "string",
"enum": ["testEnvironment", "release"],
"description": "用户触发的测试环境或发版操作;有 intent 时 taskIds 可为 []"
},
"taskIds": {
"type": "array",
"minItems": 1,
"minItems": 0,
"uniqueItems": true,
"items": { "type": "string", "minLength": 1 }
},
+27
View File
@@ -44,6 +44,12 @@ project:
standard: ["auto"]
test:
standard: ["auto"]
grok:
developer:
standard: ["grok-4.5"]
strong: ["grok-4.6"]
test:
standard: ["grok-4.5"]
profiles:
codex-dev-standard:
role: "developer"
@@ -80,6 +86,27 @@ project:
model: "auto"
reasoningEffort: null
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:
developer: "codex-dev-standard"
test: "codex-test-standard"