@joekytc/dsh-swarm 0.3.2 → 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.
- package/README.md +14 -7
- package/README.zh-CN.md +10 -5
- package/client/KanbanBoard.tsx +3 -1
- package/client/TaskDrawer.tsx +74 -2
- package/client/WorkflowRail.tsx +84 -1
- package/client/config-store.ts +3 -1
- package/client/kanban.css +28 -0
- package/client/timeline-model.ts +4 -0
- package/client/workflow-model.ts +20 -11
- package/lib/client.js +1100 -22
- package/lib/config.d.ts +5 -1
- package/lib/config.js +2 -1
- package/lib/dispatcher/agent-runner.js +25 -9
- package/lib/dispatcher/event-waker.js +4 -1
- package/lib/dispatcher/v-orchestrator.d.ts +2 -0
- package/lib/dispatcher/v-orchestrator.js +58 -9
- package/lib/domain/config-override.d.ts +6 -0
- package/lib/domain/config-override.js +16 -1
- package/lib/domain/im-message.d.ts +21 -2
- package/lib/domain/im-message.js +84 -4
- package/lib/domain/kanban-service.d.ts +8 -1
- package/lib/domain/kanban-service.js +67 -1
- package/lib/domain/permissions.d.ts +1 -1
- package/lib/domain/permissions.js +4 -0
- package/lib/domain/projection.js +8 -3
- package/lib/domain/review-evidence.js +9 -0
- package/lib/domain/review-target.d.ts +10 -0
- package/lib/domain/review-target.js +24 -0
- package/lib/domain/state-machine.js +1 -1
- package/lib/domain/types.d.ts +16 -3
- package/lib/routes/kanban-http.js +52 -2
- package/lib/services/config-provider.js +2 -0
- package/lib/services/im-bot-probe.d.ts +36 -0
- package/lib/services/im-bot-probe.js +128 -0
- package/lib/services/im-delivery.d.ts +69 -7
- package/lib/services/im-delivery.js +220 -34
- package/lib/tools/kanban-tools.js +38 -1
- package/lib/tools/main-session-tools.js +34 -9
- package/package.json +1 -1
- package/personas/kanban-p/agent.cordis.yml +16 -0
- package/personas/kanban-pt/agent.cordis.yml +19 -1
- package/personas/kanban-w/agent.cordis.yml +15 -0
- package/personas/persona-p.md +4 -0
- package/personas/persona-pt.md +4 -1
- package/personas/persona-w.md +1 -0
- 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
|
-
|
|
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
|
-
//
|
|
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'] ===
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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('-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|
package/lib/domain/im-message.js
CHANGED
|
@@ -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
|
-
/**
|
|
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(
|
|
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: [],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Role, Task } from './types.js';
|
|
2
|
-
export type KanbanAction = 'create-task' | 'create-chain' | 'claim' | 'complete' | 'block' | 'unblock' | 'comment' | 'heartbeat' | 'archive' | 'spec-approve' | 'spec-edit' | 'spec-attach' | 'wiki-write' | 'wiki-read' | 'prefetch' | 'audit-confirm' | 'update-title' | 'delete-chain' | 'create-rework-task';
|
|
2
|
+
export type KanbanAction = 'create-task' | 'create-chain' | 'claim' | 'complete' | 'block' | 'unblock' | 'comment' | 'heartbeat' | 'archive' | 'spec-approve' | 'spec-edit' | 'spec-attach' | 'wiki-write' | 'wiki-read' | 'prefetch' | 'audit-confirm' | 'update-title' | 'delete-chain' | 'create-rework-task' | 'reopen-chain' | 'waive-review';
|
|
3
3
|
export type Actor = Role | 'human' | 'system';
|
|
4
4
|
export declare function can(action: KanbanAction, actor: Actor, task: Task | null, opts?: {
|
|
5
5
|
isOwnTask?: boolean;
|
|
@@ -50,5 +50,9 @@ export function can(action, actor, task, opts = {}) {
|
|
|
50
50
|
case 'create-rework-task':
|
|
51
51
|
// 评审失败返工卡创建:仅系统(V 建执行卡、system 建返工卡,防角色伪造返工链)
|
|
52
52
|
return actor === 'system';
|
|
53
|
+
case 'reopen-chain':
|
|
54
|
+
case 'waive-review':
|
|
55
|
+
// 人工恢复/豁免:仅 human(人工为信任锚;角色与 system 一律拒,防角色越权恢复)
|
|
56
|
+
return actor === 'human';
|
|
53
57
|
}
|
|
54
58
|
}
|