dsh-agent-board 1.9.1 → 1.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@
41
41
  | **0.1.7 ~ 0.1.7-x**(含 rc) | **≥ 1.2.2(必须)** | 0.1.7 会话日志升级为 format v4:插件消息 `source.kind` 必须是生产者自有 kind。1.2.1 及更早会在**任务完成/阻塞回执**落盘时抛 `SessionFormatError: format v4 message requires a producer-owned source kind`,并连带使主窗口当前轮次失败(表现为「本轮运行失败」);同时「跳转会话」因宿主移除 `sessions.open` 而失效(控制台 `sessionsSvc.open is not a function`),卡片活动心跳读不到 v4 日志(`session.v4.jsonl.zstd`) |
42
42
  | 0.1.5-rc.1 ~ 0.1.6 | ≤ 1.2.1 | 1.2.2 起按 0.1.7 协议编写(v4 source kind、`uiWorkspace.openSession` 跳转),旧宿主未做回归验证,建议停留 1.2.1 |
43
43
 
44
- > 当前版本 **v1.9.1** 已在宿主 **0.2.1-rc.3** 全量实测:723 断言全绿 + 拆解官/力度路由/健康嗅探/帧活性挂死检测真机 E2E 全链。
44
+ > 当前版本 **v1.9.3** 已在宿主 **0.2.1-rc.5** 全量实测:739 断言全绿 + 拆解官/力度路由/健康嗅探/帧活性挂死检测真机 E2E 全链。
45
45
 
