/// FIST-Mbt core: task model & state machine.
/// 迁移自 FIST(Python) 的状态机与分层递归理念(七条金条第 4 条:唯一指挥官)。
///|
/// 任务七态状态机
pub enum TaskStatus {
/// 待领取
Pending
/// 已领取
Claimed
/// 拆分中
Splitting
/// 执行中
Executing
/// 待验收
Reviewing
/// 已完成
Completed
/// 已归档
Archived
/// 已打回(验收不通过,需重做)
Rejected
/// 已暂停(外部中断/等待依赖)
Paused
} derive(Eq, Debug)
///|
pub extend TaskStatus with Eq::{not_equal, equal}
///|
pub extend TaskStatus with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub fn TaskStatus::to_string(self : TaskStatus) -> String {
match self {
Pending => "待领取"
Claimed => "已领取"
Splitting => "拆分中"
Executing => "执行中"
Reviewing => "待验收"
Completed => "已完成"
Archived => "已归档"
Rejected => "已打回"
Paused => "已暂停"
}
}
///|
pub fn TaskStatus::from_string(s : String) -> TaskStatus? {
match s {
"待领取" => Some(Pending)
"已领取" => Some(Claimed)
"拆分中" => Some(Splitting)
"执行中" => Some(Executing)
"待验收" => Some(Reviewing)
"已完成" => Some(Completed)
"已归档" => Some(Archived)
"已打回" => Some(Rejected)
"已暂停" => Some(Paused)
_ => None
}
}
/// 排序权重(用于统计/展示)
///|
/// 状态谓词(供跨包业务层判断,避免直接构造变体)
///
/// **这个闸门比域方法宽是「故意的」——R142 曾误删 `Rejected`,R143 已回滚并写清缘由。**
///
/// 三层分工:
/// · `Task::split()`(`engine.split` 用)只收 **Claimed**;
/// · `Task::mark_decomposing()`(`engine.plan_deep` 用)收 **Pending | Claimed | Splitting**;
/// · 本闸门 = **"值得尝试"的宽集**,含 `Rejected` —— 因为 `engine` 的第二道门拒绝时会拼
/// `split_remedy(...)` 给出**出路文案**(见 `engine.mbt` 的 `split_remedy` docstring:
/// 「第一道门 `can_split` 比它宽……只报'要求状态'不报'怎么到达',调用方就会照文案去 claim
/// 一个已打回的任务,永远走不通」)。
///
/// ⇒ 若把 `Rejected` 从闸门删掉(R142 干过):调用方只会拿到「非法拆分: [已打回]」,
/// **丢掉「先 retry」这条唯一的出路提示** —— 那是 UX 倒退,不是清理死代码。
/// 判据:**宽闸门 + 窄域层 + 带出路文案的第二道门错误** = 三层各司其职,不是不一致。
pub fn TaskStatus::can_split(self : TaskStatus) -> Bool {
match self {
Pending | Claimed | Splitting | Rejected => true
_ => false
}
}
///|
/// execute 的前态谓词(engine.execute 的第一道门)。
/// BUG-2 相邻加固(守卫一致性):这里曾多出一条 `Rejected`,而 Task::execute 并不接受
/// 已打回——两条门不一致时,调用方拿到的是引擎的"非法执行: 任务处于 [已打回]"(无出路),
/// 而不是域迁移里带 retry 出路的文案。集合与 Task::execute 逐字对齐(对外结果不变:
/// 两条门对 Rejected 都是拒,只是文案统一)。
pub fn TaskStatus::can_execute(self : TaskStatus) -> Bool {
match self {
Claimed | Splitting | Executing => true
_ => false
}
}
///|
pub fn TaskStatus::is_rejected(self : TaskStatus) -> Bool {
self == Rejected
}
///|
pub fn TaskStatus::is_paused(self : TaskStatus) -> Bool {
self == Paused
}
///|
pub fn TaskStatus::is_active(self : TaskStatus) -> Bool {
match self {
Pending | Claimed | Splitting | Executing | Reviewing => true
_ => false
}
}
///|
pub fn TaskStatus::is_completed(self : TaskStatus) -> Bool {
self == Completed
}
///|
pub fn TaskStatus::is_pending(self : TaskStatus) -> Bool {
self == Pending
}
///|
pub fn TaskStatus::is_claimed(self : TaskStatus) -> Bool {
self == Claimed
}
///|
pub fn TaskStatus::is_splitting(self : TaskStatus) -> Bool {
self == Splitting
}
///|
pub fn TaskStatus::is_executing(self : TaskStatus) -> Bool {
self == Executing
}
///|
pub fn TaskStatus::is_reviewing(self : TaskStatus) -> Bool {
self == Reviewing
}
///|
pub fn TaskStatus::is_archived(self : TaskStatus) -> Bool {
self == Archived
}
///|
pub fn TaskStatus::order(self : TaskStatus) -> Int32 {
match self {
Pending => 0
Claimed => 1
Splitting => 2
Executing => 3
Reviewing => 4
Completed => 5
Archived => 6
Rejected => 7
Paused => 8
}
}
///|
/// FIST-Mbt 任务实体
pub struct Task {
/// 形如 T0 / T0.1 / T0.1.2 的层级 id
id : String
parent_id : String?
project_dir : String
/// 命名空间(多租户隔离键,默认 "default")
ns : String
priority : String
importance : String
/// K 值:根=3,每拆一层减 1;K<=1 为原子任务
depth : Int32
/// 拆分数量(默认 3)
split_n : Int32
/// 军衔等级(军衔体系展示层的真数据源):0=司令 … 7=排长,越界钳制;
/// 默认 6(连长);子任务 = 父级 + 1,封顶 7。**与 depth(树深/缩进)严格分离**。
level : Int32
status : TaskStatus
assignee : String?
description : String
deliverable : String
created_at : String
updated_at : String
completed_by : String?
cleanup_mode : String
/// 依赖的任务 id 列表(这些任务完成后,本任务才可领取)
depends_on : Array[String]
/// BUG-389 方向②:Omega 门禁开关判据从 description 文本移到不可覆盖的元数据列。
/// plan_deep(omega_strong_verify=true) 写子任务时置 true;supervisor_task_upsert
/// 覆盖描述不会影响本字段。
omega_required : Bool
/// **BUG-475**:发布者身份(`created_by`)。
///
/// 为什么加这一列:`publish` / `dag_publish` 有 `created_by` **硬检查**,
/// 但那个值**从来没落库** ⇒ 「谁发布的」事后**查不出来**,审计面为零。
/// 落库后仍**不能防冒充**(值仍是调用方自报,S1 边界,见 BUG-430/464)——
/// 补的是**审计**,不是**验证**。
///
/// 空值语义:缺省 `""` = **该任务没有记录发布者**(旧库迁移后的历史任务就是这个值)。
/// 不用 `"unknown"` / `"legacy"` 这类**看起来像身份**的占位符 ——
/// 那会让审计查询把「占位符」当成「一个叫 unknown 的 agent」。
created_by : String
} derive(Eq, Debug)
///|
pub extend Task with Eq::{not_equal, equal}
///|
pub extend Task with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// 便捷状态访问
pub fn Task::status_to_string(self : Task) -> String {
self.status.to_string()
}
///|
pub fn Task::get_id(self : Task) -> String {
self.id
}
///|
pub fn Task::get_parent(self : Task) -> String? {
self.parent_id
}
///|
pub fn Task::get_status(self : Task) -> TaskStatus {
self.status
}
///|
pub fn Task::get_depth(self : Task) -> Int32 {
self.depth
}
///|
/// 军衔等级(0..7)。军衔体系展示层读取此处,而非 depth。
pub fn Task::get_level(self : Task) -> Int32 {
self.level
}
///|
pub fn Task::get_description(self : Task) -> String {
self.description
}
///|
/// BUG-389 方向②:Omega 门禁开关元数据(description 被覆盖后仍为真)
pub fn Task::get_omega_required(self : Task) -> Bool {
self.omega_required
}
///|
pub fn Task::get_assignee(self : Task) -> String? {
self.assignee
}
///|
pub fn Task::get_updated_at(self : Task) -> String {
self.updated_at
}
///|
pub fn Task::is_leaf(self : Task) -> Bool {
self.depth <= 1
}
///|
/// 军衔等级钳制到 0..7(<0→0,>7→7)。军衔体系展示层的真数据源,
/// 与 depth(树深)语义独立,绝不用 depth 兼任 level。
fn clamp_level(l : Int32) -> Int32 {
if l < 0 {
0
} else if l > 7 {
7
} else {
l
}
}
///|
/// 子任务军衔 = 父级 + 1,封顶排长(7)。
/// 连长(6)→排长(7)→士兵;不再上升。父级越界先钳制再 +1(防御)。
pub fn child_level(parent : Int32) -> Int32 {
let p = clamp_level(parent)
if p + 1 > 7 {
7
} else {
p + 1
}
}
///|
/// 构造器(字段带默认值)
pub fn Task::new(
id~ : String,
project_dir~ : String,
description~ : String = "",
priority~ : String = "中",
importance~ : String = "中",
depth~ : Int32 = 3,
split_n~ : Int32 = 3,
level~ : Int32 = 6,
parent_id~ : String = "",
assignee~ : String = "",
cleanup_mode~ : String = "deferred",
created_at~ : String = "",
ns~ : String = "default",
depends_on~ : Array[String] = [],
omega_required~ : Bool = false,
created_by~ : String = "",
) -> Task {
{
id,
parent_id: if parent_id == "" {
None
} else {
Some(parent_id)
},
project_dir,
ns,
priority,
importance,
depth,
split_n,
level: clamp_level(level),
status: Pending,
assignee: if assignee == "" {
None
} else {
Some(assignee)
},
description,
deliverable: "",
created_at,
updated_at: created_at,
completed_by: None,
cleanup_mode,
depends_on,
omega_required,
created_by,
}
}
///|
/// 按存储行字段直接构建任务(供 store 层反序列化使用;status 为存储字符串,非法回退 Pending)
pub fn Task::from_db(
id : String,
parent_id : String?,
project_dir : String,
ns : String,
priority : String,
importance : String,
depth : Int32,
split_n : Int32,
level : Int32,
status_s : String,
assignee : String?,
description : String,
deliverable : String,
created_at : String,
updated_at : String,
completed_by : String?,
cleanup_mode : String,
depends_on : Array[String],
omega_required : Bool,
) -> Task {
// BUG-475:`from_db` 保持 19 参签名不变(4 个测试文件在用),委托到新构造器。
Task::from_db_with_created_by(
id, parent_id, project_dir, ns, priority, importance, depth, split_n, level,
status_s, assignee, description, deliverable, created_at, updated_at, completed_by,
cleanup_mode, depends_on, omega_required, "",
)
}
///|
/// **BUG-475**:带 `created_by` 的行反序列化(20 参)。
///
/// 为什么另开一个而不是给 `from_db` 加参:`from_db` 是**位置参数**函数
/// (MoonBit 位置参数不能有默认值),全仓 **4 个测试文件**直接调它。
/// 加第 20 个必填参会让它们全部编译失败 —— 那是**用测试的改动去换一个字段**,
/// 代价不对等。⇒ 保留旧签名(委托到本函数、`created_by=""`),
/// store 层改用本函数。
pub fn Task::from_db_with_created_by(
id : String,
parent_id : String?,
project_dir : String,
ns : String,
priority : String,
importance : String,
depth : Int32,
split_n : Int32,
level : Int32,
status_s : String,
assignee : String?,
description : String,
deliverable : String,
created_at : String,
updated_at : String,
completed_by : String?,
cleanup_mode : String,
depends_on : Array[String],
omega_required : Bool,
created_by : String,
) -> Task {
let status = match TaskStatus::from_string(status_s) {
Some(s) => s
None => Pending
}
{
id,
parent_id,
project_dir,
ns,
priority,
importance,
depth,
split_n,
level: clamp_level(level),
status,
assignee,
description,
deliverable,
created_at,
updated_at,
completed_by,
cleanup_mode,
depends_on,
omega_required,
created_by,
}
}
/// —— 状态迁移 ——
/// 每个迁移都是纯函数(返回新 Task),状态机非法流转直接拒绝。
/// 返回 Err(msg) 表示非法迁移。
///|
/// 待领取 -> 已领取(角色:any active agent)
pub fn Task::claim(self : Task, assignee~ : String) -> Result[Task, String] {
match self.status {
Pending =>
if assignee == "" {
Err("claim 需要 assignee")
} else {
Ok({ ..self, status: Claimed, assignee: Some(assignee), })
}
_ =>
Err(
"非法迁移: claim 要求状态 [待领取],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 已领取 -> 拆分中(指挥官拆解)
pub fn Task::split(self : Task) -> Result[Task, String] {
match self.status {
Claimed =>
if self.is_leaf() {
Err("原子任务(K<=1)不可再拆分")
} else {
Ok({ ..self, status: Splitting, })
}
_ =>
Err(
"非法迁移: split 要求状态 [已领取],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 拆分中标记(AO 式拆解用,宽于 split:待领取/已领取/拆分中均可转入)
pub fn Task::mark_decomposing(self : Task) -> Result[Task, String] {
match self.status {
Pending | Claimed | Splitting => Ok({ ..self, status: Splitting, })
_ => Err("非法拆解: 任务处于 [\{self.status.to_string()}]")
}
}
///|
/// 拆分中/已领取/执行中 -> 执行中(进入/覆盖/追加交付物)
/// 执行中 -> execute = 覆盖/追加交付物(retry 后原地更新交付物的直觉行为)
/// BUG-2(Round 2 收口):合法前态含「执行中」。retry 的落点就是执行中,若这里不收,
/// 「被打回重做」这条最该更新交付物的路径会写不进 deliverable(omega_result_verify
/// 随即报「尚无交付物」),诱导操作者新建任务覆盖旧账、破坏 append-only 审计。
/// 选择放宽 execute 而不改 retry 的落点:retry→执行中 是**对外承诺的语义**
/// (AGENTS.md 工具表「打回后重试(→执行中)」、已发布流水线的状态判据都按它写),
/// 改它等于改一条公共状态机迁移;而「执行中重复 execute」本就是幂等追加,影响面更小。
/// 错误文案必须给**出路**(当前状态 → 该走哪条迁移),否则调用方只能猜或另起任务。
pub fn Task::execute(self : Task) -> Result[Task, String] {
match self.status {
Splitting | Claimed | Executing => Ok({ ..self, status: Executing, })
_ =>
Err(
"非法迁移: execute 要求状态 [拆分中/已领取/执行中],当前是 [" +
self.status.to_string() +
"]。出路:" +
Task::execute_exit_hint(self.status),
)
}
}
///|
/// execute 被拒时按当前状态给出可用出路(BUG-2 文案,纯函数便于回归测试)。
pub fn Task::execute_exit_hint(status : TaskStatus) -> String {
match status {
Pending => "待领取→先 claim 再 execute"
Claimed | Splitting | Executing => "已是可执行状态,直接 execute"
Rejected =>
"已打回→先 retry(→执行中)再 execute 覆盖交付物"
Reviewing =>
"待验收→若要改交付物先 reject(→已打回)再 retry(→执行中),再 execute"
Paused => "已暂停→先 resume(→已领取)再 execute"
Completed =>
"已完成→原地更正交付物请 reopen_task(→待领取)后重新走链路,勿新建任务覆盖旧账"
Archived =>
"已归档→不可写;确需返工先 reopen_task 重开,勿新建任务覆盖旧账"
}
}
///|
/// 执行中 -> 待验收
pub fn Task::submit(self : Task) -> Result[Task, String] {
match self.status {
Executing => Ok({ ..self, status: Reviewing, })
_ =>
Err(
"非法迁移: submit 要求状态 [执行中],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 待验收 -> 已完成(人类主权:completed_by 必须 human / human_steward 或明确授权)
/// 注意:此处仅校验 completed_by 非空;不引入独立的强身份认证模型(鉴权属上层
/// MCP 工具/MCP 层职责,本域不做权宜式身份伪造防护,避免脆弱的新鉴权模型)。
pub fn Task::complete(
self : Task,
completed_by~ : String,
) -> Result[Task, String] {
match self.status {
Reviewing =>
if completed_by == "" {
Err("complete 需要 completed_by 身份")
} else {
Ok({ ..self, status: Completed, completed_by: Some(completed_by), })
}
_ =>
Err(
"非法迁移: complete 要求状态 [待验收],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 已完成 -> 已归档
pub fn Task::archive(self : Task) -> Result[Task, String] {
match self.status {
Completed => Ok({ ..self, status: Archived, })
_ =>
Err(
"非法迁移: archive 要求状态 [已完成],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 强制归档(BUG-227):force=true 时放行 已完成/已暂停/已打回 三类滞留态直接入档,
/// 供运维清扫无主积压任务;force=false 时与 Task::archive 完全同义(零回归)。
/// 已领取/执行中/待验收 等在流状态不放行——它们可能仍被活跃 agent 持有,
/// 强制归档会吞掉在途工作;请先 pause 落到已暂停再走本路径。
pub fn Task::archive_force(self : Task, force~ : Bool) -> Result[Task, String] {
if !force {
return self.archive()
}
match self.status {
Completed | Paused | Rejected => Ok({ ..self, status: Archived, })
_ =>
Err(
"force archive 仅支持 [已完成/已暂停/已打回],当前是 [" +
self.status.to_string() +
"];在流状态请先 pause 再强制归档",
)
}
}
///|
/// 任意非归档状态 -> 已领取(被打回重做)
pub fn Task::reopen(self : Task) -> Result[Task, String] {
match self.status {
Archived => Err("已归档任务不可 reopen,请重新发布")
Rejected =>
Ok({ ..self, status: Claimed, assignee: None, deliverable: "", })
Paused => Ok({ ..self, status: Claimed, })
_ => Ok({ ..self, status: Claimed, })
}
}
///|
/// 打回原因单条上限(字节,UTF-8,BUG-427)。
///
/// 取 200 与 `supervisor_status` 的 description 截断口径一致 ——
/// 单次打回最多占掉回执的 200 字节窗口,不会把任务意图整体挤掉。
const REJECT_REASON_CAP : Int32 = 200
///|
/// 剥掉 description 尾部已存在的 `[打回原因: …]`(BUG-427 滚动窗口)。
///
/// 只剥**最后一次**拼上去的那一段(从最后一个 `" [打回原因: "` 起,到末尾的 `"]"`),
/// 这样「同一任务被打回 N 次」的 description 长度**不随 N 线性增长**。
///
/// ⚠️ 本仓**没有** `String::rev_find` 的先例(`grep -rn 'rev_find\|rfind' src/` 为空),
/// 故此处用「自实现反向查找」而非依赖不存在的 API。
fn strip_reject_tail(desc : String) -> String {
let marker = " [打回原因: "
let ml = marker.length()
let dl = desc.length()
if dl < ml || !desc.has_suffix("]") {
return desc
}
// 反向扫:找最后一个 marker 的起始下标
let mut i = dl - ml
let mut last = -1
while i >= 0 {
if desc[i:i + ml] == marker {
last = i
break
}
i = i - 1
}
if last < 0 {
desc
} else {
desc[0:last].to_owned()
}
}
///|
/// 按 UTF-8 字符边界截断到 `cap` 字节(不产生半个字符)。
///
/// 与既有 `desc_clip`(:700 附近)同族,但**语义是「纯截断」不追加 "…"** ——
/// 外层会再拼 `]`,加省略号反而破坏 `[打回原因: …]` 的形状。
///
/// 依赖面:`src/core/moon.pkg:3` 是 `"moonbitlang/x/encoding"`(**不是** `core/encoding/utf8`),
/// 故此处用 `@encoding.encode(UTF8, s)` / `@encoding.decode_to(...)`,
/// 与 `desc_clip` 同一套 API,**不新增依赖**。
/// **R152:提升为 `pub`** —— 全仓**唯一**的 UTF-8 边界安全截断真源。
/// 此前它是私有、只服务「拒绝理由」一处,而 `board_ascii` / `ops_selfdrive` 里对
/// **中文 description** 直接用 `desc[0:40]` / `desc[0:200]` 硬切 ⇒ 可能切在字符中间(半个汉字)。
/// 需要裁剪的地方一律走本函数,不要自己写 `[0:N]`。
pub fn clip_utf8(s : String, cap : Int32) -> String {
if cap <= 0 {
return ""
}
let bytes = @encoding.encode(UTF8, s)
if bytes.length() <= cap {
return s
}
let mut cut = cap
while cut > 0 && bytes[cut].to_int().land(0xC0) == 0x80 {
cut = cut - 1
}
let sb = StringBuilder()
@encoding.decode_to(bytes[0:cut], sb, encoding=UTF8) catch {
_ => ()
}
sb.to_string()
}
///|
/// 待验收 -> 已打回(验收不通过,需重做)
///
/// **BUG-427:打回原因不再无上限追加进 `description`。**
///
/// 旧实现每次 `reject` 都拼一段 ` [打回原因: …]`,而:
/// · 存储面无任何长度闸(`desc_clip` 只在 `to_json` 输出时截断);
/// · `description` 是三处功能的**共同进料面**:
/// `task_triage` 的能力抽取(`cap_match` 的 `desc.contains(want)`)、
/// `goal_drift_check` 的 Jaccard(`@evolve.tokens(get_description())`)、
/// `supervisor_status` 的 200 字节截断。
///
/// 实测:`T0r636.4.2` **单次打回即 4737 字节**。
///
/// 现实现两道闸:
/// ① 先剥掉**已有**的 `[打回原因: …]` 尾巴(滚动窗口 = 只留最近 1 次)
/// ② 单条原因截断到 `REJECT_REASON_CAP` 字节(按 UTF-8 字符边界,不切半字)
///
/// **只留最近 1 次是有意取舍**:打回原因属于过程信息,留住「最近为什么被打回」
/// 已经够驱动重做;留全部会让任务意图被历史噪声淹没。
/// 需要完整历史时应走独立历史表(另立,动 schema)。
pub fn Task::reject(self : Task, reason~ : String = "") -> Result[Task, String] {
match self.status {
Reviewing =>
Ok({
..self,
status: Rejected,
description: if reason == "" {
self.description
} else {
strip_reject_tail(self.description) +
" [打回原因: " +
clip_utf8(reason, REJECT_REASON_CAP) +
"]"
},
})
_ =>
Err(
"非法迁移: reject 要求状态 [待验收],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 已打回 -> 执行中(重新执行)
///
/// **BUG-426:必须清空 `deliverable`。**
///
/// 缺陷原形:`reject` 只改 status + 追加打回原因、`retry` 只改 status,
/// **两者都不动 `deliverable`** ⇒ `execute(A) → submit → reject → retry → submit`
/// 走完一圈后,**`deliverable` 还是被打回的那份 A**,而 `submit` 也不查新鲜度,
/// 于是**旧废品被当成本轮成果提交验收**。
///
/// **这不是设计选择,是遗漏** —— 同一条 `Rejected` 分支上,兄弟方法
/// `Task::reopen`(:549)**清了** `deliverable: ""` + `assignee: None`。
///
/// 危害链:`omega_strong.mbt:642` 用 `deliverable.trim() == ""` 判「尚无交付物」
/// ⇒ 旧废品被当成果复验,**判绿即真了账**;`board_ascii.mbt:252` 的假绿档因此系统性低估。
///
/// **只清 `deliverable`、保留 `assignee`**:`retry` 是「同一个人重做」,
/// 而 `reopen` 是「重新回到可认领池」(所以那边把 assignee 也清了)。
pub fn Task::retry(self : Task) -> Result[Task, String] {
match self.status {
Rejected => Ok({ ..self, status: Executing, deliverable: "", })
_ =>
Err(
"非法迁移: retry 要求状态 [已打回],当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 任意活跃状态 -> 已暂停(外部中断/等待依赖)
pub fn Task::pause(self : Task) -> Result[Task, String] {
match self.status {
Pending | Claimed | Splitting | Executing | Reviewing | Rejected =>
Ok({ ..self, status: Paused, })
Paused => Err("任务已处于暂停状态")
_ =>
Err(
"非法迁移: pause 要求活跃状态,当前是 [" +
self.status.to_string() +
"]",
)
}
}
///|
/// 已暂停 -> 已领取(恢复执行)
pub fn Task::resume_task(self : Task) -> Result[Task, String] {
match self.status {
Paused => Ok({ ..self, status: Claimed, })
_ =>
Err(
"非法迁移: resume_task 要求状态 [已暂停],当前是 [" +
self.status.to_string() +
"]",
)
}
}
/// 生成子任务 id:T0.1.2 -> T0.1.2.1
///|
/// 记录类复制更新(跨包不便直接构造 record,由 core 提供)
pub fn Task::with_updated_at(self : Task, now : String) -> Task {
{ ..self, updated_at: now, }
}
///|
pub fn Task::with_deliverable(
self : Task,
deliverable : String,
now : String,
) -> Task {
{ ..self, deliverable, updated_at: now, }
}
///|
/// 追加一条前置依赖(去重)并更新时间戳;返回带新 depends_on 的副本。
/// 供显式 DAG 构建(dag_depend)使用——不只靠 plan_deep 的隐式父子关系。
pub fn Task::with_depends_on(
self : Task,
dep_id : String,
now : String,
) -> Task {
let deps : Array[String] = if self.depends_on.contains(dep_id) {
self.depends_on
} else {
// 拷贝原依赖数组并追加(Array 可变容器,push 就地生效)
let out : Array[String] = []
for d in self.depends_on {
out.push(d)
}
out.push(dep_id)
out
}
{ ..self, depends_on: deps, updated_at: now, }
}
///|
pub fn child_id(parent_id~ : String, idx~ : Int32) -> String {
"\{parent_id}.\{idx}"
}
///|
/// 任务转 JSON(供 MCP resource / tool 输出)
///|
/// 给 AI 编排用的描述截断:**按 UTF-8 字节上限**截断并回退到字符边界。
///
/// 为什么按字节而不是按 `String` 码元(2026-10-23 实测修正):本工具链的 `String` 内部是 UTF-16,
/// 原先的实现 `if s.length() <= desc_limit` + `s[cut]` 是**码元**语义,而本函数的契约(以及它的用途)
/// 是「控 MCP 返回**体积**」——体积是字节。旧实现在纯中文描述上会把 6 字节当成 6 个字符,
/// 实测 `desc_clip("一二三四五六七八九十", 6)` 返回 `"一二三四五六…"` 而不是 `"一二…"`,
/// 与随本函数同一提交落地的 `core_task_desc_clip_wbtest.mbt:44`(dc_3,期望 `"一二…"`)直接矛盾。
/// 现在先按 UTF-8 编码取真实字节数,超限才切,并在切点回退到 continuation byte 之前
/// (0b10xxxxxx),保证解出来仍是合法 UTF-8。
/// `desc_limit <= 0` 原样返回(零回归锚)。
fn desc_clip(s : String, desc_limit : Int32) -> String {
if desc_limit <= 0 {
return s
}
let bytes = @encoding.encode(UTF8, s)
if bytes.length() <= desc_limit {
return s
}
let mut cut = desc_limit
// 回退:continuation byte 形如 0b10xxxxxx ⇒ 退到字符起始字节
while cut > 0 && bytes[cut].to_int().land(0xC0) == 0x80 {
cut = cut - 1
}
let sb = StringBuilder()
@encoding.decode_to(bytes[0:cut], sb, encoding=UTF8) catch {
_ => ()
}
sb.to_string() + "…"
}
///|
/// 任务 JSON。`desc_limit > 0` 时 description 被截断到该字节上限(字符边界安全),
/// 供查询/管家类工具按需控体积——否则膨胀的任务描述(gradient/父计划/边界四问等元信息
/// 递归继承后动辄数 KB)会把 MCP 返回体积炸掉。默认 -1 = 不截断(零回归)。
pub fn Task::to_json(self : Task, desc_limit~ : Int32 = -1) -> Json {
let m : Map[String, Json] = Map([])
m.set("id", Json::string(self.id))
match self.parent_id {
Some(p) => m.set("parent_id", Json::string(p))
None => m.set("parent_id", Json::null())
}
m.set("project_dir", Json::string(self.project_dir))
m.set("namespace", Json::string(self.ns))
m.set("priority", Json::string(self.priority))
m.set("importance", Json::string(self.importance))
m.set("depth", Json::number(self.depth.to_double()))
m.set("split_n", Json::number(self.split_n.to_double()))
m.set("level", Json::number(self.level.to_double()))
m.set("status", Json::string(self.status.to_string()))
match self.assignee {
Some(a) => m.set("assignee", Json::string(a))
None => m.set("assignee", Json::null())
}
m.set("description", Json::string(desc_clip(self.description, desc_limit)))
m.set("deliverable", Json::string(self.deliverable))
m.set("created_at", Json::string(self.created_at))
m.set("updated_at", Json::string(self.updated_at))
match self.completed_by {
Some(c) => m.set("completed_by", Json::string(c))
None => m.set("completed_by", Json::null())
}
m.set("cleanup_mode", Json::string(self.cleanup_mode))
m.set(
"depends_on",
Json::array(self.depends_on.map(fn(d) { Json::string(d) })),
)
m.set("is_leaf", Json::boolean(self.is_leaf()))
m.set("omega_required", Json::boolean(self.omega_required))
Json::object(m)
}
///|
/// 追加(覆盖)任务描述并更新时间戳;返回带新 description 的副本。
/// 供超级管家 supervisor_task_upsert 使用——只改文本,不动状态机。
pub fn Task::with_description(
self : Task,
description : String,
now : String,
) -> Task {
{ ..self, description, updated_at: now, }
}
///|
/// 覆盖任务优先级(易/中/难一类的中文档位文本)并更新时间戳。
pub fn Task::with_priority(
self : Task,
priority : String,
now : String,
) -> Task {
{ ..self, priority, updated_at: now, }
}
///|
/// 设置 Omega 强验证元数据列(`omega_required`),并更新时间戳。
/// 与 `with_description` 的文本标记路径**互为补集**:本方法只动元数据面,
/// 不改描述文本 —— 两者由 `omega_enabled()` 的 `||` 合并为单一判据(判例-16:不要引第 5 种判法)。
///
/// 为什么要有:BUG-389 方向② 把门禁判据搬进元数据列之后,描述被
/// `supervisor_task_upsert` 覆盖也不会丢开关;但**没有对应的写入入口**。
/// F3 继承锁(`omega_required_column_wbtest.mbt` 的 wb1)必须把「父任务已开着 Omega」
/// 这条前置真的落库——用 `with_description` 塞文本标记等于绕开元数据面,锁就测不到单真源。
///
/// 它**不是**产品面的「关闭入口」:引擎侧写入点仍只有 `decompose_rec` 那一处
/// (单调 OR,见 engine.mbt 的 F2 留痕);显式关闭语义属 advance 面,不在本单。
pub fn Task::with_omega_required(
self : Task,
omega_required : Bool,
now : String,
) -> Task {
{ ..self, omega_required, updated_at: now, }
}