@joekytc/dsh-swarm 0.3.1 → 0.3.6

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.
Files changed (53) hide show
  1. package/README.md +14 -7
  2. package/README.zh-CN.md +10 -5
  3. package/client/KanbanBoard.tsx +3 -1
  4. package/client/TaskDrawer.tsx +74 -2
  5. package/client/WorkflowRail.tsx +84 -1
  6. package/client/config-store.ts +3 -1
  7. package/client/kanban.css +28 -0
  8. package/client/timeline-model.ts +4 -0
  9. package/client/workflow-model.ts +20 -11
  10. package/lib/client.js +1100 -22
  11. package/lib/config.d.ts +5 -1
  12. package/lib/config.js +2 -1
  13. package/lib/dispatcher/agent-runner.js +25 -9
  14. package/lib/dispatcher/event-waker.js +4 -1
  15. package/lib/dispatcher/v-orchestrator.d.ts +2 -0
  16. package/lib/dispatcher/v-orchestrator.js +58 -9
  17. package/lib/domain/config-override.d.ts +6 -0
  18. package/lib/domain/config-override.js +16 -1
  19. package/lib/domain/im-message.d.ts +21 -2
  20. package/lib/domain/im-message.js +84 -4
  21. package/lib/domain/kanban-service.d.ts +8 -1
  22. package/lib/domain/kanban-service.js +67 -1
  23. package/lib/domain/ocr-review.d.ts +6 -2
  24. package/lib/domain/ocr-review.js +20 -11
  25. package/lib/domain/permissions.d.ts +1 -1
  26. package/lib/domain/permissions.js +4 -0
  27. package/lib/domain/projection.js +8 -3
  28. package/lib/domain/review-evidence.js +9 -0
  29. package/lib/domain/review-target.d.ts +10 -0
  30. package/lib/domain/review-target.js +24 -0
  31. package/lib/domain/state-machine.js +1 -1
  32. package/lib/domain/types.d.ts +16 -3
  33. package/lib/routes/kanban-http.js +66 -6
  34. package/lib/services/config-provider.js +2 -0
  35. package/lib/services/im-bot-probe.d.ts +36 -0
  36. package/lib/services/im-bot-probe.js +128 -0
  37. package/lib/services/im-delivery.d.ts +69 -7
  38. package/lib/services/im-delivery.js +220 -34
  39. package/lib/tools/kanban-tools.js +38 -1
  40. package/lib/tools/main-session-tools.js +34 -9
  41. package/lib/tools/ocr-review-tools.js +13 -6
  42. package/package.json +6 -1
  43. package/personas/kanban-d/agent.cordis.yml +0 -1
  44. package/personas/kanban-dt/agent.cordis.yml +4 -3
  45. package/personas/kanban-p/agent.cordis.yml +16 -0
  46. package/personas/kanban-pt/agent.cordis.yml +19 -1
  47. package/personas/kanban-w/agent.cordis.yml +15 -0
  48. package/personas/persona-d.md +0 -1
  49. package/personas/persona-dt.md +2 -2
  50. package/personas/persona-p.md +4 -0
  51. package/personas/persona-pt.md +4 -1
  52. package/personas/persona-w.md +1 -0
  53. package/personas/swarm/agent.cordis.yml +11 -0
package/lib/config.d.ts CHANGED
@@ -59,13 +59,17 @@ export interface KanbanConfig {
59
59
  forbidden: string[];
60
60
  };
61
61
  /** IM 主动投递(企微,2026-09-07 评审决议):W3 收尾/链阻塞时经 dsh-im 投群。
62
- * enabled=false(默认)功能关闭;botId/targetId 留空=运行时自动发现(唯一 wecom bot + 唯一已保存群目标)。 */
62
+ * enabled=false(默认)功能关闭;botId/targetId 留空=运行时自动发现(唯一 wecom bot + 唯一已保存群目标)。
63
+ * 多机器人接入时:botId 留空 → 按会话 preset 匹配机器人(探针读 dsh-im 侧 agentPreset);
64
+ * 未命中 → fallbackBotId;仍未设默认 → 手动投递路径交互选择(auto 路径直接 fail-closed 列候选)。 */
63
65
  imDelivery: {
64
66
  enabled: boolean;
65
67
  botId: string;
66
68
  targetId: string;
67
69
  /** 私聊目标(自由投递 /sms -s):留空=自动发现唯一 kind:'user' 已保存目标,仅且只有一个。 */
68
70
  dmTargetId: string;
71
+ /** 默认机器人(预设未命中的落点):空=未设默认,多机器人下手动投递每次都要交互选择。 */
72
+ fallbackBotId: string;
69
73
  };
70
74
  /** 评审引擎双模:delegate=沿用各角色自有模型评审;managed=统一经 dsh「模型链」评审。
71
75
  * managed.provider/model = dsh「模型链」llm-catalog 的 provider/model id,wire 时写成 ocr 自定义 provider(dsh-managed);key 不落本配置。 */