46
46
  ```sh
47
47
  dsh plugin --profile web add dsh-agent-board@latest # 0.1.7+/0.2.x 宿主(推荐)
@@ -119,8 +119,8 @@ dsh plugin --profile web remove dsh-agent-board
119
119
 
120
120
  - **看板**:六列状态流、拖拽 / 多选批量 / 文本·优先级·标签筛选、归档留档、会话隔离
121
121
  - **派发**:一次性 Worker / 独立 Verifier、依赖调度(DFS 环检测)、touches 文件级排他、可续跑 Worker、Team 托管、**spawn 落闸锁内重验**(spawnGateOk:claim 占位与落闸决策读同一把锁的同一份快照,terminate/dependsOn 后补 落在 spawn 在途窗口即弃权——竞态与后补依赖双双根治)、**高优干预分态**(intervene 无活跃 run 时区分「续跑窗口,干预随续跑注入」与「真无 run」,返回体带 state 字段)、**建卡派发预警**(将自动派发时 lintWarnings 瞬态提示「约 1s 内被领取,想占位请 draft:true」)、**宿主 spawn API 双兼容**(v0.2.1 宿主把 `subagents.start/startContinuable` 统一重构为 `startActivation(spec)`——能力探测自动选路:新契约按 `delivery` 语义分发(continuable=`parent` 结算留言 / 一次性=`caller` 只回 result),Activation 适配旧 run 形状下游零改动;老宿主逐字走旧双路径)+ **continuable 结算兜底臂**(`activation.result` 在子会话被宿主拆除时落定即接失败结算——rate-limit/模型崩溃的死子不再挂到硬超时)
122
- - **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令 + **dependsOn 序号接线**:提案内以 1 起序号互相引用,物化时映射真实任务 id,前排约束天然无环、坏序号丢弃留痕)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见;**落定走正式回执**:「🧩 已拆解 N 个(待确认发布)」聚合分区——split 列子卡清单 / keep 列理由,源卡停 draft 也必送达(投递过滤豁免,archived 才丢),失败仍走 ⚠️ 系统异常)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见;**探测错误码分治**:额度>凭据>连接信号才判失败,纯空响应(供应商怪癖)判 🟡 degraded 存活——不喂冷却、路由仍可选用;**探测预算独立**:每次尝试各 20s(慢模型重试不再被共享窗口饿死),重试预算耗尽沿用首探判定不判死、超时不覆盖既有观测)
123
- - **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集+冲突预检+hold 撞锁自动等锁复工)+ **续跑健康闸**(病模型续跑自动改 fresh spawn、宿主秒拆喂熔断、死会话哨兵 5s 即重派)+ **sizeHint 动态调档**(Worker 执行中自报档位修正,下次 spawn 生效)+ **opsLog 人工留痕**(GUI/脚本写集落盘截尾 50 条 + Team 注入「最近人工操作」摘要)
122
+ - **智能调度** `☆`:**拆解官工作流**(自然语言需求卡 → `task_decompose` 拆解官先调研后提案(outputSchema 强箍,子卡必带档位/touches/验收命令 + **dependsOn 序号接线**:提案内以 1 起序号互相引用,物化时映射真实任务 id,前排约束天然无环、坏序号丢弃留痕)→ 主窗口呈现、你确认调档 → 逐张 publish;拆解官在池条「🧩 拆解中」与源卡徽标可见;**落定走正式回执**:「🧩 已拆解 N 个(待确认发布)」聚合分区——split 列子卡清单 / keep 列理由,源卡停 draft 也必送达(投递过滤豁免,archived 才丢),失败仍走 ⚠️ 系统异常)+ **力度路由**(轻/标/重三档 × Worker/Verifier 六条有序链,四级取模链:卡级特例→档位链→板级→父模型,每跳过健康闸)+ **备用模型链** `fallbackModels`(额度到顶/故障先跨供应商同档切换,链尽才降档)+ **冷却与熔断**(限流带重置时间解析、连接/网关故障 10 分钟短窗自恢复)+ **模型健康嗅探**(`llm.stream` 极简探测定时扫已配置模型,统一 modelHealth 视图——冷却/熔断/近 5 次 run 聚合,弹层链芯片带 🟢🟡🔴⚪ 健康点,事前调度可见;**探测错误码分治**:额度>凭据>连接信号才判失败,纯空响应(供应商怪癖)判 🟡 degraded 存活——不喂冷却、路由仍可选用;**探测预算独立**:每次尝试各 20s(慢模型重试不再被共享窗口饿死),重试预算耗尽沿用首探判定不判死、超时不覆盖既有观测;**探测恢复熔断解除**:已熔断模型连续 2 个探测周期通过即自动翻绿+解除拉黑+连击归零(单次成功不复位,过滤偶然响应——探测管可调性的对称面:能判死就能证活))
123
+ - **执行纪律面** `☆`:**计划闸口**(Worker 动笔前报「计划改动」清单,越界+撞他卡锁双查警示,点名持锁卡;清单解析容错:正则残片/中文散文片不误判为路径)+ **仲裁扩权入锁**(`task_arbitrate` 的 `expandTouches` 把放行文件并入有效锁集——闸口比对即认扩权边界+冲突预检+hold 撞锁自动等锁复工)+ **touches 更新通道**(`task_update` 带 `touches` 替换式更新:规划期 draft/pending/blocked/cancelled 与落定后 resolved 主窗口可直改(GUI 详情页亦可编辑);**运行态 in-progress/verifying 拒写**——持锁期边界以 Worker 实际覆盖面申报为准,走 escalate→仲裁扩权;归一化与建卡同款,history 留「N 项→M 项」审计轨)+ **续跑健康闸**(病模型续跑自动改 fresh spawn、宿主秒拆喂熔断、死会话哨兵 5s 即重派)+ **sizeHint 动态调档**(Worker 执行中自报档位修正,下次 spawn 生效)+ **opsLog 人工留痕**(GUI/脚本写集落盘截尾 50 条 + Team 注入「最近人工操作」摘要)
124
124
  - **验收治理** `★`:硬性验收脚本独立复跑、驳回全量带回重派、Verifier 自测指南(userTest)、风险队列「待你过目」、验收时序防护(Verifier 在场时主窗口抢批需显式 `force:true`;有活跃 run 的卡禁止归档;Verifier 迟到结论落 `lateVerdict` 留痕+直投告警)、**迟到 verdict 三保险**(①入参校验:`verdict` 非 approved/rejected 拒收并返回重调指引——漏带字段不再被当驳回落账;②通知分态:approved 迟到=「✅ 迟到复核·全过追认」notice 级,🚨「迟到驳回」只留给真 rejected;③指纹去重:同 taskId+by+verdict 只投一次,重复落账零重复通知)
125
125
  - **度量** `★`:Token 消耗(总量 / 有效 / 缓存读,按模型·按任务·按日)+ 记分卡(模型表现 / 质量趋势 / 驳回聚类)
126
126
  - **可运维性** `★`:运行时健康自检(派发冻结 / 结算停滞告警)、异常驱动审视(风险队列 / 质量异动告警 / 批次摘要)、起草 lint、数据安全(原子写盘 / 腐坏隔离 / 自动 salvage)、**僵尸 run 留档清扫**(host 重启后「落定卡上的 running 无 endedAt 留档」在派发周期自动关账 `stale-closed` + 清认领位,归档门禁不再被幻影 run 永久拦截)、**子会话生命周期收口**(一次性 run 落定/迟到 verdict 落账后主动 interrupt 子会话——「结算≠停止」空转根治;continuable 收紧为「续跑位真在本 child 手上」才豁免,残影 Worker 无处藏身)、**terminate 三保险**(no-rec 也按认领位盲杀补刀 + interrupt 失败 err 级可见化 + `hold:true` 原子「终止+转手动」)、**配置审计**(set-board-config 成功写入落板级 configLog {key,from,to,actor,at} 截尾 50 条;模型下拉两段式选中≠保存)
@@ -206,7 +206,7 @@ draft → pending → in-progress → verifying → resolved → archived
206
206
  - 详情父子区块:父卡详情列子任务清单(状态色点 + 标题,**含已归档留档**——归档行灰化 + 行尾「已归档」徽章并沉底,点击直达子卡详情,标题行汇总 settled/total);子卡详情顶行「↳ 史诗:父标题」点击回跳父卡
207
207
  - **可选 hooks(史诗可选点位:前置/后置各是一次真实 agent 运行)**:`epic.hooks = { pre: { enabled, prompt, state, runId }, post: 同构 }`——**前置**让子任务具备开跑条件(串行闸门:未完成前该史诗的子任务一张都不派),**后置**把这批活收口(全部子任务了结后跑一遍,完成后史诗才转验证中);prompt 只给薄框架契约,具体动作由 hook agent **自行决策**,**吃不准就走歧义上报**(失败即转阻塞等人裁决:重试/跳过/放弃,不自动重跑)。hook run 记在 `epic.runs[]`(role `hook-pre`/`hook-post`),卡片带相位徽章(`⏳ 前置准备中` / `🧪 收尾中` / `⚠️ hook 失败待裁决`,无 hooks 的老卡零渲染),详情页 hooks 区可编辑 enabled+prompt、查看 state/耗时并跳转 run 会话、失败一键跳裁决区;**hooks 仅主窗口可设**(工具与 RPC 双通道门禁),commit/push 不入任何默认形态
208
208
  - **删除通道(真删,无 undo)**:`delete-task` RPC(卡片 hover 垃圾桶按钮 / 详情页「删除」按钮,均先 `confirm('删除不可恢复,确认删除「标题」?')`)+ `batch-op op='delete'`(多选模式底部「批量删除」,同样 confirm)。状态门禁:**草稿/待办/阻塞可删**;进行中/验证中拒绝并提示先用 `terminate-agent` 终止(避免在跑的 run 变孤儿);已完成/取消引导改用归档(`archive-task`,留档可检索);有**未归档子任务**时拒删(防 `parentId` 悬空破坏父任务自动流转);已归档任务幂等返回 ok。是真删(从 `tasks` 数组移除),因此**不产生 `batch-undo` 撤销快照**(批量条对 delete 不显示「↩️ 撤销」),删除操作在 host 端 `console.error` 留一行日志便于溯源
209
- - **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker 槽),子任务全了结转 verifying 后派 Verifier 验收不受影响
209
+ - **任务粒度建议**:单任务 **10~30 分钟**可独立完成为甜区;预计超过 30 分钟的大任务先建一张 **epic 父卡**(`pipeline: direct`,不进池派发),再挂若干 10~30 分钟的子任务(`task_create` 传 `parentId=父卡 id`,有先后顺序用 `dependsOn` 串联),子任务全部完成后父卡自动流转(`checkParentAuto`)——`task_create` 工具描述与 Team 模式提示词都写了这条契约;**容器卡结构性不派发**:凡有子任务的卡(epic)永不进派发候选(与 `pipeline` 字段无关,不再只靠 `direct` 口头约定——手滑建 `full` 档 epic 也不会被派出占 worker 槽),子任务全了结后按父卡 `pipeline` 分态收口——`full`/`work` 父卡转 verifying 派 Verifier 验收,`direct` 父卡直接 resolved(`direct` 不进派发池对 verifier 同成立,推进 verifying 只会把它推进 verifier 过滤闸够不到的状态)
210
210
  - **suggestSplit 软提示**:`task_create` / `create-task` 发现描述超 500 字符、或标题/描述命中「全量 / 整体 / 系统级 / 全面 / 重构 / 所有模块 / 整个」等史诗特征词时,返回体附带一行 `suggestSplit` 建议文案(**只提示,不阻断创建与派发**;未命中则不出现该字段,老调用方无感)
211
211
  - **开关 `epicSplit`(史诗拆分总开关)**(⚙️ 入池配置弹层「**功能**」小节,默认**开**):关掉只停**引导**——Team 提示词第 6~8 条拆分 doctrine 整条不注入、`create-task`/`task_create` 响应不再附 `suggestSplit` 软提示;**机制不禁**(显式传 `parentId` 建子卡、史诗自动收口与 hooks 状态机照常工作,明确要拆时不受阻);老看板文件没有该字段 → 读路径自动补 `true`(与升级前行为一致)。工具描述里的粒度契约是静态工具定义(随 request header 快照),不在开关范围
212
212
  - **模型字段写保护**:`set-board-config` 对 `workerModel`/`verifierModel` 的非空值校验当前网关可用清单(`list-models` 同款枚举)——手写非法 id(如把熔断通知里的显示名抄成 id)直接拒写并返回合法清单前 8 个,验收 spawn 不再 UNKNOWN_MODEL 秒死;空串=清除放行;宿主无 llm 服务时降级跳过校验不炸。配套熔断通知分态:`UNKNOWN_MODEL` 类失败报「配置非法(不在可用清单)」,与「验收连续失败」区分——mistake 归 mistake,quality 归 quality
@@ -221,7 +221,7 @@ draft → pending → in-progress → verifying → resolved → archived
221
221
  - **Worker/Verifier 均可配置异构模型**(⚙️ 弹出层下拉选择,空 = 继承父级),避免同源盲点;模型故障自动熔断回退父级模型
222
222
  - **力度路由(三档 × 双角色模型表,`task-mvnemvf5` 地基 + `task-mvneq4mw` GUI)**:卡级 `size`(light/standard/heavy,对话 `task_update` 调档,缺省=标档语义、存量卡零迁移)查板级 `laneModels` 路由表定模——取模链四级:**卡级 `model` 特例(最高)→ 档位 lane 槽 → 板级 workerModel/verifierModel 行 → 父模型**(每跳照常过熔断,命中熔断降下一跳;实际命中跳随 run 落 `modelSource` 留档审计,档位随 run 落 `size` 留档供记分卡按档位聚合);「模型路由」弹层(工具栏 cpu 图标)「**力度路由**」小节:三档 × Worker/Verifier 共 **6 条有序链编辑器**(下拉添加+芯片 × 移除/↑ 前移,改动即写),空链 =「继承板级」(继承上方 Worker/Verifier 模型行);写通道 = `set-board-config key='laneModels'` **部分合并**(只合显式给出的槽位,非法模型整体拒写不落半截 + configLog 审计行);**档位槽支持有序数组=同档优先序**(`task-mvnxckhq`:槽位 `string[]` 第一位=首选,按序取首个健康模型——首选冷却/熔断则次选赢,槽内全病照旧 fallback→降档→父模型;GUI 为链式编辑器——下拉添加+芯片 ×/↑ 排序,读写整个有序数组,`task-mvokikl3`;老单字符串槽位读入自动归一单元素数组,零迁移);卡面 size/卡级 model 徽章只读展示(GUI 不编辑,调档走对话 `task_update`)
223
223
  - 孤儿回收:子代理 run 结束/丢失超 2 分钟 → 任务自动回待办重派
224
- - 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s 墙钟)
224
+ - 看门狗:**帧活性双层判定**——运行中 assistant 帧静默 ≥120s → 标「🟡 疑似卡顿」(仅标卡、不杀,裁决权交主窗口/用户);帧静默 ≥600s → 判死按模型面故障结算重派并计入熔断连击;无事件订阅能力/零帧观测的宿主退回日志零增长兜底(连续 40 轮零增长 × ≥600s 墙钟);**「继续观察」有真语义**(点 dismiss 即重置帧静默基线 `frameSilenceSince=now` + 120s 防抖窗 `stuckRearmUntil`——模型需再静默满 120s 才会重新标卡,防抖窗不拦 600s 判死闸;日志兜底路径的 `zeroGrowthSince` 同款重置)
225
225
  - 歧义上报:Worker 遇到歧义不猜测,上报等主窗口裁决(任何模式下都通知);裁决后新 Worker 携带裁决答案接手
226
226
  - 驳回详情全量带回:三条驳回路径(`board_verdict` 工具 / Verifier 文本结算 / 手动 `task_verify`·`verify-task`)统一往 `t.messages` 落一条 `kind: "rejection"` 完整驳回包(summary + checks 逐条核对证据;手动路径补写 `t.verification`),经 `buildMessages` 全量注入重派 Worker prompt——新 Worker 据此返工,不再只看到 300 字截断的 history 记录
227
227
  - **Verifier 自测指南(userTest)**:Verifier prompt 末尾追加「## 自测指南」段契约——四字段 `gist`(一句人话说改了什么)/ `steps[]`(用户操作步骤,每条一步)/ `expect`(预期看到什么)/ `tier`(`ui`=界面可操作 | `metric`=看指标变化 | `internal`=纯内部无用户可感知面;`internal` 时 steps 可空、expect 写「验证靠测试套件」);**诚实护栏写死在 prompt**:只给亲自验过/从 diff 可推导的步骤,不许编没验过的操作,UI 特性给具体路径(哪个区哪个按钮),host-only 改动如实标 `internal`。双模落账 `t.verification.userTest`(工具通道 `board_verdict` 的 `userTest` 参数 / 文本通道收「## 自测指南」段;tier 非法/缺省一律归 `internal` 保守档,缺段/字段全空不挂字段);详情页验证记录区渲染「📋 自测指南」块(gist + 编号步骤 + 预期 + tier 徽章:ui 绿 / metric 蓝 / internal 灰),报告导出聚合「## 本版自测清单」段(按验收通过时间倒序近 10 张已验收卡,`internal` 收末尾并标注「无用户可感知面」)
@@ -287,6 +287,7 @@ Team 托管档独有(调度员体验):
287
287
  - 主窗口 system prompt 注入派发引导(提示词层面建议实质性改动走看板,不硬拦截);引导含**上下文书写提示**——子代理是全新会话、无会话记忆,description 写不够会自行调研跑偏
288
288
  - **默认草稿护栏**:`task_create` / `create-task` 缺省建为草稿(草稿不派发),先把所有任务的 dependsOn、contextNotes/contextFiles 补齐,再逐个 `task_update publish=true` 统一发布;确实要立即派发的单个任务才显式传 `draft:false`
289
289
  - **歧义通知 25s 去抖**:通知延迟 25s 投递,投递前重读看板——歧义已被裁决、或任务已 resolved/archived 就静默跳过(消除主窗口 turn 排队导致的过期回声);同一任务连续多次上报只投最新一条
290
+ - **判档与管线指引**(v1.9.3,修「全落标档」「全落 full」两个提示词盲区):Team 注入带三档具象样例——`light`=单文件小改/文案/配置/断言增补/≤15 分钟(样例:改一句提示词、加一条断言、调一个常量);`standard`=单模块多文件/常规特性/需新断言(样例:加一个开关、修一个判定分支);`heavy`=跨模块/改判定链核心/拆分类/高风险(样例:新机制、重构、动结算链)——配**判档三问**(几个文件?动不动判定核心?要不要全新机制?)与**「拿不准按低一档报」**(低报有 `sizeHint` 执行中自纠通道,高报只会白烧额度);同步 pipeline 场景指引——`work`=回归测试/巡检/文档/纯查询/纯报表(只做不验);`full`=代码改动/修 bug/新特性(默认,要 Verifier 独立复跑验收);`direct`=主窗口亲办不进池;**「用户说做回归/巡检/盘点 → 归 work」显式点名**。`task_create` 工具描述与拆解官判档文案同口径同步
290
291
  - 任务完成/阻塞时主窗口收到**批量聚合回执**(45s 窗口或满 5 条聚合,等主窗口空闲再发,不打断对话);⚙️ 设置区「通知」小节有两个回执开关——`notifyDispatch`(⚡ 派发回执:任务被 Worker/Verifier 领走时播报)与 `notifyDone`(✅ 完成回执:完成/阻塞时聚合播报),缺省均**开**(老看板文件缺字段自动补 `true`);**歧义裁决通知不受开关影响**(裁决通道不是回执,任务等人裁决必须提醒)
291
292
 
292
293
  > 兼容:旧的 `set-board-mode` / `set-team-mode` 两个 RPC 原样保留(旧客户端与脚本不受影响),
@@ -330,7 +331,7 @@ Team 托管档独有(调度员体验):
330
331
 
331
332
  ### host 模块边界(lib/*.mjs)
332
333
 
333
- v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域模块,
334
+ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域模块;v1.9.2 再把 dispatch.mjs(2798 行峰值)按职责域四拆(-51%),
334
335
  共享闭包状态收进显式 `state` 对象逐模块注入——模块边界即任务边界,并行任务不再全员互锁:
335
336
 
336
337
  | 模块 | 域 | 内容 |
@@ -340,9 +341,14 @@ v1.6.0 起 host 端从单体 index.mjs(1487 行)拆为薄壳 + 7 个领域
340
341
  | `session.mjs` | 会话 | root 解析缓存 / 会话 id 归一 / workMode 派生 / runsFor |
341
342
  | `store.mjs` | 持久化 | boardPath / rt / wt 原子落盘 / 跨重启继承 / fileLocks 串行化 / mutateLocked |
342
343
  | `notify.mjs` | 通知 | makeMsg / 歧义 25s 去抖 / 回执聚合 + 空闲门控 / 投递前过滤 |
343
- | `dispatch.mjs` | 派发引擎 | poolCycle / spawnOneShot / settleRun / 两级超时 / 孤儿回收 / 可续跑 Worker(continuable + 重启 reconcile + usage 水位线) |
344
+ | `hung.mjs` | 挂死检测 | 挂死常量 / 帧活性追踪(assistant-stream 双层判定)/ 日志零增长兜底扫描 |
345
+ | `model-health.mjs` | 模型健康 | 冷却嗅探 / 熔断连击 / 健康表 / 目录失效 / 主动探测全家桶(错误码分治 + 预算独立 + 恢复解除) |
346
+ | `settle.mjs` | 结算族 | settleRun 主干 / 四角色结算(worker/verifier/hook/decompose)/ usage 水位 / 重启 reconcile / 僵尸+幽灵清扫 |
347
+ | `dispatch.mjs` | 派发引擎 | poolCycle / spawnOneShot / 两级超时 / 可续跑 Worker(continuable)——结算/健康/挂死域经工厂闭包注入三兄弟模块 |
344
348
  | `rpc.mjs` | 接口层 | RPC 路由 + 16 个 Agent 工具注册 |
345
349
 
350
+ (import 单行道无环:hung←core;model-health←hung,core;settle←core,usage,policy,model-health,hung;dispatch←全部;store→dispatch 的循环依赖由 `deps.poolCycle` 晚绑定解开;index.mjs 对外 re-export 契约不变。)
351
+
346
352
  (store→dispatch 的循环依赖由 `deps.poolCycle` 晚绑定解开;index.mjs 对外 re-export 契约不变。)
347
353
 
348
354
  ### 前端模块边界(lib/client/)
@@ -283,6 +283,35 @@
283
283
  React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
284
284
  }
285
285
 
286
+ // touches 声明编辑(task-mvxw7hn5):文件锁声明主窗口/GUI 可改(生命周期门禁在 host——运行中拒写)。
287
+ // 未持锁态(draft/pending/blocked/cancelled/resolved)渲染 textarea + 保存,提交走 update-task RPC 的
288
+ // touches 通道(替换式更新,空清单=清除声明回「无锁」态);in-progress/verifying 只读展示 + 申报指引
289
+ //(此阶段边界由 Worker 以实际覆盖面申报:escalate→仲裁扩权;落定后可在此修正)。
290
+ // 分隔符兼容逗号/顿号/分号/换行;Ctrl+Enter 快捷提交。
291
+ function TouchesEditor(props) {
292
+ var task = props.task
293
+ var running = task.status === 'in-progress' || task.status === 'verifying'
294
+ var _R = React; var useState = _R.useState
295
+ var _s = useState((task.touches || []).join(', ')), draft = _s[0], setDraft = _s[1]
296
+ var _m = useState(''), msg = _m[0], setMsg = _m[1]
297
+ function commit() {
298
+ var touches = draft.split(/[,,、;;\n]+/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 }).slice(0, 20)
299
+ rpc('update-task', { taskId: task.id, touches: touches }).then(function (r) {
300
+ if (r && r.ok === false) setMsg('⛔ ' + ((r && r.error) || '保存失败'))
301
+ else setMsg('✅ touches 已更新(' + touches.length + ' 项' + (touches.length ? '' : '——空清单=清除声明,回「无锁」态') + ')')
302
+ fetchTasks()
303
+ }).catch(function (e) { setMsg('⛔ ' + String(e)) })
304
+ }
305
+ return React.createElement('div', { style: { marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } },
306
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🔒 文件级排他(touches · ' + (running ? '运行中只读' : '逗号/换行分隔,空=清除声明') + ')'),
307
+ running ? React.createElement('div', null,
308
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, wordBreak: 'break-all' } }, '🔒 ' + ((Array.isArray(task.touches) && task.touches.length) ? task.touches.join('、') : '(未声明)')),
309
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '运行中任务的 touches 由 Worker 以实际覆盖面申报(escalate→仲裁扩权);落定后可在此修正。')) : React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'flex-start' } },
310
+ React.createElement('textarea', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) commit() }, placeholder: '如 src/x.mjs、src/**(每行一条或逗号分隔;空=清除声明)', rows: 2, style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box', resize: 'vertical' } }),
311
+ React.createElement('button', { onClick: commit, title: '保存 touches 声明(替换式更新;空清单=清除声明回「无锁」态)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')),
312
+ msg ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, wordBreak: 'break-all' } }, msg) : null)
313
+ }
314
+
286
315
  function DetailView() {
287
316
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
288
317
  var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
@@ -362,7 +391,7 @@
362
391
  React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, '本任务声明的 touches 与以下进行中任务重叠(同一批文件并行改会互踩);它们提交验收/完成后自动派发。'),
363
392
  React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
364
393
  Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
365
- Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
394
+ React.createElement(TouchesEditor, { key: task.id + '|touches', task: task }),
366
395
  // 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
367
396
  // 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
368
397
  Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
package/lib/client.js CHANGED
@@ -1392,6 +1392,35 @@ function apply(ctx) {
1392
1392
  React.createElement('button', { onClick: commit, title: '保存标签(逗号分隔,自动 trim + 去空)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')))
1393
1393
  }
1394
1394
 
1395
+ // touches 声明编辑(task-mvxw7hn5):文件锁声明主窗口/GUI 可改(生命周期门禁在 host——运行中拒写)。
1396
+ // 未持锁态(draft/pending/blocked/cancelled/resolved)渲染 textarea + 保存,提交走 update-task RPC 的
1397
+ // touches 通道(替换式更新,空清单=清除声明回「无锁」态);in-progress/verifying 只读展示 + 申报指引
1398
+ //(此阶段边界由 Worker 以实际覆盖面申报:escalate→仲裁扩权;落定后可在此修正)。
1399
+ // 分隔符兼容逗号/顿号/分号/换行;Ctrl+Enter 快捷提交。
1400
+ function TouchesEditor(props) {
1401
+ var task = props.task
1402
+ var running = task.status === 'in-progress' || task.status === 'verifying'
1403
+ var _R = React; var useState = _R.useState
1404
+ var _s = useState((task.touches || []).join(', ')), draft = _s[0], setDraft = _s[1]
1405
+ var _m = useState(''), msg = _m[0], setMsg = _m[1]
1406
+ function commit() {
1407
+ var touches = draft.split(/[,,、;;\n]+/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 }).slice(0, 20)
1408
+ rpc('update-task', { taskId: task.id, touches: touches }).then(function (r) {
1409
+ if (r && r.ok === false) setMsg('⛔ ' + ((r && r.error) || '保存失败'))
1410
+ else setMsg('✅ touches 已更新(' + touches.length + ' 项' + (touches.length ? '' : '——空清单=清除声明,回「无锁」态') + ')')
1411
+ fetchTasks()
1412
+ }).catch(function (e) { setMsg('⛔ ' + String(e)) })
1413
+ }
1414
+ return React.createElement('div', { style: { marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } },
1415
+ React.createElement('div', { style: { fontSize: 11, fontWeight: 600, color: C.text2, marginBottom: 4 } }, '🔒 文件级排他(touches · ' + (running ? '运行中只读' : '逗号/换行分隔,空=清除声明') + ')'),
1416
+ running ? React.createElement('div', null,
1417
+ React.createElement('div', { style: { fontSize: 10, color: C.text2, wordBreak: 'break-all' } }, '🔒 ' + ((Array.isArray(task.touches) && task.touches.length) ? task.touches.join('、') : '(未声明)')),
1418
+ React.createElement('div', { style: { fontSize: 9, color: C.text2, marginTop: 2 } }, '运行中任务的 touches 由 Worker 以实际覆盖面申报(escalate→仲裁扩权);落定后可在此修正。')) : React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'flex-start' } },
1419
+ React.createElement('textarea', { value: draft, onChange: function (e) { setDraft(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) commit() }, placeholder: '如 src/x.mjs、src/**(每行一条或逗号分隔;空=清除声明)', rows: 2, style: { flex: 1, minWidth: 0, fontSize: 11, padding: '4px 8px', border: '1px solid ' + C.border2, borderRadius: 4, background: C.card, color: C.text, fontFamily: 'inherit', boxSizing: 'border-box', resize: 'vertical' } }),
1420
+ React.createElement('button', { onClick: commit, title: '保存 touches 声明(替换式更新;空清单=清除声明回「无锁」态)', style: { fontSize: 10, padding: '3px 10px', border: 'none', borderRadius: 4, background: C.brand, color: C_INV, cursor: 'pointer', flexShrink: 0 } }, '保存')),
1421
+ msg ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 3, wordBreak: 'break-all' } }, msg) : null)
1422
+ }
1423
+
1395
1424
  function DetailView() {
1396
1425
  var _R = React; var useState = _R.useState, useEffect = _R.useEffect; var task = getTask(state.detailId)
1397
1426
  var _a = useState(task ? task.title : ''), editTitle = _a[0], setEditTitle = _a[1]; var _b = useState(task ? task.description || '' : ''), editDesc = _b[0], setEditDesc = _b[1]; var _c = useState(false), saving = _c[0], setSaving = _c[1]; var _d = useState(state.boardMode), mode = _d[0], setMode = _d[1]
@@ -1471,7 +1500,7 @@ function apply(ctx) {
1471
1500
  React.createElement('div', { style: { fontSize: 10, color: C.text2, marginBottom: 4 } }, '本任务声明的 touches 与以下进行中任务重叠(同一批文件并行改会互踩);它们提交验收/完成后自动派发。'),
1472
1501
  React.createElement('div', { style: { fontSize: 10, display: 'flex', flexWrap: 'wrap', gap: 5 } }, task.waitingForTouches.map(function (id) { var t2 = getTask(id); return React.createElement('span', { key: id, onClick: function () { if (t2) { state.detailId = id; notify() } }, title: t2 ? (t2.title + ' (' + id + ')') : id, style: { padding: '1px 5px', borderRadius: 3, background: C.nested, border: '1px solid ' + C.border, color: t2 ? C.brand : C.text2, cursor: t2 ? 'pointer' : 'default' } }, (t2 ? t2.title.slice(0, 20) : shortId(id)) + ' · ' + shortId(id)) })),
1473
1502
  Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 10, color: C.text2, marginTop: 4 } }, '本任务 touches: ' + task.touches.join('、')) : null) : null,
1474
- Array.isArray(task.touches) && task.touches.length ? React.createElement('div', { style: { fontSize: 11, color: C.text2, marginBottom: 6, padding: '4px 6px', background: C.nested, borderRadius: 4 } }, '🔒 文件级排他(touches): ' + task.touches.join('、')) : null,
1503
+ React.createElement(TouchesEditor, { key: task.id + '|touches', task: task }),
1475
1504
  // 起草 lint(task.lintWarnings[],host 建卡/更新卡现算落盘,get-tasks 透出):
1476
1505
  // 四规则软警告(粒度过粗/双仓库歧义/缺验收/标题缺动词或描述空)随卡展示,缺字段/空数组整块不渲染(老卡零影响)。
1477
1506
  Array.isArray(task.lintWarnings) && task.lintWarnings.length ? React.createElement('div', { style: { fontSize: 11, color: C.text, marginBottom: 6, padding: '5px 8px', border: '1px solid ' + C.warn, borderRadius: 6, background: 'color-mix(in srgb, ' + C.warn + ' 8%, transparent)' } },
package/lib/core.mjs CHANGED
@@ -97,10 +97,13 @@ export function parsePlanFiles(question) {
97
97
  for (var i = 0; i < parts.length; i++) {
98
98
  // 顺序:列表符 → 引号对提取/首空白全角标点截断 → 残留反引号 → trim → 像路径判定 → 归一
99
99
  var v = parts[i].trim().replace(/^[-*•·]\s+/, '')
100
+ // 残片哨兵证据取自「截断前」原文(task-mvtz2jjq):正则残片 `/if \(isHungKillFailure/` 的首空白
101
+ // 截断会把斜杠后的 '(' 与 '\' 转义一起截掉、只剩 `/if`——截断后再看哨兵就丢了证据,故先记。
102
+ var fragSrc = v
100
103
  // 引号包裹路径(「除非引号包裹」例外分支,行内代码反引号):取首对反引号内容为条目、可含
101
104
  // 空格,配对后尾巴(说明/注释)一并忽略;无配对落空走下方截断兜底(残留端点反引号仍剥)
102
105
  var bt = /^`+([^`]+)`+/.exec(v)
103
- if (bt) { v = bt[1] } else {
106
+ if (bt) { v = bt[1]; fragSrc = v } else {
104
107
  // 截到首个空白/全角标点前的路径前缀(task-mvr44s0y):截完是「行首前缀」——路径在前的
105
108
  // 混合行(「path(注释)」「path — 说明」「path 说明文字」)截出路径本体;散文段(说明在
106
109
  // 前、路径碎片在后)截完是散文首词,过不了像路径判定 → 整条丢弃(选定语义,断言锁死)
@@ -113,7 +116,15 @@ export function parsePlanFiles(question) {
113
116
  // 反斜杠——normTouch 同源等价)或以代码扩展名结尾(宽正则,.mjs/.md/.json/.py/.yml 等)才
114
117
  // 入选;条目全被丢光走既有「空清单」语义(rpc.mjs 有前缀但零路径 → 重报提示自纠正)。
115
118
  // 判定在归一前做:`lib/`、`D:\dir` 的分隔符证据会被 normTouch 剥尾斜杠/改写,先记再归一。
116
- var likePath = /[\/\\]/.test(v) || /\.[a-z0-9]{1,6}$/i.test(v)
119
+ // 解析伪影收紧(task-mvtz2jjq,n-mvtejrusnhi6 生产实证两类碎片——「仲裁扩权后重派仍被拦」的
120
+ // 真凶不是闸口不认 extraTouches[闸口 rpc.mjs 已读 effectiveTouches],而是这两类残片被误判路径
121
+ // 后永远不在边界内):①含中日韩字符(CJK 统一表意 + 全角形式区)的 token 判非路径——「调用点/
122
+ // 接线断言仍锁」这类含半角斜杠的中文散文片即死(本仓库路径全 ASCII);②正则/代码残片:以 '/'
123
+ // 开头且其后含 '(' 或 '\' 转义 → 判非路径(`/if \(...` 即死)。两者只收紧「伪路径入选」,真路径
124
+ // (ASCII 相对/绝对、反斜杠、尾斜杠、裸扩展名)判定与既有归一语义逐字不变。
125
+ var cjk = /[\u4e00-\u9fff\uff00-\uffef]/.test(v)
126
+ var frag = /^\//.test(fragSrc) && /[()\\]/.test(fragSrc.slice(1))
127
+ var likePath = !cjk && !frag && (/[\/\\]/.test(v) || /\.[a-z0-9]{1,6}$/i.test(v))
117
128
  v = normTouch(v)
118
129
  if (v && likePath && out.indexOf(v) < 0) out.push(v)
119
130
  }
@@ -452,7 +463,9 @@ export function claimApply(d, t, sid, note) { var ps = t.status; t.status = 'in-
452
463
  // 状态闸门即拦,级联不会反向误推动父卡。
453
464
  export function isChildSettled(t) { return !!t && (t.status === 'resolved' || t.status === 'cancelled' || t.status === 'archived') }
454
465
  // 史诗父卡自动收口(共享 helper,唯一判定口径):父卡存在、父卡 in-progress、且全部子任务 ∈ 终态完成集
455
- // → 父卡转 verifying(交人验收)。幂等:父卡已 verifying/resolved/archived 一律返回 null,重复调用无副作用。
466
+ // → 按父卡 pipeline 分态收口:full/work 转 verifying(交人/Verifier 验收);direct 直接 resolved
467
+ // (direct 不进派发池对 verifier 同成立,推进 verifying 等于推进它够不到的状态)。
468
+ // 幂等:父卡已 verifying/resolved/archived 一律返回 null,重复调用无副作用。
456
469
  export function maybeAutoCloseParent(d, childTask) {
457
470
  if (!d || !childTask || !isb(childTask)) return null
458
471
  var p = gpt(childTask, d.tasks)
@@ -478,6 +491,21 @@ export function maybeAutoCloseParent(d, childTask) {
478
491
  }
479
492
  }
480
493
  if (p.hooks && p.hooks.post && p.hooks.post.pending) delete p.hooks.post.pending
494
+ // ===== 收口分态(task-mvxmxl00):direct 父卡直接 resolved,full/work 维持推进 verifying =====
495
+ // 设计矛盾实证(反馈 n-mvxmue8zggw0,本体板三张 direct 史诗 task-mvwk48c2/48rr/49fw):旧实现
496
+ // 一律推进 verifying,但 pickDispatch 的 verifier 候选过滤是 `(!t.pipeline || t.pipeline === 'full')`
497
+ // ——direct 卡永远不进 verifs,卡尸停在 verifying(派 Verifier 永不可能),把人锁死在这个状态。
498
+ // 口径修正:direct 语义「不进派发池」对 verifier 同样成立,故 direct 父卡在子任务全了结后直接
499
+ // resolved(子任务每张都已独立走完 worker+verifier 验收,父卡验收是形式收口;父卡本就是人建的容器)。
500
+ // full/work 零变化:照旧推进 verifying 走验收(work 卡在 resolveApply 侧本就落 resolved,不经此分支)。
501
+ // 边界:只修推进侧不再把 direct 卡推进它够不到的状态;派发侧过滤闸(pickDispatch)一字不动。
502
+ if (p.pipeline === 'direct') {
503
+ p.status = 'resolved'
504
+ p.resolvedAt = new Date().toISOString()
505
+ p.resolution = 'all subtasks resolved'
506
+ ah(p, 'in-progress', 'resolved', 'system', 'auto: all subtasks resolved(direct 父卡直接收口)')
507
+ return p
508
+ }
481
509
  p.status = 'verifying'
482
510
  p.resolvedAt = new Date().toISOString()
483
511
  p.resolution = 'all subtasks resolved'