fix(task-chain): PM审核断链三处修复——未识别输出不再默认completed、review_complete仅终结角色可用、迭代边界只认显式completed/cancelled

根因:需求任务验收通过后设计任务未自动创建,项目卡在「无任务运行」的假终态。

1. pm_review_run 的 else 分支把未识别 PM 输出(deliver/纯文本/格式错误)静默当作 review_complete,
   任务置 completed 且不创建下一角色任务,任务链断裂。改为追加纠错提示继续下一轮,轮次耗尽兜底 rejected。
2. 新增防御:非终结角色(requirement/design/develop/deploy_test/test)收到 review_complete 时修正为 approved。
3. 迭代边界检查从「非最新迭代即终止」改为「仅迭代状态 completed/cancelled 才终止」,
   新建二期迭代不再阻断一期仍在跑的链。
4. PM prompt 澄清 review_complete 仅 deploy_prod 最后阶段使用。
This commit is contained in:
ymq 2026-08-19 16:15:05 +08:00
parent 41232c8295
commit eb910bc7b8

View File

@ -502,6 +502,7 @@ PM_SYSTEM_PROMPT = """你是项目经理PM。你的职责是项目计划
## 任务链不能断(关键规则)
- 审核通过里程碑任务requirement / design产出含多个应用/模块或评审意见指明后续要落地开发必须先规划并用 create_tasks 创建后续开发任务 review_approve
- 系统在 approve 后会自动补一个线性链的下一角色任务作为兜底里程碑的并发拆分由你显式创建不要等不要断链
- **review_complete 只在无下一阶段时使用**即最后一个角色 deploy_prod 验收通过后requirement/design/develop/deploy_test/test 各阶段后面都还有下一角色验收通过一律用 review_approve系统自动创建下一角色任务严禁用 review_complete 收尾否则任务链断裂下一阶段任务不会自动创建
## 项目信息
工作目录__WORKSPACE__
@ -537,7 +538,7 @@ __REPO_STATE__
派发后续任务{"action":"tool_call","tool":"create_tasks","params":{"tasks":[{"title":"契约定义","role":"design","key":"contract","description":"定义数据模型/API/目录规范"},{"title":"hr-system 开发","role":"develop","key":"hrsys","depends_on":["contract"],"description":"实现 hr-system 基础能力"},{"title":"hr-org 开发","role":"develop","key":"hrorg","depends_on":["contract"],"description":"实现 hr-org 基础能力"}]}}
批准{"action":"review_approve","comment":"审核意见","next_task_title":"下阶段标题","next_task_description":"描述"}
驳回{"action":"review_reject","comment":"原因","questions":"修改要求"}
完成{"action":"review_complete","comment":"总结"}
完成{"action":"review_complete","comment":"总结"} deploy_prod 最后阶段验收通过时使用
回退{"action":"review_rollback","rollback_role":"develop","comment":"回退原因"}
审核标准
@ -1476,8 +1477,15 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
msgs.append({"role": "assistant", "content": raw})
msgs.append({"role": "user", "content": f"工具 {tool} 结果:\n{result}"})
else:
decision = {'status': 'completed', 'comment': raw[:200]}
break
# 未识别的 PM 输出deliver/纯文本/格式错误)绝不能静默当作 review_complete——那会把任务
# 置 completed 且不创建下一角色任务,任务链就此断掉(正是「需求验收通过但设计任务不自动创建」的根因)。
# 正确做法:追加纠错提示继续下一轮,让 PM 重新给出正确决策;轮次耗尽由下方「审核超时」兜底为 rejected。
logger.warning(f"pm_review_run 未识别 PM 输出 action={act.get('action', '?')}, turn={turn}: {raw[:200]}")
msgs.append({"role": "assistant", "content": raw})
msgs.append({"role": "user", "content":
"你的输出无法解析。请严格按格式输出单个 JSON"
"review_approve / review_reject / review_complete / review_rollback 之一,不要输出其它内容。"})
continue
if not decision:
decision = {'status': 'rejected', 'comment': '审核超时'}
@ -1485,6 +1493,16 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
status = decision['status']
comment = decision.get('comment', '')
# 防御:非终结角色被 review_complete 收尾 → 修正为 approved避免断链
# 只有无下一角色deploy_prod的 review_complete 才是合法终结。否则 requirement/design/develop/
# deploy_test/test 被误收尾,下一阶段任务不会自动创建,项目卡在「无任务运行」却非「已结束」的假终态。
if status == 'completed':
_nr = await _get_next_role(task_role, project_id)
if _nr:
logger.warning(f"pm_review_run 非终结角色 {task_role} 收到 review_complete修正为 approved 继续任务链 → {_nr}")
status = 'approved'
decision['status'] = 'approved'
if status == 'approved':
from appPublic.uniqueID import getID
pm_did = getID()
@ -1509,16 +1527,17 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
await approve_task(task_id, project_id, who="agent.pm", agent_id=agent_id, comment=comment)
next_role = await _get_next_role(task_role, project_id)
if next_role:
# 迭代边界:任务若归属老迭代(非当前迭代=该项目最新创建的迭代),
# 链到此终止,不再自动触发下一角色任务——「新迭代创建后老迭代不再产生新任务」。
# 迭代边界仅当任务归属的迭代已显式结束completed/cancelled才终止链。
# 若该迭代仍 planning/in_progress/active链继续在该迭代内创建下一角色任务——
# 新建其它迭代(如二期)不应阻断一期仍在跑的链(否则「需求验收通过但设计任务不自动创建」)。
task_iter = _task_iteration_name(task)
if task_iter:
cur = await sor.sqlExe(
"SELECT iteration_name FROM sd_iterations WHERE project_id=${pid}$ ORDER BY created_at DESC LIMIT 1",
{"pid": project_id})
cur_name = getattr(cur[0], 'iteration_name', '') if cur else ''
if cur_name and task_iter != cur_name:
logger.info(f"迭代边界:任务 {task_id} 归属迭代「{task_iter}(当前「{cur_name}」),链终止")
_it = await sor.sqlExe(
"SELECT status FROM sd_iterations WHERE project_id=${pid}$ AND iteration_name=${name}$",
{"pid": project_id, "name": task_iter})
_it_status = getattr(_it[0], 'status', '') if _it else ''
if _it_status in ('completed', 'cancelled'):
logger.info(f"迭代边界:任务 {task_id} 归属迭代「{task_iter}{_it_status},链终止")
return {"status": "completed", "task_id": task_id, "comment": comment or "迭代已结束,链终止"}
next_tid, next_title = await _create_next_task(sor, project_id, task, next_role, comment)
return {"status": "approved", "task_id": task_id, "next_task_id": next_tid, "next_role": next_role, "comment": comment}