diff --git a/pipeline_service/sdlc_ability.py b/pipeline_service/sdlc_ability.py index ece5a72..102e7b9 100644 --- a/pipeline_service/sdlc_ability.py +++ b/pipeline_service/sdlc_ability.py @@ -29,15 +29,22 @@ from pipeline_core import ( SDL_TOOLS = [ ToolDefinition( name="create_task", - description="创建开发任务,分配给角色agent执行", + description="创建任务,分配给角色agent执行。角色必须属于当前产线(不同产线角色集不同)," + "不确定有哪些角色时先调 list_roles 查看,禁止凭记忆用其他产线的角色名", parameters={ "title": "任务标题", - "role": "目标角色(requirement/design/develop/test/deploy)", + "role": "目标角色(必须是当前产线的角色,用 list_roles 查看)", "description": "任务详细描述", "iteration_id": "归属迭代(可选,默认当前迭代)", }, category="task", ), + ToolDefinition( + name="list_roles", + description="列出当前产线的全部角色(角色名/职责/别名)。建任务前用它确认当前产线有哪些可用角色", + parameters={}, + category="task", + ), ToolDefinition( name="list_tasks", description="查看当前项目的任务列表", @@ -326,6 +333,22 @@ SDL_ROLES = [ # ── handler 独立函数(签名统一 async def handler(sor, params, ctx) -> str) ── +async def _h_list_roles(sor, p, ctx): + """列出当前产线的全部角色(名称/职责/别名)。""" + from pipeline_core import list_roles as _list_roles + cur_pipeline = ctx.get("pipeline_id", "") + if not cur_pipeline: + return "无产线上下文(请先进入某产线的项目)" + specs = _list_roles(cur_pipeline) + if not specs: + return "产线「%s」未注册角色集" % cur_pipeline + lines = [] + for s in specs: + aliases = "(别名: %s)" % "/".join(s.aliases) if getattr(s, "aliases", None) else "" + lines.append("- %s:%s%s" % (s.name, s.description, aliases)) + return "当前产线「%s」共 %d 个角色:\n" % (cur_pipeline, len(specs)) + "\n".join(lines) + + async def _h_create_task(sor, p, ctx): pid = ctx.get("project_id", "") if not pid: @@ -344,6 +367,21 @@ async def _h_create_task(sor, p, ctx): from .agent_loop import _normalize_role role = _normalize_role(role) + # 产线角色守卫:角色必须属于当前产线的角色集,否则拒绝创建。 + # 背景:共享 create_task 被多产线复用,PM 曾按工具描述里硬编码的开发角色名 + # (requirement/design/develop)在投标产线里建章节任务 → 角色无人认领、任务死循环。 + # 校验用 pipeline_core 注册表按当前产线查角色;能力包未注册时放行(逃逸阀,不阻塞)。 + from pipeline_core import get_role_spec, list_roles as _list_roles + cur_pipeline = ctx.get("pipeline_id", "") + if cur_pipeline and _list_roles(cur_pipeline): + spec = get_role_spec(cur_pipeline, role) + if not spec: + avail = "、".join("%s(%s)" % (s.name, s.description) for s in _list_roles(cur_pipeline)) + return ("⚠️ 拒绝创建:角色「%s」不属于当前产线「%s」。当前产线可用角色:%s。" + "请改用上述角色重新创建(可用 list_roles 随时查看)。" + % (role, cur_pipeline, avail)) + role = spec.name # 归一到注册表规范名(比裸名拼接更可靠) + tid = getID() # 迭代归属:显式指定 > 当前迭代(该 project 最新创建的迭代)。 # 「当前迭代」概念:新建迭代后当前迭代即新建的那个,任务默认归到它。 @@ -1784,6 +1822,7 @@ async def _h_verify_env(sor, p, ctx): SDL_HANDLERS = { "create_task": _h_create_task, + "list_roles": _h_list_roles, "list_tasks": _h_list_tasks, "task_detail": _h_task_detail, "reset_task_retry": _h_reset_task_retry, diff --git a/pipeline_service/shared_ability.py b/pipeline_service/shared_ability.py index 7cc8da5..4b0dbc0 100644 --- a/pipeline_service/shared_ability.py +++ b/pipeline_service/shared_ability.py @@ -20,7 +20,7 @@ logger = logging.getLogger("pipeline.shared_ability") # sd_test_* / sd_deploy_envs 的 SDLC 专属工具都不在此列。 _GENERIC_TOOL_NAMES = [ # 任务 - "create_task", "list_tasks", "task_detail", "reset_task_retry", + "create_task", "list_roles", "list_tasks", "task_detail", "reset_task_retry", "approve_task", "complete_task", "cancel_task", # agent 运行 / 诊断 "start_agents", "diagnose_project", "check_progress",