package/lib/config.js CHANGED
@@ -62,7 +62,8 @@ export const Config = Schema.object({
62
62
  botId: Schema.string().default(''),
63
63
  targetId: Schema.string().default(''),
64
64
  dmTargetId: Schema.string().default(''),
65
- }).default({ enabled: false, botId: '', targetId: '', dmTargetId: '' }),
65
+ fallbackBotId: Schema.string().default(''),
66
+ }).default({ enabled: false, botId: '', targetId: '', dmTargetId: '', fallbackBotId: '' }),
66
67
  reviewEngine: Schema.object({
67
68
  mode: Schema.union(['delegate', 'managed']).default('delegate'),
68
69
  managed: Schema.object({
@@ -155,25 +155,41 @@ ${task.body}`);
155
155
  }
156
156
  }
157
157
  // 返工场景(task.reworkOfTaskId 非空且 reviewStatus='pending'):
158
- // 注入 review/failed 的 issues 清单与建议方向(评审卡 verdict=fail 后的返工卡上下文)
158
+ // 注入上一轮评审的对账状态 + 未修复项速览。完整修复清单由返工卡 body「本轮修复清单」节承载
159
+ //(单一信息源,2026-09-15 P0-3)——避免同一份 issues 在上下文里重复两遍。
159
160
  if (task.reworkOfTaskId && task.reviewStatus === 'pending') {
161
+ // 2026-09-15 P0-1 修正:review/failed 的 targetTaskId 恒为 root(原卡),而 reworkOfTaskId 指向
162
+ // 上一张返工卡 —— 必须沿 reworkOfTaskId 链回溯到 root 再匹配。旧实现第 2 张返工卡起永不命中,
163
+ // 注入 "no review/failed evidence found",P 盲修(ch_1_mu235xfz 不收敛的直接机制 L1)。
164
+ let rootId = task.reworkOfTaskId;
165
+ const seenIds = new Set([rootId]);
166
+ for (;;) {
167
+ const up = state.tasks.get(rootId);
168
+ if (!up?.reworkOfTaskId || seenIds.has(up.reworkOfTaskId))
169
+ break;
170
+ rootId = up.reworkOfTaskId;
171
+ seenIds.add(rootId);
172
+ }
160
173
  const reviewFailed = [...state.events]
161
174
  .reverse()
162
- .find((e) => e.kind === 'review/failed' && e.payload['targetTaskId'] === task.reworkOfTaskId);
175
+ .find((e) => e.kind === 'review/failed' && e.payload['targetTaskId'] === rootId);
163
176
  parts.push('## Review guidance (rework task)');
164
- parts.push('- 上游任务: ' + task.reworkOfTaskId);
177
+ parts.push('- 上游任务: ' + task.reworkOfTaskId + (rootId !== task.reworkOfTaskId ? `(root: ${rootId})` : ''));
165
178
  if (reviewFailed) {
166
179
  const evidence = reviewFailed.payload['evidence'];
167
180
  const issues = evidence?.issues ?? [];
168
- parts.push('- review issues:');
169
- for (const issue of issues) {
170
- parts.push(` - [${issue.severity}] ${issue.title}${issue.resolved ? ' (resolved)' : ''}`);
181
+ const unresolved = issues.filter((i) => i.resolved !== true);
182
+ parts.push(`- 上一轮评审 ${issues.length} 条 issue:未修复 ${unresolved.length} 条、已修复 ${issues.length - unresolved.length} 条。`);
183
+ parts.push('- 完整修复清单(含定位、问题原文与建议改法)见任务体「本轮修复清单」节:逐条处理未修复项,只做定点修复,禁止无关联全文重写与任务编号重排。');
184
+ if (unresolved.length > 0) {
185
+ parts.push('- 未修复项速览:');
186
+ for (const i of unresolved) {
187
+ parts.push(` - [${i.severity}]${i.legacy ? '[遗留]' : '[新问题]'} ${i.title}${i.location ? `(${i.location})` : ''}`);
188
+ }
171
189
  }
172
- if (issues.length === 0)
173
- parts.push(' - (no issues recorded in review evidence)');
174
190
  }
175
191
  else {
176
- parts.push('- no review/failed evidence found; re-verify the upstream deliverable before completing');
192
+ parts.push('- 事件流未找到 review/failed 记录;请先核对任务体「本轮修复清单」节,再完成返工。');
177
193
  }
178
194
  }
179
195
  return parts.join('\n\n');
@@ -8,7 +8,10 @@ export class EventWaker {
8
8
  async onEvent(ev) {
9
9
  // V 仅在规格卡批准后(spec-card/approved)或任务完成/阻塞(驱动阶段推进)时唤醒;
10
10
  // chain/created 不再唤醒 V(V 不再处理旧 w1 预取阶段,等 /openspec: 建卡 + 批准后再行动)。
11
- const wakeable = (ev.kind === 'task/completed' || ev.kind === 'task/blocked' || ev.kind === 'spec-card/approved');
11
+ // 2026-09-15 恢复能力:人工恢复(chain/reopened)与评审豁免(review/waived)同样唤醒 V,
12
+ // 重入编排按最后一条 review 事件判定推进(否则恢复动作后无人推进 = 恢复形同虚设)。
13
+ const wakeable = (ev.kind === 'task/completed' || ev.kind === 'task/blocked' || ev.kind === 'spec-card/approved'
14
+ || ev.kind === 'chain/reopened' || ev.kind === 'review/waived');
12
15
  if (!wakeable)
13
16
  return;
14
17
  if (this.inFlight.has(ev.chainId))
@@ -11,6 +11,8 @@ export interface ChainOrchestration {
11
11
  sessionId: string | null;
12
12
  waitingOn: string | null;
13
13
  stallCount?: number;
14
+ /** 已消费的人工恢复事件 seq(chain/reopened):恢复后重置 stallCount 一次(2026-09-15,防反复清零)。 */
15
+ reopenConsumedSeq?: number;
14
16
  }
15
17
  export declare const R20_PHASE_ORDER: VPhase[];
16
18
  /** 每 phase 的期望建卡(pt 由 P 交付 pt_decision.needed=true 触发;dt 固定)。 */
@@ -24,8 +24,11 @@ export const PHASE_INSTRUCTIONS = {
24
24
  '## P 阶段任务体要求(计划者,非执行者)',
25
25
  'body 写入规划指令:读规格卡(含 file-prefetch/kb 附件=需求澄清清单)→ 产出 openspec 实施计划。仓库事实不足时先只读自查仓库代码实证(fs/search/grep 均可),再产出计划。',
26
26
  '产物路径(OpenSpec 规范):写入目标仓库 `openspec/changes/<change_name>/` 下 proposal.md / design.md / tasks.md(change_name 依规格自拟 kebab-case,如 autoNote-tab);complete 时 metadata.artifacts_path = 该目录绝对路径。',
27
- 'tasks.md 拆分粒度(结构准备度,与 PT 同一判据):每个任务条目必须有单一、可独立核对的完成判据;禁止一条任务/一条测试用例混 N≥2 个互不依赖的可观察结果——按可独立验证的行为逐条拆分。',
27
+ 'tasks.md 拆分粒度(结构准备度,与 PT 同一判据):每个任务条目必须有单一、可独立核对的完成判据;禁止一条任务/一条测试用例混 N≥2 个互不依赖的可观察结果——按可独立验证的行为逐条拆分。完成判据必须是实施完成后的单一终态;不写执行层阶段态判据(RED/GREEN/REFACTOR、"先失败再通过"属 D 阶段执行协议,见 D 阶段模板)。',
28
28
  'proposal.md 必须含「上游协议遵循说明」节:逐条列规格卡/澄清清单声明的工程协议引用(可定位出处:路径/明确协议名)+ 计划如何遵守;未声明任何协议则该节写「无」。禁止引用规格卡之外的协议标准。',
29
+ '需求对齐(颗粒度):澄清清单/规格卡的任务项、任务点、注意点 ↔ 计划任务逐条双向映射——不遗漏;超纲任务须能溯源到澄清回答。',
30
+ '逻辑交互一致性:计划内部不得自相矛盾、依赖顺序可执行;impl_decisions 多方案必须在计划内收敛为唯一路径,不留未定分支。',
31
+ '返工轮约束(2026-09-15):任务体含「本轮修复清单」节时,沿用既有任务编号、只做清单内条目的定点修复;禁止重排编号与清单无关的全文重写/扩写。',
29
32
  '铁律:P 是计划者,绝不执行任何 git/worktree/commit/push、不改源码/README、不跑构建部署——执行是 D 的职责;只读自查仅限读仓库,写边界仅限 openspec/changes/ 目录(会话工具级硬护栏强制,其余一律只读)。',
30
33
  'complete 时 metadata 必须带 schema 合法的 pt_decision = { needed: boolean, reason?: string }(needed=true 时 reason 必填)。按下列复杂度清单判定(逐条勾选,禁止"感觉"):',
31
34
  ' needed=true(需 PT 计划评审),满足任一条:① 跨 ≥2 模块/目录,或改动公共接口/共享类型/配置文件;② 破坏性变更(对外 API、数据格式、迁移、兼容性);③ impl_decisions 含 ≥2 个互斥方案需仲裁;④ tasks ≥8 条,或含新增/重写核心模块;⑤ 涉安全/权限/并发/数据迁移等高风险面;⑥ OpenSpec change 中 specs/ 模块的 spec.md 文件数 ≥3。',
@@ -34,9 +37,10 @@ export const PHASE_INSTRUCTIONS = {
34
37
  ].join('\n'),
35
38
  pt: [
36
39
  '## PT 阶段任务体要求(计划评审,只读)',
37
- 'body 写入计划评审指令:P 已判定需要计划评审(理由见上)。只读评审 P 的计划产物,按五要素核对:需求对齐(澄清清单条目↔任务双向映射,不遗漏不超纲)/完整性/逻辑交互一致性/结构准备度(每任务条目单一、可独立核对的完成判据;混 N≥2 个互不依赖的可观察结果=混行为须拆分)/工程协议一致性(只对账 proposal「上游协议遵循说明」节,未声明协议不得自行引入)。输出 verdict+issues 入交接 metadata.review_evidence。',
40
+ 'body 写入计划评审指令:P 已判定需要计划评审(理由见上)。只读评审 P 的计划产物,按五要素核对:需求对齐(澄清清单/规格卡条目↔任务双向映射,不遗漏不超纲)/完整性/逻辑交互一致性/结构准备度(每任务条目单一、可独立核对的完成判据;混 N≥2 个互不依赖的可观察结果=混行为须拆分;看计划文本自身,不要求执行层字样)/工程协议一致性(只对账 proposal「上游协议遵循说明」节,未声明协议不得自行引入;TDD/RED-GREEN 属 D 阶段内置协议,不在对账范围)。输出 verdict+issues 入交接 metadata.review_evidence。',
41
+ '评审源正相关(2026-09-15):评审源 = 规格卡功能面 + 计划文本;环境/工具/凭证/权限/接口来源等需求外事项不得作为阻断性判据(有价值者置非阻塞建议,由执行环节验证)。',
38
42
  'issues 四要素缺一无效:location(哪条任务/哪节)+ 依据(上游声明引用或计划内部矛盾点)+ 问题 + 可执行建议;pass 时 issues 只放非阻塞建议。',
39
- '返工复审:body 含「上一轮评审未通过 issues」节时必须先逐条三态对账(已修复/未修复/部分修复),未修复旧 issue 原样沿用,再提新问题。',
43
+ '返工复审:body 含「上一轮评审未通过 issues」节时必须先逐条三态对账(已修复/未修复/部分修复),未修复旧 issue 原样沿用,再提新问题。对账条目必须写 legacy=true、本轮新问题 legacy=false/省略——收敛闸据此判定:旧账未清继续返工;旧账清零后仅未解决 critical 可继续 fail,其余降级为下游「评审遗留建议」。不要在旧账已修复的同一轮用更细的新维度继续 fail(不在五要素内的细化不算阻断)。',
40
44
  '评审闸硬要求(必须写入 body):complete 时 handoff metadata 顶层必须带 artifacts_path=<被评审计划的 openspec 目录绝对路径,直接继承被评审 P 卡 handoff 里的 artifacts_path 值>,或在 review_evidence 里给 reviewPage;review_evidence 形状不变({ verdict, issues, ...reviewPage 可选 })——两者都缺时评审闸拒绝 pass。',
41
45
  '铁律:PT 是只读评审角色,绝不修改任何产物/源码;不调用 kanban_create、不执行代码。',
42
46
  ].join('\n'),
@@ -108,7 +112,11 @@ function extractPtPassSuggestions(state, chainId) {
108
112
  .filter(({ rt }) => !!rt && rt.chainId === chainId && rt.assignee === 'pt' && rt.mode === 'review-plan')
109
113
  .at(-1);
110
114
  const ev = passed?.e.payload['evidence'];
111
- return Array.isArray(ev?.issues) ? ev.issues : [];
115
+ if (!Array.isArray(ev?.issues))
116
+ return [];
117
+ // 收敛闸降级通过:issues 含已对账(resolved=true)条目 → 只把未解决项传给下游;
118
+ // 真 pass 保持全量(pass 时 issues 即非阻塞建议留档)。
119
+ return ev.downgraded ? ev.issues.filter((i) => i.resolved !== true) : ev.issues;
112
120
  }
113
121
  /** 仅评审卡:archived 且从未处理过 verdict = 作废(void)。human 归档评审卡即作废,编排器视其不存在。
114
122
  * 非评审卡(p/d/w…)一律返回 false——避免误判"该阶段无卡"而重复建卡。 */
@@ -220,6 +228,14 @@ export class VOrchestrator {
220
228
  const specCard = chain.specCardId ? state.specCards.get(chain.specCardId) : null;
221
229
  if (chain.status === 'completed' || chain.status === 'aborted' || chain.status === 'blocked')
222
230
  return;
231
+ // 2026-09-15 恢复能力:人工恢复(chain/reopened)后重置 V 内建 stall 计数——每个恢复事件只消费一次
232
+ //(不可每轮无条件清零,否则 stall 计数失效、[create-failed] 永不触发)。给足重试额度,避免因历史
233
+ // stallCount 残留立即再撞上限(LRN-20260902-002:事件永不再来=永久停滞)。
234
+ const lastReopen = [...state.events].reverse().find((e) => e.kind === 'chain/reopened' && e.chainId === chainId);
235
+ if (lastReopen && lastReopen.seq > (orch.reopenConsumedSeq ?? -1)) {
236
+ orch.stallCount = 0;
237
+ orch.reopenConsumedSeq = lastReopen.seq;
238
+ }
223
239
  // 阻塞复核 pass(全量阻塞恢复):链上有 status=blocked 且尚无 [blocked-review] 复核评论的任务
224
240
  // → 向 V 发一轮阻塞复核(本轮 V 唯一动作),V 用 kanban_comment 以 [blocked-review] 开头逐一
225
241
  // 评论给方向后待命;无此类任务则正常推进。扩展为所有 blocked(含 kb-insufficient 等),
@@ -488,10 +504,16 @@ export class VOrchestrator {
488
504
  const evidence = (handoff?.metadata?.['review_evidence']);
489
505
  if (!evidence)
490
506
  return 'gave-up'; // 无证据(异常路径;完成闸已拦)
491
- // 幂等:该评审卡已有 review 事件(重启恢复)→ 按 passed 判定推进,否则保持
492
- const hasReviewEvent = (kind) => state.events.some((e) => e.taskId === reviewTask.id && e.kind === kind);
493
- if (hasReviewEvent('review/passed') || hasReviewEvent('review/failed') || hasReviewEvent('review/gave-up')) {
494
- return hasReviewEvent('review/passed') ? 'advanced' : 'rework';
507
+ // 幂等:该评审卡已有 review 终态事件(重启恢复 / 人工恢复后重入)→ 按**最后一条** review 事件判定:
508
+ // passed / waived → 推进;failed / gave-up → 保持(等返工链或人工裁决)。
509
+ // 2026-09-15 修正(ch_1_mu235xfz 死结第二节):旧实现用布尔 OR,gave-up 恒落 'rework' →
510
+ // 人工豁免/恢复后重入仍走不出去(该分支在 getVAgent 之前,纯空转零事件)。
511
+ const lastReview = [...state.events]
512
+ .reverse()
513
+ .find((e) => e.taskId === reviewTask.id
514
+ && (e.kind === 'review/passed' || e.kind === 'review/failed' || e.kind === 'review/gave-up' || e.kind === 'review/waived'));
515
+ if (lastReview) {
516
+ return lastReview.kind === 'review/passed' || lastReview.kind === 'review/waived' ? 'advanced' : 'rework';
495
517
  }
496
518
  // 被评审任务:复审卡经 reworkOfTaskId/parents 指向 rework 任务;首次评审回退到阶段源任务(done)
497
519
  const srcAssignee = role === 'pt' ? 'p' : 'd';
@@ -516,6 +538,28 @@ export class VOrchestrator {
516
538
  }
517
539
  // fail
518
540
  await this.kanban.recordReview(reviewTask.id, root.id, evidence, 'system');
541
+ // 收敛闸(2026-09-15 Q7/Q10,治本):旧账清零后,仅未解决的 critical 允许继续返工;其余降级为
542
+ // 下游「评审遗留建议」并按通过处理 —— 保证必然收敛。ch_1_mu235xfz 教训:4 轮对账 12 条全清仍 fail
543
+ //("计划自述类"新问题可在返工文本上无限再生),唯一出口是 gave-up 熔断。
544
+ const unresolved = evidence.issues.filter((i) => i.resolved !== true);
545
+ const legacyUnresolved = unresolved.filter((i) => i.legacy === true); // 漏标=false=视为新问题(方向安全)
546
+ // 生效范围(2026-09-15 修正):仅 PT 计划评审 + 复审轮(reviewAttempt ≥ 1)。
547
+ // 首轮评审保持原语义(high 阻断,给计划 1 轮返工机会,符合"小需求 1 轮返工通过"预期);
548
+ // DT 评审依据机械实证(test/build/lint/diff),severity 门槛会误放行真问题 → 不适用。
549
+ const isPtReworkRound = role === 'pt' && (currentTarget.reviewAttempt ?? 0) >= 1;
550
+ if (isPtReworkRound && legacyUnresolved.length === 0 && !unresolved.some((i) => i.severity === 'critical')) {
551
+ await this.kanban.comment(reviewTask.id, [
552
+ `[review-final] 收敛闸:旧账已清零(0 条未修复遗留),本轮 ${unresolved.length} 条新问题无 critical → 降级为下游「评审遗留建议」,本轮评审按通过处理。`,
553
+ '## 降级条目(原文)',
554
+ formatIssues(unresolved) || ' -(无)',
555
+ ].join('\n'), 'system');
556
+ await this.kanban.recordReview(reviewTask.id, root.id, {
557
+ ...evidence,
558
+ verdict: 'pass',
559
+ downgraded: { from: 'fail', reason: '旧账清零且无未解决 critical(收敛闸)' },
560
+ }, 'system');
561
+ return 'advanced';
562
+ }
519
563
  const maxR = this.configProvider.getEffective().dispatcher?.maxReworksPerRole?.[role] ?? 3;
520
564
  if ((currentTarget.reviewAttempt ?? 0) >= maxR) {
521
565
  await this.kanban.reviewGaveUp(reviewTask.id, root.id, 'exceeded max reworks (' + maxR + ')', 'system');
@@ -544,7 +588,12 @@ export class VOrchestrator {
544
588
  formatIssues(evidence.issues) || ' -(无)',
545
589
  '对账规则:每条旧 issue 给出三态结论——已修复(resolved=true)/未修复(resolved=false,detail 原样沿用)/部分修复(resolved=false,detail 注明剩余部分);未修复旧 issue 必须原样保留在 issues 中,禁止跳过对账只提新问题。',
546
590
  ].join('\n');
547
- const rework = await this.kanban.createReworkTask({ sourceTaskId: currentTarget.id, reviewTaskId: reviewTask.id, reason: 'review failed' }, 'system');
591
+ const rework = await this.kanban.createReworkTask({
592
+ sourceTaskId: currentTarget.id,
593
+ reviewTaskId: reviewTask.id,
594
+ reason: 'review failed',
595
+ issues: evidence.issues, // 2026-09-15 P0-3:issues 全文随卡传递(返工卡 body「本轮修复清单」节)
596
+ }, 'system');
548
597
  await this.kanban.createTask({
549
598
  chainId,
550
599
  title: role === 'pt' ? '计划复审' : '实现复审',
@@ -21,6 +21,9 @@ export interface EditableOverride {
21
21
  model?: string;
22
22
  };
23
23
  };
24
+ imDelivery?: {
25
+ fallbackBotId?: string;
26
+ };
24
27
  }
25
28
  export interface EditableModelSnapshot {
26
29
  provider: string;
@@ -42,6 +45,9 @@ export interface EditableSnapshot {
42
45
  model: string;
43
46
  };
44
47
  };
48
+ imDelivery: {
49
+ fallbackBotId: string;
50
+ };
45
51
  }
46
52
  export type ConfigSource = 'override' | 'inherited';
47
53
  export type SourceMap = Record<string, ConfigSource>;
@@ -1,4 +1,6 @@
1
1
  export const ROLES = ['v', 'p', 'w', 'd', 'pt', 'dt'];
2
+ /** botId 形态(与 dsh-im 的 id 校验同口径):非空时只允许 id 安全字符。 */
3
+ const BOT_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
2
4
  const MODEL_FIELDS = ['provider', 'model', 'reasoningEffort'];
3
5
  export function mergeConfig(baseline, override) {
4
6
  const wiki = { ...baseline.wikiVault };
@@ -33,7 +35,10 @@ export function mergeConfig(baseline, override) {
33
35
  if (cur.provider && cur.model)
34
36
  models[role] = cur;
35
37
  }
36
- return { ...baseline, wikiVault: wiki, roles: { ...baseline.roles, models }, reviewEngine };
38
+ const imDelivery = { ...baseline.imDelivery };
39
+ if (override?.imDelivery?.fallbackBotId !== undefined)
40
+ imDelivery.fallbackBotId = override.imDelivery.fallbackBotId;
41
+ return { ...baseline, wikiVault: wiki, roles: { ...baseline.roles, models }, reviewEngine, imDelivery };
37
42
  }
38
43
  export function computeSources(override) {
39
44
  const src = {};
@@ -50,6 +55,7 @@ export function computeSources(override) {
50
55
  src['reviewEngine.mode'] = override?.reviewEngine?.mode !== undefined ? 'override' : 'inherited';
51
56
  src['reviewEngine.managed.provider'] = override?.reviewEngine?.managed?.provider !== undefined ? 'override' : 'inherited';
52
57
  src['reviewEngine.managed.model'] = override?.reviewEngine?.managed?.model !== undefined ? 'override' : 'inherited';
58
+ src['imDelivery.fallbackBotId'] = override?.imDelivery?.fallbackBotId !== undefined ? 'override' : 'inherited';
53
59
  return src;
54
60
  }
55
61
  export function projectEditable(effective) {
@@ -66,6 +72,7 @@ export function projectEditable(effective) {
66
72
  mode: effective.reviewEngine?.mode ?? 'delegate',
67
73
  managed: { provider: effective.reviewEngine?.managed?.provider ?? '', model: effective.reviewEngine?.managed?.model ?? '' },
68
74
  },
75
+ imDelivery: { fallbackBotId: effective.imDelivery?.fallbackBotId ?? '' },
69
76
  };
70
77
  }
71
78
  export function validateConfig(snapshot) {
@@ -89,6 +96,10 @@ export function validateConfig(snapshot) {
89
96
  if (!m.model?.trim())
90
97
  errs.push(`roles.models.${role}.model`);
91
98
  }
99
+ // 默认机器人:留空=未设默认(合法,语义为「未命中就交互」);非空必须形如 dsh-im 的 botId。
100
+ const fb = (snapshot.imDelivery?.fallbackBotId ?? '').trim();
101
+ if (fb && !BOT_ID_RE.test(fb))
102
+ errs.push('imDelivery.fallbackBotId');
92
103
  return errs;
93
104
  }
94
105
  export function diffOverride(baseline, snapshot) {
@@ -134,5 +145,9 @@ export function diffOverride(baseline, snapshot) {
134
145
  re.managed = reManaged;
135
146
  if (Object.keys(re).length)
136
147
  out.reviewEngine = re;
148
+ const bFb = (baseline.imDelivery?.fallbackBotId ?? '').trim();
149
+ const sFb = (snapshot.imDelivery?.fallbackBotId ?? '').trim();
150
+ if (sFb !== bFb)
151
+ out.imDelivery = { fallbackBotId: sFb };
137
152
  return out;
138
153
  }
@@ -2,7 +2,25 @@ import type { BoardState, KanbanEvent, Task } from './types.js';
2
2
  /** W3 收尾判据(镜像 kanban-service.completeTask 机械规则,但不含 dEvidenceOk 与链状态要求):
3
3
  * 完成卡为 w+kb、链上无未终态卡、它是最后完成的卡、且链上存在 done 的 D 卡(防 W2 中间态误投)。 */
4
4
  export declare function isFinalW3Completion(state: BoardState, chainId: string, completedTaskId: string): boolean;
5
- /** 成功汇报(markdown)。判据不满足(W2 中间态/非 w:kb/非收尾)返回 null。 */
5
+ /** 交付汇报结构化字段(W agent 收尾时写入 W3 交接 metadata.report;内容有出处=任务卡,渲染器只定骨架)。 */
6
+ export interface ChainReportFields {
7
+ requirement: string;
8
+ status: string;
9
+ branch: string;
10
+ /** 功能清单:openspec/changes/<id>/tasks.md 勾选态逐条映射(done=已勾)。 */
11
+ tasks: Array<{
12
+ text: string;
13
+ done: boolean;
14
+ }>;
15
+ acceptance: string;
16
+ verification: string;
17
+ leftovers: string[];
18
+ todos: string[];
19
+ }
20
+ /** 校验并归一 metadata.report:任一必需字段缺失/形状不符 → null(渲染回退机械格式,绝不渲染半残骨架)。 */
21
+ export declare function parseChainReport(raw: unknown): ChainReportFields | null;
22
+ /** 成功汇报(markdown)。判据不满足(W2 中间态/非 w:kb/非收尾)返回 null。
23
+ * W3 交接带合法 metadata.report → 交付汇报骨架;否则回退机械格式(旧链兼容)。 */
6
24
  export declare function buildCompletionMessage(state: BoardState, chainId: string, completedTaskId: string, now: number): string | null;
7
25
  export declare function suggestionsFor(reason: string): string[];
8
26
  /** 链下全部 blocked 卡(title + 最近一次 task/blocked 原因)。 */
@@ -11,5 +29,6 @@ export declare function latestBlockedTasks(events: KanbanEvent[], tasks: Iterabl
11
29
  title: string;
12
30
  reason: string;
13
31
  }>;
14
- /** 阻塞通知(markdown)。 */
32
+ /** 阻塞通知(markdown)。阻塞是系统性事件(看门狗/stall/人工),时刻 LLM 大多不在环:
33
+ * 保持机械渲染(事实 + 静态建议,防编造),仅做视觉对齐(标题/链 id/已完成进度)。 */
15
34
  export declare function buildBlockMessage(state: BoardState, chainId: string, chainReason: string, storageDir: string): string;
@@ -22,6 +22,46 @@ function strMeta(handoff, key) {
22
22
  const v = (handoff?.metadata ?? {})[key];
23
23
  return typeof v === 'string' ? v.trim() : '';
24
24
  }
25
+ function reportText(v) {
26
+ return typeof v === 'string' ? v.trim() : '';
27
+ }
28
+ function reportLines(v) {
29
+ if (!Array.isArray(v))
30
+ return [];
31
+ const out = [];
32
+ for (const item of v) {
33
+ const s = reportText(item);
34
+ if (s)
35
+ out.push(s);
36
+ }
37
+ return out;
38
+ }
39
+ /** 校验并归一 metadata.report:任一必需字段缺失/形状不符 → null(渲染回退机械格式,绝不渲染半残骨架)。 */
40
+ export function parseChainReport(raw) {
41
+ if (typeof raw !== 'object' || raw === null)
42
+ return null;
43
+ const r = raw;
44
+ const requirement = reportText(r['requirement']);
45
+ const status = reportText(r['status']);
46
+ const branch = reportText(r['branch']);
47
+ const acceptance = reportText(r['acceptance']);
48
+ const verification = reportText(r['verification']);
49
+ if (!requirement || !status || !branch || !acceptance || !verification)
50
+ return null;
51
+ if (!Array.isArray(r['tasks']) || r['tasks'].length === 0)
52
+ return null;
53
+ const tasks = [];
54
+ for (const entry of r['tasks']) {
55
+ if (typeof entry !== 'object' || entry === null)
56
+ return null;
57
+ const text = reportText(entry['text']);
58
+ const done = entry['done'];
59
+ if (!text || typeof done !== 'boolean')
60
+ return null;
61
+ tasks.push({ text, done });
62
+ }
63
+ return { requirement, status, branch, tasks, acceptance, verification, leftovers: reportLines(r['leftovers']), todos: reportLines(r['todos']) };
64
+ }
25
65
  /** 人工关注点:TDD 跳过声明、D 证据缺失(链完成门禁拦截)、审计警告状态。 */
26
66
  function completionAttention(state, chainId, dHandoff) {
27
67
  const out = [];
@@ -38,7 +78,37 @@ function completionAttention(state, chainId, dHandoff) {
38
78
  out.push(`审计警告已于 ${fmtTime(audit.confirmedAt)} 确认`);
39
79
  return out;
40
80
  }
41
- /** 成功汇报(markdown)。判据不满足(W2 中间态/非 w:kb/非收尾)返回 null。 */
81
+ /** 交付汇报骨架(内容来自 W3 metadata.report,系统只定字段与顺序)。
82
+ * 待办 = report.todos + 机械关注点(TDD 跳过/证据缺失/审计警告不因 W 遗忘而消失);遗留为空则整节省略。 */
83
+ function renderDeliveryReport(chain, chainId, report, wHandoff, attention) {
84
+ const lines = [];
85
+ lines.push(`**【交付汇报】${chain.title}(${chainId})**`);
86
+ lines.push(`需求:${report.requirement}`);
87
+ lines.push(`状态:${report.status}`);
88
+ lines.push(`分支:${report.branch}`);
89
+ lines.push('功能清单:');
90
+ for (const t of report.tasks)
91
+ lines.push(`- ${t.done ? '✅' : '❌'} ${t.text}`);
92
+ lines.push(`验收:${report.acceptance}`);
93
+ lines.push(`验证:${report.verification}`);
94
+ if (report.leftovers.length > 0) {
95
+ lines.push('遗留(非阻塞):');
96
+ for (const l of report.leftovers)
97
+ lines.push(`- ${l}`);
98
+ }
99
+ const todos = [...report.todos, ...attention];
100
+ if (todos.length > 0) {
101
+ lines.push('待办:');
102
+ for (const t of todos)
103
+ lines.push(`- ${t}`);
104
+ }
105
+ const kbUrl = strMeta(wHandoff, 'kb_url');
106
+ if (kbUrl)
107
+ lines.push(`KB:${kbUrl}`);
108
+ return lines.join('\n');
109
+ }
110
+ /** 成功汇报(markdown)。判据不满足(W2 中间态/非 w:kb/非收尾)返回 null。
111
+ * W3 交接带合法 metadata.report → 交付汇报骨架;否则回退机械格式(旧链兼容)。 */
42
112
  export function buildCompletionMessage(state, chainId, completedTaskId, now) {
43
113
  if (!isFinalW3Completion(state, chainId, completedTaskId))
44
114
  return null;
@@ -51,6 +121,10 @@ export function buildCompletionMessage(state, chainId, completedTaskId, now) {
51
121
  const dHandoff = state.handoffs.get(dTask.id);
52
122
  const pTask = [...chainTasks].reverse().find((t) => t.assignee === 'p' && t.status === 'done');
53
123
  const artifacts = strMeta(pTask ? state.handoffs.get(pTask.id) : undefined, 'artifacts_path');
124
+ const attention = completionAttention(state, chainId, dHandoff);
125
+ const report = parseChainReport((wHandoff?.metadata ?? {})['report']);
126
+ if (report)
127
+ return renderDeliveryReport(chain, chainId, report, wHandoff, attention);
54
128
  const lines = [];
55
129
  lines.push(`**【DSH 需求完成】${chain.title}**`);
56
130
  lines.push('');
@@ -66,7 +140,6 @@ export function buildCompletionMessage(state, chainId, completedTaskId, now) {
66
140
  if (pagePath)
67
141
  lines.push(`- KB 页:${pagePath}`);
68
142
  lines.push(`- 产出物:${artifacts || '(无)'}`);
69
- const attention = completionAttention(state, chainId, dHandoff);
70
143
  if (attention.length > 0) {
71
144
  lines.push('');
72
145
  lines.push('**人工关注点**');
@@ -120,12 +193,15 @@ export function latestBlockedTasks(events, tasks, chainId) {
120
193
  }
121
194
  return out;
122
195
  }
123
- /** 阻塞通知(markdown)。 */
196
+ /** 阻塞通知(markdown)。阻塞是系统性事件(看门狗/stall/人工),时刻 LLM 大多不在环:
197
+ * 保持机械渲染(事实 + 静态建议,防编造),仅做视觉对齐(标题/链 id/已完成进度)。 */
124
198
  export function buildBlockMessage(state, chainId, chainReason, storageDir) {
125
199
  const chain = state.chains.get(chainId);
126
200
  const blocked = latestBlockedTasks(state.events, state.tasks.values(), chainId);
201
+ const chainTasks = [...state.tasks.values()].filter((t) => t.chainId === chainId);
202
+ const doneCount = chainTasks.filter((t) => t.status === 'done' || t.status === 'archived').length;
127
203
  const lines = [];
128
- lines.push(`**【DSH 需求阻塞】${chain?.title ?? chainId}**`);
204
+ lines.push(`**【交付阻塞】${chain?.title ?? chainId}(${chainId})**`);
129
205
  lines.push('');
130
206
  lines.push('**阻塞卡**');
131
207
  if (blocked.length === 0)
@@ -136,6 +212,10 @@ export function buildBlockMessage(state, chainId, chainReason, storageDir) {
136
212
  lines.push('');
137
213
  lines.push('**阻塞原因**');
138
214
  lines.push(`> ${chainReason}`);
215
+ if (chainTasks.length > 0) {
216
+ lines.push('');
217
+ lines.push(`**已完成**:${doneCount}/${chainTasks.length} 卡`);
218
+ }
139
219
  lines.push('');
140
220
  lines.push('**排查建议**');
141
221
  for (const s of suggestionsFor(chainReason))
@@ -1,6 +1,6 @@
1
1
  import type { EventStore } from './event-store.js';
2
2
  import { type Actor } from './permissions.js';
3
- import type { AuditEvidence, BoardState, Chain, Handoff, KanbanEvent, SpecCard, SpecCardAttachment, SpecCardSections, Task, TaskMode, Role, ReviewEvidence } from './types.js';
3
+ import type { AuditEvidence, BoardState, Chain, Handoff, KanbanEvent, SpecCard, SpecCardAttachment, SpecCardSections, Task, TaskMode, Role, ReviewEvidence, ReviewIssue } from './types.js';
4
4
  export type KanbanListener = (event: KanbanEvent) => void;
5
5
  /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(需求标题规范化)。 */
6
6
  export declare function firstSentence(text: string): string;
@@ -91,6 +91,12 @@ export declare class KanbanService {
91
91
  recordReview(reviewTaskId: string, targetTaskId: string, evidence: ReviewEvidence, actor: Actor): Promise<KanbanEvent>;
92
92
  /** 评审超限放弃:review/gave-up(含证据链信息)。仅 system。 */
93
93
  reviewGaveUp(reviewTaskId: string, targetTaskId: string, reason: string, actor: Actor): Promise<KanbanEvent>;
94
+ /** 人工恢复被 blocked 的链(2026-09-15 恢复能力):仅 human;fail-closed 只接受 blocked。
95
+ * 发 chain/reopened(状态机 blocked → executing),并向链末锚点卡写 [recovery] 审计评论(禁止静默恢复)。 */
96
+ reopenChain(chainId: string, reason: string, actor: Actor): Promise<KanbanEvent>;
97
+ /** 人工评审豁免(2026-09-15 恢复能力):仅 human;要求目标 reviewStatus ∈ {failed, gave-up}(防误豁免 passed/pending)。
98
+ * 发 review/waived(投影更新 target.reviewStatus='waived'),并向目标卡写 [recovery] 审计评论。 */
99
+ waiveReview(reviewTaskId: string, targetTaskId: string, reason: string, actor: Actor): Promise<KanbanEvent>;
94
100
  /** 评审失败返工卡创建(评审失败闭环):原任务保持 done(不可变),新建返工卡继承 rework 字段。
95
101
  * 仅 system(can('create-rework-task')=system);V 建执行卡、system 建返工卡。
96
102
  * 语义(2026-09-07 修正):返工=该卡自己的独立会话——resumeSessionId 不继承源卡会话(置 null),
@@ -101,6 +107,7 @@ export declare class KanbanService {
101
107
  sourceTaskId: string;
102
108
  reviewTaskId: string;
103
109
  reason: string;
110
+ issues?: ReviewIssue[];
104
111
  }, actor: Actor): Promise<Task>;
105
112
  snapshot(): Promise<BoardState>;
106
113
  listTasks(opts?: {
@@ -18,6 +18,39 @@ export function buildChainTitle(requirementName, _openspecRest, problem) {
18
18
  const src = (requirementName && requirementName.trim()) || problem.trim();
19
19
  return `【需求】${src ? firstSentence(src) : '未命名需求'}`;
20
20
  }
21
+ /** 返工卡「本轮修复清单」节(2026-09-15 P0-3):系统确定性注入上一轮评审 issues 全文——
22
+ * 旧实现 body 逐字拷贝源卡,返工方看不到要修什么 → 全文重写 → 判据面再生(ch_1_mu235xfz 根因 L1/L4)。 */
23
+ function reworkChecklistSection(issues) {
24
+ if (!issues || issues.length === 0)
25
+ return '';
26
+ const unresolved = issues.filter((i) => i.resolved !== true);
27
+ const resolved = issues.filter((i) => i.resolved === true);
28
+ const lines = [
29
+ '',
30
+ '',
31
+ '## 本轮修复清单(系统确定性注入:上一轮评审未通过项)',
32
+ `上一轮评审共 ${issues.length} 条:未修复 ${unresolved.length} 条、已修复 ${resolved.length} 条。`,
33
+ '本卡为返工轮:只做定点修复,禁止无关联的全文重写与任务编号重排(2026-09-15 收敛决议)。',
34
+ ];
35
+ if (unresolved.length > 0) {
36
+ lines.push('### 未修复(本轮必须处理)');
37
+ for (const i of unresolved) {
38
+ lines.push(`- [${i.severity}] ${i.title}`);
39
+ if (i.location)
40
+ lines.push(` - 定位: ${i.location}`);
41
+ if (i.detail)
42
+ lines.push(` - 问题: ${i.detail}`);
43
+ if (i.fix)
44
+ lines.push(` - 建议改法: ${i.fix}`);
45
+ }
46
+ }
47
+ if (resolved.length > 0) {
48
+ lines.push('### 已修复(不需再改,保持现状)');
49
+ for (const i of resolved)
50
+ lines.push(`- [${i.severity}] ${i.title}`);
51
+ }
52
+ return lines.join('\n');
53
+ }
21
54
  /** 看板领域门面:三界面(工具/CLI/UI)统一路由的唯一入口。 */
22
55
  export class KanbanService {
23
56
  state;
@@ -435,6 +468,39 @@ export class KanbanService {
435
468
  throw new Error('unknown target task: ' + targetTaskId);
436
469
  return this.emit({ chainId: t.chainId, taskId: reviewTaskId, kind: 'review/gave-up', payload: { reviewTaskId, targetTaskId, reason }, author: actor, at: Date.now() });
437
470
  }
471
+ /** 人工恢复被 blocked 的链(2026-09-15 恢复能力):仅 human;fail-closed 只接受 blocked。
472
+ * 发 chain/reopened(状态机 blocked → executing),并向链末锚点卡写 [recovery] 审计评论(禁止静默恢复)。 */
473
+ async reopenChain(chainId, reason, actor) {
474
+ if (!can('reopen-chain', actor, null))
475
+ throw new Error('permission denied: only human may reopen chain');
476
+ const chain = await this.chainOf(chainId);
477
+ if (chain.status !== 'blocked')
478
+ throw new Error('reopenChain requires blocked chain, got: ' + chain.status);
479
+ if (!reason || !reason.trim())
480
+ throw new Error('reason required');
481
+ const ev = await this.emit({ chainId, taskId: null, kind: 'chain/reopened', payload: { reason }, author: actor, at: Date.now() });
482
+ const anchor = [...this.state.tasks.values()].filter((t) => t.chainId === chainId).at(-1);
483
+ if (anchor)
484
+ await this.comment(anchor.id, `[recovery] 人工恢复:链 ${chainId} 由 blocked 恢复为 executing(reason: ${reason})`, actor);
485
+ return ev;
486
+ }
487
+ /** 人工评审豁免(2026-09-15 恢复能力):仅 human;要求目标 reviewStatus ∈ {failed, gave-up}(防误豁免 passed/pending)。
488
+ * 发 review/waived(投影更新 target.reviewStatus='waived'),并向目标卡写 [recovery] 审计评论。 */
489
+ async waiveReview(reviewTaskId, targetTaskId, reason, actor) {
490
+ if (!can('waive-review', actor, null))
491
+ throw new Error('permission denied: only human may waive review');
492
+ const t = this.state.tasks.get(targetTaskId);
493
+ if (!t)
494
+ throw new Error('unknown target task: ' + targetTaskId);
495
+ if (t.reviewStatus !== 'failed' && t.reviewStatus !== 'gave-up') {
496
+ throw new Error('waiveReview requires target reviewStatus failed|gave-up, got: ' + t.reviewStatus);
497
+ }
498
+ if (!reason || !reason.trim())
499
+ throw new Error('reason required');
500
+ const ev = await this.emit({ chainId: t.chainId, taskId: reviewTaskId, kind: 'review/waived', payload: { reviewTaskId, targetTaskId, reason }, author: actor, at: Date.now() });
501
+ await this.comment(targetTaskId, `[recovery] 评审豁免:${reviewTaskId} → ${targetTaskId} 由人工豁免(reason: ${reason})`, actor);
502
+ return ev;
503
+ }
438
504
  /** 评审失败返工卡创建(评审失败闭环):原任务保持 done(不可变),新建返工卡继承 rework 字段。
439
505
  * 仅 system(can('create-rework-task')=system);V 建执行卡、system 建返工卡。
440
506
  * 语义(2026-09-07 修正):返工=该卡自己的独立会话——resumeSessionId 不继承源卡会话(置 null),
@@ -450,7 +516,7 @@ export class KanbanService {
450
516
  if (source.status !== 'done')
451
517
  throw new Error('rework source must be done: ' + source.status);
452
518
  const task = {
453
- id: nid('t'), chainId: source.chainId, title: `[返工] ${source.title}`, body: source.body,
519
+ id: nid('t'), chainId: source.chainId, title: `[返工] ${source.title}`, body: source.body + reworkChecklistSection(input.issues),
454
520
  assignee: source.assignee, status: 'todo', mode: source.mode, priority: 1,
455
521
  parents: [...source.parents], children: [], createdBy: 'auto', // system 自动创建(返工卡)
456
522
  attempts: 0, heartbeats: [],
@@ -7,8 +7,12 @@ export interface OcrArgsInput {
7
7
  to?: string;
8
8
  commit?: string;
9
9
  paths?: string[];
10
+ /** 业务上下文,托管评审时提升评审质量(仅 managed 分支透传)。 */
11
+ background?: string;
10
12
  }
11
- /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review 并恒带 JSON 输出。 */
13
+ /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review。
14
+ * preview 默认输出 text,必须显式 --format json 才能拿到可解析输出;
15
+ * managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。 */
12
16
  export declare function buildOcrArgs(sub: OcrSub, a: OcrArgsInput): string[];
13
17
  /** 预览结果归一化结构。 */
14
18
  export interface PreviewResult {
@@ -23,7 +27,7 @@ export interface PreviewResult {
23
27
  }[];
24
28
  mergeBase: string | null;
25
29
  }
26
- /** 解析 delegate preview 的 JSON 输出,失败或结构异常时回退 unknown。 */
30
+ /** 解析 delegate preview 的 JSON 输出(官方字段 reviewable_files/excluded_files/exclude_reason),失败或结构异常时回退 unknown。 */
27
31
  export declare function parsePreviewJson(stdout: string): PreviewResult;
28
32
  /** 托管评审结果归一化结构。 */
29
33
  export interface ManagedResult {