feat: unify coordinator semantics (Muse role alignment phase 1)

Platform coordinator.py is the single source of truth; the skill
coordinator template (templates/roles/00-coordinator.md + SKILL.md
appendix 00 + body) now derives from it instead of describing a
second, model-played coordinator (U1-U13).

- server: _create_task accepts parent_task_id (404/400 checks,
  coordinator-under-coordinator rejected); follow-ups to a running
  coordinator return 409 at entry (_create_followup /
  _deliver_to_thread) with documented alternatives (U14/U15).
- tests: 8 new coordinator tests (depth limit, lineage, replay,
  follow-up 409); appendix sync/provenance/transplant suites green.
- check_appendix.py: 12 chapters match templates/roles/.
This commit is contained in:
host committed 2026-11-07 19:43:03 +00:00
1 parent 834eced686
commit f6df76ee8f
5 files changed
+308 -28

No files matched your search

+41 -1
View File
@@ -3812,6 +3812,31 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
"error": "协调员任务不跑模型回合,不能与"
" goal/plan/验收命令同用"})
return
# 父任务血缘(Muse 角色对齐 P1):创建请求可携带
# parent_task_id;父任务是协调员且本次新建也是协调员
# 时拒绝(深度恒 ≤1)。父不存在 404、跨项目 400。
# 不带 coordinator_parts 时 parent_task_id 仅作血缘记录
# (第二阶段旁聊的父子关系用同一入参)。
parent_task_id = body.get("parent_task_id")
if parent_task_id is not None:
if not isinstance(parent_task_id, str):
self._send_json(400, {
"error": "parent_task_id must be a string"})
return
parent_row = self._fetch_task(parent_task_id)
if parent_row is None:
self._send_json(404, {"error": "parent task not found"})
return
parent_task = _task_to_dict(parent_row)
if parent_task["project_id"] != project_id:
self._send_json(400, {
"error": "parent task belongs to another project"})
return
if coord_parts and parent_task.get("coordinator_parts"):
self._send_json(400, {
"error": "coordinator task cannot be created"
" under a coordinator task"})
return
idempotency_key = body.get("idempotency_key")
if idempotency_key is not None and not isinstance(idempotency_key, str):
self._send_json(400, {"error": "idempotency_key must be a string"})
@@ -4031,7 +4056,7 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
model, effort, approval, plan, goal_mode,
environment_id,
json.dumps(coord_parts) if coord_parts else None,
None, None, child_model, child_effort,
parent_task_id, None, child_model, child_effort,
agent_name_val, agent_version_val,
agent_instructions_val, agent_tools_val,
agent_tool_approvals_val, audit_loop_val),
@@ -4440,6 +4465,15 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
return
if self._reject_if_over_quota_for_task(user_id, task):
return
# 协调员任务不接受后续消息(Muse 角色对齐 P1):写入侧
# 会 201 成功、消费侧没有任何读点,宁可入口 409 明拒。
# 改分工请取消后重建协调员;给分工追加指令请直接发给
# 该子任务。
if task.get("coordinator_parts"):
self._send_json(409, {
"error": "coordinator tasks do not accept"
" follow-up messages"})
return
text, mode, body_err = _validate_followup_body(body)
if body_err is not None:
self._send_json(body_err[0], body_err[1])
@@ -5922,6 +5956,12 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
(http_code, payload),不直接 _send_json;配额检查
由调用方在调本方法前做。"""
task_id = task["id"]
# 协调员任务不接受后续消息(Muse 角色对齐 P1):与
# _create_followup 同款入口 409,见该处注释。
if task.get("coordinator_parts"):
return 409, {
"error": "coordinator tasks do not accept"
" follow-up messages"}
goal_target = _parse_goal_command(text)
# /goal 前置判定见 _classify_goal_followup(纯计算,与
# _create_followup 共用,但只用于 goal_target 非空时:
+89 -17
View File
@@ -367,16 +367,22 @@ G0 的六问缺一问,G0 就不算过。用户没回复时,协调员等,
角色模板的权威来源是本文件末尾的附录。附录随 SKILL.md 一起播种,
所以租户任务内一定能看到。`templates/roles/` 下的文件是编辑用的源文件,
发布前必须核对附录和源文件一致,不一致时以附录为准并先修源文件。
协调员派生子任务时,必须把对应角色模板全文贴进子任务提示词,
并补上本任务的输入、目录、分支和完成定义。
协调员派生子任务时,主聊人工派生须把对应角色模板全文贴进子任务提示词,
并补上本任务的输入、目录、分支和完成定义;平台协调员任务的分工 prompt
即子任务 prompt 全文,角色模板不由平台注入,如需模板内容,创建人须自己
写进分工 prompt。
只写一句“按 software-engineering skill 做”是不够的,
子任务不一定会读到本技能。
术语:本技能说的“线程”就是平台的“任务(task)”;“协调员任务”就是
`coordinator_parts` 非空的任务;“分工”就是 `coordinator_parts` 的一项;
“子任务”就是 `parent_task_id` 指向协调员的任务;`forked_from` 链与
`parent_task_id` 是两条血缘,不要混用。
## 协调员派生子任务的必填字段
每个子任务提示词必须包含,缺一项就不派生:
1. 角色模板全文(来自本文件附录,租户任务内可直接引用)。
1. 角色模板内容(主聊人工派生时贴模板全文,来自本文件附录,租户任务内可直接引用;平台协调员任务的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,如需模板内容,创建人须自己写进分工 prompt)。
2. 本阶段输入:PRD 哪个版本、契约哪个版本、设计稿哪个版本。
3. 拥有目录:只能写这一个目录;只读目录:契约、他人目录。
4. 分支名:`feat/swe/<阶段>-<角色>-<简称>`,例如 `feat/swe/client-web`、
@@ -400,6 +406,8 @@ G0 的六问缺一问,G0 就不算过。用户没回复时,协调员等,
| 完成定义 | 产出文件清单 + 必须跑过的自测命令 + 必须提交的证据 |
| TODO-UPDATE 要求 | 结尾报告必须带 TODO-UPDATE 小节,格式见“待办机制” |
校验表里的拥有目录 / 分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,平台只校验分工字段(`coordinator.validate_parts`),不校验、不解析这些项;“缺一项就不派生”在平台侧不成立——平台只校验 `validate_parts` 的字段。
子任务精简包(裁剪规则写死,不是“尽量精简”):只给本端相关的
契约章节、设计页、PRD 故事和冻结决策摘要,绝不把 PRD、契约、
设计全文复制给每个线程。精简包由四部分组成,缺一部分就补齐再发:
@@ -442,13 +450,19 @@ TODO-UPDATE
- 冻结前:架构线程可以自由改契约,改完要通知协调员更新版本号。
- 冻结物(G2 必须齐):API 契约、数据模型、agent 协议、
任务状态机、错误 taxonomy、技术栈表、每端能力矩阵。
(7 项齐全由协调员人工核对;平台只校验契约版本号与冻结标志,
不校验冻结物清单、不发回归任务。)
任务状态机至少包含:创建、排队、运行中、等审批、已完成、
已取消、失败、超时;错误 taxonomy 至少区分:模型失败、工具失败、
用户取消、超时、权限拒绝、配额超限。同一错误在各端不得换一个名字。
- 冻结后:任何线程想改契约,必须给协调员发任务间消息,写清:
改什么、为什么、影响哪些客户端、是否向后兼容。
(收件方是扮演协调员的模型/人工;平台协调员任务本身暂不支持
后续指令,见“任务间问答默认策略与决策记录”一节。)
- 协调员批准后:契约版本号加一(v1 -> v2),架构线程改契约,
受影响的客户端各收到一条变更通知和一个回归任务。
(版本号加一与回归任务派发是人工/协调员操作,平台不执行;
平台只校验版本号与冻结标志。)
- 协调员不批准:提议线程按旧契约做,差异记入已知问题,不悄悄分叉。
## 任务间问答默认策略与决策记录(v0.3 新增)
@@ -458,6 +472,7 @@ TODO-UPDATE
协调员在下一轮答复:确认假设、纠正假设(发回归任务)、或升级为契约变更;
答复本身也要记入决策记录。等答复期间线程不阻塞在原地,先做契约内
无争议的部分。
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息会被入口直接拒绝(409),因为平台循环内没有消费点。替代方案见附录 00“任务间问答与决策记录”一节。
- 决策记录落盘路径:`docs/decisions.md`,由协调员维护,子线程只在报告里
提议、不直接改。只追加不改写历史:一条决策写清日期、议题、决定、理由、
影响端;推翻旧决策时追加新条写明“取代 D-n”,绝不改旧条原文、不删旧条。
@@ -531,6 +546,8 @@ tests/ # QA 线程专属,含跨端一致性用例
(只改拥有目录,契约变更经批准除外;检查命令与输出文件清单必须
记入合并记录,见“目录与分支规则”);③ 契约版本是冻结版;
④ 协调员评审通过;⑤ 无未解决评审意见。有一项不过就不合并。
协调员批准与契约冻结版本是每次调合并门禁时提交的证据,平台不持久化;
谎报由审计与 PR/Checks 只读核对事后发现。
- CI 怎么查:用只读方式查,例如本平台的 GET /tasks/{id}/pr/checks。
它是只读诊断,绝不重跑检查、不批准、不合并;还没建 PR 时它会 409
(查无可查),这时先补 PR,不要把它当成 CI 通过。
@@ -538,9 +555,8 @@ tests/ # QA 线程专属,含跨端一致性用例
要求 PR、要求 CI 通过、禁止直接推。目标仓库没开保护时,
协调员要在决策记录里写明风险并去开通,不能假装已保护,
不能靠“大家自觉”代替保护。
- 合并门禁(2026-10-30 补,代码在 cloud/merge_gate.py,服务端
POST /tasks/{id}/merge 已接线;2026-10-31 硬门禁轨加
cloud/merge_gate_hard.py):保护状态只能由调用方或环境变量
- 合并门禁(服务端 POST /tasks/{id}/merge 已接线,门禁实现在
cloud/merge_gate_hard.py,默认 hard):保护状态只能由调用方或环境变量
CLOUD_BRANCH_PROTECTION 显式声明,代码不去 GitHub 核实、不声称
已保护。模式只由环境变量 CLOUD_MERGE_GATE_MODE 控制,默认
hard:上面五项全查,缺一 409 且任务状态不变,并写
@@ -624,8 +640,10 @@ tests/ # QA 线程专属,含跨端一致性用例
- G5 缺陷回路:缺陷回对应客户端或架构线程修,修完回 G5 回归,
回归范围按契约变更影响面定,不是每次都全量重测。
- 成本控制:分阶段本身就是成本控制。G4a 先验证,G4b / G4c 复用已验证物;
协调员、产品、架构用较强模型,重复性强的客户端实现和 QA 可用
较经济的模型,具体以项目实际可用模型和配额为准;
模型与强度按平台字段走:分工不写 model/effort 时,继承协调员任务的
child_model/child_effort 或自身模型;“协调员、产品、架构用较强模型,
重复性强的客户端实现和 QA 可用较经济的模型”只是建议,具体以项目
实际可用模型和配额为准;
配额将满时先停新线程,不先透支再解释。
@@ -750,6 +768,8 @@ tests/ # QA 线程专属,含跨端一致性用例
出一份单独签字的审计结论。谁写、谁测、谁审、谁执行、谁签字
五职分开:写是开发线程,测是 QA 线程,审是审计线程,
执行合并/发布是人或宿主操作,签字是审计签字人。
- 平台的 `audit_loop` 是协调员任务的自动重排循环,不是独立审计线程;
独立审计仍须另开线程,两者阻塞关系均为“与”,不可互换。
- 同一人不兼两职,可核查:审计记录的签字人字段必须与任务执行人
字段、协调员批准人字段、QA 测试人字段都不同,有一项相同,
审计结论本身无效,按审计 fail 处理。代码里这就是
@@ -1263,6 +1283,31 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
建仓分支要诚实(组织仓可经 App token 建,用户个人仓需安装会话里先建空仓,
记为 needs_user_once,不说成已自动建仓)。
## 平台协调员(以平台代码为准,skill 不得另说一套)
- 平台协调员任务(`coordinator_parts` 非空的任务)不跑模型回合:
worker 按分工自动扇出并行子任务、等齐、合流成一份汇总报告写回
父任务 `result_text`,汇总报告就是协调员任务的终局产物;合并决定
仍由你(模型/人工)在评审环节给,但评审发生在子任务终态之后,
报告之前没有“逐个放行”。
- 分工一次建齐、并行认领;分工最多 20 个;子任务 prompt 即分工 prompt
原文,平台不贴角色模板、不贴校验表、不贴精简包。
- 有界委派(建任务时声明,事后不可改):单分工超时 `timeout_seconds`
(1..86400 秒,超时只取消该分工、其余继续等)、失败容忍
`on_failure`(`fail` 默认 / `tolerate`:容忍分工失败不拖垮整体,
重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排)、单项截断
`result_chars`(默认 500、上限 2000)。全局超时 3600 秒:到时取消
挂起子任务、父任务失败。平台不提供分工 token/费用预算、分工轮数
上限、按分工隔离写范围——不要许诺这三类边界。
- 配额预留:分工数超过 1 时,剩余配额须覆盖分工数 × $1(可经环境变量
调),不足建任务即 402;子任务费用记父任务创建人账。
- 取消与孤儿收口由平台自动做:取消/超时终局取消未终态子任务并标
`interrupted`,常驻循环每轮扫尾父已终态、子仍未终态的孤儿;你不需、
也不能手动“取消子任务”。
- 模型与强度:分工不写 `model`/`effort` 时,继承协调员任务的
`child_model`/`child_effort` 或自身模型;“协调员、产品、架构用
较强模型”只是建议,不是平台字段。
## 你必须先做的事
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
@@ -1281,12 +1326,17 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
完成定义。之后所有状态以这份待办为准,不以对话为准。
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
禁止事项、TODO-UPDATE 要求。缺一项不派生。
3. 每个子任务提示词必须包含:本阶段输入的版本号、拥有目录、
只读目录、分支名、完成定义、禁止事项、TODO-UPDATE 要求,
以及角色模板内容——主聊人工派生时贴模板全文;平台协调员任务
的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,
如需模板内容,创建人须自己写进分工 prompt。缺一项不派生。
派生前校验表(必填表,填好的表原样贴进提示词,缺一项就不发任务):
客户端名、拥有目录、分支名、契约版本、PRD 版本、设计版本、
完成定义、TODO-UPDATE 要求,共 8 项。
完成定义、TODO-UPDATE 要求,共 8 项。校验表里的拥有目录 /
分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,
平台只校验分工字段(`coordinator.validate_parts`),不校验、
不解析这些项。
子任务精简包(裁剪规则写死):只给本端相关的契约章节、设计页、
PRD 故事和冻结决策摘要(不超过 10 条),绝不把 PRD、契约、设计全文
复制给每个线程;提示词里必须列出精简包所含章节清单,只写“已精简”不算数;
@@ -1298,7 +1348,8 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
技术栈表、每端能力矩阵。
技术栈表、每端能力矩阵。(7 项齐全由你人工核对;平台只校验契约版本号
与冻结标志,不校验冻结物清单、不发回归任务。)
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
@@ -1327,6 +1378,9 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
(问题清单、热修复记录、回滚触发是否命中)+ v1.1 交接清单齐全,
两项缺一不进 G8;G8 的完成不是下一版已发布,是下一轮 G0-G6 的输入
已冻结齐全。
(阶段顺序是流程编排;平台协调员任务建出后各分工并行执行,不按 G1–G8
分阶段放行——需要分阶段时,分多次建协调员任务,上一阶段的协调员终态
后再建下一阶段。)
## 你的合并规则(评审与执行拆分)
@@ -1357,6 +1411,11 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- 决策记录落盘 `docs/decisions.md`,由你维护,只追加不改写历史;
一条决策写清日期、议题、决定、理由、影响端;推翻旧决策追加新条
写明“取代 D-n”,绝不改旧条原文、不删旧条。
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息
会被入口直接拒绝(409),因为平台循环内没有消费点,写了也等于没发。
替代方案:改分工请取消协调员任务后重建一个新的;给某个进行中分工
追加指令请直接发给该子任务;已终态的协调员不要走复活,重新建一个
协调员任务,或对相关子任务复活后重跑。
## 跨任务产物传递(协调员)
@@ -1376,7 +1435,12 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- 有进度的中断任务:断点续跑。
- 从未开始的任务:全新重跑,不直接判死。
- 单个客户端失败:只重开它,不重开全部。
- 单个客户端失败:只重开它,不重开全部——这是流程口径;平台语义
(`cloud/coordinator.py` 的 `evaluate_outcome`)是:任一非容忍分工
未完成,协调员任务即失败;`on_failure=tolerate` 的分工失败不拖垮
整体,重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排;
未知分工名按失败处理;`merged` 视同完成。`on_failure=tolerate`
须在建协调员任务时声明,事后不可改。
## 你的交付物
@@ -1387,6 +1451,12 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
---
## 独立审计与平台 audit_loop 的区分
- 平台的 `audit_loop` 是协调员任务的自动重排循环(推回包拆分到分工、
子任务重排),不是独立审计线程;独立审计仍须另开线程(附录 11-auditor),
审计 fail 自动推回并阻塞合并/发布,签字人与执行人、协调员批准人、
QA 测试人全不同。两者阻塞关系均为“与”,不可互换。
## GitHub 保证(协调员)
@@ -1401,13 +1471,15 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
未解决的评审意见不能合并。
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
你评审通过、无未解决评审意见,五项缺一不可。
你评审通过、无未解决评审意见,五项缺一不可。协调员批准与契约冻结
版本是每次调合并门禁时提交的证据,平台不持久化;谎报由审计与
PR/Checks 只读核对事后发现。
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
不批准、不合并;无 PR 时它会 409,先补 PR。
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
- 合并门禁:同正文,代码在 cloud/merge_gate.py 与
cloud/merge_gate_hard.py,模式由 CLOUD_MERGE_GATE_MODE 控制,
- 合并门禁:同正文,门禁实现在 cloud/merge_gate_hard.py(默认 hard),
模式由 CLOUD_MERGE_GATE_MODE 控制,
默认 hard:五项全查、缺一 409、状态不变、写 merge_gate_blocked
审计,无证据不放行,unknown 不等于 protected,未保护即使带
显式确认也不放行;record / enforce / off 须显式设置才生效,
@@ -14,6 +14,31 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
建仓分支要诚实(组织仓可经 App token 建,用户个人仓需安装会话里先建空仓,
记为 needs_user_once,不说成已自动建仓)。
## 平台协调员(以平台代码为准,skill 不得另说一套)
- 平台协调员任务(`coordinator_parts` 非空的任务)不跑模型回合:
worker 按分工自动扇出并行子任务、等齐、合流成一份汇总报告写回
父任务 `result_text`,汇总报告就是协调员任务的终局产物;合并决定
仍由你(模型/人工)在评审环节给,但评审发生在子任务终态之后,
报告之前没有“逐个放行”。
- 分工一次建齐、并行认领;分工最多 20 个;子任务 prompt 即分工 prompt
原文,平台不贴角色模板、不贴校验表、不贴精简包。
- 有界委派(建任务时声明,事后不可改):单分工超时 `timeout_seconds`
(1..86400 秒,超时只取消该分工、其余继续等)、失败容忍
`on_failure`(`fail` 默认 / `tolerate`:容忍分工失败不拖垮整体,
重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排)、单项截断
`result_chars`(默认 500、上限 2000)。全局超时 3600 秒:到时取消
挂起子任务、父任务失败。平台不提供分工 token/费用预算、分工轮数
上限、按分工隔离写范围——不要许诺这三类边界。
- 配额预留:分工数超过 1 时,剩余配额须覆盖分工数 × $1(可经环境变量
调),不足建任务即 402;子任务费用记父任务创建人账。
- 取消与孤儿收口由平台自动做:取消/超时终局取消未终态子任务并标
`interrupted`,常驻循环每轮扫尾父已终态、子仍未终态的孤儿;你不需、
也不能手动“取消子任务”。
- 模型与强度:分工不写 `model`/`effort` 时,继承协调员任务的
`child_model`/`child_effort` 或自身模型;“协调员、产品、架构用
较强模型”只是建议,不是平台字段。
## 你必须先做的事
1. 按 `software-engineering` 技能的 G0 问卷问清:目标用户、核心能力、
@@ -32,12 +57,17 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
完成定义。之后所有状态以这份待办为准,不以对话为准。
你是待办文件的唯一写作者,子线程只在结尾报告里写 TODO-UPDATE,
你核对证据后才勾选。不要虚构任何待办标记语法,不要依赖模型会自动写标记块。
3. 每个子任务提示词必须包含:角色模板全文(来自 SKILL.md 附录)、
本阶段输入的版本号、拥有目录、只读目录、分支名、完成定义、
禁止事项、TODO-UPDATE 要求。缺一项不派生。
3. 每个子任务提示词必须包含:本阶段输入的版本号、拥有目录、
只读目录、分支名、完成定义、禁止事项、TODO-UPDATE 要求,
以及角色模板内容——主聊人工派生时贴模板全文;平台协调员任务
的分工 prompt 即子任务 prompt 全文,角色模板不由平台注入,
如需模板内容,创建人须自己写进分工 prompt。缺一项不派生。
派生前校验表(必填表,填好的表原样贴进提示词,缺一项就不发任务):
客户端名、拥有目录、分支名、契约版本、PRD 版本、设计版本、
完成定义、TODO-UPDATE 要求,共 8 项。
完成定义、TODO-UPDATE 要求,共 8 项。校验表里的拥有目录 /
分支名 / 版本 / TODO-UPDATE 是写入分工 prompt 的约定内容,
平台只校验分工字段(`coordinator.validate_parts`),不校验、
不解析这些项。
子任务精简包(裁剪规则写死):只给本端相关的契约章节、设计页、
PRD 故事和冻结决策摘要(不超过 10 条),绝不把 PRD、契约、设计全文
复制给每个线程;提示词里必须列出精简包所含章节清单,只写“已精简”不算数;
@@ -49,7 +79,8 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- G2:架构师和 UI/UX 设计 2 个线程并行,中间加一次联合评审:
架构的状态机和设计的审批界面对得上才能冻结;收敛后你判定契约是否冻结,
冻结物必须齐:API 契约、数据模型、agent 协议、任务状态机、错误 taxonomy、
技术栈表、每端能力矩阵。
技术栈表、每端能力矩阵。(7 项齐全由你人工核对;平台只校验契约版本号
与冻结标志,不校验冻结物清单、不发回归任务。)
- G3:共享核心 1 个线程(含 mock 服务和契约测试);后端线程如有,
在 G3 阶段就参与契约测试,不要等到 G4 才对接。
- G3.5:可行性 spike。核心链路真跑通(agent 工具就是任务能创建、能跑出进度、
@@ -78,6 +109,9 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
(问题清单、热修复记录、回滚触发是否命中)+ v1.1 交接清单齐全,
两项缺一不进 G8;G8 的完成不是下一版已发布,是下一轮 G0-G6 的输入
已冻结齐全。
(阶段顺序是流程编排;平台协调员任务建出后各分工并行执行,不按 G1–G8
分阶段放行——需要分阶段时,分多次建协调员任务,上一阶段的协调员终态
后再建下一阶段。)
## 你的合并规则(评审与执行拆分)
@@ -108,6 +142,11 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- 决策记录落盘 `docs/decisions.md`,由你维护,只追加不改写历史;
一条决策写清日期、议题、决定、理由、影响端;推翻旧决策追加新条
写明“取代 D-n”,绝不改旧条原文、不删旧条。
- 平台协调员任务暂不支持后续指令:给进行中的协调员任务发后续消息
会被入口直接拒绝(409),因为平台循环内没有消费点,写了也等于没发。
替代方案:改分工请取消协调员任务后重建一个新的;给某个进行中分工
追加指令请直接发给该子任务;已终态的协调员不要走复活,重新建一个
协调员任务,或对相关子任务复活后重跑。
## 跨任务产物传递(协调员)
@@ -127,7 +166,12 @@ full-auto-executor + installation id。质量门禁全部自动且阻塞合并
- 有进度的中断任务:断点续跑。
- 从未开始的任务:全新重跑,不直接判死。
- 单个客户端失败:只重开它,不重开全部。
- 单个客户端失败:只重开它,不重开全部——这是流程口径;平台语义
(`cloud/coordinator.py` 的 `evaluate_outcome`)是:任一非容忍分工
未完成,协调员任务即失败;`on_failure=tolerate` 的分工失败不拖垮
整体,重跑时其 `failed` 子任务不重排、`interrupted` 的仍重排;
未知分工名按失败处理;`merged` 视同完成。`on_failure=tolerate`
须在建协调员任务时声明,事后不可改。
## 你的交付物
@@ -138,6 +182,12 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
---
## 独立审计与平台 audit_loop 的区分
- 平台的 `audit_loop` 是协调员任务的自动重排循环(推回包拆分到分工、
子任务重排),不是独立审计线程;独立审计仍须另开线程(附录 11-auditor),
审计 fail 自动推回并阻塞合并/发布,签字人与执行人、协调员批准人、
QA 测试人全不同。两者阻塞关系均为“与”,不可互换。
## GitHub 保证(协调员)
@@ -152,13 +202,15 @@ G8 的交付物是 v1.1 交接清单终版与下一轮 G0-G6 输入冻结清单
测试证据、已知问题、关联待办或 PRD 项;评审意见逐条处理,
未解决的评审意见不能合并。
- 合并门:GitHub Checks 全绿、目录归属检查通过、契约版本是冻结版、
你评审通过、无未解决评审意见,五项缺一不可。
你评审通过、无未解决评审意见,五项缺一不可。协调员批准与契约冻结
版本是每次调合并门禁时提交的证据,平台不持久化;谎报由审计与
PR/Checks 只读核对事后发现。
CI 用只读方式查,例如 GET /tasks/{id}/pr/checks,它绝不重跑检查、
不批准、不合并;无 PR 时它会 409,先补 PR。
- 主分支保护:要求 PR、要求 CI 通过、禁止直接推,这是 GitHub 仓库设置,
不是本平台代码能替你开的。没开保护时记为风险并去开通,不假装已保护。
- 合并门禁:同正文,代码在 cloud/merge_gate.py 与
cloud/merge_gate_hard.py,模式由 CLOUD_MERGE_GATE_MODE 控制,
- 合并门禁:同正文,门禁实现在 cloud/merge_gate_hard.py(默认 hard),
模式由 CLOUD_MERGE_GATE_MODE 控制,
默认 hard:五项全查、缺一 409、状态不变、写 merge_gate_blocked
审计,无证据不放行,unknown 不等于 protected,未保护即使带
显式确认也不放行;record / enforce / off 须显式设置才生效,
+116
View File
@@ -238,6 +238,25 @@ class WorkerCoordinatorTestCase(unittest.TestCase):
self.assertEqual(backend["effort"], "high")
self.assertEqual(backend["status"], "queued")
def test_sync_children_never_carry_coordinator_parts(self):
# 纵深防御(Muse 角色对齐 P1):_coord_sync_children 建出
# 的子任务行 coordinator_parts 恒为 NULL,子任务永为普通任务。
_insert_task(self.db_path, "coord", coordinator_parts=self._parts())
worker = self._worker()
task = worker.get_task("coord")
worker._coord_sync_children(task, task["coordinator_parts"])
conn = sqlite3.connect(self.db_path)
try:
rows = conn.execute(
"SELECT id, coordinator_parts FROM tasks"
" WHERE parent_task_id = ?",
("coord",)).fetchall()
finally:
conn.close()
self.assertEqual(len(rows), 2)
for task_id, parts in rows:
self.assertIsNone(parts, task_id)
def test_sync_requeues_failed_keeps_completed(self):
_insert_task(self.db_path, "coord", coordinator_parts=self._parts())
_insert_task(self.db_path, "c-old", status="failed",
@@ -469,6 +488,103 @@ class ServerCoordinatorTestCase(unittest.TestCase):
self.assertEqual(status, 200)
self.assertEqual(len(data["tasks"]), 3)
def _create_task(self, extra=None):
body = {"project_id": self.project["id"], "prompt": "做点事"}
if extra:
body.update(extra)
return self._req("POST", "/tasks", body, self.owner_key)
def test_create_child_coordinator_under_plain_parent_ok(self):
status, parent = self._create_task()
self.assertEqual(status, 201)
status, child = self._create_coord(
extra={"parent_task_id": parent["id"]})
self.assertEqual(status, 201)
self.assertEqual(child["parent_task_id"], parent["id"])
self.assertEqual(len(child["coordinator_parts"]), 2)
def test_create_coordinator_under_coordinator_rejected(self):
status, coord = self._create_coord()
self.assertEqual(status, 201)
status, body = self._create_coord(
extra={"parent_task_id": coord["id"]})
self.assertEqual(status, 400)
self.assertIn("coordinator task cannot be created under a"
" coordinator task", body["error"])
status, data = self._req(
"GET", "/tasks?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(len(data["tasks"]), 1)
def test_create_parent_task_checks(self):
status, _body = self._create_coord(
extra={"parent_task_id": "no-such-task"})
self.assertEqual(status, 404)
status, other = self._req(
"POST", "/projects", {"name": "other"}, self.owner_key)
self.assertEqual(status, 201)
status, parent = self._create_task(
extra={"project_id": other["id"]})
self.assertEqual(status, 201)
status, body = self._create_coord(
extra={"parent_task_id": parent["id"]})
self.assertEqual(status, 400)
status, body = self._create_coord(
extra={"parent_task_id": 123})
self.assertEqual(status, 400)
def test_create_lineage_without_coordinator_ok(self):
status, coord = self._create_coord()
self.assertEqual(status, 201)
status, child = self._create_task(
extra={"parent_task_id": coord["id"]})
self.assertEqual(status, 201)
self.assertEqual(child["parent_task_id"], coord["id"])
def test_create_idempotent_replay_keeps_single_task(self):
status, parent = self._create_task()
self.assertEqual(status, 201)
extra = {"idempotency_key": "idem-parent",
"parent_task_id": parent["id"]}
status, first = self._create_task(extra=extra)
self.assertEqual(status, 201)
status, second = self._create_task(extra=extra)
self.assertEqual(status, 200)
self.assertEqual(second["id"], first["id"])
status, data = self._req(
"GET", "/tasks?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(len(data["tasks"]), 2)
def test_followup_to_coordinator_rejected(self):
status, coord = self._create_coord()
self.assertEqual(status, 201)
status, body = self._req(
"POST", "/tasks/" + coord["id"] + "/followups",
{"body": "改一下分工", "mode": "queued"}, self.owner_key)
self.assertEqual(status, 409)
self.assertIn("coordinator tasks do not accept follow-up",
body["error"])
status, body = self._req(
"POST", "/tasks/" + coord["id"] + "/thread/message",
{"body": "改一下分工", "mode": "queued"}, self.owner_key)
self.assertEqual(status, 409)
self.assertIn("coordinator tasks do not accept follow-up",
body["error"])
def test_followup_to_plain_task_ok(self):
status, task = self._create_task()
self.assertEqual(status, 201)
status, _body = self._req(
"POST", "/tasks/" + task["id"] + "/followups",
{"body": "继续", "mode": "queued"}, self.owner_key)
self.assertEqual(status, 201)
status, body = self._req(
"POST", "/tasks/" + task["id"] + "/thread/message",
{"body": "继续", "mode": "queued"}, self.owner_key)
self.assertEqual(status, 201)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -12,7 +12,7 @@ seeded file -- a dropped middle chapter (01..07) would pass CI silently.
The bidirectional assertion previously discussed on the #209 track is
unmerged, so main does not have it; this file supplies it on the main
base (skill text v0.2, 9 appendix chapters 00..08;
integrated text is v0.5, 11 appendix chapters 00..10).
integrated text is v0.5, 12 appendix chapters 00..11).
What "bidirectional" means here, concretely (all derived from SKILL.md
itself, no new semantics invented):