/// engine/engine_supervisor.mbt —— 超级管家专属工具的引擎侧纯计算/编排
///
/// 对应 docs/2026-10-12-fist-mbt-supervisor-tools-plan.md:
/// supervisor_status / supervisor_task_tree / supervisor_kill_plan / supervisor_task_upsert
/// 复用既有 store/engine API,不新增表、不改状态机;kill 只出方案不执行(金条四)。
///|
/// 全库(或指定 ns)状态聚合:供 supervisor_status 使用。纯读。
/// `detail=false`(默认)时 active 里每个任务的 description 截断到 200 字节
/// (字符边界安全,见 `@core.desc_clip`),控 MCP 返回体积——task_plan_deep 开满
/// gradient/reinject_context/boundary_probe 后描述会递归膨胀到数 KB,全量回传会炸。
/// `detail=true` 返回完整描述(调用方按需取全文)。
pub fn FistEngine::supervisor_status(
self : FistEngine,
ns_filter~ : String = "",
detail~ : Bool = false,
) -> Json {
let tasks = self.list_all()
let by_status : Map[String, Int32] = Map([])
let by_ns : Map[String, Int32] = Map([])
let active : Array[Json] = []
let mut done = 0
let mut total = 0
let desc_limit = if detail { -1 } else { 200 }
for t in tasks {
let st = t.status_to_string()
if t.ns == ns_filter || ns_filter == "" {
total = total + 1
let cur = by_status.get(st).unwrap_or(0)
by_status.set(st, cur + 1)
let cur_ns = by_ns.get(t.ns).unwrap_or(0)
by_ns.set(t.ns, cur_ns + 1)
if st == "已完成" || st == "已归档" {
done = done + 1
}
let a = t.get_status()
let is_active = !a.is_pending() && !a.is_completed() && !a.is_archived()
if is_active {
active.push(t.to_json(desc_limit~))
}
}
}
let status_obj : Map[String, Json] = Map([])
by_status.each(fn(k, v) { status_obj.set(k, Json::number(v.to_double())) })
let ns_obj : Map[String, Json] = Map([])
by_ns.each(fn(k, v) { ns_obj.set(k, Json::number(v.to_double())) })
let progress = if total == 0 {
0.0
} else {
done.to_double() * 100.0 / total.to_double()
}
Json::object({
"total": Json::number(total.to_double()),
"by_status": Json::object(status_obj),
"by_namespace": Json::object(ns_obj),
"progress_pct": Json::number(progress),
"active_count": Json::number(active.length().to_double()),
"active": Json::array(active),
})
}
///|
/// 任务树节点:递归聚合子任务完成度。返回 (节点 JSON, 子树完成数, 子树总数)。纯读。
/// `desc_limit > 0` 时节点 task 的 description 截断(控返回体积,与 supervisor_status 同口径)。
fn FistEngine::supervisor_tree_node(
self : FistEngine,
idx : Map[String, Array[String]],
task_id : String,
desc_limit : Int32,
) -> (Json, Int32, Int32)? {
match self.get_task(task_id) {
None => None
Some(t) => {
let children : Array[Json] = []
let mut child_total = 0
let mut child_done = 0
match idx.get(task_id) {
Some(ids) =>
for cid in ids {
match self.supervisor_tree_node(idx, cid, desc_limit) {
Some((cj, done, total)) => {
child_done = child_done + done
child_total = child_total + total
children.push(cj)
}
None => ()
}
}
None => ()
}
let st = t.get_status()
if st.is_completed() || st.is_archived() {
child_done = child_done + 1
}
child_total = child_total + 1
let node : Json = Json::object({
"task": t.to_json(desc_limit~),
"subtree_total": Json::number(child_total.to_double()),
"subtree_done": Json::number(child_done.to_double()),
"children": Json::array(children),
})
Some((node, child_done, child_total))
}
}
}
///|
/// 任务树状态与进度汇报:root 起递归,每节点带 subtree_done/subtree_total。
/// `detail=false`(默认)时各节点 task 的 description 截断到 200 字节(与 supervisor_status 同口径)。
pub fn FistEngine::supervisor_task_tree(
self : FistEngine,
root_id : String,
detail~ : Bool = false,
) -> Result[Json, String] {
match self.get_task(root_id) {
None => Err("task not found: \{root_id}")
Some(_) => {
let idx = self.children_index()
let desc_limit = if detail { -1 } else { 200 }
match self.supervisor_tree_node(idx, root_id, desc_limit) {
None => Err("task not found: \{root_id}")
Some((tree, _done, _total)) =>
Ok(Json::object({ "root_id": Json::string(root_id), "tree": tree }))
}
}
}
}
///|
/// kill 硬门链单门结果。
priv struct SupervisorGate {
gate : String
passed : Bool
reason : String
}
///|
fn supervisor_gate_json(g : SupervisorGate) -> Json {
Json::object({
"gate": Json::string(g.gate),
"passed": Json::boolean(g.passed),
"reason": Json::string(g.reason),
})
}
///|
/// kill 硬门链评估(**只读纯计算,不执行任何终止**):
/// G1 身份门:by == human_steward 或现任主助理
/// G2 目标存在
/// G3 不可杀目标:根任务(拳长承诺,design v2 的 kill 权唯一性)不可杀
/// G4 终态检查:已完成/已归档不可杀
/// G5 force+reason:force=true 且 reason 非空才 allowed,否则 only_plan
/// verdict ∈ allowed / only_plan / blocked
pub fn FistEngine::supervisor_kill_plan(
self : FistEngine,
task_id~ : String,
by~ : String,
reason~ : String,
force~ : Bool = false,
apply_pause~ : Bool = false,
now~ : String = "",
) -> Json {
let gates : Array[SupervisorGate] = []
// G1 身份门
let primary = self.assistant_active_primary()
let primary_agent = match primary {
Some(r) => r.agent
None => ""
}
let g1_pass = by == "human_steward" ||
(primary_agent != "" && by == primary_agent)
gates.push({
gate: "G1_identity",
passed: g1_pass,
reason: if g1_pass {
"操作者是人类指挥官或现任主助理"
} else {
"操作者既不是 human_steward 也不是现任主助理(kill 权唯一,见 multi-agent-lock design v2)"
},
})
// G2 目标存在
match self.get_task(task_id) {
None => {
gates.push({
gate: "G2_exists",
passed: false,
reason: "task not found: \{task_id}",
})
let gate_js : Array[Json] = gates.map(supervisor_gate_json)
return Json::object({
"task_id": Json::string(task_id),
"verdict": Json::string("blocked"),
"gates": Json::array(gate_js),
"steps": Json::array([]),
"note": Json::string("本工具只出方案不执行(金条四)"),
})
}
Some(t) => {
gates.push({ gate: "G2_exists", passed: true, reason: "目标存在", })
// G3 不可杀目标:根任务 = 拳长承诺(planner 分级近似),不可杀
let is_root = match t.get_parent() {
None => true
Some(_) => false
}
if is_root {
gates.push({
gate: "G3_protected_planner",
passed: false,
reason: "根任务=拳长承诺,不可杀(force 不可越过;确需撤销请由指挥官走 archive/人工裁决)",
})
} else {
gates.push({
gate: "G3_protected_planner",
passed: true,
reason: "非根任务,可进入终止方案",
})
}
// G4 终态检查
let st = t.get_status()
let terminal = st.is_completed() || st.is_archived()
gates.push({
gate: "G4_terminal_state",
passed: !terminal,
reason: if terminal {
"目标已是终态 [\{st.to_string()}],无需终止"
} else {
"目标处于活跃态,可终止"
},
})
// G5 force+reason
let g5_pass = force && reason != ""
gates.push({
gate: "G5_force_reason",
passed: g5_pass,
reason: if g5_pass {
"force=true 且 reason 非空"
} else {
"需要 force=true 且非空 reason 才给出 allowed(当前仅出方案)"
},
})
let blocked = !g1_pass || is_root || terminal
let verdict = if blocked {
"blocked"
} else if g5_pass {
"allowed"
} else {
"only_plan"
}
// 补偿建议(advisory,不自动执行)
let steps : Array[Json] = []
ignore(
steps.push(
Json::string(
"按 saga_register 为该任务已交付的前向步骤登记可补偿动作(如尚未登记)",
),
),
)
ignore(
steps.push(
Json::string(
"终止前先 supervisor_kill_plan(apply_pause=true) 走可逆暂停,观察无副作用再人工裁决",
),
),
)
ignore(
steps.push(
Json::string(
"真正的 kill(删除子树/杀进程)由指挥官人工执行或走 assistant_kill(v3 设计,PID 通道另立项)",
),
),
)
let gate_js : Array[Json] = gates.map(supervisor_gate_json)
let m : Map[String, Json] = Map([
("task_id", Json::string(task_id)),
("by", Json::string(by)),
("status", Json::string(st.to_string())),
("verdict", Json::string(verdict)),
("gates", Json::array(gate_js)),
("steps", Json::array(steps)),
(
"compensation_hint",
Json::string(
"saga_register / saga_rollback / saga_repair(LIFO 局部补偿)",
),
),
(
"note",
Json::string(
"本工具只出方案不执行(金条四);apply_pause=true 时另行走可逆的 pause",
),
),
])
// 可逆出口:apply_pause=true 且未被 G3/G4 拦下时执行 pause(已暂停可 resume)
if apply_pause && !is_root && !terminal {
// engine 包不依赖 @env:now 缺省回退任务自身 updated_at
let ts = if now == "" { t.get_updated_at() } else { now }
match self.pause_task(task_id~, now=ts) {
Ok(t) =>
m.set(
"pause_applied",
Json::object({
"ok": Json::boolean(true),
"status": Json::string(t.status_to_string()),
"reversible": Json::boolean(true),
}),
)
Err(e) =>
m.set(
"pause_applied",
Json::object({
"ok": Json::boolean(false),
"error": Json::string(e),
}),
)
}
}
Json::object(m)
}
}
}
///|
/// 可选的可逆出口:按 kill 方案走 pause(已暂停可 resume,可逆)。
pub fn FistEngine::supervisor_kill_apply_pause(
self : FistEngine,
task_id~ : String,
now~ : String,
) -> Result[@core.Task, String] {
self.pause_task(task_id~, now~)
}
///|
/// 单条 upsert item 的解析结果(内部结构)。
priv struct SupervisorUpsertItem {
task_id : String
description : String
priority : String
depends_on : Array[String]
}
///|
fn parse_upsert_item(v : Json) -> SupervisorUpsertItem? {
match v {
Json::Object(m) => {
let get = fn(k : String) -> String {
match m.get(k) {
Some(Json::String(s)) => s
_ => ""
}
}
let deps : Array[String] = match m.get("depends_on") {
Some(Json::Array(a)) =>
a.map(fn(d) { if d is Json::String(s) { s } else { "" } })
_ => []
}
Some({
task_id: get("task_id"),
description: get("description"),
priority: get("priority"),
depends_on: deps,
})
}
_ => None
}
}
///|
/// 更新目标项目任务列表:无 task_id 的 item=追加新根任务(publish,仅 human_steward),
/// 有 task_id 的 item=更新描述/优先级/追加依赖。终态任务拒改。逐条独立成败。
pub fn FistEngine::supervisor_task_upsert(
self : FistEngine,
project_dir~ : String,
by~ : String,
items : Array[Json],
now~ : String,
ns~ : String = "default",
) -> Json {
let results : Array[Json] = []
let mut ok_count = 0
for raw in items {
match parse_upsert_item(raw) {
None =>
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"error": Json::string("item 不是对象或缺少 description"),
}),
),
)
Some(item) =>
if item.task_id != "" {
// 更新已有任务
match self.get_task(item.task_id) {
None =>
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"task_id": Json::string(item.task_id),
"error": Json::string("task not found"),
}),
),
)
Some(t) => {
let st = t.get_status()
if st.is_completed() || st.is_archived() {
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"task_id": Json::string(item.task_id),
"error": Json::string(
"终态任务 [\{st.to_string()}] 拒改;请走 reopen_task 或重新发布",
),
}),
),
)
} else {
let mut c = t
if item.description != "" {
c = c.with_description(
preserve_plan_markers(t.description, item.description),
now,
)
}
if item.priority != "" {
c = c.with_priority(item.priority, now)
}
for d in item.depends_on {
if d != "" {
c = c.with_depends_on(d, now)
}
}
match self.store.update_task(c) {
Ok(_) => {
ok_count = ok_count + 1
ignore(
results.push(
Json::object({
"ok": Json::boolean(true),
"task_id": Json::string(item.task_id),
"action": Json::string("updated"),
}),
),
)
}
Err(e) =>
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"task_id": Json::string(item.task_id),
"error": Json::string("更新落库失败: \{e}"),
}),
),
)
}
}
}
}
} else {
// 追加新任务:复用 publish(内部含 human_steward 硬门)
match
self.publish(
project_dir~,
description=item.description,
created_by=by,
now~,
ns~,
) {
Ok(id) => {
ok_count = ok_count + 1
// 追加任务可带依赖
if item.depends_on.length() > 0 {
match self.get_task(id) {
Some(t) => {
let mut c = t
for d in item.depends_on {
if d != "" {
c = c.with_depends_on(d, now)
}
}
match self.store.update_task(c) {
Ok(_) => ()
Err(e) =>
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"task_id": Json::string(id),
"action": Json::string("dep_update_failed"),
"error": Json::string(e),
}),
),
)
}
}
None => ()
}
}
ignore(
results.push(
Json::object({
"ok": Json::boolean(true),
"task_id": Json::string(id),
"action": Json::string("created"),
}),
),
)
}
Err(e) =>
ignore(
results.push(
Json::object({
"ok": Json::boolean(false),
"error": Json::string(e),
}),
),
)
}
}
}
}
Json::object({
"project_dir": Json::string(project_dir),
"namespace": Json::string(ns),
"total": Json::number(items.length().to_double()),
"ok_count": Json::number(ok_count.to_double()),
"results": Json::array(results),
})
}
///|
/// ISO 时间(YYYY-MM-DDTHH:MM:SS,忽略尾部 Z/毫秒/时区)→ epoch 秒。
/// 解析失败返回 None(调用方按不可比较处理,不阻断轮询)。
fn iso_to_epoch(s : String) -> Int32? {
if s.length() < 19 ||
s[4] != '-' ||
s[7] != '-' ||
s[10] != 'T' ||
s[13] != ':' ||
s[16] != ':' {
return None
}
let nums : Array[Int32] = []
let idxs : Array[(Int32, Int32)] = [
(0, 4),
(5, 7),
(8, 10),
(11, 13),
(14, 16),
(17, 19),
]
for p in idxs {
let (lo, hi) = p
let mut v = 0
let mut bad = false
for i in lo.. 9 {
bad = true
}
v = v * 10 + d
}
if bad {
return None
}
nums.push(v)
}
let yy = nums[0]
let mo = nums[1]
let dd = nums[2]
let hh = nums[3]
let mi = nums[4]
let ss = nums[5]
if mo < 1 || mo > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 59 {
return None
}
let a = (14 - mo) / 12
let y = yy + 4800 - a
let m = mo + 12 * a - 3
let jd = dd + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045
Some((jd - 2451545) * 86400 - 43200 + hh * 3600 + mi * 60 + ss)
}
///|
/// 提醒者轮询(纯计算,只读不修改):扫描全库任务产出提醒清单。
/// R2 停滞:活跃任务最近更新早于 now-stale_hours;
/// R3 事项超时:待处理任务最近更新早于 now-pending_hours。
/// 时间解析失败的任务跳过(不阻断轮询)。
pub fn FistEngine::supervisor_reminder_tick(
self : FistEngine,
now~ : String,
stale_hours~ : Int32 = 24,
pending_hours~ : Int32 = 48,
) -> Json {
let now_ep = iso_to_epoch(now).unwrap_or(0)
let stale_cut = now_ep - stale_hours * 3600
let pending_cut = now_ep - pending_hours * 3600
let reminders : Array[Json] = []
for t in self.list_all() {
let st = t.get_status()
let active = !st.is_pending() && !st.is_completed() && !st.is_archived()
let up = t.get_updated_at()
let up_ep = iso_to_epoch(up).unwrap_or(0)
let ts_ok = now_ep > 0 && up_ep > 0
if active && ts_ok && up_ep < stale_cut {
reminders.push(
Json::object({
"kind": Json::string("stale"),
"severity": Json::string("info"),
"task_id": Json::string(t.get_id()),
"ns": Json::string(t.ns),
"status": Json::string(st.to_string()),
"reason": Json::string(
"活跃任务超过 \{stale_hours} 小时未更新",
),
"updated_at": Json::string(up),
}),
)
}
if st.is_pending() && ts_ok && up_ep < pending_cut {
reminders.push(
Json::object({
"kind": Json::string("pending_timeout"),
"severity": Json::string("warn"),
"task_id": Json::string(t.get_id()),
"ns": Json::string(t.ns),
"status": Json::string(st.to_string()),
"reason": Json::string(
"待处理任务超过 \{pending_hours} 小时未被接管",
),
"updated_at": Json::string(up),
}),
)
}
}
Json::object({
"ts": Json::string(now),
"stale_hours": Json::number(stale_hours.to_double()),
"pending_hours": Json::number(pending_hours.to_double()),
"reminder_count": Json::number(reminders.length().to_double()),
"reminders": Json::array(reminders),
})
}