fix(orchestration): 元景三问题根治——task_title归位+问题通道统一冒泡+依赖启动策略+确认去重
问题1(部署早于开发启动):
- _check_orchestration_gaps 重构为结构化缺口(kind/fingerprint/task_id/fix_ids)
- 缺口不再只打日志:派发后当场回给PM + 持久化pipeline_pm_notices注入PM下回合上下文
- 新增 update_task_deps 原语:PM核实缺口后补依赖(修正是LLM决策,代码只查漏)
问题2(env补齐后waiting不唤醒):
- _bubble_deploy_env_missing 从自建人工任务死胡同迁移到统一问题通道(raise_problem)
- owner在待办回答→resolve_problem自动唤醒任务→QA注入agent prompt,信息真正交给LLM
问题3(task_title错位+重复确认):
- SDL_ROLES task_title 各归其位(需求分析/架构设计/脚手架开发/部署测试/功能测试)
- 确认去重:同迭代已确认且非回退重做→跳过重复确认直接派发
- _create_next_task 单例阶段幂等:requirement/design复用同迭代任意活跃任务
机制增强:
- 依赖启动策略 params.dep_policy: all(默认)/any/at_least{n},纯函数_eval_deps_policy
- init.py poller 手写依赖求值收敛到同一纯函数(消除两处语义漂移)
This commit is contained in:
parent
98b90d4f21
commit
d9827c391b
26
models/pipeline_pm_notices.json
Normal file
26
models/pipeline_pm_notices.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "pipeline_pm_notices",
|
||||||
|
"title": "PM 编排查漏通知表",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "tenant_id", "title": "项目ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "task_id", "title": "关联任务ID", "type": "str", "length": 32, "nullable": "yes"},
|
||||||
|
{"name": "fingerprint", "title": "缺口指纹(去重)", "type": "str", "length": 64, "nullable": "no"},
|
||||||
|
{"name": "gap_kind", "title": "缺口类型(G1/G2/G3)", "type": "str", "length": 8, "nullable": "yes"},
|
||||||
|
{"name": "gap_text", "title": "缺口事实描述", "type": "text"},
|
||||||
|
{"name": "status", "title": "通知状态", "type": "str", "length": 16, "nullable": "no", "default": "pending"},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_ppn_tenant", "idxtype": "index", "idxfields": ["tenant_id"]},
|
||||||
|
{"name": "idx_ppn_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
|
{"name": "idx_ppn_fp", "idxtype": "unique", "idxfields": ["tenant_id", "fingerprint"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -524,9 +524,10 @@ __ROLE_SKILLS__
|
|||||||
- list_files(path) — 列目录
|
- list_files(path) — 列目录
|
||||||
- git_status() — 查看git状态
|
- git_status() — 查看git状态
|
||||||
- run_shell(command) — 执行命令(编译/测试验证)
|
- run_shell(command) — 执行命令(编译/测试验证)
|
||||||
- create_tasks(tasks) — 批量创建并派发后续任务(tasks 是 JSON 数组,每项 {title, role, description, key?, depends_on?, parent_id?};key 供同批任务间 depends_on 引用,depends_on 是前序 key 或任务ID 数组,空=并行/非空=串行)
|
- create_tasks(tasks) — 批量创建并派发后续任务(tasks 是 JSON 数组,每项 {title, role, description, key?, depends_on?, dep_policy?, parent_id?};key 供同批任务间 depends_on 引用,depends_on 是前序 key 或任务ID 数组,空=并行/非空=串行;dep_policy 是可选的启动策略:{"mode":"all"}(默认,前置全部结束才启动)/{"mode":"any"}(任一前置结束即启动)/{"mode":"at_least","n":k}(至少 k 个前置结束即启动))
|
||||||
- list_tasks(role, state) — 列出项目现有任务(派发前先查,避免重复)
|
- list_tasks(role, state) — 列出项目现有任务(派发前先查,避免重复)
|
||||||
- cancel_task(task_id) — 取消任务(重做/作废前必须先取消旧任务,避免两个相同任务并存)
|
- cancel_task(task_id) — 取消任务(重做/作废前必须先取消旧任务,避免两个相同任务并存)
|
||||||
|
- update_task_deps(task_id, add_depends_on, dep_policy?) — 给已存在任务补前置依赖(系统发现编排缺口时通知你,核实后用此工具修正;add_depends_on 是要追加的任务ID数组,dep_policy 可选、同上)
|
||||||
|
|
||||||
## 审核流程
|
## 审核流程
|
||||||
1. 先用工具检查代码/交付件是否实际产出(git_status / list_files / read_file)
|
1. 先用工具检查代码/交付件是否实际产出(git_status / list_files / read_file)
|
||||||
@ -597,8 +598,8 @@ __ROLE_SKILLS__
|
|||||||
|
|
||||||
# ── Agent 核心逻辑 ──
|
# ── Agent 核心逻辑 ──
|
||||||
|
|
||||||
async def _task_deps_satisfied(sor, depends_on_raw, task_id=''):
|
async def _task_deps_satisfied(sor, depends_on_raw, task_id='', dep_policy=None):
|
||||||
"""依赖门控:depends_on 全部处于终态(completed/approved)才可认领。
|
"""依赖门控:按启动策略求值(默认 all=全部终态;见 _eval_deps_policy)。
|
||||||
|
|
||||||
返回 (satisfied: bool, reason: str)。reason 非空说明未满足的原因,供逃逸阀冒泡用。
|
返回 (satisfied: bool, reason: str)。reason 非空说明未满足的原因,供逃逸阀冒泡用。
|
||||||
|
|
||||||
@ -630,23 +631,92 @@ async def _task_deps_satisfied(sor, depends_on_raw, task_id=''):
|
|||||||
recs = await sor.sqlExe(
|
recs = await sor.sqlExe(
|
||||||
"SELECT id, state, title FROM pipeline_tasks WHERE id IN (" + ids + ")", {})
|
"SELECT id, state, title FROM pipeline_tasks WHERE id IN (" + ids + ")", {})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
found = {getattr(r, 'id', ''): (getattr(r, 'state', ''), getattr(r, 'title', '') or '')
|
state_of = {}
|
||||||
for r in (recs or [])}
|
title_of = {}
|
||||||
|
for r in (recs or []):
|
||||||
|
_id = getattr(r, 'id', '')
|
||||||
|
state_of[_id] = getattr(r, 'state', '')
|
||||||
|
title_of[_id] = getattr(r, 'title', '') or ''
|
||||||
|
|
||||||
DONE = ('completed', 'approved')
|
satisfied, reason, _kind = _eval_deps_policy(deps, state_of, dep_policy, title_of)
|
||||||
DEAD = ('cancelled', 'failed') # 永不可达终态 → 死锁,必须冒泡
|
return satisfied, reason
|
||||||
missing, dead, pending = [], [], []
|
|
||||||
|
|
||||||
|
# ── 依赖启动策略(2026-08-27 新增)──────────────────────────────────────────
|
||||||
|
# 每个任务可带 params.dep_policy 声明前置任务的启动策略(通用数据模型,
|
||||||
|
# 代码不编码任何「某角色必须依赖某角色」的业务规则,依赖编排决策权归 PM/LLM):
|
||||||
|
# {"mode": "all"} 前置全部结束才启动(默认,等同历史行为)
|
||||||
|
# {"mode": "any"} 任一前置结束即启动
|
||||||
|
# {"mode": "at_least", "n": k} 至少 k 个前置结束即启动
|
||||||
|
# 不变量(任何策略下都成立,代码守门):
|
||||||
|
# · 依赖任务不存在 → 阻塞(可能是编造 ID,D1/D3)
|
||||||
|
# · 依赖处于 cancelled/failed → 永不可达,阻塞并冒泡(D4)
|
||||||
|
# · 自依赖 / depends_on 解析失败 → 阻塞
|
||||||
|
DEP_DONE = ('completed', 'approved')
|
||||||
|
DEP_DEAD = ('cancelled', 'failed')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_dep_policy(policy):
|
||||||
|
"""解析 params.dep_policy → (mode, n)。缺省/非法 → ('all', 0)(保守兜底)。"""
|
||||||
|
if isinstance(policy, str):
|
||||||
|
try:
|
||||||
|
policy = json.loads(policy)
|
||||||
|
except (json.JSONDecodeError, TypeError, ValueError):
|
||||||
|
policy = None
|
||||||
|
if not isinstance(policy, dict):
|
||||||
|
return 'all', 0
|
||||||
|
mode = str(policy.get('mode') or 'all').strip().lower()
|
||||||
|
if mode not in ('all', 'any', 'at_least'):
|
||||||
|
return 'all', 0
|
||||||
|
n = 0
|
||||||
|
if mode == 'at_least':
|
||||||
|
try:
|
||||||
|
n = int(policy.get('n') or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
n = 0
|
||||||
|
if n < 1:
|
||||||
|
return 'all', 0 # 非法 n → 保守退回 all
|
||||||
|
return mode, n
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_deps_policy(deps, state_of, policy=None, title_of=None):
|
||||||
|
"""纯函数:对依赖列表按启动策略求值(不访问 DB,两处调用方共用,语义单一来源)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deps: 依赖任务 id 列表(已去重)
|
||||||
|
state_of: {dep_id: state} 状态映射;id 不在映射中 = 任务不存在
|
||||||
|
policy: params.dep_policy(dict 或 JSON 字符串,可为空=默认 all)
|
||||||
|
title_of: 可选 {dep_id: title},用于生成可读 reason
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(satisfied: bool, reason: str, kind: str)
|
||||||
|
kind ∈ ('', 'dead', 'wait'):
|
||||||
|
'' 已满足(或无依赖)
|
||||||
|
'dead' 永不可达(不存在/已作废/失败)→ 调用方应冒泡人工
|
||||||
|
'wait' 仅等待中 → 保持等待,不冒泡
|
||||||
|
"""
|
||||||
|
title_of = title_of or {}
|
||||||
|
if not deps:
|
||||||
|
return True, '', ''
|
||||||
|
|
||||||
|
mode, n = _parse_dep_policy(policy)
|
||||||
|
|
||||||
|
# at_least 越界保护:n > 依赖总数时该策略永不可达(死等),
|
||||||
|
# 保守钳到依赖总数——宁可提前放行也不能制造永久阻塞。
|
||||||
|
if mode == 'at_least' and n > len(deps):
|
||||||
|
n = len(deps)
|
||||||
|
|
||||||
|
missing, dead, pending, done = [], [], [], []
|
||||||
for d in deps:
|
for d in deps:
|
||||||
if d not in found:
|
st = state_of.get(d)
|
||||||
|
if st is None:
|
||||||
missing.append(d) # D1/D3:依赖不存在 → 阻塞(不再放行)
|
missing.append(d) # D1/D3:依赖不存在 → 阻塞(不再放行)
|
||||||
continue
|
elif st in DEP_DONE:
|
||||||
st, ti = found[d]
|
done.append(d)
|
||||||
if st in DONE:
|
elif st in DEP_DEAD:
|
||||||
continue
|
dead.append(f"{title_of.get(d) or d[:8]}({st})")
|
||||||
if st in DEAD:
|
|
||||||
dead.append(f"{ti or d[:8]}({st})")
|
|
||||||
else:
|
else:
|
||||||
pending.append(f"{ti or d[:8]}({st})")
|
pending.append(f"{title_of.get(d) or d[:8]}({st})")
|
||||||
|
|
||||||
if missing or dead:
|
if missing or dead:
|
||||||
parts = []
|
parts = []
|
||||||
@ -654,10 +724,21 @@ async def _task_deps_satisfied(sor, depends_on_raw, task_id=''):
|
|||||||
parts.append("依赖任务不存在:" + "、".join(x[:8] for x in missing))
|
parts.append("依赖任务不存在:" + "、".join(x[:8] for x in missing))
|
||||||
if dead:
|
if dead:
|
||||||
parts.append("依赖已作废/失败,永不可达:" + "、".join(dead))
|
parts.append("依赖已作废/失败,永不可达:" + "、".join(dead))
|
||||||
return False, ";".join(parts) + "(需人工修正依赖或重建被依赖任务)"
|
return False, ";".join(parts) + "(需人工修正依赖或重建被依赖任务)", 'dead'
|
||||||
|
|
||||||
|
if mode == 'any':
|
||||||
|
if done:
|
||||||
|
return True, '', ''
|
||||||
|
return False, "等待任一前置完成(启动策略=any):" + "、".join(pending), 'wait'
|
||||||
|
if mode == 'at_least':
|
||||||
|
if len(done) >= n:
|
||||||
|
return True, '', ''
|
||||||
|
return False, (f"等待至少 {n} 个前置完成(启动策略=at_least {n},"
|
||||||
|
f"已完成 {len(done)}):" + "、".join(pending)), 'wait'
|
||||||
|
# mode == 'all'(默认)
|
||||||
if pending:
|
if pending:
|
||||||
return False, "等待依赖完成:" + "、".join(pending)
|
return False, "等待依赖完成:" + "、".join(pending), 'wait'
|
||||||
return True, ''
|
return True, '', ''
|
||||||
|
|
||||||
|
|
||||||
PLACEHOLDER_MARKS = ('待明确', '待确认', '待补充', 'TODO', 'todo', 'xxx', 'XXX', '占位', '<', '未知')
|
PLACEHOLDER_MARKS = ('待明确', '待确认', '待补充', 'TODO', 'todo', 'xxx', 'XXX', '占位', '<', '未知')
|
||||||
@ -740,15 +821,24 @@ async def _check_deploy_env_ready(sor, project_id, role):
|
|||||||
|
|
||||||
|
|
||||||
async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missing, env_file):
|
async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missing, env_file):
|
||||||
"""部署环境信息缺失 → 冒泡给 owner 补齐(去重)。
|
"""部署环境信息缺失 → 走统一问题通道冒泡给项目 owner(去重)。
|
||||||
|
|
||||||
这是「逃逸阀」而非「门禁」:任务不会静默死等,owner 待办里会出现一条明确的
|
统一冒泡机制(2026-08-27 定调:缺外部信息的问题一律走问题通道,不再按类型各写补丁):
|
||||||
「补齐部署环境信息」任务,写清缺哪些字段、文件在哪。
|
① raise_problem 建问题记录、任务置 waiting(清 claimed_by);
|
||||||
|
first_handler_agentid=owner → owner 的「我的待办」里出现一条待答问题
|
||||||
|
(list_my_human_todos 已 union pipeline_agent_questions,按 agentid 命中)。
|
||||||
|
② owner 在待办里回答(待办详情已支持 question 渲染 + 回答框)→
|
||||||
|
question_answer → resolve_problem → 任务自动 waiting→submitted。
|
||||||
|
③ poller 下轮重新派发 → 本函数调用方重跑 env 检查:补齐 → 认领,
|
||||||
|
_build_qna_section 把 owner 的回答注入 agent prompt(信息真正交给 LLM);
|
||||||
|
仍未补齐 → 旧问题已 answered、去重不拦 → 冒新一条。诚实循环,不静默死等。
|
||||||
"""
|
"""
|
||||||
from .human_task_capability import create_human_task
|
from .communication import raise_problem
|
||||||
|
# 去重只看系统冒泡的(from_role='system.orchestrator')——need_info 是通用类型,
|
||||||
|
# 角色 agent 缺信息也会用 need_info 提问,不加此条件会误拦。
|
||||||
exists = await sor.sqlExe(
|
exists = await sor.sqlExe(
|
||||||
"SELECT id FROM pipeline_human_tasks WHERE project_id=${pid}$ AND task_id=${tid}$ "
|
"SELECT id FROM pipeline_agent_questions WHERE tenant_id=${pid}$ AND task_id=${tid}$ "
|
||||||
"AND task_type='deploy_env_required' AND status='pending' LIMIT 1",
|
"AND problem_type='need_info' AND from_role='system.orchestrator' AND status='pending' LIMIT 1",
|
||||||
{"pid": project_id, "tid": task_id})
|
{"pid": project_id, "tid": task_id})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
if exists:
|
if exists:
|
||||||
@ -756,20 +846,22 @@ async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missi
|
|||||||
_own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
_own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01'
|
owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01'
|
||||||
await create_human_task(
|
question = (
|
||||||
project_id,
|
f"部署任务「{task_title or task_id[:8]}」无法开始:真实部署环境信息尚未提供。"
|
||||||
f"补齐部署环境信息:{task_title or task_id[:8]}",
|
"这属于外部输入(agent 无法自行产出),因此不回退需求/设计/开发阶段,只等这里补齐。\n\n"
|
||||||
"部署任务无法开始:真实部署环境信息尚未提供(这属于外部输入,agent 无法自行产出,"
|
|
||||||
"因此不回退需求/设计/开发阶段,只等这里补齐)。\n\n"
|
|
||||||
f"配置文件:{env_file}\n\n"
|
f"配置文件:{env_file}\n\n"
|
||||||
"缺失字段:\n" + "\n".join(f" · {m}" for m in missing) +
|
"缺失字段:\n" + "\n".join(f" · {m}" for m in missing) +
|
||||||
"\n\n补齐后本任务会自动继续(无需重跑上游阶段)。",
|
"\n\n请补齐以上字段后提交回答(回答内容会随任务上下文交给执行 agent,"
|
||||||
task_type='deploy_env_required',
|
"任务将自动恢复执行、重新校验环境信息)。"
|
||||||
assignee_id=owner_id,
|
|
||||||
created_by='system.orchestrator',
|
|
||||||
task_id=task_id,
|
|
||||||
)
|
)
|
||||||
logger.warning(f"部署环境信息缺失冒泡: task={task_id} missing={missing}")
|
await raise_problem(
|
||||||
|
"need_info", question, "system.orchestrator",
|
||||||
|
tenant_id=project_id, task_id=task_id,
|
||||||
|
first_handler_role="owner.superuser", first_handler_agentid=owner_id,
|
||||||
|
context={"env_file": env_file, "missing": missing},
|
||||||
|
suspend_task=True,
|
||||||
|
)
|
||||||
|
logger.warning(f"部署环境信息缺失冒泡(问题通道): task={task_id} missing={missing} owner={owner_id}")
|
||||||
|
|
||||||
|
|
||||||
async def _bubble_blocked_dependency(sor, project_id, task_id, task_title, reason):
|
async def _bubble_blocked_dependency(sor, project_id, task_id, task_title, reason):
|
||||||
@ -825,8 +917,14 @@ async def _claim_task(sor, tenant_id, role, state='submitted', match_role=True,
|
|||||||
blocked = [] # [(task_id, title, reason)] 依赖未满足的任务,供逃逸阀冒泡
|
blocked = [] # [(task_id, title, reason)] 依赖未满足的任务,供逃逸阀冒泡
|
||||||
for rec in recs:
|
for rec in recs:
|
||||||
_tid = getattr(rec, 'id', '')
|
_tid = getattr(rec, 'id', '')
|
||||||
|
# 启动策略:任务可在 params.dep_policy 声明 all/any/at_least(缺省 all)
|
||||||
|
try:
|
||||||
|
_tp = json.loads(getattr(rec, 'params', '{}') or '{}')
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
_tp = {}
|
||||||
ok_dep, why = await _task_deps_satisfied(
|
ok_dep, why = await _task_deps_satisfied(
|
||||||
sor, getattr(rec, 'depends_on', '') or '', task_id=_tid)
|
sor, getattr(rec, 'depends_on', '') or '', task_id=_tid,
|
||||||
|
dep_policy=_tp.get('dep_policy'))
|
||||||
if not ok_dep:
|
if not ok_dep:
|
||||||
blocked.append((_tid, getattr(rec, 'title', '') or '', why))
|
blocked.append((_tid, getattr(rec, 'title', '') or '', why))
|
||||||
continue
|
continue
|
||||||
@ -1173,6 +1271,10 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
→ 脚手架会先跑完,打破「应用级 develop approved = 编码完成」这个 deploy_test 门控前提
|
→ 脚手架会先跑完,打破「应用级 develop approved = 编码完成」这个 deploy_test 门控前提
|
||||||
G3 design 产出的模块清单(spec.json generated_modules)有模块没有对应 develop 任务
|
G3 design 产出的模块清单(spec.json generated_modules)有模块没有对应 develop 任务
|
||||||
→ PM 漏派,模块不会被开发却照样部署
|
→ PM 漏派,模块不会被开发却照样部署
|
||||||
|
|
||||||
|
返回结构化缺口列表 [{kind, fingerprint, text, task_id, fix_ids}]:
|
||||||
|
fingerprint 用于通知去重;task_id/fix_ids 让 PM 能直接调 update_task_deps 修正,
|
||||||
|
不需要再反查任务 ID(G2 元景故障的教训:告警只进日志没人看,事实必须送到 PM 手上)。
|
||||||
"""
|
"""
|
||||||
gaps = []
|
gaps = []
|
||||||
_iter = _task_iteration_name(task)
|
_iter = _task_iteration_name(task)
|
||||||
@ -1206,9 +1308,11 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
title_of = {t['id']: t['title'] for t in tasks}
|
title_of = {t['id']: t['title'] for t in tasks}
|
||||||
for tid, ds in dep_map.items():
|
for tid, ds in dep_map.items():
|
||||||
if tid in ds:
|
if tid in ds:
|
||||||
gaps.append(f"G1 任务「{title_of.get(tid, tid[:8])}」依赖自身,必然死锁")
|
gaps.append({'kind': 'G1', 'fingerprint': f"G1:{tid}", 'task_id': tid, 'fix_ids': [],
|
||||||
|
'text': f"G1 任务「{title_of.get(tid, tid[:8])}」({tid}) 依赖自身,必然死锁"})
|
||||||
WHITE, GRAY, BLACK = 0, 1, 2
|
WHITE, GRAY, BLACK = 0, 1, 2
|
||||||
color = {k: WHITE for k in dep_map}
|
color = {k: WHITE for k in dep_map}
|
||||||
|
_cyc_seen = set()
|
||||||
|
|
||||||
def _dfs(n, path):
|
def _dfs(n, path):
|
||||||
color[n] = GRAY
|
color[n] = GRAY
|
||||||
@ -1217,10 +1321,16 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
continue
|
continue
|
||||||
if color[m] == GRAY:
|
if color[m] == GRAY:
|
||||||
cyc = path[path.index(m):] if m in path else [m]
|
cyc = path[path.index(m):] if m in path else [m]
|
||||||
gaps.append("G1 依赖成环,必然死锁:" +
|
fp = "G1:cycle:" + ",".join(sorted(cyc))
|
||||||
" → ".join(title_of.get(x, x[:8]) for x in cyc + [m]))
|
if fp in _cyc_seen:
|
||||||
|
continue
|
||||||
|
_cyc_seen.add(fp)
|
||||||
|
gaps.append({'kind': 'G1', 'fingerprint': fp, 'task_id': (cyc[0] if cyc else ''),
|
||||||
|
'fix_ids': [],
|
||||||
|
'text': "G1 依赖成环,必然死锁:" +
|
||||||
|
" → ".join(title_of.get(x, x[:8]) for x in cyc + [m])})
|
||||||
elif color[m] == WHITE:
|
elif color[m] == WHITE:
|
||||||
_dfs(m, path + [m])
|
_dfs(m, path + [n])
|
||||||
color[n] = BLACK
|
color[n] = BLACK
|
||||||
|
|
||||||
for n in list(dep_map.keys()):
|
for n in list(dep_map.keys()):
|
||||||
@ -1237,13 +1347,17 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
for a in app_devs:
|
for a in app_devs:
|
||||||
if a['state'] not in ACTIVE and a['state'] != 'approved':
|
if a['state'] not in ACTIVE and a['state'] != 'approved':
|
||||||
continue
|
continue
|
||||||
missing = [m['title'] for m in unfinished_mods if m['id'] not in a['deps']]
|
miss_tasks = [m for m in unfinished_mods if m['id'] not in a['deps']]
|
||||||
if missing:
|
if miss_tasks:
|
||||||
gaps.append(
|
miss_ids = [m['id'] for m in miss_tasks]
|
||||||
f"G2 应用级 develop「{a['title']}」未依赖以下未完成的模块任务:"
|
gaps.append({
|
||||||
+ "、".join(missing)
|
'kind': 'G2', 'fingerprint': "G2:" + a['id'] + ":" + ",".join(sorted(miss_ids)),
|
||||||
+ " → 脚手架可能先于模块完成,使 deploy_test 在编码未完成时启动"
|
'task_id': a['id'], 'fix_ids': miss_ids,
|
||||||
+ f"(修法:给该任务 depends_on 补上这些模块任务 id)")
|
'text': (f"G2 应用级 develop「{a['title']}」({a['id']}) 未依赖以下未完成的模块任务:"
|
||||||
|
+ "、".join(f"{m['title']}({m['id']})" for m in miss_tasks)
|
||||||
|
+ " → 脚手架可能先于模块完成,使 deploy_test 在编码未完成时启动"
|
||||||
|
+ "(修法:update_task_deps(task_id=\"" + a['id']
|
||||||
|
+ "\", add_depends_on=" + json.dumps(miss_ids) + "))")})
|
||||||
|
|
||||||
# G3 设计模块清单 vs 实际派发的 develop 任务(漏派检测)
|
# G3 设计模块清单 vs 实际派发的 develop 任务(漏派检测)
|
||||||
try:
|
try:
|
||||||
@ -1263,16 +1377,85 @@ async def _check_orchestration_gaps(sor, project_id, task, next_role='', next_ta
|
|||||||
hit = any(str(m).lower() in (t['title'] or '').lower()
|
hit = any(str(m).lower() in (t['title'] or '').lower()
|
||||||
for t in tasks if t['role'] == 'agent.develop')
|
for t in tasks if t['role'] == 'agent.develop')
|
||||||
if not hit:
|
if not hit:
|
||||||
gaps.append(
|
gaps.append({
|
||||||
f"G3 设计清单里的模块「{m}」没有对应的 develop 任务"
|
'kind': 'G3', 'fingerprint': f"G3:{m}", 'task_id': '', 'fix_ids': [],
|
||||||
f"({_os.path.basename(spec_path)} generated_modules)→ PM 漏派,"
|
'text': (f"G3 设计清单里的模块「{m}」没有对应的 develop 任务"
|
||||||
f"该模块不会被开发却会进入部署")
|
f"({_os.path.basename(spec_path)} generated_modules)→ PM 漏派,"
|
||||||
|
f"该模块不会被开发却会进入部署(修法:create_tasks 补派该模块)")})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"_check_orchestration_gaps G3 跳过: {e}")
|
logger.debug(f"_check_orchestration_gaps G3 跳过: {e}")
|
||||||
|
|
||||||
return gaps
|
return gaps
|
||||||
|
|
||||||
|
|
||||||
|
def _gap_texts(gaps):
|
||||||
|
"""结构化缺口 → 纯文本列表(日志用)。"""
|
||||||
|
return [g['text'] for g in (gaps or []) if isinstance(g, dict) and g.get('text')]
|
||||||
|
|
||||||
|
|
||||||
|
async def _save_gap_notices(sor, project_id, gaps, source_task_id=''):
|
||||||
|
"""把编排查漏结果持久化为 PM 通知(pending),供 PM 下一回合注入上下文。
|
||||||
|
|
||||||
|
去重:(tenant_id, fingerprint) 逻辑唯一——同一缺口重复检出只留一条。
|
||||||
|
实现用 SELECT 先查再 UPDATE/INSERT,不依赖 unique 索引(create_tables.py
|
||||||
|
的幂等建表会跳过 CREATE INDEX 语句,索引可能不存在)。
|
||||||
|
已存在(含已 delivered)→ 重置回 pending:缺口未修复再次检出时,
|
||||||
|
PM 必须再看到(若用 INSERT IGNORE 会被挡住,缺口就永远静默了)。
|
||||||
|
"""
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
for g in (gaps or []):
|
||||||
|
if not isinstance(g, dict) or not g.get('fingerprint'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
_ex = await sor.sqlExe(
|
||||||
|
"SELECT id FROM pipeline_pm_notices WHERE tenant_id=${pid}$ AND fingerprint=${fp}$",
|
||||||
|
{"pid": project_id, "fp": g['fingerprint']})
|
||||||
|
_tid = g.get('task_id') or source_task_id or ''
|
||||||
|
if _ex:
|
||||||
|
await sor.sqlExe(
|
||||||
|
"UPDATE pipeline_pm_notices SET status='pending', gap_text=${t}$, "
|
||||||
|
"task_id=${tid}$, gap_kind=${k}$, updated_at=NOW() WHERE id=${id}$",
|
||||||
|
{"t": g.get('text') or '', "tid": _tid, "k": g.get('kind') or '',
|
||||||
|
"id": getattr(_ex[0], 'id', '')})
|
||||||
|
else:
|
||||||
|
await sor.sqlExe(
|
||||||
|
"INSERT INTO pipeline_pm_notices "
|
||||||
|
"(id, tenant_id, task_id, fingerprint, gap_kind, gap_text, status, created_at, updated_at) "
|
||||||
|
"VALUES (${id}$, ${pid}$, ${tid}$, ${fp}$, ${k}$, ${t}$, 'pending', NOW(), NOW())",
|
||||||
|
{"id": getID(), "pid": project_id, "tid": _tid,
|
||||||
|
"fp": g['fingerprint'], "k": g.get('kind') or '', "t": g.get('text') or ''})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"_save_gap_notices 跳过 {g.get('fingerprint')}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_pm_notices(sor, project_id):
|
||||||
|
"""取本项目全部 pending 编排通知并标记 delivered(每个缺口只进一次 PM 上下文,避免重复噪声)。
|
||||||
|
|
||||||
|
返回拼接好的文本块;无通知返回 ''。
|
||||||
|
"""
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id, gap_text FROM pipeline_pm_notices "
|
||||||
|
"WHERE tenant_id=${pid}$ AND status='pending' ORDER BY created_at ASC",
|
||||||
|
{"pid": project_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if not recs:
|
||||||
|
return ''
|
||||||
|
ids = [getattr(r, 'id', '') for r in recs]
|
||||||
|
ph = ",".join("${i%d}$" % n for n in range(len(ids)))
|
||||||
|
kw = {("i%d" % n): v for n, v in enumerate(ids)}
|
||||||
|
await sor.sqlExe(
|
||||||
|
"UPDATE pipeline_pm_notices SET status='delivered', updated_at=NOW() "
|
||||||
|
"WHERE id IN (" + ph + ")", kw)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
lines = ["⚠️ 编排完备性检查发现以下缺口(代码查漏,修正决策权在你):"]
|
||||||
|
for r in recs:
|
||||||
|
lines.append("· " + (getattr(r, 'gap_text', '') or ''))
|
||||||
|
lines.append("请核实后用 update_task_deps / create_tasks / cancel_task 修正;"
|
||||||
|
"若缺口是误报(如任务已另行安排),忽略即可。")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
async def _create_next_task(sor, project_id, task, next_role, pm_comment='',
|
async def _create_next_task(sor, project_id, task, next_role, pm_comment='',
|
||||||
next_title='', next_desc=''):
|
next_title='', next_desc=''):
|
||||||
from appPublic.uniqueID import getID
|
from appPublic.uniqueID import getID
|
||||||
@ -1337,13 +1520,20 @@ async def _create_next_task(sor, project_id, task, next_role, pm_comment='',
|
|||||||
"ORDER BY created_at DESC LIMIT 20",
|
"ORDER BY created_at DESC LIMIT 20",
|
||||||
{"pid": project_id, "r": next_role})
|
{"pid": project_id, "r": next_role})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
# 单例阶段:需求/设计每迭代只有一个(不像 develop 按模块并行多任务)。
|
||||||
|
# 系统派生时若同迭代已有活跃任务——无论来源(含 PM create_tasks 派发、无 previous_role)——
|
||||||
|
# 都复用,不重复创建。元景故障:PM 派的真设计任务(无 previous_role)不在旧幂等匹配范围,
|
||||||
|
# 导致自动派生的错位设计任务与它并存、各自 approved 各触发一次设计确认。
|
||||||
|
_single_stage = next_role in ('agent.requirement', 'agent.design')
|
||||||
for _d in (_dup or []):
|
for _d in (_dup or []):
|
||||||
try:
|
try:
|
||||||
_dp = json.loads(getattr(_d, 'params', '{}') or '{}')
|
_dp = json.loads(getattr(_d, 'params', '{}') or '{}')
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
_dp = {}
|
_dp = {}
|
||||||
|
if (_dp.get('iteration_id') or '') != _iter:
|
||||||
|
continue
|
||||||
# 同迭代 + 同角色 + 同为系统派生(有 previous_role)→ 认定为同一阶段任务,幂等复用
|
# 同迭代 + 同角色 + 同为系统派生(有 previous_role)→ 认定为同一阶段任务,幂等复用
|
||||||
if (_dp.get('iteration_id') or '') == _iter and _dp.get('previous_role'):
|
if _dp.get('previous_role') or _single_stage:
|
||||||
_did = getattr(_d, 'id', '')
|
_did = getattr(_d, 'id', '')
|
||||||
logger.info(f"_create_next_task 幂等命中:{next_role} 同迭代已有活跃任务 {_did},跳过创建")
|
logger.info(f"_create_next_task 幂等命中:{next_role} 同迭代已有活跃任务 {_did},跳过创建")
|
||||||
return _did, getattr(_d, 'title', '') or new_title
|
return _did, getattr(_d, 'title', '') or new_title
|
||||||
@ -2101,6 +2291,12 @@ async def _pm_create_tasks(sor, project_id, params, parent_task_id=None):
|
|||||||
tparams['task_kind'] = 'new_dev'
|
tparams['task_kind'] = 'new_dev'
|
||||||
if iteration_name:
|
if iteration_name:
|
||||||
tparams["iteration_id"] = iteration_name
|
tparams["iteration_id"] = iteration_name
|
||||||
|
# 启动策略(可选):PM 可为任务声明前置任务的启动方式
|
||||||
|
# {"mode": "all"} / {"mode": "any"} / {"mode": "at_least", "n": k}
|
||||||
|
# 非法值会被 _parse_dep_policy 保守兜底为 all,不会破坏编排。
|
||||||
|
_dp = t.get('dep_policy')
|
||||||
|
if isinstance(_dp, dict) and _dp.get('mode'):
|
||||||
|
tparams['dep_policy'] = _dp
|
||||||
tid = getID()
|
tid = getID()
|
||||||
await sor.C('pipeline_tasks', {
|
await sor.C('pipeline_tasks', {
|
||||||
'id': tid, 'tenant_id': project_id, 'pipeline_id': 'role_task',
|
'id': tid, 'tenant_id': project_id, 'pipeline_id': 'role_task',
|
||||||
@ -2164,10 +2360,28 @@ async def _pm_create_tasks(sor, project_id, params, parent_task_id=None):
|
|||||||
return 'FAIL: 没有可创建的任务(缺少 title)'
|
return 'FAIL: 没有可创建的任务(缺少 title)'
|
||||||
# C 之后立即 COMMIT,释放行锁,供 agent_poller 下一轮可见认领
|
# C 之后立即 COMMIT,释放行锁,供 agent_poller 下一轮可见认领
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
# 编排查漏:派发后立即跑 G1/G2/G3,缺口当场返回给 PM(同回合即可修正)+
|
||||||
|
# 持久化通知兜底(本回合未修,下一回合仍能看到)。元景故障教训:只打日志没人看。
|
||||||
|
gap_note = ''
|
||||||
|
try:
|
||||||
|
_anchor = None
|
||||||
|
for _, _tid0, _, _ in created:
|
||||||
|
_ar = await sor.sqlExe("SELECT id, params FROM pipeline_tasks WHERE id=${i}$", {"i": _tid0})
|
||||||
|
if _ar:
|
||||||
|
_anchor = _ar[0]
|
||||||
|
break
|
||||||
|
_gaps = await _check_orchestration_gaps(sor, project_id, _anchor) if _anchor else []
|
||||||
|
if _gaps:
|
||||||
|
await _save_gap_notices(sor, project_id, _gaps, source_task_id=parent_task_id or '')
|
||||||
|
logger.warning(f"编排完备性告警(派发后) project={project_id}: {_gap_texts(_gaps)}")
|
||||||
|
gap_note = ("\n⚠️ 编排完备性检查发现缺口:\n" + "\n".join("· " + g['text'] for g in _gaps)
|
||||||
|
+ "\n请核实后立即用 update_task_deps/create_tasks 修正。")
|
||||||
|
except Exception as _ge:
|
||||||
|
logger.warning(f"派发后编排查漏失败: {_ge}")
|
||||||
note = f"(已自动取消 {len(cancelled)} 个同标题活跃任务)" if cancelled else ""
|
note = f"(已自动取消 {len(cancelled)} 个同标题活跃任务)" if cancelled else ""
|
||||||
warn_note = f"({len(warnings)} 个未知依赖引用已忽略)" if warnings else ""
|
warn_note = f"({len(warnings)} 个未知依赖引用已忽略)" if warnings else ""
|
||||||
return f"OK: 已派发 {len(created)} 个任务" + note + warn_note + ":" + \
|
return f"OK: 已派发 {len(created)} 个任务" + note + warn_note + ":" + \
|
||||||
";".join([f"{title}({role})" for _, _, title, role in created])
|
";".join([f"{title}({role})" for _, _, title, role in created]) + gap_note
|
||||||
|
|
||||||
|
|
||||||
async def _pm_list_tasks(sor, project_id, params):
|
async def _pm_list_tasks(sor, project_id, params):
|
||||||
@ -2223,6 +2437,81 @@ async def _pm_cancel_task(sor, project_id, params):
|
|||||||
return f'OK: 已取消任务「{title}」({task_id}),正在执行的 agent 将在下一轮心跳中止'
|
return f'OK: 已取消任务「{title}」({task_id}),正在执行的 agent 将在下一轮心跳中止'
|
||||||
|
|
||||||
|
|
||||||
|
async def _pm_update_task_deps(sor, project_id, params):
|
||||||
|
"""PM 给已存在任务补前置依赖(G2/G3 查漏的修正出口)。
|
||||||
|
|
||||||
|
代码只发现缺口并把事实交给 PM(流转决策权归 LLM),PM 核实后用本原语修正。
|
||||||
|
params: {task_id, add_depends_on: [任务ID...], dep_policy?: {mode, n?}}
|
||||||
|
"""
|
||||||
|
task_id = (params.get('task_id') or params.get('id') or '').strip()
|
||||||
|
add = params.get('add_depends_on') or []
|
||||||
|
if isinstance(add, str):
|
||||||
|
try:
|
||||||
|
add = json.loads(add)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
add = []
|
||||||
|
add = [str(x).strip() for x in (add or []) if str(x).strip()]
|
||||||
|
if not task_id:
|
||||||
|
return 'FAIL: 需要 task_id'
|
||||||
|
if not add:
|
||||||
|
return 'FAIL: 需要 add_depends_on(要追加的任务ID数组)'
|
||||||
|
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id, title, state, depends_on, params FROM pipeline_tasks "
|
||||||
|
"WHERE id=${tid}$ AND tenant_id=${pid}$",
|
||||||
|
{"tid": task_id, "pid": project_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if not recs:
|
||||||
|
return 'FAIL: 任务不存在或不属于当前项目'
|
||||||
|
rec = recs[0]
|
||||||
|
title = getattr(rec, 'title', '')
|
||||||
|
state = getattr(rec, 'state', '')
|
||||||
|
if state in ('completed', 'cancelled', 'failed', 'approved'):
|
||||||
|
return f'FAIL: 任务「{title}」已是终态 {state},改依赖无意义'
|
||||||
|
|
||||||
|
# 校验追加的依赖真实存在(防编造 ID,与 _pm_create_tasks 的 D3 校验同标准)
|
||||||
|
resolved, bad = [], []
|
||||||
|
for d in add:
|
||||||
|
_ex = await sor.sqlExe("SELECT id FROM pipeline_tasks WHERE id=${i}$", {"i": d})
|
||||||
|
if _ex:
|
||||||
|
resolved.append(d)
|
||||||
|
else:
|
||||||
|
bad.append(d[:12])
|
||||||
|
if bad:
|
||||||
|
return 'FAIL: 以下任务ID不存在:' + '、'.join(bad)
|
||||||
|
|
||||||
|
# 合并现有 depends_on(去重、防自依赖)
|
||||||
|
try:
|
||||||
|
cur = json.loads(getattr(rec, 'depends_on', '') or '[]')
|
||||||
|
cur = [str(x) for x in cur if x] if isinstance(cur, list) else []
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
cur = []
|
||||||
|
if task_id in resolved:
|
||||||
|
return 'FAIL: 不能依赖自身'
|
||||||
|
merged = cur + [d for d in resolved if d not in cur]
|
||||||
|
|
||||||
|
# 可选更新启动策略
|
||||||
|
try:
|
||||||
|
tparams = json.loads(getattr(rec, 'params', '{}') or '{}')
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
tparams = {}
|
||||||
|
_dp = params.get('dep_policy')
|
||||||
|
if isinstance(_dp, dict) and _dp.get('mode'):
|
||||||
|
tparams['dep_policy'] = _dp
|
||||||
|
|
||||||
|
await sor.sqlExe(
|
||||||
|
"UPDATE pipeline_tasks SET depends_on=${deps}$, params=${p}$, updated_at=NOW() WHERE id=${tid}$",
|
||||||
|
{"deps": json.dumps(merged, ensure_ascii=False),
|
||||||
|
"p": json.dumps(tparams, ensure_ascii=False), "tid": task_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
from .audit import record_audit
|
||||||
|
await record_audit(project_id, 'pipeline_tasks', task_id, 'update_deps',
|
||||||
|
who='agent.pm',
|
||||||
|
detail=f"depends_on += {resolved}", sor=sor)
|
||||||
|
return (f'OK: 任务「{title}」depends_on 已更新为 {merged}'
|
||||||
|
+ (f',启动策略={_dp}' if isinstance(_dp, dict) and _dp.get('mode') else ''))
|
||||||
|
|
||||||
|
|
||||||
async def pm_review_run(project_id, agent_id=None, model_name=None):
|
async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||||
db = _get_db()
|
db = _get_db()
|
||||||
async with db.sqlorContext("pipeline") as sor:
|
async with db.sqlorContext("pipeline") as sor:
|
||||||
@ -2288,6 +2577,14 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
|
|
||||||
msgs = [{"role": "system", "content": pm_system}]
|
msgs = [{"role": "system", "content": pm_system}]
|
||||||
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}):\n\n{content_preview}"})
|
msgs.append({"role": "user", "content": f"请审核以下交付件(类型:{deliverable_type}):\n\n{content_preview}"})
|
||||||
|
# 编排缺口通知注入:代码查漏发现的 G1/G2/G3 缺口(此前只进日志没人看),
|
||||||
|
# 这里随任务上下文交给 PM——PM 可在本回合核实后用 update_task_deps 等修正。
|
||||||
|
try:
|
||||||
|
_notices = await _load_pm_notices(sor, project_id)
|
||||||
|
if _notices:
|
||||||
|
msgs.append({"role": "user", "content": _notices})
|
||||||
|
except Exception as _ne:
|
||||||
|
logger.warning(f"_load_pm_notices failed: {_ne}")
|
||||||
|
|
||||||
from .llm_bridge import llm_call_msgs
|
from .llm_bridge import llm_call_msgs
|
||||||
|
|
||||||
@ -2344,6 +2641,8 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
result = await _pm_list_tasks(sor, project_id, params)
|
result = await _pm_list_tasks(sor, project_id, params)
|
||||||
elif tool in ('cancel_task', 'cancel'):
|
elif tool in ('cancel_task', 'cancel'):
|
||||||
result = await _pm_cancel_task(sor, project_id, params)
|
result = await _pm_cancel_task(sor, project_id, params)
|
||||||
|
elif tool in ('update_task_deps', 'update_deps'):
|
||||||
|
result = await _pm_update_task_deps(sor, project_id, params)
|
||||||
elif tool == 'load_skill':
|
elif tool == 'load_skill':
|
||||||
result = await _load_skill_by_name(sor, project_id, 'pm', org_id, params.get('name', ''), params.get('file_path') or None)
|
result = await _load_skill_by_name(sor, project_id, 'pm', org_id, params.get('name', ''), params.get('file_path') or None)
|
||||||
else:
|
else:
|
||||||
@ -2452,20 +2751,42 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
# 而是发「需求/设计确认」人工任务给项目 owner。owner 确认通过 → confirm_stage_gate
|
# 而是发「需求/设计确认」人工任务给项目 owner。owner 确认通过 → confirm_stage_gate
|
||||||
# 继续派发下一角色;不确定 → 退回原角色重做(带修改意见)。
|
# 继续派发下一角色;不确定 → 退回原角色重做(带修改意见)。
|
||||||
if task_role in ('agent.requirement', 'agent.design'):
|
if task_role in ('agent.requirement', 'agent.design'):
|
||||||
_own = await sor.sqlExe(
|
|
||||||
"SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
owner_id = getattr(_own[0], 'created_by', '') if _own else ''
|
|
||||||
if not owner_id:
|
|
||||||
owner_id = 'user-01' # 历史项目 NULL 回填 admin
|
|
||||||
confirm_type = 'requirement_confirmation' if task_role == 'agent.requirement' else 'design_confirmation'
|
confirm_type = 'requirement_confirmation' if task_role == 'agent.requirement' else 'design_confirmation'
|
||||||
confirm_label = '需求' if task_role == 'agent.requirement' else '设计'
|
confirm_label = '需求' if task_role == 'agent.requirement' else '设计'
|
||||||
|
# 确认去重(2026-08-27 修复):同迭代已确认过(status=done)且本任务非回退重做
|
||||||
|
# → 不再重复发确认任务,直接走确认后派发。元景故障:错位的重复 design 任务各被
|
||||||
|
# approved 一次,owner 连续收到两条「设计确认」待办。回退重做(有 rollback_from)
|
||||||
|
# 产出的是新交付件,必须重新确认,不走去重。
|
||||||
iter_id = ''
|
iter_id = ''
|
||||||
if task_iter:
|
if task_iter:
|
||||||
_it2 = await sor.sqlExe(
|
_it2 = await sor.sqlExe(
|
||||||
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ AND iteration_name=${n}$",
|
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ AND iteration_name=${n}$",
|
||||||
{"pid": project_id, "n": task_iter})
|
{"pid": project_id, "n": task_iter})
|
||||||
iter_id = getattr(_it2[0], 'id', '') if _it2 else ''
|
iter_id = getattr(_it2[0], 'id', '') if _it2 else ''
|
||||||
|
is_redo = bool(_tp.get('rollback_from'))
|
||||||
|
if iter_id and not is_redo:
|
||||||
|
_done_conf = await sor.sqlExe(
|
||||||
|
"SELECT id FROM pipeline_human_tasks WHERE iteration_id=${iid}$ "
|
||||||
|
"AND task_type=${ct}$ AND status='done' LIMIT 1",
|
||||||
|
{"iid": iter_id, "ct": confirm_type})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if _done_conf:
|
||||||
|
logger.info(
|
||||||
|
f"确认去重:{confirm_type} 本迭代已确认过(非回退重做),"
|
||||||
|
f"跳过重复确认,直接派发 {next_role}: task={task_id}")
|
||||||
|
next_tid, next_title = await _create_next_task(
|
||||||
|
sor, project_id, task, next_role, comment,
|
||||||
|
next_title=decision.get('next_title', '') or '',
|
||||||
|
next_desc=decision.get('next_desc', '') or '')
|
||||||
|
return {"status": "approved", "task_id": task_id,
|
||||||
|
"next_task_id": next_tid, "next_role": next_role,
|
||||||
|
"comment": comment, "confirm_skipped": "already_confirmed"}
|
||||||
|
_own = await sor.sqlExe(
|
||||||
|
"SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
owner_id = getattr(_own[0], 'created_by', '') if _own else ''
|
||||||
|
if not owner_id:
|
||||||
|
owner_id = 'user-01' # 历史项目 NULL 回填 admin
|
||||||
from .human_task_capability import create_human_task
|
from .human_task_capability import create_human_task
|
||||||
ok, hid = await create_human_task(
|
ok, hid = await create_human_task(
|
||||||
project_id,
|
project_id,
|
||||||
@ -2492,12 +2813,13 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
|||||||
sor, project_id, task, next_role, comment,
|
sor, project_id, task, next_role, comment,
|
||||||
next_title=decision.get('next_title', '') or '',
|
next_title=decision.get('next_title', '') or '',
|
||||||
next_desc=decision.get('next_desc', '') or '')
|
next_desc=decision.get('next_desc', '') or '')
|
||||||
# 完备性校验(代码只查漏、不替 PM 决策):把结构化事实回给 PM/日志,
|
# 完备性校验(代码只查漏、不替 PM 决策):缺口事实持久化为通知,
|
||||||
# 漏派模块、脚手架缺依赖、依赖成环都在这里暴露。
|
# 注入 PM 下一回合上下文(元景故障教训:只打日志没人看,事实必须送到 PM 手上)。
|
||||||
try:
|
try:
|
||||||
_gaps = await _check_orchestration_gaps(sor, project_id, task, next_role, next_tid)
|
_gaps = await _check_orchestration_gaps(sor, project_id, task, next_role, next_tid)
|
||||||
if _gaps:
|
if _gaps:
|
||||||
logger.warning(f"编排完备性告警 task={next_tid}: {_gaps}")
|
logger.warning(f"编排完备性告警 task={next_tid}: {_gap_texts(_gaps)}")
|
||||||
|
await _save_gap_notices(sor, project_id, _gaps, source_task_id=next_tid)
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
logger.warning(f"_check_orchestration_gaps failed: {_e}")
|
logger.warning(f"_check_orchestration_gaps failed: {_e}")
|
||||||
return {"status": "approved", "task_id": task_id, "next_task_id": next_tid, "next_role": next_role, "comment": comment}
|
return {"status": "approved", "task_id": task_id, "next_task_id": next_tid, "next_role": next_role, "comment": comment}
|
||||||
|
|||||||
@ -672,7 +672,7 @@ def load_pipeline_service():
|
|||||||
# 排除 paused 项目:paused = 完全暂停推进(不派发/不审查/不审核),三个 poller 语义统一,
|
# 排除 paused 项目:paused = 完全暂停推进(不派发/不审查/不审核),三个 poller 语义统一,
|
||||||
# 否则会出现「任务照常派发执行、却卡在 PM 审核」的半吊子状态(2026-08 实测)。
|
# 否则会出现「任务照常派发执行、却卡在 PM 审核」的半吊子状态(2026-08 实测)。
|
||||||
recs = await sor.sqlExe(
|
recs = await sor.sqlExe(
|
||||||
"SELECT t.id, t.tenant_id, t.role, t.depends_on FROM pipeline_tasks t "
|
"SELECT t.id, t.tenant_id, t.role, t.depends_on, t.params FROM pipeline_tasks t "
|
||||||
"WHERE t.state='submitted' AND t.pipeline_id='role_task' "
|
"WHERE t.state='submitted' AND t.pipeline_id='role_task' "
|
||||||
"AND t.claimed_by IS NULL "
|
"AND t.claimed_by IS NULL "
|
||||||
"AND NOT EXISTS (SELECT 1 FROM sd_projects p WHERE p.id=t.tenant_id AND p.status='paused') "
|
"AND NOT EXISTS (SELECT 1 FROM sd_projects p WHERE p.id=t.tenant_id AND p.status='paused') "
|
||||||
@ -689,7 +689,11 @@ def load_pipeline_service():
|
|||||||
|
|
||||||
# 依赖门控提前到候选筛选:批量查 depends_on 里所有依赖任务的状态,
|
# 依赖门控提前到候选筛选:批量查 depends_on 里所有依赖任务的状态,
|
||||||
# 排除依赖未满足的「僵尸 submitted」任务——否则它们占住队列头、饿死其他就绪任务
|
# 排除依赖未满足的「僵尸 submitted」任务——否则它们占住队列头、饿死其他就绪任务
|
||||||
# (一项目卡死 → 全平台瘫痪)。终态 = completed/approved,与 _task_deps_satisfied 一致。
|
# (一项目卡死 → 全平台瘫痪)。
|
||||||
|
# 求值统一走 agent_loop._eval_deps_policy(纯函数,支持 all/any/at_least 启动策略)——
|
||||||
|
# 此前这里是手写 all 语义,与 _task_deps_satisfied 并存构成两处实现,
|
||||||
|
# 语义漂移过(2026-08-25 修复注释可查)。现在单一来源,两处必一致。
|
||||||
|
from .agent_loop import _eval_deps_policy
|
||||||
all_dep_ids = set()
|
all_dep_ids = set()
|
||||||
for rec in (recs or []):
|
for rec in (recs or []):
|
||||||
all_dep_ids.update(_deps_of(getattr(rec, 'depends_on', '') or ''))
|
all_dep_ids.update(_deps_of(getattr(rec, 'depends_on', '') or ''))
|
||||||
@ -703,35 +707,26 @@ def load_pipeline_service():
|
|||||||
|
|
||||||
ready = []
|
ready = []
|
||||||
blocked_dead = [] # 依赖永不可达 → 冒泡人工(逃逸阀)
|
blocked_dead = [] # 依赖永不可达 → 冒泡人工(逃逸阀)
|
||||||
DONE_ = ('completed', 'approved')
|
|
||||||
DEAD_ = ('cancelled', 'failed')
|
|
||||||
for rec in (recs or []):
|
for rec in (recs or []):
|
||||||
_rid = getattr(rec, 'id', '')
|
_rid = getattr(rec, 'id', '')
|
||||||
deps = _deps_of(getattr(rec, 'depends_on', '') or '')
|
deps = _deps_of(getattr(rec, 'depends_on', '') or '')
|
||||||
if not deps:
|
if not deps:
|
||||||
ready.append(rec)
|
ready.append(rec)
|
||||||
continue
|
continue
|
||||||
# 多依赖门控(2026-08-25 修复):必须**逐个**确认依赖处于终态。
|
if _rid in deps:
|
||||||
# 原实现用 dep_state.get(d,'') 判定,虽能拦住「不存在」,但与 agent_loop 的
|
|
||||||
# _task_deps_satisfied 语义不一致(后者查「不在终态」的行,依赖不存在时
|
|
||||||
# 查出 0 行 → 误放行)。两处实现必须同语义,否则一处拦住另一处放行。
|
|
||||||
_miss = [d for d in deps if d not in dep_state]
|
|
||||||
_dead = [d for d in deps if dep_state.get(d, '') in DEAD_]
|
|
||||||
_self = _rid in deps
|
|
||||||
if _miss or _dead or _self:
|
|
||||||
why = []
|
|
||||||
if _miss:
|
|
||||||
why.append("依赖任务不存在:" + "、".join(x[:8] for x in _miss))
|
|
||||||
if _dead:
|
|
||||||
why.append("依赖已作废/失败,永不可达:" + "、".join(
|
|
||||||
f"{x[:8]}({dep_state.get(x,'')})" for x in _dead))
|
|
||||||
if _self:
|
|
||||||
why.append("依赖自身")
|
|
||||||
blocked_dead.append((_rid, getattr(rec, 'tenant_id', ''),
|
blocked_dead.append((_rid, getattr(rec, 'tenant_id', ''),
|
||||||
";".join(why)))
|
"依赖自身(" + _rid[:8] + "),永久阻塞,需人工修正 depends_on"))
|
||||||
continue # 永不放行,交人工
|
continue
|
||||||
if all(dep_state.get(d, '') in DONE_ for d in deps):
|
try:
|
||||||
|
_pp = json.loads(getattr(rec, 'params', '{}') or '{}')
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
_pp = {}
|
||||||
|
_ok, _why, _kind = _eval_deps_policy(deps, dep_state, _pp.get('dep_policy'))
|
||||||
|
if _ok:
|
||||||
ready.append(rec)
|
ready.append(rec)
|
||||||
|
elif _kind == 'dead':
|
||||||
|
blocked_dead.append((_rid, getattr(rec, 'tenant_id', ''), _why))
|
||||||
|
continue # 永不放行,交人工
|
||||||
|
|
||||||
# 逃逸阀:依赖永不可达的任务冒泡人工(去重由 _bubble_blocked_dependency 内部做)
|
# 逃逸阀:依赖永不可达的任务冒泡人工(去重由 _bubble_blocked_dependency 内部做)
|
||||||
if blocked_dead:
|
if blocked_dead:
|
||||||
|
|||||||
@ -272,7 +272,7 @@ SDL_ROLES = [
|
|||||||
aliases=["requirements", "requirement_analysis"],
|
aliases=["requirements", "requirement_analysis"],
|
||||||
system_prompt="""你是需求分析师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
system_prompt="""你是需求分析师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
||||||
next_role="agent.design",
|
next_role="agent.design",
|
||||||
task_title="应用架构与模块设计",
|
task_title="需求分析",
|
||||||
),
|
),
|
||||||
RoleSpec(
|
RoleSpec(
|
||||||
name="agent.design",
|
name="agent.design",
|
||||||
@ -280,7 +280,7 @@ SDL_ROLES = [
|
|||||||
aliases=["designer", "ui", "ux"],
|
aliases=["designer", "ui", "ux"],
|
||||||
system_prompt="""你是系统设计师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
system_prompt="""你是系统设计师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
||||||
next_role="agent.develop",
|
next_role="agent.develop",
|
||||||
task_title="应用脚手架开发",
|
task_title="应用架构与模块设计",
|
||||||
),
|
),
|
||||||
RoleSpec(
|
RoleSpec(
|
||||||
name="agent.develop",
|
name="agent.develop",
|
||||||
@ -288,7 +288,7 @@ SDL_ROLES = [
|
|||||||
aliases=["developer", "dev", "development", "coding"],
|
aliases=["developer", "dev", "development", "coding"],
|
||||||
system_prompt="""你是开发工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
system_prompt="""你是开发工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
||||||
next_role="agent.deploy_test",
|
next_role="agent.deploy_test",
|
||||||
task_title="部署测试",
|
task_title="应用脚手架开发",
|
||||||
),
|
),
|
||||||
RoleSpec(
|
RoleSpec(
|
||||||
name="agent.deploy_test",
|
name="agent.deploy_test",
|
||||||
@ -296,7 +296,7 @@ SDL_ROLES = [
|
|||||||
aliases=["deploy_staging", "staging"],
|
aliases=["deploy_staging", "staging"],
|
||||||
system_prompt="""你是测试环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
system_prompt="""你是测试环境部署工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
||||||
next_role="agent.test",
|
next_role="agent.test",
|
||||||
task_title="功能测试",
|
task_title="部署测试",
|
||||||
),
|
),
|
||||||
RoleSpec(
|
RoleSpec(
|
||||||
name="agent.test",
|
name="agent.test",
|
||||||
@ -305,6 +305,7 @@ SDL_ROLES = [
|
|||||||
system_prompt="""你是测试工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
system_prompt="""你是测试工程师。先 load_skill 加载 role 技能,按其中的职责与应遵守规范执行任务。""",
|
||||||
# 生产部署需人工指令:test 通过后不自动派发 deploy_prod,deploy_prod 由用户明确指令触发
|
# 生产部署需人工指令:test 通过后不自动派发 deploy_prod,deploy_prod 由用户明确指令触发
|
||||||
next_role="",
|
next_role="",
|
||||||
|
task_title="功能测试",
|
||||||
),
|
),
|
||||||
RoleSpec(
|
RoleSpec(
|
||||||
name="agent.deploy_prod",
|
name="agent.deploy_prod",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user