/// engine_triage.mbt —— 下一步该做啥(Task Triage):给"更好的 AI 项目管理工具"补推荐式挑选原语
///
/// dag_ready/board 只"列出"可领取任务;本原语进一步"推荐排序"——对指定命名空间(缺省全量)
/// 把"待领取且依赖已满足"的任务按下述序排出优先级,并给出一条"建议下一步",供 agent
/// 无需全量扫描就能知道此刻该领哪条:
/// 排序键:能力匹配(want) → **skill 命中度(skill 非空时)** → priority(高>中>低) →
/// importance(高>中>低) → depth(叶=可执行单元优先,越小越前) → id(稳定)。
/// 难度标签直接复用 plan_deep gradient 嵌入子任务描述的 `难度梯度 i/n:易/中/难` 标记(未命中按深度粗判)。
/// skill 可选(逗号分隔 skill 关键词):非空时把任务描述与关键词做大小写不敏感的子串匹配,把"skill 库"
/// 变成路由信号,并在 suggestion 里按 SKILL→MODE 建议表给出建议的开发模式;空串 = 不做 skill 路由(零回归)。
/// 复用现有 store.list_all / deps_satisfied / get_status,无新表/依赖。
///|
/// 从任务描述提取 gradient 难度档(易/中/难),复用 R40 的 extract_difficulty 单一抽取来源:
/// 相比旧 `:易]` 后缀粗匹配,能正确识别 calibrate 的真实难度(如 `易 d=1.0` → 易)。
/// 未命中返回空串(由调用方按叶/分支兜底)。
fn triage_label_of(desc : String) -> String {
let d = extract_difficulty(desc)
if d.contains("难") {
"难"
} else if d.contains("中") {
"中"
} else if d.contains("易") {
"易"
} else {
""
}
}
///|
/// 优先级/重要度的档位秩(越小越高优)。
fn ranking(x : String) -> Int32 {
match x {
"高" => 0
"中" => 1
"低" => 2
_ => 1
}
}
///|
/// 截断描述到 max 字符(避免返回里塞一整棵描述)。
fn truncate(s : String, max : Int32) -> String {
if s.length() <= max {
s
} else if max > 0 && max <= s.length() {
s[0:max].to_owned() + "…"
} else {
s
}
}
///|
/// 下一步排行行(triage 内部行)。
priv struct TriageRow {
id : String
ns : String
priority : String
importance : String
difficulty : String
depth : Int32
desc : String
depends_on : Array[String]
// skill 路由:命中的 skill 关键词(原样大小写);skill 为空时恒为空数组(零回归)。
skill_hits : Array[String]
}
///|
/// 候选描述/ id 是否命中能力关键词(want 为空视为全命中,即不做能力偏好)。
fn cap_match(r : TriageRow, want : String) -> Bool {
want == "" || r.desc.contains(want) || r.id.contains(want)
}
///|
/// 把逗号分隔的 skill 关键词串切成数组(trim + 去空)。
fn skill_keys(skill : String) -> Array[String] {
if skill == "" {
[]
} else {
skill
.split(",")
.map(fn(s) { s.trim().to_owned() })
.filter(fn(s) { s != "" })
.collect()
}
}
///|
/// 纯计算 skill 命中打分:对每个 key 做大小写不敏感的子串匹配(key 与 desc 都 lowercase 后 contains)。
/// 返回 (命中条数, 命中的原样关键词列表)。关键词可能含 `-`,只 lowercase 不做分词。
fn skill_hit(desc : String, keys : Array[String]) -> (Int32, Array[String]) {
let d = desc.to_lower()
let hits : Array[String] = []
for k in keys {
if d.contains(k.to_lower()) {
hits.push(k)
}
}
(hits.length(), hits)
}
///|
/// SKILL→MODE 建议表(R§5.2-4):只匹配关键词前缀,未命中返回空串。
/// 把 skill 库变成"建议开发模式"的路由信号——命中哪个 skill,就建议按哪种模式去跑。
fn skill_mode(key : String) -> String {
let k = key.to_lower()
let table : Array[(String, String)] = [
("tdd", "fix_and_merge"),
("test-driven-development", "fix_and_merge"),
("systematic-debugging", "bugfind"),
("debugging", "bugfind"),
("requesting-code-review", "verify"),
("code-review", "verify"),
("subagent-driven-development", "advance"),
("spike", "explore"),
("explore", "explore"),
("plan", "advance"),
("research-paper-writing", "polish"),
("ideation", "explore"),
("grill-me", "verify"),
("superpowers", "advance"),
]
for (prefix, mode) in table {
if k.has_prefix(prefix) {
return mode
}
}
""
}
///|
/// 排行主键之后的后继键:priority(高>中>低) → importance(高>中>低) → depth(小者前) → id(稳定)。
/// 在能力匹配与 skill 命中度都打平时复用(同一排序语义的单一真源)。
fn triage_rest(a : TriageRow, b : TriageRow) -> Int32 {
let pc = ranking(a.priority) - ranking(b.priority)
if pc != 0 {
return pc
}
let ic = ranking(a.importance) - ranking(b.importance)
if ic != 0 {
return ic
}
if a.depth != b.depth {
if a.depth < b.depth {
return -1
} else {
return 1
}
}
a.id.compare(b.id)
}
///|
/// 下一步推荐(Triage,带能力偏好路由 + skill 命中度路由):返回 { count, ns, agent, want, skill?, suggestion?, tasks:[...] }。
/// ns_filter 为空表示全量;只统计"待领取且依赖已满足"的任务。want 为能力关键词(Marketplookup 雏形):
/// 非空时,命中该关键词的候选排在头部(按能力匹配路由"这个 agent 擅长啥就先领啥"),不命中者照旧跟进。
/// skill 可选(逗号分隔 skill 关键词):非空时按 skill 命中度排在能力匹配之后、优先级之前,并在 suggestion
/// 里给出建议的开发模式(SKILL→MODE 建议表);空串 = 不做 skill 路由,排序与输出与旧版完全一致(零回归)。
pub fn FistEngine::triage(
self : FistEngine,
ns_filter : String,
agent : String,
want : String,
skill~ : String = "",
) -> Json {
let keys = skill_keys(skill)
let rows : Array[TriageRow] = []
// R12-R7 性能:与 ready_tasks 同款——deps_satisfied 逐条 DB 读,
// 这里一次遍历建 completed 映射,O(1) 判依赖。
let all_tasks = self.list_all()
let completed : Map[String, Bool] = Map([])
for t in all_tasks {
completed.set(t.get_id(), t.get_status().is_completed())
}
for t in all_tasks {
if ns_filter != "" && t.ns != ns_filter {
continue
}
if !t.get_status().is_pending() {
continue
}
if !deps_ready_memo(t.depends_on, completed) {
continue
}
let lab : String = triage_label_of(t.description)
let difficulty = if lab != "" {
lab
} else if t.is_leaf() {
"叶(可执行)"
} else {
"分支"
}
let desc = truncate(t.description, 60)
let (_, hits) = skill_hit(desc, keys)
rows.push({
id: t.get_id(),
ns: t.ns,
priority: t.priority,
importance: t.importance,
difficulty,
depth: t.get_depth(),
desc,
depends_on: t.depends_on,
skill_hits: hits,
})
}
rows.sort_by(fn(a, b) {
// 能力偏好:命中 want 的排在头部(Router 雏形)
let am = cap_match(a, want)
let bm = cap_match(b, want)
if am != bm {
if am {
-1
} else {
1
}
} else if skill != "" {
// skill 命中度:命中多的排前(只在 skill 非空时生效,零回归)
let ah = a.skill_hits.length()
let bh = b.skill_hits.length()
if ah != bh {
bh - ah
} else if ah > 0 {
// 相等再按命中的第一个关键词字典序(越小越前,稳定可复现)
let ka = a.skill_hits[0].to_lower()
let kb = b.skill_hits[0].to_lower()
if ka != kb {
ka.compare(kb)
} else {
triage_rest(a, b)
}
} else {
triage_rest(a, b)
}
} else {
triage_rest(a, b)
}
})
let tasks : Array[Json] = []
for r in rows {
tasks.push(
Json::object({
"id": Json::string(r.id),
"ns": Json::string(r.ns),
"priority": Json::string(r.priority),
"importance": Json::string(r.importance),
"difficulty": Json::string(r.difficulty),
"depth": Json::number(r.depth.to_double()),
"desc": Json::string(r.desc),
// 真实 DAG 前驱(复用 R39 gradient_dag 建边)——"就绪原因"看得见
"depends_on": Json::array(r.depends_on.map(fn(s) { Json::string(s) })),
// skill 命中(skill 为空时为空数组,零回归)
"skill_hits": Json::array(r.skill_hits.map(fn(s) { Json::string(s) })),
}),
)
}
let m : Map[String, Json] = Map([
("count", Json::number(rows.length().to_double())),
("ns", Json::string(ns_filter)),
("agent", Json::string(agent)),
("want", Json::string(want)),
("skill", Json::string(skill)),
("tasks", Json::array(tasks)),
])
if !rows.is_empty() {
let top = rows[0]
let cap = if cap_match(top, want) && want != "" {
";能力匹配<\{want}>"
} else {
""
}
let sk = if top.skill_hits.length() > 0 { top.skill_hits[0] } else { "" }
let mode = skill_mode(sk)
let skSuffix = if sk != "" {
";skill=<\{sk}>,建议走 <\{mode}> 模式"
} else {
""
}
let reason = "依赖已满足;优先级\{top.priority}/重要度\{top.importance}/难度\{top.difficulty}\{cap}\{skSuffix},建议优先领取"
m.set(
"suggestion",
Json::object({
"task_id": Json::string(top.id),
"ns": Json::string(top.ns),
"reason": Json::string(reason),
"skill": Json::string(sk),
"mode": Json::string(mode),
}),
)
}
Json::object(m)
}
///|
/// 下一步自动取单(自驱推进):按 triage 的能力/skill 推荐取顶部候选并认领给 `agent`(0 状态迁移:待领取→已领取)。
/// 复用 `triage`(排行+suggestion)与 `claim`(认领),只动一条推荐任务,不动看门狗主流程(零回归)。
/// skill 可选(逗号分隔 skill 关键词):非空时按 skill 命中度路由取单;空串 = 不做 skill 路由(零回归)。
/// 返回 { picked, remaining, claimed, ns, want, skill, task?, note|error? }。
pub fn FistEngine::pick_next(
self : FistEngine,
ns_filter : String,
agent : String,
want : String,
now : String,
skill~ : String = "",
) -> Json {
let tg = self.triage(ns_filter, agent, want, skill~)
let m = match tg {
Object(mm) => mm
_ => Map([])
}
let count = match m.get("count") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let m2 = match m.get("suggestion") {
Some(Object(s)) => s
_ => Map([])
}
let picked = match m2.get("task_id") {
Some(String(id)) => Some(id)
_ => None
}
match picked {
None =>
Json::object({
"picked": Json::string(""),
"remaining": Json::number(count.to_double()),
"claimed": Json::boolean(false),
"ns": Json::string(ns_filter),
"want": Json::string(want),
"skill": Json::string(skill),
"note": Json::string(
"无可领取的推荐任务(待领取且依赖已满足集合为空)",
),
})
Some(pid) =>
match self.claim(task_id=pid, assignee=agent, now~) {
Ok(t) =>
Json::object({
"picked": Json::string(pid),
"remaining": Json::number(
(if count > 0 { count - 1 } else { 0 }).to_double(),
),
"claimed": Json::boolean(true),
"ns": Json::string(ns_filter),
"want": Json::string(want),
"skill": Json::string(skill),
"task": t.to_json(),
"note": Json::string("已按推荐认领该条"),
})
Err(e) =>
Json::object({
"picked": Json::string(""),
"remaining": Json::number(count.to_double()),
"claimed": Json::boolean(false),
"ns": Json::string(ns_filter),
"want": Json::string(want),
"skill": Json::string(skill),
"error": Json::string(e),
"note": Json::string("认领失败,未改动任何状态"),
})
}
}
}