From f6df76ee8f54176c330777eca430272604bb2606 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 7 Nov 2026 19:43:03 +0000 Subject: [PATCH] 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/. --- cloud/server.py | 42 ++++++- cloud/skills/software-engineering/SKILL.md | 106 +++++++++++++--- .../templates/roles/00-coordinator.md | 70 +++++++++-- cloud/tests/test_coordinator.py | 116 ++++++++++++++++++ cloud/tests/test_skill_appendix_sync.py | 2 +- 5 files changed, 308 insertions(+), 28 deletions(-) diff --git a/cloud/server.py b/cloud/server.py index 538291c..7754974 100644 --- a/cloud/server.py +++ b/cloud/server.py @@ -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 非空时: diff --git a/cloud/skills/software-engineering/SKILL.md b/cloud/skills/software-engineering/SKILL.md index c93e4cf..2c3f88d 100644 --- a/cloud/skills/software-engineering/SKILL.md +++ b/cloud/skills/software-engineering/SKILL.md @@ -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 须显式设置才生效, diff --git a/cloud/skills/software-engineering/templates/roles/00-coordinator.md b/cloud/skills/software-engineering/templates/roles/00-coordinator.md index 9930ee8..ae6cd8d 100644 --- a/cloud/skills/software-engineering/templates/roles/00-coordinator.md +++ b/cloud/skills/software-engineering/templates/roles/00-coordinator.md @@ -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 须显式设置才生效, diff --git a/cloud/tests/test_coordinator.py b/cloud/tests/test_coordinator.py index 9398a4e..fee4ea1 100644 --- a/cloud/tests/test_coordinator.py +++ b/cloud/tests/test_coordinator.py @@ -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() diff --git a/cloud/tests/test_skill_appendix_sync.py b/cloud/tests/test_skill_appendix_sync.py index a2d32a7..443d2bd 100644 --- a/cloud/tests/test_skill_appendix_sync.py +++ b/cloud/tests/test_skill_appendix_sync.py @@ -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):