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