feat: 任务来源分类机制 + classify_task 工具 + 模块技能注入项目空间

根因修复(develop 不调 fix_bug 导致 bug 恒 open、链断):
1. 三条任务创建路径打 task_kind 标记:_create_next_task=new_dev、
   _rollback_task_chain=rework、_pm_create_tasks 按标题判 bug_fix/new_dev
2. bug_capability 新增 classify_task 工具:读 task_kind 标记判断任务来源
   (老任务按 rollback_from/title 兜底),develop 据此决定是否走 fix_bug 状态机
3. TOOL_SCHEMAS 注册 classify_task + capability_ctx 注入 task_id
4. 增强能力:_collect_module_skills 运行时扫描 workspace repos/*/skill/SKILL.md,
   作为「项目模块」scope 注入技能目录 + load_skill 支持加载模块技能全文,
   让项目角色知道引用的业务模块怎么用(架构/数据模型/挂载函数/坑)
This commit is contained in:
ymq 2026-08-23 21:23:52 +08:00
parent bf5013f220
commit 7ede8f4d7a
3 changed files with 143 additions and 2 deletions

View File

@ -623,6 +623,66 @@ async def _get_workspace_dir(sor, project_id):
return _resolve_workspace(ws)
def _parse_skill_frontmatter(content):
"""解析技能 SKILL.md 的 frontmatter--- 之间),返回 dict。无 frontmatter 返回 {}"""
content = (content or "").strip()
if not content.startswith("---"):
return {}
end = content.find("---", 3)
if end == -1:
return {}
fm = {}
for line in content[3:end].strip().split("\n"):
line = line.strip()
if ":" in line:
k, _, v = line.partition(":")
k = k.strip()
v = v.strip().strip('"').strip("'")
if v.startswith("[") and v.endswith("]"):
v = [x.strip().strip('"').strip("'") for x in v[1:-1].split(",") if x.strip()]
fm[k] = v
return fm
def _collect_module_skills(workspace_dir):
"""扫描项目 workspace 的 repos/*/skill/SKILL.md收集模块技能模块怎么用
模块仓库自带 skill/SKILL.md架构/数据模型/挂载函数/坑位但这些不进 skill_loader
的静态文件树导致项目角色不知道模块怎么用这里运行时扫描作为项目模块scope
注入技能目录 + 支持 load_skill 加载全文返回 [{name, description, content, path}]
"""
modules = []
repos_dir = os.path.join(workspace_dir, 'repos')
if not os.path.isdir(repos_dir):
return modules
for entry in sorted(os.listdir(repos_dir)):
skill_file = os.path.join(repos_dir, entry, 'skill', 'SKILL.md')
if not os.path.isfile(skill_file):
continue
try:
with open(skill_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception:
continue
fm = _parse_skill_frontmatter(content)
name = (fm.get('name') or entry).strip()
description = (fm.get('description') or '').strip()
if not description:
for line in content.split("\n"):
line = line.strip()
if line and not line.startswith("#") and not line.startswith("---"):
description = line[:200]
break
modules.append({
"name": name,
"description": description,
"content": content,
"path": skill_file,
"repo": entry,
})
return modules
async def _build_qna_section(sor, task_id, role, agent_id=None):
from .communication import get_task_qa
qa = await get_task_qa(task_id, role=role, agentid=agent_id)
@ -780,6 +840,9 @@ async def _create_next_task(sor, project_id, task, next_role, pm_comment=''):
new_title = f"{title}{next_role}阶段)"
new_params = {**params, 'previous_role': _normalize_role(getattr(task, 'role', '')),
'previous_task_id': getattr(task, 'id', ''), 'pm_comment': pm_comment}
# 任务来源标记系统自动派生design→develop→deploy_test→test都是「新开发」链
# 不含 bug 修复语义。develop 角色据此判断是否要走 fix_bug 状态机。
new_params['task_kind'] = 'new_dev'
# 清除 pm_assigned它是「PM 按模块清单派发」的标记,只作用于当前任务;
# 下一角色任务是系统自动创建(非 PM 派发),若继承会污染——例如 PM 派发的 design 任务
# 带 pm_assigned=Truedesign approved 后自动创建的 develop 任务继承了它,被 1785 行的
@ -859,6 +922,8 @@ async def _rollback_task_chain(sor, project_id, task_id, rollback_role, comment)
except (json.JSONDecodeError, TypeError):
tp = {}
new_params = {**tp, 'rollback_from': task_id, 'rollback_comment': comment}
# 任务来源标记:回退重做 = 修复导致上游失败的缺陷质检重做develop 据此必须走 bug 状态机。
new_params['task_kind'] = 'rework'
# 清 pm_assigned回退重做任务是系统重建的任务非 PM 按模块清单派发),继承 target_task 的
# pm_assigned 会污染——若 target 是应用级 develop历史脏数据带 pm_assigned=Trueapprove 后被
# 「模块级 develop → 跳过 deploy_test」误判任务链断在 approved。与 _create_next_task 的 pop 对齐。
@ -1088,8 +1153,6 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""):
loader = get_skill_loader(skills_dir)
merged = loader.get_merged(pipeline_id=pid, role=role,
project_id=project_id, org_id=org_id or '0')
if not merged:
return ""
skills = sorted(merged.values(),
key=lambda s: (-int(getattr(s, 'essential', False)),
-SCOPE_PRIORITY.get(getattr(s, 'scope', ''), 0)))
@ -1097,6 +1160,17 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""):
for s in skills[:60]:
tag = SCOPE_TAG.get(getattr(s, 'scope', ''), getattr(s, 'scope', ''))
lines.append(f"- [{tag}] {s.name}: {(s.description or '')[:100]}")
# 项目模块技能workspace repos/*/skill/SKILL.md模块怎么用——架构/数据模型/挂载函数/坑位),
# 运行时扫描注入让项目角色知道引用了哪些模块、每个模块怎么挂载load_xxx 入口/库名/坑)。
try:
ws = await _get_workspace_dir(sor, project_id)
module_skills = _collect_module_skills(ws)
if module_skills:
lines.append("\n## 项目模块技能本项目引用的业务模块load_skill 加载全文了解模块怎么用)")
for m in module_skills:
lines.append(f"- [项目模块] {m['name']}: {(m['description'] or '')[:100]}")
except Exception as e:
logger.warning(f"module skills collect failed: {e}")
return "\n".join(lines)
except Exception as e:
logger.warning(f"role skills load failed: {e}")
@ -1154,6 +1228,14 @@ async def _load_skill_by_name(sor, project_id, role, org_id, name, file_path=Non
merged = loader.get_merged(pipeline_id=pid, role=role,
project_id=project_id, org_id=org_id or '0')
if name not in merged:
# 项目模块技能workspace repos/*/skill/SKILL.md不在 skill_loader 静态树里,运行时补查
try:
ws = await _get_workspace_dir(sor, project_id)
for m in _collect_module_skills(ws):
if m['name'] == name or m['repo'] == name:
return f"## [项目模块] {m['name']}\n{m['description']}\n\n{m['content']}"
except Exception:
pass
names = ", ".join(sorted(merged.keys())) or "(无可用技能)"
return f"FAIL: 技能 '{name}' 不存在。可用技能: {names}"
skill = merged[name]
@ -1237,6 +1319,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
"iteration_id": _iter.get('id', '') if _iter else '',
"who": role,
"agent_id": agent_id,
"task_id": task_id,
}
system = (AGENT_SYSTEM_PROMPT
@ -1483,6 +1566,13 @@ async def _pm_create_tasks(sor, project_id, params, parent_task_id=None):
cancelled.append(did)
tparams = {"description": desc, "pm_assigned": True}
# 任务来源标记PM 派发的任务按 title/description 判断——
# 含「修复 Bug」= bug_fixdevelop 必须走 fix_bug 状态机);否则 = new_dev模块开发等
_title_desc = f"{title} {desc}"
if '修复 Bug' in _title_desc or '修复Bug' in _title_desc or '修复bug' in _title_desc:
tparams['task_kind'] = 'bug_fix'
else:
tparams['task_kind'] = 'new_dev'
if iteration_name:
tparams["iteration_id"] = iteration_name
tid = getID()

View File

@ -11,6 +11,7 @@ scope 约定sd_bugs 无 project_id 列,用 iteration_id 做范围校验C
"""
import logging
import json
from datetime import datetime
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
@ -223,3 +224,46 @@ def _rec_to_dict(rec):
except Exception:
pass
return {}
async def classify_task(task_id, iteration_id="", who=None, agent_id=None):
"""判断任务来源new_dev(新开发) / bug_fix(修复 Bug) / rework(质检重做)。
优先读任务 params.task_kind 标记三条创建路径已打老任务无标记则按
params.rollback_from / title 兜底推断develop 据此决定是否走 fix_bug 状态机
- bug_fix / rework任务开始 start_fix完成 fix_bug
- new_dev纯开发不走 bug 状态机除非过程中自己 report 了新 bug
返回 (ok, {"task_kind": ..., "reason": ..., "title": ...} 或错误消息)
"""
if not task_id:
return False, "缺少 task_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(
"SELECT id, title, params FROM pipeline_tasks WHERE id=${tid}$", {"tid": task_id})
await sor.sqlExe("COMMIT", {})
if not recs:
return False, f"任务不存在: {task_id}"
rec = recs[0]
title = getattr(rec, 'title', '') or ''
params_str = getattr(rec, 'params', '{}') or '{}'
try:
params = json.loads(params_str) if isinstance(params_str, str) else (params_str or {})
except (json.JSONDecodeError, TypeError):
params = {}
kind = (params.get('task_kind') or '').strip()
reason = "params.task_kind 标记"
if not kind:
# 老任务兜底rollback_from 存在 → reworktitle 含「修复 Bug」→ bug_fix否则 new_dev
if params.get('rollback_from'):
kind = 'rework'
reason = "params.rollback_from 存在(回退重做)"
elif ('修复 Bug' in title) or ('修复Bug' in title) or ('修复bug' in title):
kind = 'bug_fix'
reason = "title 含「修复 Bug」"
else:
kind = 'new_dev'
reason = "无 bug 相关标记,默认新开发"
return True, {"task_id": task_id, "task_kind": kind, "title": title, "reason": reason}

View File

@ -139,6 +139,12 @@ TOOL_SCHEMAS = {
"params": {"status": "状态(可选)", "severity": "严重度(可选)"},
"required": [],
},
"classify_task": {
"module": "bug_capability",
"description": "判断任务来源new_dev(新开发)/bug_fix(修复Bug)/rework(质检重做)。develop 接任务后先调用,据此决定是否走 start_fix→fix_bug 状态机",
"params": {"task_id": "任务ID(可选,默认当前任务)"},
"required": [],
},
"start_fix": {
"module": "bug_capability",
"description": "开始修复 Bugopen/confirmed → fixing记录处理人。回退重做/修复 Bug 任务里修完代码必须调用open 状态也可直接修复PM review_rollback 回退时不走 confirm_bug",
@ -269,6 +275,7 @@ async def exec_capability_tool(tool_name, params, ctx):
p.setdefault("iteration_id", ctx.get("iteration_id", ""))
p.setdefault("who", ctx.get("who", ""))
p.setdefault("agent_id", ctx.get("agent_id", ""))
p.setdefault("task_id", ctx.get("task_id", ""))
try:
mod = importlib.import_module(f"pipeline_service.{schema['module']}")