feat(ack): support project agent routing overrides
This commit is contained in:
+14
-2
@@ -37,7 +37,8 @@ pouch add ack -g # 全局
|
||||
## 使用前准备
|
||||
|
||||
- 项目是 Git 仓库。
|
||||
- 自动拉起 Developer / Test worker 时需要 Orca,以及项目里配置好的 Codex、Cursor、Grok 或 OMP。
|
||||
- 自动拉起 Developer / Test worker 时需要 Orca,以及项目里配置好的 Codex、Cursor、Grok、OMP 或 Pi。
|
||||
- 使用 Pi 时需安装 `pi`;默认模型用 `agents.yaml` 中 `cli: pi`、`model: null` 表示。
|
||||
- 布测试环境需要同时安装 [deployer](../deployer/README.md),并准备好 SSH 和目标机器的 Docker Compose。
|
||||
- 飞书收 bug 是可选的,要在 `project.md` 里配置 `bugIntake`。
|
||||
- 不要 `pouch init builder` 或 `pouch init deployer`。测试环境和发版直接对 `/ack` 说;缺 deployer 时 ACK 会转去它的初始化。
|
||||
@@ -57,7 +58,8 @@ pouch init ack --project ~/code/my-app
|
||||
├── tasks.yaml
|
||||
├── knowledge.yaml
|
||||
├── delivery.yaml # 默认 enabled: false
|
||||
└── regression.yaml # 默认 cases: []
|
||||
├── regression.yaml # 默认 cases: []
|
||||
└── agents.yaml # 可选:临时覆盖 Developer/Test 的 CLI、模型和权限
|
||||
```
|
||||
|
||||
不会把 ACK Skill 复制进项目。规范、模板和脚本始终从已安装的 Skill 目录读取。
|
||||
@@ -94,6 +96,16 @@ pouch init ack --project ~/code/my-app
|
||||
/ack 处理这个需求:…。任务验证通过后执行 review profile。
|
||||
```
|
||||
|
||||
临时调整角色启动方式时直接说明意图,例如:
|
||||
|
||||
```text
|
||||
/ack 让这个项目的 dev agent 用 pi 默认模型,test agent 用 cursor-agent auto 模型
|
||||
```
|
||||
|
||||
ACK 会在 `.pouch/ack/agents.yaml` 保存结构化覆盖配置。它存在时优先于
|
||||
`tasks.yaml.project.orchestration`;删除后自动回退。配置 hash 会绑定启动计划和 receipt,
|
||||
修改后必须重新审阅并生成 plan。
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
1. 初始化或检查 `.pouch/ack/`,缺项按统一格式列出;不从 README/CI 猜测并启用交付或知识。
|
||||
|
||||
+29
-10
@@ -9,9 +9,14 @@ description: >-
|
||||
# ACK 项目协作入口
|
||||
|
||||
项目只在 `.pouch/ack/` 保存 `project.md`、`tasks.yaml`、`knowledge.yaml`、
|
||||
`delivery.yaml` 和 `regression.yaml`。不要复制 Skill 内容,不要修改 `AGENTS.md`、
|
||||
`delivery.yaml`、`regression.yaml`,以及可选的 `agents.yaml` 角色启动覆盖配置。
|
||||
不要复制 Skill 内容,不要修改项目的 `AGENTS.md`、
|
||||
`CLAUDE.md`。
|
||||
|
||||
固定状态文件为 `.pouch/ack/project.md`、`.pouch/ack/tasks.yaml`、
|
||||
`.pouch/ack/knowledge.yaml`、`.pouch/ack/delivery.yaml` 和
|
||||
`.pouch/ack/regression.yaml`。
|
||||
|
||||
开始时解析当前 `SKILL.md` 所在目录为 `<ack-skill-dir>`。优先
|
||||
`git rev-parse --show-toplevel` 作为项目根。
|
||||
|
||||
@@ -19,22 +24,25 @@ description: >-
|
||||
|
||||
- 初始化、接入 ACK:执行「初始化」。
|
||||
- 检查配置是否完整:执行「检查」。
|
||||
- 临时调整 Developer/Test 的 CLI 或模型:执行「角色启动配置维护」。
|
||||
- 做需求、修 bug 或继续任务:执行「工作」。修 bug 不写大 PRD。
|
||||
- 增改关闭交付、说明怎么布测试环境或发版:执行「交付配置维护」。
|
||||
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer,不复制 compose)。
|
||||
- 部署或重布测试环境:执行「运行测试环境」(加载 deployer skill 的「初始化」或运行流程,不复制 compose)。
|
||||
- 发布版本:执行「运行版本发布」。回归:执行「运行回归」。
|
||||
- 任务板使用 Orca 时,编排命令见 [orca-adapter.md](references/orca-adapter.md)。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不覆盖已有 `.pouch/ack`;除用户确认的任务或 delivery profile 外,不擅自提交、推送、创建终端、新 worktree、发布或部署。
|
||||
- 不覆盖已有 `.pouch/ack`;除用户确认的任务、agents profile 或 delivery profile 外,不擅自提交、推送、创建终端、新 worktree、发布或部署。
|
||||
- 不猜测命令、地址、worker handle 或模型名;无法确定写 `n/a`。
|
||||
- `agents.yaml` 是可选的临时角色启动覆盖;存在时优先于 `tasks.yaml.project.orchestration`,不存在时回退到任务板配置。
|
||||
- `agents.yaml` 只能描述结构化 CLI、模型、reasoning effort 和权限,不能保存 command、argv、shell 或环境变量;Pi 的默认模型用 `cli: pi`、`model: null` 表示。
|
||||
- 不把完整 `tasks.yaml` / `knowledge.yaml` / `regression.yaml` 注入上下文;用对应 `select_*.py`。写回前跑完整校验。
|
||||
- 只有 Coordinator 写任务板、知识库、回归目录、`deliveryRuns` 和 `regressionRuns`。`delivery.yaml` 只在「交付配置维护」中改。见 [roles-and-permissions.md](references/roles-and-permissions.md)。
|
||||
- 知识检查只经 `run_verification.py` 的 registry ID;不把知识正文拼成 shell。
|
||||
- 自动 worker 禁止 full-access、bypass、关闭 sandbox、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
||||
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。`indeterminate` / `reconcile required` 不直接重试。
|
||||
- 产品失败才占三轮;环境失败写 `environmentIncidents`。见 [optimization-method.md](references/optimization-method.md) §4。
|
||||
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。只复用同轮、空闲且完成清理历史消息的 worker;无法确认清理成功时创建 fresh worker。`indeterminate` / `reconcile required` 不直接重试。
|
||||
- 产品失败才占三轮;环境失败写 `environmentIncidents`,环境失败不占复验轮次。见 [optimization-method.md](references/optimization-method.md) §4。
|
||||
|
||||
## 初始化
|
||||
|
||||
@@ -50,9 +58,12 @@ python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
|
||||
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
|
||||
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
|
||||
--tasks .pouch/ack/tasks.yaml
|
||||
|
||||
# 仅在存在临时角色配置时运行
|
||||
python3 <ack-skill-dir>/scripts/validate_agents.py .pouch/ack/agents.yaml
|
||||
```
|
||||
|
||||
4. 检查上述文件是否仍有 `<...>` 占位符。结构校验通过且必填项目事实完整才称「完成」。除非用户明确要求,不提交、不推送。
|
||||
4. 检查上述文件是否仍有 `<...>` 占位符;没有真实任务时使用 `tasks: []`。结构校验通过且必填项目事实完整才称「完成」。除非用户明确要求,不提交、不推送。
|
||||
|
||||
```text
|
||||
## ack 初始化:完成 | 部分完成 | 阻塞
|
||||
@@ -65,18 +76,26 @@ python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yam
|
||||
|
||||
## 检查
|
||||
|
||||
只读,不自动修复。核对清单见 [adoption-checklist.md](references/adoption-checklist.md)。用 `select_tasks.py` 看配置;跑与「初始化」相同的四个校验器。对比 `VERSION` 与 `ackVersion`(旧 `kitVersion` 仍可读);从 `0.10.0` 起 `project.orchestration` 与 `workerReceipts` 必须同时存在。旧项目可无 delivery/regression,存在或被引用时必须校验。若有 `bugIntake` 再跑 `feishu_bug_intake.py check`。若测试环境走 deployer,只读跑其 `check.py`;未通过列入待配置,不要复制 compose 或静默初始化 deployer。用同一报告格式,标题改为 `## ack 检查:…`。
|
||||
只读,不自动修复。核对清单见 [adoption-checklist.md](references/adoption-checklist.md)。用 `select_tasks.py` 看配置;跑与「初始化」相同的四个校验器;存在 `agents.yaml` 时再跑 `validate_agents.py`。对比 `VERSION` 与 `ackVersion`(旧 `kitVersion` 仍可读);从 `0.10.0` 起 `project.orchestration` 与 `workerReceipts` 必须同时存在。旧项目可无 delivery/regression,存在或被引用时必须校验。若有 `bugIntake` 再跑 `feishu_bug_intake.py check`。若测试环境走 deployer,只读跑其 `check.py`;未通过列入待配置,不要复制 compose 或静默初始化 deployer。用同一报告格式,标题改为 `## ack 检查:…`。
|
||||
|
||||
## 工作
|
||||
|
||||
1. `.pouch/ack` 不存在:停止并建议先 `/ack` 初始化;不要静默初始化。
|
||||
2. 读 `project.md`;用 `select_tasks.py`(已知任务加 `--task-id`)和 `select_knowledge.py` 取当前任务相关 `active` 条目。超预算时缩小选择,不回退为完整 yaml。
|
||||
2. 读 `project.md`;若存在 `agents.yaml`,先用 `validate_agents.py` 校验并读取其 `defaults`;用 `select_tasks.py`(已知任务加 `--task-id`)和 `select_knowledge.py` 取当前任务相关 `active` 条目。超预算时缩小选择,不回退为完整 yaml。
|
||||
3. 新需求或尚未确认的计划:读 [kickoff.md](references/kickoff.md)。修 bug 写短问题说明、复现和可观测验收,不写大 PRD。
|
||||
4. 用户已确认后按 [closed-loop.md](references/closed-loop.md) 执行。当前会话担任 Coordinator,不亲自写代码或跑测试。
|
||||
4. 用户已确认后按 [closed-loop.md](references/closed-loop.md) 执行。当前会话担任 Coordinator,不亲自写代码或跑测试;worker profile 按 `agents.yaml`(若存在)否则按任务板配置选择。
|
||||
5. 存在 `project.bugIntake` 时先完成「飞书收件」。创建或更换 worker 走「启动 worker」。若 `intents.testEnvironment` 已启用,派 Test 前先走「运行测试环境」。
|
||||
6. 知识 `verification.ref` 只经 `run_verification.py`。不把 `worker_done` 或 Test 自报成功当作完成。环境失败先有界恢复并报告 `userAction`;三轮产品失败记 `leftover`。
|
||||
7. 任务 `verified` 且改了可见行为或 API 后,转入「运行回归」收获用例。若本次计划含交付,再走对应交付模式;默认最多到 `validation_ready` 或 `review_ready`。
|
||||
8. 整轮完成后 `reclaim_workers.py` 先 dry-run 再 `--apply`,只回收仅属于 `verified` 任务的 worker。关闭结果不确定则记录,不伪报。
|
||||
8. 整轮任务完成后 `reclaim_workers.py` 先 dry-run 再 `--apply`,回收所有只属于 `verified` 任务的 worker 终端,并核对关闭回执;仍关联未完成任务的 worker 不设置 TTL。关闭结果不确定则记录,不伪报。
|
||||
|
||||
不能停在“任务已 `verified` 却声称整轮 ACK 已结束”:必须完成回归决定和 worker 回收。
|
||||
|
||||
## 角色启动配置维护
|
||||
|
||||
用户明确说「让这个项目的 dev agent 用 pi 默认模型、test agent 用 cursor-agent auto」等临时调整时,创建或更新 `.pouch/ack/agents.yaml`。配置文件形状见 [agents.template.yaml](templates/agents.template.yaml) 和 [agents.example.yaml](examples/agents.example.yaml),只写结构化 profile,不写自由命令。
|
||||
|
||||
存在 `agents.yaml` 时,ACK 的 `profile-hash`、`plan`、`launch` 和 receipt 全部绑定该文件的 hash;修改配置后必须重新 plan,旧 fingerprint 不能继续启动。删除该文件即可回退到 `tasks.yaml.project.orchestration`。配置本身不代表提交、推送、发布或部署授权。
|
||||
|
||||
## 飞书收件
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.19.0
|
||||
0.20.0
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Example .pouch/ack/agents.yaml for a temporary routing choice.
|
||||
profileVersion: 1
|
||||
mode: "orca"
|
||||
modelAllowlist:
|
||||
pi:
|
||||
developer:
|
||||
standard: [null]
|
||||
strong: [null]
|
||||
cursor-agent:
|
||||
test:
|
||||
standard: ["auto"]
|
||||
profiles:
|
||||
pi-dev-default:
|
||||
role: "developer"
|
||||
cli: "pi"
|
||||
tier: "standard"
|
||||
model: null
|
||||
reasoningEffort: "medium"
|
||||
permissionMode: "workspace-write"
|
||||
pi-dev-strong:
|
||||
role: "developer"
|
||||
cli: "pi"
|
||||
tier: "strong"
|
||||
model: null
|
||||
reasoningEffort: "high"
|
||||
permissionMode: "workspace-write"
|
||||
cursor-test-auto:
|
||||
role: "test"
|
||||
cli: "cursor-agent"
|
||||
tier: "standard"
|
||||
model: "auto"
|
||||
reasoningEffort: null
|
||||
permissionMode: "workspace-write"
|
||||
defaults:
|
||||
developer: "pi-dev-default"
|
||||
test: "cursor-test-auto"
|
||||
developerUpgraded: "pi-dev-strong"
|
||||
@@ -1,6 +1,6 @@
|
||||
# notes-web Agent 协作协议(示例,项目覆盖层)
|
||||
|
||||
> 本项目基于 ack v0.19.0。
|
||||
> 本项目基于 ack v0.20.0。
|
||||
> 通用规范由 `/ack` 从 Skill 自身的 `references/` 读取,本文件只填项目差异。
|
||||
> 覆盖层文件放在 `.pouch/ack/project.md`,不占用 `AGENTS.md`。
|
||||
> ACK 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
@@ -33,8 +33,9 @@
|
||||
|
||||
## Worker 路由
|
||||
|
||||
结构化配置位于 `.pouch/ack/tasks.yaml` 的 `project.orchestration`,启动记录位于顶层
|
||||
`workerReceipts`。本项目默认使用:
|
||||
结构化配置默认位于 `.pouch/ack/tasks.yaml` 的 `project.orchestration`;存在
|
||||
`.pouch/ack/agents.yaml` 时以后者为准。启动记录位于顶层 `workerReceipts`。
|
||||
本项目默认使用:
|
||||
|
||||
| 角色 | profile ID | 档位 |
|
||||
|------|------------|------|
|
||||
@@ -42,8 +43,8 @@
|
||||
| Test | `codex-test-standard` | standard |
|
||||
| Developer 升级 | `codex-dev-strong` | strong |
|
||||
|
||||
项目如改用 Cursor 或 Grok,应修改结构化 profile、allowlist 和 defaults,再运行
|
||||
任务板校验。本文件不保存或拼接 worker shell 命令。
|
||||
项目如临时改用 Pi、Cursor 或 Grok,应修改 `agents.yaml` 的结构化 profile、allowlist
|
||||
和 defaults,再运行对应校验。本文件不保存或拼接 worker shell 命令。
|
||||
|
||||
## 路径权限
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
version: 1
|
||||
updatedAt: "2026-07-06T09:40:00+08:00"
|
||||
source: "Coordinator (PM) Agent"
|
||||
ackVersion: "0.19.0"
|
||||
ackVersion: "0.20.0"
|
||||
project:
|
||||
name: "notes-web"
|
||||
baseUrl: "http://localhost:5173"
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
`project.knowledgeFile` 指针,没有重跑初始化或覆盖其它项目状态。
|
||||
- [ ] 项目中没有 ACK Skill 的复制目录或 `kit`、`framework` 软链接。
|
||||
- [ ] `tasks.yaml` 使用 `ackVersion` 记录 ACK Skill 版本。
|
||||
- [ ] 若存在 `.pouch/ack/agents.yaml`,已运行 `validate_agents.py`;它优先于任务板
|
||||
`project.orchestration`,且只包含结构化 profile,不包含自由 command/argv/env。
|
||||
|
||||
## 项目覆盖层
|
||||
|
||||
@@ -24,7 +26,9 @@
|
||||
- [ ] 技术栈、运行、构建、单测和集成测试命令均来自项目证据。
|
||||
- [ ] Coordinator、Developer、Test 的模型档位和升级规则已明确。
|
||||
- [ ] `project.orchestration` 使用受支持的 profileVersion,模型都命中项目
|
||||
allowlist,默认 profile 与角色/档位一致。
|
||||
allowlist,默认 profile 与角色/档位一致。
|
||||
- [ ] `agents.yaml`(若存在)使用受支持的 profileVersion,Pi 默认模型使用
|
||||
`cli: pi` + `model: null`,配置 hash 会绑定启动 plan/fingerprint/receipt。
|
||||
- [ ] `allowedWorktrees` 已废弃(v0.19 起),新任务板不配置;worker 默认在
|
||||
`--project-root` 工作,其它 worktree 由 launcher 按同 git 仓库且已注册约束放行。
|
||||
- [ ] `tasks.yaml` 不需要保存 `repoPath` 或 `devWorktree`;项目状态从当前
|
||||
|
||||
@@ -34,7 +34,7 @@ Coordinator 发现或读取 open 任务
|
||||
-> 从 knowledge.yaml 按 scope 推荐 active 知识,Coordinator 确认固定 revision 的 knowledgeRefs
|
||||
-> 为新轮次生成稳定 attemptId(<task-id>-A<round>),Developer 与 Test 共用
|
||||
-> 决定 worktree:当前 worktree 起子 agent,还是新建隔离 worktree(见下节「子任务放哪」)
|
||||
-> 解析安全 profile:机器配置只读 tasks.yaml.project.orchestration
|
||||
-> 解析安全 profile:机器配置只读 agents.yaml(存在时),否则读取 tasks.yaml.project.orchestration
|
||||
-> 运行 launcher plan 并审阅 launchFingerprint
|
||||
-> 检查同轮空闲 worker;可信清理历史消息成功才复用,否则带 expected fingerprint 启动 fresh worker
|
||||
-> 把本次 task/attempt receipt 写回 tasks.yaml(见 orca-adapter.md)
|
||||
|
||||
@@ -103,6 +103,9 @@ pouch init ack --project <project-root>
|
||||
- `allowedWorktrees` 已废弃(v0.19 起),新任务板不生成该字段;worker 默认在
|
||||
`--project-root` 工作。模型 allowlist、profiles 和 defaults 使用项目实际允许值。
|
||||
不要把完整启动命令、`extraArgs`、`env` 或任意 executable 写进任务板。
|
||||
- `.pouch/ack/agents.yaml` 是可选的临时 worker 路由覆盖;存在时优先于任务板中的
|
||||
`project.orchestration`。它必须使用结构化 profile;Pi 默认模型写成 `cli: pi`、
|
||||
`model: null`,不能写自由命令。
|
||||
- 非服务项目的 `baseUrl` 写为 `n/a`。
|
||||
- 没有真实任务时使用 `tasks: []`,不要保留或虚构示例任务。
|
||||
|
||||
@@ -152,6 +155,8 @@ python3 <ack-skill-dir>/scripts/validate_delivery.py .pouch/ack/delivery.yaml \
|
||||
--tasks .pouch/ack/tasks.yaml --project-root <project-root>
|
||||
python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yaml \
|
||||
--tasks .pouch/ack/tasks.yaml
|
||||
# 存在临时角色配置时
|
||||
python3 <ack-skill-dir>/scripts/validate_agents.py .pouch/ack/agents.yaml
|
||||
```
|
||||
|
||||
同时确认:
|
||||
@@ -164,6 +169,8 @@ python3 <ack-skill-dir>/scripts/validate_regression.py .pouch/ack/regression.yam
|
||||
- Developer 与 Test 的验证命令可执行。
|
||||
- `project.orchestration` 的 profile/allowlist/defaults 通过校验,自动模式只允许
|
||||
`read-only` 或 `workspace-write`;旧任务板未迁移时保持手动模式。
|
||||
- 若存在 `agents.yaml`,其 profile/allowlist/defaults 也通过校验,且启动配置 hash
|
||||
会绑定 plan、fingerprint 和 receipt。
|
||||
- 顶层 `workerReceipts` 和 `dispatch.developer/test` 的 task/role/profile/attempt
|
||||
引用一致;`receiptId` 与 `attemptId` 同时为空或同时填写。持久 receipt 只作审计,
|
||||
不能单独授权复用旧终端;复用还需要空闲状态、身份匹配和可信历史清理证明。
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
|
||||
**你(发起编排的强模型会话)就是 Coordinator (PM) / 产品。** 你负责写文档、拆任务、
|
||||
编排、终检,**不亲自写代码、不亲自跑测试**。开发和测试是另起的 worker agent;
|
||||
具体 CLI、模型、reasoning effort 和执行模式的机器事实源是
|
||||
`.pouch/ack/tasks.yaml` 的 `project.orchestration`。`.pouch/ack/project.md` 只解释项目
|
||||
具体 CLI、模型、reasoning effort 和执行模式的机器事实源优先是
|
||||
`.pouch/ack/agents.yaml`,不存在时回退到 `.pouch/ack/tasks.yaml` 的
|
||||
`project.orchestration`。`.pouch/ack/project.md` 只解释项目
|
||||
差异,不能提供另一套启动命令。
|
||||
|
||||
---
|
||||
@@ -65,8 +66,8 @@ python3 <ack-skill-dir>/scripts/validate_tasks.py .pouch/ack/tasks.yaml
|
||||
python3 <ack-skill-dir>/scripts/validate_knowledge.py .pouch/ack/knowledge.yaml --tasks .pouch/ack/tasks.yaml
|
||||
```
|
||||
|
||||
`project.orchestration` 是 worker profile 的机器 SSOT;未知字段、非 allowlist 模型、
|
||||
不匹配的角色/档位和非安全权限必须在派发前 fail closed。
|
||||
有效 worker routing(`agents.yaml` 优先,否则 `project.orchestration`)是 profile 的机器
|
||||
SSOT;未知字段、非 allowlist 模型、不匹配的角色/档位和非安全权限必须在派发前 fail closed。
|
||||
|
||||
读取任务上下文使用:
|
||||
|
||||
@@ -96,7 +97,7 @@ python3 <ack-skill-dir>/scripts/select_tasks.py .pouch/ack/tasks.yaml \
|
||||
|
||||
## 第 3 步:审阅并启动 worker
|
||||
|
||||
先读 `tasks.yaml.project.orchestration` 的结构化 profiles。历史 receipt 或既有 Orca
|
||||
先读有效 routing(`agents.yaml` 优先,否则 `tasks.yaml.project.orchestration`)的结构化 profiles。历史 receipt 或既有 Orca
|
||||
terminal 不能单独授权复用。复用候选必须属于同一轮 ACK、处于空闲状态,且角色、
|
||||
profile、worktree 与启动身份仍完全匹配;还必须通过受信后端清理历史消息并取得可核对
|
||||
的新会话身份。当前 Orca 接口缺少该清理证明,所以 Orca 派发仍创建 fresh worker。
|
||||
@@ -196,7 +197,7 @@ Coordinator 最后标记整轮任务完成后,用 `scripts/reclaim_workers.py`
|
||||
## 一句话
|
||||
|
||||
产品文档 + 验收信号写在前(你,强模型)→ 确认显式 `knowledgeRefs` → 从
|
||||
`tasks.yaml.project.orchestration` 解析安全 profile → 审阅 plan 并用 expected
|
||||
有效 routing(`agents.yaml` 优先,否则 `tasks.yaml.project.orchestration`)解析安全 profile → 审阅 plan 并用 expected
|
||||
fingerprint 启动 fresh DEV/TEST → dispatch / 复测 / 终检循环 → 任务结论落
|
||||
`tasks.yaml` → 可选收获回归用例到 `regression.yaml` → 可选 delivery profile
|
||||
到审核点,验证后的跨任务知识由 Coordinator 落 `knowledge.yaml` → 整轮完成后回收
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 模型路由(稳定核心)
|
||||
|
||||
本文件是**三角色默认模型档位、升级规则和 worker profile 选择规则**的单一规范源。
|
||||
本文件是**三角色默认模型档位、升级规则和 worker profile 选择规则**的规范源。
|
||||
目标是在不牺牲质量的前提下降低 token 和模型成本:把昂贵的强模型留给需要判断的
|
||||
工作,把机械执行交给较弱模型。
|
||||
|
||||
@@ -64,23 +64,32 @@ profile 升级时不得复用旧 worker,Test 也不得使用 Developer 的强
|
||||
|
||||
## 结构化 worker profile
|
||||
|
||||
### 机器事实源
|
||||
### 机器事实源与项目临时覆盖
|
||||
|
||||
worker 路由的机器可读事实只保存在 `.pouch/ack/tasks.yaml` 的
|
||||
`project.orchestration`。`project.md` 可以解释项目为何选某个档位,但不能另写一份
|
||||
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 为准。
|
||||
worker 路由的机器可读事实默认保存在 `.pouch/ack/tasks.yaml` 的
|
||||
`project.orchestration`。项目需要临时调整角色 CLI 或模型时,可以在同一目录增加
|
||||
`.pouch/ack/agents.yaml`;它使用与 `project.orchestration` 相同的结构,且优先级更高。
|
||||
删除该文件即回退到任务板配置。`project.md` 只能解释项目为何选某个档位,不能另写一份
|
||||
完整启动命令或覆盖机器配置。字段结构以 `templates/tasks.schema.json` 和
|
||||
`templates/agents.schema.json` 为准。
|
||||
|
||||
`agents.yaml` 的配置 hash 会进入 profile-hash、launch plan、fingerprint 和 receipt。
|
||||
因此修改临时配置后必须重新审阅 plan;旧 fingerprint 不会被接受。
|
||||
|
||||
每个 profile 明确声明:
|
||||
|
||||
- `role`:`developer` 或 `test`;
|
||||
- `cli`:受支持的 Agent CLI(`codex`、`cursor-agent`、`grok`、`omp`);
|
||||
- `cli`:受支持的 Agent CLI(`codex`、`cursor-agent`、`grok`、`omp`、`pi`);
|
||||
- `tier`:角色模型档位;
|
||||
- `model`:项目 allowlist 中的精确模型 ID;
|
||||
- `model`:项目 allowlist 中的精确模型 ID;Pi 可用 `null` 表示其默认模型;
|
||||
- `reasoningEffort`:CLI 支持时显式声明;
|
||||
- `permissionMode`:只允许 `read-only` 或 `workspace-write`。
|
||||
|
||||
`project.orchestration` 必须声明按 CLI/角色/档位分组的 `modelAllowlist`、命名
|
||||
`profiles` 和角色 `defaults`(`allowedWorktrees` 自 v0.19 起废弃,不再配置)。
|
||||
有效路由配置必须声明按 CLI/角色/档位分组的 `modelAllowlist`、命名 `profiles` 和角色
|
||||
`defaults`(`allowedWorktrees` 自 v0.19 起废弃,不再配置)。`cli: pi` 使用真实的
|
||||
Pi CLI;`model: null` 表示使用 Pi 自己保存的默认模型。当前 launcher 对 Pi 只构造
|
||||
固定的 `--thinking`,工作目录由受信 bootstrap 设置;Pi 的 `read-only` profile 只允许
|
||||
`read,grep,find,ls` 工具。
|
||||
launcher 请求的 worktree 必须是同 Git 仓库的已注册 worktree,profile 的模型也必须
|
||||
命中对应 CLI/角色/档位的精确列表。
|
||||
|
||||
@@ -142,10 +151,10 @@ worker 实际工作的绝对路径,两者可以不同。后者仍必须与项
|
||||
|
||||
项目状态文件固定从 `--project-root/.pouch/ack/` 解析;任务板不需要保存 `repoPath` 或
|
||||
`devWorktree`。旧任务板中的这两个字段仅作兼容信息,launcher 不使用它们授权或定位。
|
||||
`projectRoot`、任务板内容摘要和 worker worktree identity 都会进入 launch fingerprint,
|
||||
`projectRoot`、任务板内容摘要、有效 agents 配置 hash 和 worker worktree identity 都会进入 launch fingerprint,
|
||||
因此切换权威项目根、任务板内容或 worker 路径后必须重新生成并审阅 plan。
|
||||
|
||||
- `profile-hash` 读取 `tasks.yaml` 中的 profile,输出确定性 hash。
|
||||
- `profile-hash` 读取有效路由配置中的 profile,并输出配置来源与确定性 hash。
|
||||
- `plan` 只校验并展示将使用的结构化计划,不创建终端。
|
||||
- `launch` 是唯一允许创建 worker 的入口;它要求传入刚审阅的
|
||||
`plan.launchFingerprint`,当前 profile、任务/attempt、worktree、CLI 或 argv
|
||||
|
||||
@@ -63,8 +63,8 @@ reset/attestation 后才能启用上述复用路径,不能用项目文档声
|
||||
## 通过 launcher 创建 worker
|
||||
|
||||
**模型不由 Orca dispatch 设置。** `task-create` / `dispatch` 只投递任务;Agent CLI、
|
||||
模型、reasoning effort 和权限由 `tasks.yaml.project.orchestration` 的结构化 profile
|
||||
决定,并且只能由 ACK launcher 构造。
|
||||
模型、reasoning effort 和权限由 `.pouch/ack/agents.yaml`(存在时)或
|
||||
`tasks.yaml.project.orchestration` 的结构化 profile 决定,并且只能由 ACK launcher 构造。
|
||||
|
||||
先只读生成计划:
|
||||
|
||||
|
||||
@@ -45,13 +45,14 @@ from worker_profiles import ( # noqa: E402
|
||||
executable_basename_matches_cli,
|
||||
profile_hash,
|
||||
render_worker_argv,
|
||||
validate_routing_document,
|
||||
validate_orchestration,
|
||||
)
|
||||
|
||||
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
|
||||
RECEIPT_VERSION = 1
|
||||
ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
|
||||
TASKS_RELATIVE_PATH = Path(".pouch/ack/tasks.yaml")
|
||||
AGENTS_RELATIVE_PATH = Path(".pouch/ack/agents.yaml")
|
||||
MAX_CONTROL_OUTPUT = 1024 * 1024
|
||||
MAX_RECORD_SIZE = 256 * 1024
|
||||
LAUNCH_TTL_SECONDS = 120
|
||||
@@ -85,12 +86,49 @@ WORKER_CREDENTIAL_NAMES = {
|
||||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||||
"grok": frozenset({"XAI_API_KEY"}),
|
||||
"omp": frozenset({"OPENCODE_API_KEY"}),
|
||||
"pi": frozenset(
|
||||
{
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANT_LING_API_KEY",
|
||||
"AZURE_OPENAI_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"NVIDIA_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"MISTRAL_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"CEREBRAS_API_KEY",
|
||||
"CLOUDFLARE_API_KEY",
|
||||
"CLOUDFLARE_ACCOUNT_ID",
|
||||
"CLOUDFLARE_GATEWAY_ID",
|
||||
"XAI_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"AI_GATEWAY_API_KEY",
|
||||
"ZAI_API_KEY",
|
||||
"ZAI_CODING_CN_API_KEY",
|
||||
"OPENCODE_API_KEY",
|
||||
"RADIUS_API_KEY",
|
||||
"FIREWORKS_API_KEY",
|
||||
"TOGETHER_API_KEY",
|
||||
"BASETEN_API_KEY",
|
||||
"KIMI_API_KEY",
|
||||
"MINIMAX_API_KEY",
|
||||
"MINIMAX_CN_API_KEY",
|
||||
"QWEN_TOKEN_PLAN_API_KEY",
|
||||
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
||||
"XIAOMI_API_KEY",
|
||||
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
||||
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
||||
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
||||
}
|
||||
),
|
||||
}
|
||||
CLI_TITLE_LABELS = {
|
||||
"codex": "CODEX",
|
||||
"cursor-agent": "CURSOR",
|
||||
"grok": "GROK",
|
||||
"omp": "OMP",
|
||||
"pi": "PI",
|
||||
}
|
||||
INHERITED_ENVIRONMENT_PREFIXES = (
|
||||
"LC_",
|
||||
@@ -262,12 +300,55 @@ def authoritative_tasks_path(project_root: Path) -> Path:
|
||||
return tasks_path
|
||||
|
||||
|
||||
def optional_agents_path(project_root: Path) -> Path | None:
|
||||
agents_path = project_root / AGENTS_RELATIVE_PATH
|
||||
try:
|
||||
metadata = os.lstat(agents_path)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as exc:
|
||||
raise LaunchError(f"agents.yaml 不可访问: {agents_path}") from exc
|
||||
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
|
||||
raise LaunchError(f"agents.yaml 必须是普通文件且不能是 symlink: {agents_path}")
|
||||
_assert_no_symlink_components(agents_path, "agents.yaml 路径")
|
||||
if metadata.st_size > MAX_RECORD_SIZE:
|
||||
raise LaunchError(f"agents.yaml 超过 {MAX_RECORD_SIZE} 字节上限")
|
||||
return agents_path
|
||||
|
||||
|
||||
def load_effective_orchestration(
|
||||
project_root: Path,
|
||||
board: dict,
|
||||
) -> tuple[dict, str, str]:
|
||||
"""Load the optional project override and return routing, hash, source."""
|
||||
|
||||
agents_path = optional_agents_path(project_root)
|
||||
if agents_path is not None:
|
||||
agents = load_document(agents_path)
|
||||
errors = validate_orchestration(agents, where="agents.yaml")
|
||||
if errors:
|
||||
formatted = "\n".join(f" - {error}" for error in dict.fromkeys(errors))
|
||||
raise LaunchError(f"agents.yaml 未通过 worker 路由校验:\n{formatted}")
|
||||
return agents, canonical_sha256(agents), str(agents_path)
|
||||
|
||||
project = board.get("project")
|
||||
orchestration = project.get("orchestration") if isinstance(project, dict) else None
|
||||
if not isinstance(orchestration, dict):
|
||||
raise LaunchError("任务板缺少 project.orchestration,且没有 agents.yaml")
|
||||
return orchestration, canonical_sha256(orchestration), "tasks.yaml:project.orchestration"
|
||||
|
||||
|
||||
def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
|
||||
project_root = canonical_directory(project_root_value, "项目根目录")
|
||||
tasks_path = authoritative_tasks_path(project_root)
|
||||
board = load_document(tasks_path)
|
||||
errors = validate_builtin(board)
|
||||
errors.extend(validate_routing_document(board))
|
||||
effective, _, _ = load_effective_orchestration(project_root, board)
|
||||
errors = validate_builtin(board, orchestration_override=effective)
|
||||
if optional_agents_path(project_root) is not None:
|
||||
project = board.get("project")
|
||||
board_orchestration = project.get("orchestration") if isinstance(project, dict) else None
|
||||
if board_orchestration is not None:
|
||||
errors.extend(validate_orchestration(board_orchestration))
|
||||
if errors:
|
||||
formatted = "\n".join(f" - {error}" for error in dict.fromkeys(errors))
|
||||
raise LaunchError(f"任务板未通过 worker 路由校验:\n{formatted}")
|
||||
@@ -307,8 +388,25 @@ def _is_trusted_grok_executable(resolved: Path, metadata: os.stat_result) -> boo
|
||||
return metadata.st_uid == os.getuid() and metadata.st_gid == os.getgid()
|
||||
|
||||
|
||||
def _is_trusted_pi_shim(candidate: Path, resolved: Path, metadata: os.stat_result) -> bool:
|
||||
"""Accept the mise-managed ``pi`` shim while retaining target identity checks."""
|
||||
|
||||
home, _ = account_identity()
|
||||
expected_shim = home / ".local" / "share" / "mise" / "shims" / "pi"
|
||||
if candidate != expected_shim or candidate.name != "pi":
|
||||
return False
|
||||
if resolved.name != "mise":
|
||||
return False
|
||||
if metadata.st_uid not in {0, os.getuid()} or stat.S_IMODE(metadata.st_mode) & 0o022:
|
||||
return False
|
||||
try:
|
||||
return candidate.is_symlink() and candidate.resolve(strict=True) == resolved
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def resolve_executable(name: str) -> Path:
|
||||
supported = {"codex", "cursor-agent", "grok", "omp", "git", "orca"}
|
||||
supported = {"codex", "cursor-agent", "grok", "omp", "pi", "git", "orca"}
|
||||
if name not in supported:
|
||||
raise LaunchError(f"不支持的可执行文件: {name}")
|
||||
search_paths = trusted_path_entries()
|
||||
@@ -337,6 +435,10 @@ def resolve_executable(name: str) -> Path:
|
||||
if _is_trusted_grok_executable(resolved, metadata):
|
||||
return resolved
|
||||
continue
|
||||
if name == "pi":
|
||||
if _is_trusted_pi_shim(candidate, resolved, metadata):
|
||||
return candidate
|
||||
continue
|
||||
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
||||
continue
|
||||
if resolved.name != name:
|
||||
@@ -672,14 +774,13 @@ def build_plan(
|
||||
raise LaunchError("slot 必须是 1..99 的整数")
|
||||
|
||||
project_root, board = load_authoritative_board(project_root_value)
|
||||
orchestration, routing_hash, routing_source = load_effective_orchestration(
|
||||
project_root, board
|
||||
)
|
||||
board_hash = canonical_sha256(board)
|
||||
find_task(board, task_id)
|
||||
project = board["project"]
|
||||
orchestration = project.get("orchestration")
|
||||
if not isinstance(orchestration, dict):
|
||||
raise LaunchError("旧任务板缺少 project.orchestration,只能使用手动模式")
|
||||
if orchestration.get("mode") != "orca":
|
||||
raise LaunchError("project.orchestration.mode 不是 orca,拒绝自动创建 worker")
|
||||
raise LaunchError("有效 worker 配置的 mode 不是 orca,拒绝自动创建 worker")
|
||||
profiles = orchestration.get("profiles")
|
||||
profile = profiles.get(profile_id) if isinstance(profiles, dict) else None
|
||||
if not isinstance(profile, dict):
|
||||
@@ -718,6 +819,11 @@ def build_plan(
|
||||
profile,
|
||||
profile_version=orchestration["profileVersion"],
|
||||
)
|
||||
routing_facts = (
|
||||
{}
|
||||
if routing_source == "tasks.yaml:project.orchestration"
|
||||
else {"routingSource": routing_source, "routingHash": routing_hash}
|
||||
)
|
||||
created_for = {
|
||||
"taskId": task_id,
|
||||
"attemptId": attempt_id,
|
||||
@@ -735,6 +841,7 @@ def build_plan(
|
||||
"worktree": worktree,
|
||||
"requested": requested,
|
||||
"slot": slot,
|
||||
**routing_facts,
|
||||
}
|
||||
)
|
||||
try:
|
||||
@@ -762,6 +869,7 @@ def build_plan(
|
||||
"requested": requested,
|
||||
"slot": slot,
|
||||
"title": title,
|
||||
**routing_facts,
|
||||
}
|
||||
|
||||
|
||||
@@ -1136,6 +1244,9 @@ def build_receipt(
|
||||
},
|
||||
"createdAt": created_at,
|
||||
}
|
||||
if "routingSource" in plan and "routingHash" in plan:
|
||||
receipt["routingSource"] = plan["routingSource"]
|
||||
receipt["routingHash"] = plan["routingHash"]
|
||||
receipt["receiptHash"] = canonical_sha256(receipt)
|
||||
return receipt
|
||||
|
||||
@@ -1579,8 +1690,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
def profile_hash_command(project_root_value: str, profile_id: str) -> dict:
|
||||
if not PROFILE_ID_RE.fullmatch(profile_id):
|
||||
raise LaunchError("profile-id 格式非法")
|
||||
_, board = load_authoritative_board(project_root_value)
|
||||
orchestration = board["project"].get("orchestration")
|
||||
project_root, board = load_authoritative_board(project_root_value)
|
||||
orchestration, routing_hash, routing_source = load_effective_orchestration(
|
||||
project_root, board
|
||||
)
|
||||
profiles = orchestration.get("profiles") if isinstance(orchestration, dict) else None
|
||||
profile = profiles.get(profile_id) if isinstance(profiles, dict) else None
|
||||
if not isinstance(profile, dict):
|
||||
@@ -1588,6 +1701,8 @@ def profile_hash_command(project_root_value: str, profile_id: str) -> dict:
|
||||
return {
|
||||
"profileVersion": orchestration["profileVersion"],
|
||||
"profileId": profile_id,
|
||||
"routingSource": routing_source,
|
||||
"routingHash": routing_hash,
|
||||
"profileHash": profile_hash(
|
||||
profile,
|
||||
profile_version=orchestration["profileVersion"],
|
||||
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate an optional ACK .pouch/ack/agents.yaml routing override."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from validate_tasks import load_document, validate_with_schema
|
||||
from worker_profiles import validate_orchestration
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="校验 ACK agents.yaml 结构")
|
||||
parser.add_argument("agents", nargs="?", default="agents.yaml", help="agents.yaml 路径")
|
||||
parser.add_argument("--schema", help="agents.schema.json 路径(默认自动探测)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
agents_path = Path(args.agents)
|
||||
if not agents_path.is_file() or agents_path.is_symlink():
|
||||
sys.stderr.write(f"agents.yaml 必须是普通文件且不能是 symlink: {agents_path}\n")
|
||||
return 2
|
||||
|
||||
data = load_document(agents_path)
|
||||
schema_path = Path(args.schema) if args.schema else (
|
||||
Path(__file__).resolve().parent.parent / "templates" / "agents.schema.json"
|
||||
)
|
||||
errors = validate_orchestration(data, where="agents.yaml")
|
||||
mode = "内置语义规则"
|
||||
try:
|
||||
import jsonschema # type: ignore # noqa: F401
|
||||
|
||||
if schema_path.is_file():
|
||||
errors = validate_with_schema(data, schema_path) + errors
|
||||
mode = f"schema ({schema_path.name}) + 内置语义规则"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if errors:
|
||||
sys.stderr.write(f"agents.yaml 校验失败({mode}),共 {len(errors)} 项:\n")
|
||||
for error in dict.fromkeys(errors):
|
||||
sys.stderr.write(f" - {error}\n")
|
||||
return 1
|
||||
|
||||
sys.stdout.write(f"agents.yaml 校验通过({mode}):{agents_path}\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -32,6 +32,7 @@ from yaml_subset import (
|
||||
load_yaml_subset,
|
||||
make_unique_pyyaml_loader,
|
||||
)
|
||||
from worker_profiles import validate_orchestration
|
||||
from worker_profiles import validate_routing_document
|
||||
|
||||
STATUS_ENUM = {
|
||||
@@ -894,7 +895,11 @@ def validate_with_schema(data: dict, schema_path: Path) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def validate_builtin(data: dict) -> list[str]:
|
||||
def validate_builtin(
|
||||
data: dict,
|
||||
*,
|
||||
orchestration_override: dict | None = None,
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
project_intake_workflow = "read-only-v1"
|
||||
|
||||
@@ -1044,6 +1049,8 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
if "ackVersion" in data and version_match is None:
|
||||
errors.append("ackVersion 必须是合法 SemVer(例如 0.10.0)")
|
||||
routing_required = (
|
||||
orchestration_override is not None
|
||||
or
|
||||
(isinstance(project, dict) and "orchestration" in project)
|
||||
or "workerReceipts" in data
|
||||
or (
|
||||
@@ -1053,7 +1060,12 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
)
|
||||
)
|
||||
if routing_required:
|
||||
errors.extend(validate_routing_document(data))
|
||||
errors.extend(
|
||||
validate_routing_document(
|
||||
data,
|
||||
orchestration_override=orchestration_override,
|
||||
)
|
||||
)
|
||||
|
||||
if "summary" in data:
|
||||
summary = data["summary"]
|
||||
@@ -1415,7 +1427,27 @@ def main(argv: list[str] | None = None) -> int:
|
||||
sys.stderr.write(f"找不到指定的 schema 文件: {schema_path}\n")
|
||||
return 2
|
||||
|
||||
errors = validate_builtin(data)
|
||||
agents_path = tasks_path.with_name("agents.yaml")
|
||||
agents = None
|
||||
if agents_path.is_symlink():
|
||||
errors = ["agents.yaml 不能是 symlink"]
|
||||
agents = None
|
||||
elif agents_path.is_file():
|
||||
agents = load_document(agents_path)
|
||||
if not isinstance(agents, dict):
|
||||
errors = ["agents.yaml 顶层必须是对象(mapping)"]
|
||||
agents = None
|
||||
else:
|
||||
errors = validate_orchestration(agents, where="agents.yaml")
|
||||
else:
|
||||
errors = []
|
||||
|
||||
errors.extend(validate_builtin(data, orchestration_override=agents))
|
||||
if agents is not None:
|
||||
project = data.get("project")
|
||||
board_orchestration = project.get("orchestration") if isinstance(project, dict) else None
|
||||
if board_orchestration is not None:
|
||||
errors.extend(validate_orchestration(board_orchestration))
|
||||
mode = "内置语义规则"
|
||||
try:
|
||||
import jsonschema # type: ignore # noqa: F401
|
||||
|
||||
@@ -13,7 +13,7 @@ import sys
|
||||
|
||||
MIGRATION_MESSAGE = (
|
||||
"ACK v0.10 已停用自由 worker command 校验器;请在 "
|
||||
".pouch/ack/tasks.yaml 的 project.orchestration 中声明 profile,并仅调用 "
|
||||
".pouch/ack/agents.yaml(优先)或 .pouch/ack/tasks.yaml 的 project.orchestration 中声明 profile,并仅调用 "
|
||||
"scripts/launch_worker.py profile-hash|plan|launch。"
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ LAUNCH_PROTOCOL_VERSION = 1
|
||||
MAX_ROUNDS = 3
|
||||
|
||||
ROLES = frozenset({"developer", "test"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp", "pi"})
|
||||
TIERS = frozenset({"standard", "strong"})
|
||||
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
||||
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
||||
@@ -32,6 +32,7 @@ ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
||||
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
||||
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok", "omp"})
|
||||
CLI_REQUIRES_NULL_REASONING_EFFORT = frozenset({"cursor-agent"})
|
||||
CLI_ALLOWS_DEFAULT_MODEL = frozenset({"pi"})
|
||||
|
||||
|
||||
def _cli_choice_text() -> str:
|
||||
@@ -95,7 +96,9 @@ RECEIPT_FIELDS = frozenset(
|
||||
"receiptHash",
|
||||
}
|
||||
)
|
||||
RECEIPT_CONTEXT_FIELDS = frozenset({"projectRoot", "boardHash"})
|
||||
RECEIPT_CONTEXT_FIELDS = frozenset(
|
||||
{"projectRoot", "boardHash", "routingSource", "routingHash"}
|
||||
)
|
||||
CREATED_FOR_FIELDS = frozenset({"taskId", "attemptId", "role"})
|
||||
WORKTREE_FIELDS = frozenset(
|
||||
{
|
||||
@@ -247,8 +250,10 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
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:
|
||||
errors.append(f"{where}.model: must be a safe model ID")
|
||||
if not (isinstance(cli, str) and cli in CLI_ALLOWS_DEFAULT_MODEL and model is None) and (
|
||||
not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None
|
||||
):
|
||||
errors.append(f"{where}.model: must be a safe model ID or null for Pi default")
|
||||
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
||||
errors.append(
|
||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
||||
@@ -265,6 +270,10 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
elif isinstance(cli, str) and cli == "pi" and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
||||
|
||||
if role == "test" and tier != "standard":
|
||||
errors.append(f"{where}.tier: Test may only use standard")
|
||||
@@ -312,15 +321,22 @@ def _validate_model_allowlist(value: Any, where: str) -> list[str]:
|
||||
if not isinstance(models, list) or not models:
|
||||
errors.append(f"{tier_where}: must be a non-empty model list")
|
||||
continue
|
||||
seen: set[str] = set()
|
||||
seen: set[object] = set()
|
||||
for index, model in enumerate(models):
|
||||
item_where = f"{tier_where}[{index}]"
|
||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
errors.append(f"{item_where}: must be a safe model ID")
|
||||
elif model in seen:
|
||||
if model is None and cli in CLI_ALLOWS_DEFAULT_MODEL:
|
||||
model_key: object = ("default",)
|
||||
elif not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
errors.append(
|
||||
f"{item_where}: must be a safe model ID or null for Pi default"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
model_key = model
|
||||
if model_key in seen:
|
||||
errors.append(f"{item_where}: duplicate model {model!r}")
|
||||
else:
|
||||
seen.add(model)
|
||||
seen.add(model_key)
|
||||
return errors
|
||||
|
||||
|
||||
@@ -329,7 +345,7 @@ def _allowed_models(
|
||||
cli: Any,
|
||||
role: Any,
|
||||
tier: Any,
|
||||
) -> list[str] | None:
|
||||
) -> list[Any] | None:
|
||||
if not isinstance(allowlist, dict):
|
||||
return None
|
||||
roles = allowlist.get(cli)
|
||||
@@ -519,6 +535,19 @@ def render_worker_argv(
|
||||
"--no-session",
|
||||
]
|
||||
|
||||
if cli == "pi":
|
||||
# Pi's omitted --model selects its configured startup default. Its
|
||||
# cwd is supplied by the trusted bootstrap process, because Pi has no
|
||||
# --cwd flag. Read-only profiles are restricted to observation tools;
|
||||
# workspace-write profiles use Pi's normal editing tool set.
|
||||
argv = [executable]
|
||||
if model is not None:
|
||||
argv.extend(["--model", model])
|
||||
argv.extend(["--thinking", profile["reasoningEffort"]])
|
||||
if permission == "read-only":
|
||||
argv.extend(["--tools", "read,grep,find,ls"])
|
||||
return argv
|
||||
|
||||
if cli != "grok":
|
||||
raise ValueError(f"unsupported cli: {cli}")
|
||||
grok_permission = "plan" if permission == "read-only" else "acceptEdits"
|
||||
@@ -597,8 +626,10 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
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")
|
||||
if not (isinstance(cli, str) and cli in CLI_ALLOWS_DEFAULT_MODEL and model is None) and (
|
||||
not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None
|
||||
):
|
||||
errors.append(f"{where}.model: must be a safe model ID or null for Pi default")
|
||||
effort = value.get("reasoningEffort")
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
@@ -611,6 +642,10 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
if isinstance(cli, str) and cli == "pi" and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
||||
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")
|
||||
@@ -697,6 +732,16 @@ def validate_worker_receipt(
|
||||
not isinstance(board_hash, str) or SHA256_RE.fullmatch(board_hash) is None
|
||||
):
|
||||
errors.append(f"{where}.boardHash: must be a canonical sha256 hex digest")
|
||||
routing_source = receipt.get("routingSource")
|
||||
routing_hash = receipt.get("routingHash")
|
||||
if (routing_source is None) != (routing_hash is None):
|
||||
errors.append(f"{where}: routingSource and routingHash must be present together")
|
||||
if routing_source is not None and not _is_nonempty_text(routing_source, max_length=512):
|
||||
errors.append(f"{where}.routingSource: must be non-empty single-line text")
|
||||
if routing_hash is not None and (
|
||||
not isinstance(routing_hash, str) or SHA256_RE.fullmatch(routing_hash) is None
|
||||
):
|
||||
errors.append(f"{where}.routingHash: must be a canonical sha256 hex digest")
|
||||
|
||||
version = receipt.get("receiptVersion")
|
||||
if version != RECEIPT_VERSION or isinstance(version, bool):
|
||||
@@ -818,6 +863,9 @@ def validate_worker_receipt(
|
||||
if project_root is not None and board_hash is not None:
|
||||
facts["projectRoot"] = project_root
|
||||
facts["boardHash"] = board_hash
|
||||
if routing_source is not None and routing_hash is not None:
|
||||
facts["routingSource"] = routing_source
|
||||
facts["routingHash"] = routing_hash
|
||||
expected_fingerprint = canonical_sha256(facts)
|
||||
except ValueError:
|
||||
errors.append(f"{where}.launchFingerprint: cannot hash launch facts")
|
||||
@@ -1006,17 +1054,30 @@ def _validate_dispatch_links(
|
||||
return errors
|
||||
|
||||
|
||||
def validate_routing_document(data: Any) -> list[str]:
|
||||
"""Validate routing and receipts embedded in an ACK tasks document."""
|
||||
def validate_routing_document(
|
||||
data: Any,
|
||||
*,
|
||||
orchestration_override: dict[str, Any] | None = None,
|
||||
) -> list[str]:
|
||||
"""Validate routing and receipts in an ACK tasks document.
|
||||
|
||||
``orchestration_override`` is the optional project-local agents.yaml
|
||||
routing document. The task board remains the state source, while the
|
||||
override becomes the effective profile source for receipt checks.
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return ["<root>: must be an object"]
|
||||
project = data.get("project")
|
||||
if not isinstance(project, dict):
|
||||
return ["project: must be an object"]
|
||||
if "orchestration" not in project:
|
||||
if "orchestration" not in project and orchestration_override is None:
|
||||
return ["project.orchestration: is required"]
|
||||
orchestration = project.get("orchestration")
|
||||
orchestration = (
|
||||
orchestration_override
|
||||
if orchestration_override is not None
|
||||
else project.get("orchestration")
|
||||
)
|
||||
errors = validate_orchestration(orchestration)
|
||||
|
||||
task_ids: set[str] = set()
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://git.yumee.top/laily/pouch/skills/ack/templates/agents.schema.json",
|
||||
"title": "ACK project agent routing override",
|
||||
"description": "Optional .pouch/ack/agents.yaml. It has the same routing shape as project.orchestration.",
|
||||
"type": "object",
|
||||
"required": ["profileVersion", "mode", "modelAllowlist", "profiles", "defaults"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"profileVersion": {"type": "integer", "const": 1},
|
||||
"mode": {"type": "string", "enum": ["orca", "manual"]},
|
||||
"modelAllowlist": {"$ref": "#/definitions/modelAllowlist"},
|
||||
"profiles": {
|
||||
"type": "object",
|
||||
"propertyNames": {"$ref": "#/definitions/profileId"},
|
||||
"additionalProperties": {"$ref": "#/definitions/workerProfile"}
|
||||
},
|
||||
"defaults": {"$ref": "#/definitions/defaults"}
|
||||
},
|
||||
"definitions": {
|
||||
"profileId": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"},
|
||||
"model": {
|
||||
"oneOf": [
|
||||
{"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"},
|
||||
{"type": "null", "description": "仅 pi 使用 null 表示默认模型"}
|
||||
]
|
||||
},
|
||||
"modelList": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/definitions/model"}},
|
||||
"modelTierAllowlist": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"standard": {"$ref": "#/definitions/modelList"},
|
||||
"strong": {"$ref": "#/definitions/modelList"}
|
||||
}
|
||||
},
|
||||
"modelRoleAllowlist": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"developer": {"$ref": "#/definitions/modelTierAllowlist"},
|
||||
"test": {"$ref": "#/definitions/modelTierAllowlist"}
|
||||
}
|
||||
},
|
||||
"modelAllowlist": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"codex": {"$ref": "#/definitions/modelRoleAllowlist"},
|
||||
"cursor-agent": {"$ref": "#/definitions/modelRoleAllowlist"},
|
||||
"grok": {"$ref": "#/definitions/modelRoleAllowlist"},
|
||||
"omp": {"$ref": "#/definitions/modelRoleAllowlist"},
|
||||
"pi": {"$ref": "#/definitions/modelRoleAllowlist"}
|
||||
}
|
||||
},
|
||||
"workerProfile": {
|
||||
"type": "object",
|
||||
"required": ["role", "cli", "tier", "model", "reasoningEffort", "permissionMode"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"role": {"type": "string", "enum": ["developer", "test"]},
|
||||
"cli": {"type": "string", "enum": ["codex", "cursor-agent", "grok", "omp", "pi"]},
|
||||
"tier": {"type": "string", "enum": ["standard", "strong"]},
|
||||
"model": {"$ref": "#/definitions/model"},
|
||||
"reasoningEffort": {"type": ["string", "null"], "enum": ["low", "medium", "high", "xhigh", null]},
|
||||
"permissionMode": {"type": "string", "enum": ["read-only", "workspace-write"]}
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["developer", "test"],
|
||||
"properties": {
|
||||
"developer": {"$ref": "#/definitions/profileId"},
|
||||
"test": {"$ref": "#/definitions/profileId"},
|
||||
"developerUpgraded": {"$ref": "#/definitions/profileId"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Optional project-local worker routing override.
|
||||
# Save as .pouch/ack/agents.yaml when a temporary role/CLI/model selection is needed.
|
||||
# This file intentionally has the same shape as tasks.yaml.project.orchestration.
|
||||
profileVersion: 1
|
||||
mode: "orca"
|
||||
modelAllowlist:
|
||||
pi:
|
||||
developer:
|
||||
standard: [null]
|
||||
strong: [null]
|
||||
cursor-agent:
|
||||
test:
|
||||
standard: ["auto"]
|
||||
profiles:
|
||||
pi-dev-standard:
|
||||
role: "developer"
|
||||
cli: "pi"
|
||||
tier: "standard"
|
||||
model: null
|
||||
reasoningEffort: "medium"
|
||||
permissionMode: "workspace-write"
|
||||
pi-dev-strong:
|
||||
role: "developer"
|
||||
cli: "pi"
|
||||
tier: "strong"
|
||||
model: null
|
||||
reasoningEffort: "high"
|
||||
permissionMode: "workspace-write"
|
||||
cursor-test-standard:
|
||||
role: "test"
|
||||
cli: "cursor-agent"
|
||||
tier: "standard"
|
||||
model: "auto"
|
||||
reasoningEffort: null
|
||||
permissionMode: "workspace-write"
|
||||
defaults:
|
||||
developer: "pi-dev-standard"
|
||||
test: "cursor-test-standard"
|
||||
developerUpgraded: "pi-dev-strong"
|
||||
@@ -9,7 +9,7 @@
|
||||
> 不会自动修改 `AGENTS.md`、`CLAUDE.md` 或其它 Agent 指令文件。
|
||||
> 无论叫什么,都在 `tasks.yaml` 的 `project.overlayFile` 记录实际路径。
|
||||
> `.pouch/ack/` 只保存本项目的 `project.md`、`tasks.yaml`、`knowledge.yaml`、默认关闭的
|
||||
> `delivery.yaml` 与空的 `regression.yaml`,
|
||||
> `delivery.yaml`、空的 `regression.yaml`,以及可选的 `agents.yaml` 角色启动覆盖,
|
||||
> 不复制或链接 Skill。
|
||||
|
||||
## 项目概览
|
||||
@@ -22,6 +22,7 @@
|
||||
- 项目知识:`.pouch/ack/knowledge.yaml`
|
||||
- 交付契约:`.pouch/ack/delivery.yaml`(默认关闭)
|
||||
- 回归目录:`.pouch/ack/regression.yaml`
|
||||
- 角色启动覆盖(可选):`.pouch/ack/agents.yaml`
|
||||
- 覆盖层文件:`<overlay_file_path>`(默认 `.pouch/ack/project.md`)
|
||||
|
||||
## 通用规范(由 ACK Skill 按需读取)
|
||||
@@ -39,8 +40,8 @@
|
||||
## Worker 路由
|
||||
|
||||
机器可校验的模型、reasoning effort、权限模式、默认 profile、允许 worktree 和启动
|
||||
receipt 全部以 `.pouch/ack/tasks.yaml` 的 `project.orchestration` 与顶层
|
||||
`workerReceipts` 为准。本文件不保存可执行 worker 命令。
|
||||
receipt 以 `.pouch/ack/agents.yaml`(存在时)或 `.pouch/ack/tasks.yaml` 的
|
||||
`project.orchestration` 与顶层 `workerReceipts` 为准。本文件不保存可执行 worker 命令。
|
||||
|
||||
默认 profile:
|
||||
|
||||
|
||||
@@ -591,8 +591,16 @@
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "仅 Pi 使用 null 表示其配置的默认模型"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modelTierAllowlist": {
|
||||
@@ -633,6 +641,12 @@
|
||||
},
|
||||
"grok": {
|
||||
"$ref": "#/definitions/modelRoleAllowlist"
|
||||
},
|
||||
"omp": {
|
||||
"$ref": "#/definitions/modelRoleAllowlist"
|
||||
},
|
||||
"pi": {
|
||||
"$ref": "#/definitions/modelRoleAllowlist"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -661,7 +675,8 @@
|
||||
"codex",
|
||||
"cursor-agent",
|
||||
"grok",
|
||||
"omp"
|
||||
"omp",
|
||||
"pi"
|
||||
]
|
||||
},
|
||||
"tier": {
|
||||
@@ -672,8 +687,16 @@
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "仅 Pi 使用 null 表示其配置的默认模型"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"type": [
|
||||
@@ -860,7 +883,8 @@
|
||||
"codex",
|
||||
"cursor-agent",
|
||||
"grok",
|
||||
"omp"
|
||||
"omp",
|
||||
"pi"
|
||||
]
|
||||
},
|
||||
"tier": {
|
||||
@@ -871,8 +895,16 @@
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$"
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "仅 Pi 使用 null 表示其配置的默认模型"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"type": [
|
||||
@@ -1018,6 +1050,13 @@
|
||||
"boardHash": {
|
||||
"$ref": "#/definitions/sha256"
|
||||
},
|
||||
"routingSource": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"routingHash": {
|
||||
"$ref": "#/definitions/sha256"
|
||||
},
|
||||
"slot": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
|
||||
@@ -29,6 +29,7 @@ project:
|
||||
# attachments: "<field_id>"
|
||||
# updatedAt: "<field_id>"
|
||||
orchestration:
|
||||
# 可选:临时角色路由写到 .pouch/ack/agents.yaml;存在时由 launcher 优先读取。
|
||||
# worker 默认在 --project-root(权威状态目录)工作;不再配置 allowedWorktrees 白名单
|
||||
# (v0.19 起废弃)。需要隔离 worktree 时由 Coordinator 在派发时显式指定。
|
||||
profileVersion: 1
|
||||
|
||||
@@ -432,6 +432,80 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
|
||||
|
||||
class PlanTests(unittest.TestCase):
|
||||
def test_agents_yaml_overrides_board_routing_and_binds_hash(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
project = Path(temporary).resolve()
|
||||
ack_dir = project / ".pouch" / "ack"
|
||||
ack_dir.mkdir(parents=True)
|
||||
task_board = board(project)
|
||||
agent_routing = {
|
||||
"profileVersion": 1,
|
||||
"mode": "orca",
|
||||
"modelAllowlist": {
|
||||
"pi": {"developer": {"standard": [None]}},
|
||||
"cursor-agent": {"test": {"standard": ["auto"]}},
|
||||
},
|
||||
"profiles": {
|
||||
"pi-dev": {
|
||||
"role": "developer",
|
||||
"cli": "pi",
|
||||
"tier": "standard",
|
||||
"model": None,
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"cursor-test": {
|
||||
"role": "test",
|
||||
"cli": "cursor-agent",
|
||||
"tier": "standard",
|
||||
"model": "auto",
|
||||
"reasoningEffort": None,
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
},
|
||||
"defaults": {"developer": "pi-dev", "test": "cursor-test"},
|
||||
}
|
||||
agents_path = ack_dir / "agents.yaml"
|
||||
agents_path.write_text(json.dumps(agent_routing), encoding="utf-8")
|
||||
(ack_dir / "tasks.yaml").write_text(
|
||||
json.dumps(task_board), encoding="utf-8"
|
||||
)
|
||||
executable = project / "pi"
|
||||
executable.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
executable.chmod(0o700)
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
launch_worker,
|
||||
"capture_worktree_identity",
|
||||
return_value=worktree_identity(project),
|
||||
),
|
||||
mock.patch.object(
|
||||
launch_worker,
|
||||
"resolve_executable",
|
||||
return_value=executable,
|
||||
),
|
||||
mock.patch.object(
|
||||
launch_worker,
|
||||
"run_text",
|
||||
return_value="pi 0.85.1",
|
||||
),
|
||||
):
|
||||
plan = launch_worker.build_plan(
|
||||
project_root_value=str(project),
|
||||
task_id="TASK-001",
|
||||
attempt_id="TASK-001-A1",
|
||||
role="developer",
|
||||
profile_id="pi-dev",
|
||||
worktree_value=str(project),
|
||||
slot=1,
|
||||
)
|
||||
|
||||
self.assertEqual(plan["routingSource"], str(agents_path))
|
||||
self.assertEqual(plan["routingHash"], worker_profiles.canonical_sha256(agent_routing))
|
||||
self.assertEqual(plan["requested"]["model"], None)
|
||||
self.assertEqual(plan["requested"]["argv"], [str(executable), "--thinking", "medium"])
|
||||
|
||||
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
project = Path(temporary).resolve()
|
||||
|
||||
@@ -28,6 +28,9 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
"select_knowledge.py",
|
||||
"select_regression.py",
|
||||
"references/kickoff.md",
|
||||
"agents.yaml",
|
||||
"validate_agents.py",
|
||||
"model: null",
|
||||
"不要修改项目的 `AGENTS.md`",
|
||||
"当前会话担任 Coordinator",
|
||||
"intents.testEnvironment",
|
||||
@@ -137,6 +140,10 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
"scripts/run_verification.py",
|
||||
"scripts/worker_profiles.py",
|
||||
"scripts/launch_worker.py",
|
||||
"scripts/validate_agents.py",
|
||||
"templates/agents.schema.json",
|
||||
"templates/agents.template.yaml",
|
||||
"examples/agents.example.yaml",
|
||||
"templates/delivery.template.yaml",
|
||||
"templates/delivery.schema.json",
|
||||
"examples/delivery.example.yaml",
|
||||
@@ -144,7 +151,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
):
|
||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||
self.assertEqual(version, "0.19.0")
|
||||
self.assertEqual(version, "0.20.0")
|
||||
self.assertIn(
|
||||
f'ackVersion: "{version}"',
|
||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||
|
||||
@@ -224,6 +224,39 @@ class CanonicalHashTests(unittest.TestCase):
|
||||
|
||||
|
||||
class ProfileValidationTests(unittest.TestCase):
|
||||
def test_pi_default_model_is_explicitly_supported(self) -> None:
|
||||
routing = {
|
||||
"profileVersion": 1,
|
||||
"mode": "orca",
|
||||
"modelAllowlist": {
|
||||
"pi": {
|
||||
"developer": {"standard": [None], "strong": [None]},
|
||||
},
|
||||
"cursor-agent": {"test": {"standard": ["auto"]}},
|
||||
},
|
||||
"profiles": {
|
||||
"pi-dev": {
|
||||
"role": "developer",
|
||||
"cli": "pi",
|
||||
"tier": "standard",
|
||||
"model": None,
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"cursor-test": {
|
||||
"role": "test",
|
||||
"cli": "cursor-agent",
|
||||
"tier": "standard",
|
||||
"model": "auto",
|
||||
"reasoningEffort": None,
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
},
|
||||
"defaults": {"developer": "pi-dev", "test": "cursor-test"},
|
||||
}
|
||||
|
||||
self.assertEqual(worker_profiles.validate_orchestration(routing), [])
|
||||
|
||||
def test_valid_orchestration_passes(self) -> None:
|
||||
self.assertEqual(
|
||||
worker_profiles.validate_orchestration(valid_orchestration()),
|
||||
@@ -406,6 +439,44 @@ class ProfileValidationTests(unittest.TestCase):
|
||||
|
||||
|
||||
class ArgvRendererTests(unittest.TestCase):
|
||||
def test_pi_default_model_omits_model_flag(self) -> None:
|
||||
profile = {
|
||||
"role": "developer",
|
||||
"cli": "pi",
|
||||
"tier": "standard",
|
||||
"model": None,
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
worker_profiles.render_worker_argv(
|
||||
profile,
|
||||
"/home/ace/.local/share/mise/shims/pi",
|
||||
"/repo/demo",
|
||||
),
|
||||
[
|
||||
"/home/ace/.local/share/mise/shims/pi",
|
||||
"--thinking",
|
||||
"medium",
|
||||
],
|
||||
)
|
||||
|
||||
def test_pi_read_only_limits_tools(self) -> None:
|
||||
profile = {
|
||||
"role": "test",
|
||||
"cli": "pi",
|
||||
"tier": "standard",
|
||||
"model": None,
|
||||
"reasoningEffort": "low",
|
||||
"permissionMode": "read-only",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
worker_profiles.render_worker_argv(profile, "/usr/local/bin/pi", "/repo/demo"),
|
||||
["/usr/local/bin/pi", "--thinking", "low", "--tools", "read,grep,find,ls"],
|
||||
)
|
||||
|
||||
def test_codex_exact_safe_argv(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user