@joekytc/dsh-swarm 0.2.0 → 0.3.1

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 (105) hide show
  1. package/README.md +177 -139
  2. package/README.zh-CN.md +173 -125
  3. package/client/ConfigSection.tsx +96 -5
  4. package/client/ConfigSelect.tsx +3 -1
  5. package/client/TaskDrawer.tsx +2 -0
  6. package/client/config-store.ts +48 -7
  7. package/client/kanban.css +69 -0
  8. package/client/timeline-model.ts +2 -0
  9. package/lib/client.js +172 -14
  10. package/lib/config.d.ts +20 -1
  11. package/lib/config.js +15 -0
  12. package/lib/dispatcher/agent-runner.d.ts +13 -5
  13. package/lib/dispatcher/agent-runner.js +78 -38
  14. package/lib/dispatcher/chain-auditor.d.ts +4 -4
  15. package/lib/dispatcher/chain-auditor.js +10 -8
  16. package/lib/dispatcher/dispatcher.d.ts +18 -18
  17. package/lib/dispatcher/dispatcher.js +59 -43
  18. package/lib/dispatcher/event-waker.d.ts +1 -1
  19. package/lib/dispatcher/event-waker.js +2 -2
  20. package/lib/dispatcher/git-credentials.d.ts +2 -2
  21. package/lib/dispatcher/git-credentials.js +2 -2
  22. package/lib/dispatcher/model-candidates.d.ts +1 -1
  23. package/lib/dispatcher/model-candidates.js +1 -1
  24. package/lib/dispatcher/session-events.d.ts +1 -1
  25. package/lib/dispatcher/session-events.js +1 -1
  26. package/lib/dispatcher/target-repo.d.ts +2 -2
  27. package/lib/dispatcher/target-repo.js +3 -3
  28. package/lib/dispatcher/v-orchestrator.d.ts +8 -8
  29. package/lib/dispatcher/v-orchestrator.js +41 -41
  30. package/lib/dispatcher/watchdog.d.ts +1 -1
  31. package/lib/dispatcher/watchdog.js +2 -2
  32. package/lib/domain/config-override.d.ts +14 -0
  33. package/lib/domain/config-override.js +38 -1
  34. package/lib/domain/delivery-contract.d.ts +2 -2
  35. package/lib/domain/delivery-contract.js +5 -5
  36. package/lib/domain/delivery-evidence.d.ts +1 -1
  37. package/lib/domain/delivery-evidence.js +1 -1
  38. package/lib/domain/event-store.js +1 -1
  39. package/lib/domain/im-message.d.ts +15 -0
  40. package/lib/domain/im-message.js +149 -0
  41. package/lib/domain/kanban-service.d.ts +13 -10
  42. package/lib/domain/kanban-service.js +36 -28
  43. package/lib/domain/memory.d.ts +6 -1
  44. package/lib/domain/memory.js +25 -8
  45. package/lib/domain/ocr-review.d.ts +47 -0
  46. package/lib/domain/ocr-review.js +105 -0
  47. package/lib/domain/permissions.js +5 -5
  48. package/lib/domain/projection.js +6 -6
  49. package/lib/domain/state-machine.js +1 -1
  50. package/lib/domain/task-parents.js +1 -1
  51. package/lib/domain/types.d.ts +4 -4
  52. package/lib/index.js +14 -9
  53. package/lib/roles/clean-fs-tools.js +3 -4
  54. package/lib/roles/preset-installer.js +3 -3
  55. package/lib/roles/skill-installer.js +1 -1
  56. package/lib/roles/toolsets.d.ts +60 -16
  57. package/lib/roles/toolsets.js +265 -47
  58. package/lib/roles/wiki-worker.js +3 -3
  59. package/lib/routes/kanban-http.d.ts +25 -2
  60. package/lib/routes/kanban-http.js +171 -11
  61. package/lib/routes/kanban-sse.d.ts +1 -1
  62. package/lib/routes/kanban-sse.js +1 -1
  63. package/lib/routes/planning-driver.d.ts +4 -2
  64. package/lib/routes/planning-driver.js +7 -3
  65. package/lib/routes/prefix-router.d.ts +5 -3
  66. package/lib/routes/prefix-router.js +14 -5
  67. package/lib/services/config-provider.d.ts +0 -4
  68. package/lib/services/config-provider.js +7 -9
  69. package/lib/services/im-delivery.d.ts +109 -0
  70. package/lib/services/im-delivery.js +301 -0
  71. package/lib/services/kanban-provider.d.ts +1 -1
  72. package/lib/services/kanban-provider.js +2 -2
  73. package/lib/services/ocr-cli.d.ts +35 -0
  74. package/lib/services/ocr-cli.js +128 -0
  75. package/lib/tools/kanban-tools.d.ts +2 -2
  76. package/lib/tools/kanban-tools.js +1 -1
  77. package/lib/tools/main-session-tools.d.ts +7 -3
  78. package/lib/tools/main-session-tools.js +120 -21
  79. package/lib/tools/ocr-review-tools.d.ts +11 -0
  80. package/lib/tools/ocr-review-tools.js +63 -0
  81. package/lib/tools/planning-tools.d.ts +5 -2
  82. package/lib/tools/planning-tools.js +19 -11
  83. package/lib/tools/wiki-tools.js +2 -2
  84. package/lib/wiki/kb-linkage.d.ts +1 -1
  85. package/lib/wiki/local-kb-client.js +3 -3
  86. package/lib/wiki/local-kb.d.ts +1 -1
  87. package/lib/wiki/local-kb.js +2 -2
  88. package/lib/wiki/page-path.d.ts +1 -1
  89. package/lib/wiki/page-path.js +6 -3
  90. package/lib/wiki/wiki-vault-client.d.ts +1 -1
  91. package/lib/wiki/wiki-vault-client.js +1 -1
  92. package/package.json +1 -1
  93. package/personas/kanban-d/agent.cordis.yml +33 -12
  94. package/personas/kanban-dt/agent.cordis.yml +23 -5
  95. package/personas/kanban-p/agent.cordis.yml +3 -3
  96. package/personas/kanban-pt/agent.cordis.yml +1 -1
  97. package/personas/kanban-v/agent.cordis.yml +3 -3
  98. package/personas/kanban-w/agent.cordis.yml +21 -11
  99. package/personas/persona-d.md +8 -6
  100. package/personas/persona-dt.md +12 -4
  101. package/personas/persona-p.md +1 -1
  102. package/personas/persona-v.md +2 -2
  103. package/personas/persona-w.md +2 -2
  104. package/personas/swarm/agent.cordis.yml +44 -0
  105. package/personas/swarm/preset.yml +5 -0
@@ -0,0 +1,149 @@
1
+ import { hasDeliveryEvidence } from './delivery-evidence.js';
2
+ /** W3 收尾判据(镜像 kanban-service.completeTask 机械规则,但不含 dEvidenceOk 与链状态要求):
3
+ * 完成卡为 w+kb、链上无未终态卡、它是最后完成的卡、且链上存在 done 的 D 卡(防 W2 中间态误投)。 */
4
+ export function isFinalW3Completion(state, chainId, completedTaskId) {
5
+ const task = state.tasks.get(completedTaskId);
6
+ if (!task || task.chainId !== chainId || task.assignee !== 'w' || task.mode !== 'kb')
7
+ return false;
8
+ const chainTasks = [...state.tasks.values()].filter((t) => t.chainId === chainId);
9
+ if (chainTasks.some((t) => t.status !== 'done' && t.status !== 'archived'))
10
+ return false;
11
+ const completedEvents = state.events.filter((e) => e.chainId === chainId && e.kind === 'task/completed' && e.taskId);
12
+ if (completedEvents.at(-1)?.taskId !== completedTaskId)
13
+ return false;
14
+ return chainTasks.some((t) => (t.mode === 'execute' || t.mode === 'align') && t.status === 'done');
15
+ }
16
+ function fmtTime(ms) {
17
+ const d = new Date(ms);
18
+ const p = (n) => String(n).padStart(2, '0');
19
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
20
+ }
21
+ function strMeta(handoff, key) {
22
+ const v = (handoff?.metadata ?? {})[key];
23
+ return typeof v === 'string' ? v.trim() : '';
24
+ }
25
+ /** 人工关注点:TDD 跳过声明、D 证据缺失(链完成门禁拦截)、审计警告状态。 */
26
+ function completionAttention(state, chainId, dHandoff) {
27
+ const out = [];
28
+ const tdd = (dHandoff?.metadata ?? {})['tdd'];
29
+ const skipReason = tdd?.skipped && typeof tdd.skipped['reason'] === 'string' ? tdd.skipped['reason'].trim() : '';
30
+ if (skipReason)
31
+ out.push(`TDD 曾跳过:${skipReason}`);
32
+ if (!hasDeliveryEvidence(dHandoff))
33
+ out.push('D 卡缺 git 产物证据,链未自动收链,请人工核对');
34
+ const audit = state.auditWarnings.get(chainId);
35
+ if (audit && !audit.confirmedAt)
36
+ out.push(`审计警告待 GUI 确认(${audit.evidence.length} 条证据)`);
37
+ else if (audit?.confirmedAt)
38
+ out.push(`审计警告已于 ${fmtTime(audit.confirmedAt)} 确认`);
39
+ return out;
40
+ }
41
+ /** 成功汇报(markdown)。判据不满足(W2 中间态/非 w:kb/非收尾)返回 null。 */
42
+ export function buildCompletionMessage(state, chainId, completedTaskId, now) {
43
+ if (!isFinalW3Completion(state, chainId, completedTaskId))
44
+ return null;
45
+ const chain = state.chains.get(chainId);
46
+ if (!chain)
47
+ return null;
48
+ const chainTasks = [...state.tasks.values()].filter((t) => t.chainId === chainId);
49
+ const wHandoff = state.handoffs.get(completedTaskId);
50
+ const dTask = chainTasks.find((t) => (t.mode === 'execute' || t.mode === 'align') && t.status === 'done');
51
+ const dHandoff = state.handoffs.get(dTask.id);
52
+ const pTask = [...chainTasks].reverse().find((t) => t.assignee === 'p' && t.status === 'done');
53
+ const artifacts = strMeta(pTask ? state.handoffs.get(pTask.id) : undefined, 'artifacts_path');
54
+ const lines = [];
55
+ lines.push(`**【DSH 需求完成】${chain.title}**`);
56
+ lines.push('');
57
+ lines.push(`**完成清单**(${fmtTime(now)})`);
58
+ for (const t of chainTasks)
59
+ lines.push(`- ✅ ${t.title}`);
60
+ lines.push('');
61
+ lines.push('**产出**');
62
+ const kbUrl = strMeta(wHandoff, 'kb_url');
63
+ const pagePath = strMeta(wHandoff, 'page_path');
64
+ if (kbUrl)
65
+ lines.push(`- KB 文档:${kbUrl}`);
66
+ if (pagePath)
67
+ lines.push(`- KB 页:${pagePath}`);
68
+ lines.push(`- 产出物:${artifacts || '(无)'}`);
69
+ const attention = completionAttention(state, chainId, dHandoff);
70
+ if (attention.length > 0) {
71
+ lines.push('');
72
+ lines.push('**人工关注点**');
73
+ for (const a of attention)
74
+ lines.push(`- ${a}`);
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+ /** 阻塞建议映射表(静态工程知识,禁止模型现场生成——评审决议)。 */
79
+ const SUGGESTION_TABLE = [
80
+ {
81
+ pattern: /\[create-failed\]/,
82
+ lines: [
83
+ 'V 连续多轮建卡失败:查 dispatcher.log 的 [wakeV] / [tick] error 行,确认编排会话报错原因',
84
+ '核对 roles.models.v 模型路由配置;历史案例:网关回复缓存回放(from-cache)会致零工具调用,可更换 provider/model 后重试',
85
+ ],
86
+ },
87
+ {
88
+ pattern: /\[stall-watchdog\]/,
89
+ lines: [
90
+ '链持续无进展且重唤醒耗尽:查 dispatcher.log [stall-watchdog] 行与 orchestration.json 的 phase/stallCount',
91
+ '检查最后一张卡会话(kbn-<taskId>)session.jsonl 是否空壳(仅 4 行骨架)——空壳需人工清理后删链重跑',
92
+ ],
93
+ },
94
+ ];
95
+ export function suggestionsFor(reason) {
96
+ const out = [];
97
+ for (const row of SUGGESTION_TABLE) {
98
+ if (row.pattern.test(reason))
99
+ out.push(...row.lines);
100
+ }
101
+ if (out.length === 0) {
102
+ out.push('查 events.jsonl 最近事件定位阻塞前最后一个动作');
103
+ out.push('GUI 打开该链逐卡核对状态;人工恢复 = 删链重跑');
104
+ }
105
+ return out;
106
+ }
107
+ /** 链下全部 blocked 卡(title + 最近一次 task/blocked 原因)。 */
108
+ export function latestBlockedTasks(events, tasks, chainId) {
109
+ const reasons = new Map();
110
+ for (const e of events) {
111
+ if (e.chainId !== chainId || e.kind !== 'task/blocked' || !e.taskId)
112
+ continue;
113
+ reasons.set(e.taskId, String(e.payload['reason'] ?? ''));
114
+ }
115
+ const out = [];
116
+ for (const t of tasks) {
117
+ if (t.chainId !== chainId || t.status !== 'blocked')
118
+ continue;
119
+ out.push({ id: t.id, title: t.title, reason: reasons.get(t.id) ?? '(原因见事件流)' });
120
+ }
121
+ return out;
122
+ }
123
+ /** 阻塞通知(markdown)。 */
124
+ export function buildBlockMessage(state, chainId, chainReason, storageDir) {
125
+ const chain = state.chains.get(chainId);
126
+ const blocked = latestBlockedTasks(state.events, state.tasks.values(), chainId);
127
+ const lines = [];
128
+ lines.push(`**【DSH 需求阻塞】${chain?.title ?? chainId}**`);
129
+ lines.push('');
130
+ lines.push('**阻塞卡**');
131
+ if (blocked.length === 0)
132
+ lines.push('- 无(链级停滞,见阻塞原因)');
133
+ else
134
+ for (const b of blocked)
135
+ lines.push(`- ${b.title}:${b.reason}`);
136
+ lines.push('');
137
+ lines.push('**阻塞原因**');
138
+ lines.push(`> ${chainReason}`);
139
+ lines.push('');
140
+ lines.push('**排查建议**');
141
+ for (const s of suggestionsFor(chainReason))
142
+ lines.push(`- ${s}`);
143
+ lines.push('');
144
+ lines.push('**取证路径**');
145
+ lines.push(`- 事件:${storageDir}/events.jsonl`);
146
+ lines.push(`- 编排:${storageDir}/orchestration.json`);
147
+ lines.push(`- 调度:${storageDir}/dispatcher.log`);
148
+ return lines.join('\n');
149
+ }
@@ -2,7 +2,7 @@ import type { EventStore } from './event-store.js';
2
2
  import { type Actor } from './permissions.js';
3
3
  import type { AuditEvidence, BoardState, Chain, Handoff, KanbanEvent, SpecCard, SpecCardAttachment, SpecCardSections, Task, TaskMode, Role, ReviewEvidence } from './types.js';
4
4
  export type KanbanListener = (event: KanbanEvent) => void;
5
- /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(T7 需求标题规范化)。 */
5
+ /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(需求标题规范化)。 */
6
6
  export declare function firstSentence(text: string): string;
7
7
  /** 默认链标题:【需求】<一句话需求描述>。来源优先级 /plan: rest 首句 → checklist.problem 首句 → 未命名需求。 */
8
8
  export declare function buildChainTitle(requirementName: string | null, _openspecRest: string, problem: string): string;
@@ -18,18 +18,18 @@ export declare class KanbanService {
18
18
  private gateHook;
19
19
  constructor(store: EventStore, getKbUrlBase?: () => string | undefined);
20
20
  private emit;
21
- /** D23:注入链完成核对钩子(由调度层设置;仅一个消费者)。 */
21
+ /** 注入链完成核对钩子(由调度层设置;仅一个消费者)。 */
22
22
  setOnChainCompleted(hook: (chainId: string) => void | Promise<void>): void;
23
- /** Q3&5:注入任务完成互链登记钩子(由调度层设置;仅一个消费者)。 */
23
+ /** 注入任务完成互链登记钩子(由调度层设置;仅一个消费者)。 */
24
24
  setOnTaskCompleted(hook: (taskId: string) => void | Promise<void>): void;
25
- /** P1:注入实测闸钩子(由装配层设置;null=关闭实测闸,行为与旧版逐字节一致)。 */
25
+ /** 注入实测闸钩子(由装配层设置;null=关闭实测闸,行为与旧版逐字节一致)。 */
26
26
  setGateHook(hook: ((task: Task, handoff: Handoff) => Promise<{
27
27
  ok: boolean;
28
28
  detail: string;
29
29
  } | null>) | null): void;
30
- /** T22:订阅持久化后的看板事件;返回解除订阅函数。listener 异常不影响已落盘状态。 */
30
+ /** 订阅持久化后的看板事件;返回解除订阅函数。listener 异常不影响已落盘状态。 */
31
31
  subscribe(listener: KanbanListener): () => void;
32
- /** T22:返回 seq >= 入参 的事件(与 EventStore.readSince 同为 inclusive 语义)。 */
32
+ /** 返回 seq >= 入参 的事件(与 EventStore.readSince 同为 inclusive 语义)。 */
33
33
  eventsSince(seq: number): Promise<KanbanEvent[]>;
34
34
  private publish;
35
35
  private chainOf;
@@ -45,6 +45,9 @@ export declare class KanbanService {
45
45
  /** 链级停滞终态(防线A,看门狗/V stall 超限专用机械记账):executing → blocked。
46
46
  * 非 executing 调用即抛(fail-closed);人工恢复=GUI 删链重跑(blocked 无出边)。 */
47
47
  blockChain(chainId: string, reason: string): Promise<void>;
48
+ /** IM 投递失败留痕(仅 system 机械记账):非状态转换注记事件,仅落盘供取证/GUI 观察。
49
+ * 投递是旁路通知,失败绝不 block 链(评审决议);留痕满足「禁止静默 skip」红线。 */
50
+ noteImDeliveryFailed(chainId: string, detail: string, actor: Actor): Promise<KanbanEvent>;
48
51
  createTask(input: {
49
52
  chainId: string;
50
53
  title: string;
@@ -58,11 +61,11 @@ export declare class KanbanService {
58
61
  completeTask(taskId: string, handoff: Handoff, actor: Actor, opts?: {
59
62
  boundTaskId?: string;
60
63
  }): Promise<Task>;
61
- /** D23:链完成验收核对发警告(仅 system/dispatcher 可发)。Chain 状态保持 completed。 */
64
+ /** 链完成验收核对发警告(仅 system/dispatcher 可发)。Chain 状态保持 completed。 */
62
65
  auditWarning(chainId: string, evidence: AuditEvidence[], actor: Actor): Promise<KanbanEvent>;
63
- /** D23:用户确认产物归属(仅 human,GUI confirm-audit action)。放行最终汇报。 */
66
+ /** 用户确认产物归属(仅 human,GUI confirm-audit action)。放行最终汇报。 */
64
67
  confirmAudit(chainId: string, actor: Actor): Promise<KanbanEvent>;
65
- /** T7:链标题改名(仅 human,GUI)。发 chain/title-updated 事件(非状态转换)。 */
68
+ /** 链标题改名(仅 human,GUI)。发 chain/title-updated 事件(非状态转换)。 */
66
69
  updateChainTitle(chainId: string, title: string, actor: Actor): Promise<Chain>;
67
70
  /** 整链硬删除(含其下全部角色卡/规格卡事件;仅 human,GUI 二次确认)。物理 purge 事件行,不可恢复。 */
68
71
  deleteChain(chainId: string, actor: Actor): Promise<void>;
@@ -80,7 +83,7 @@ export declare class KanbanService {
80
83
  }): Promise<Task>;
81
84
  comment(taskId: string, body: string, actor: Actor): Promise<KanbanEvent>;
82
85
  archiveTask(taskId: string, actor: Actor): Promise<Task>;
83
- /** T10.5:仅 draft 规格卡可挂附件(V 挂清单附件(/openspec: 建链)/ human GUI 上传)。 */
86
+ /** draft 规格卡可挂附件(V 挂清单附件(/openspec: 建链)/ human GUI 上传)。 */
84
87
  addSpecCardAttachment(cardId: string, attachment: SpecCardAttachment, actor: Actor): Promise<SpecCard>;
85
88
  /** 评审事件(交付质量链):recordReview 记录评审卡结论并更新被评审任务 reviewStatus。
86
89
  * actor 必须 system(V/角色不可伪造评审结论);verdict=pass → review/passed,否则 review/failed。
@@ -6,7 +6,7 @@ import { validateReviewEvidence } from './review-evidence.js';
6
6
  import { resolveTaskParents, nonTerminalSemanticParents } from './task-parents.js';
7
7
  let seqCounter = 0;
8
8
  const nid = (p) => `${p}_${(++seqCounter).toString(36)}_${Date.now().toString(36)}`;
9
- /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(T7 需求标题规范化)。 */
9
+ /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(需求标题规范化)。 */
10
10
  export function firstSentence(text) {
11
11
  const t = text.trim();
12
12
  const cut = t.search(/[\n。?!]/);
@@ -22,23 +22,23 @@ export function buildChainTitle(requirementName, _openspecRest, problem) {
22
22
  export class KanbanService {
23
23
  state;
24
24
  store;
25
- // Q4:kb_url host 前缀硬校验基准(config.wikiVault.baseUrl)。null = 不校验前缀(兼容测试/旧调用)。
26
- // 配置面板热生效(Task 6):改为 getter,每次校验时取最新值——改 baseUrl 无需重建服务。
25
+ // kb_url host 前缀硬校验基准(config.wikiVault.baseUrl)。null = 不校验前缀(兼容测试/旧调用)。
26
+ // 配置面板热生效:改为 getter,每次校验时取最新值——改 baseUrl 无需重建服务。
27
27
  getKbUrlBase;
28
28
  emitQueue = Promise.resolve();
29
29
  listeners = new Set();
30
- // D23:链完成验收核对钩子(dispatcher 注入:读主会话事件/产物归属核对 → auditWarning)
30
+ // 链完成验收核对钩子(dispatcher 注入:读主会话事件/产物归属核对 → auditWarning)
31
31
  onChainCompletedHook = null;
32
- // Q3&5:W2/W3 完成互链登记钩子(dispatcher 注入:拿 page_path → 机械写三方互链,失败不阻塞完成)
32
+ // W2/W3 完成互链登记钩子(dispatcher 注入:拿 page_path → 机械写三方互链,失败不阻塞完成)
33
33
  onTaskCompletedHook = null;
34
- // P1:实测闸钩子(装配层注入:gate-policy 派生 + gate-runner 实测执行 + 分支核对)。
34
+ // 实测闸钩子(装配层注入:gate-policy 派生 + gate-runner 实测执行 + 分支核对)。
35
35
  // null=未启用(行为不变);hook 返回 null=跳过(零感知,无 gate 事件)。
36
36
  // **无 actor 参数——human 无豁免**(2026-09-02 决议收紧)。
37
37
  gateHook = null;
38
38
  constructor(store, getKbUrlBase) {
39
39
  this.store = store;
40
40
  this.getKbUrlBase = getKbUrlBase ?? null;
41
- // P0-3:同步重投影,消除"构造后立即调用基于空状态"的竞态
41
+ // 同步重投影,消除"构造后立即调用基于空状态"的竞态
42
42
  this.state = project(store.readAllSync());
43
43
  }
44
44
  async emit(ev) {
@@ -54,24 +54,24 @@ export class KanbanService {
54
54
  await pending;
55
55
  return emitted;
56
56
  }
57
- /** D23:注入链完成核对钩子(由调度层设置;仅一个消费者)。 */
57
+ /** 注入链完成核对钩子(由调度层设置;仅一个消费者)。 */
58
58
  setOnChainCompleted(hook) {
59
59
  this.onChainCompletedHook = hook;
60
60
  }
61
- /** Q3&5:注入任务完成互链登记钩子(由调度层设置;仅一个消费者)。 */
61
+ /** 注入任务完成互链登记钩子(由调度层设置;仅一个消费者)。 */
62
62
  setOnTaskCompleted(hook) {
63
63
  this.onTaskCompletedHook = hook;
64
64
  }
65
- /** P1:注入实测闸钩子(由装配层设置;null=关闭实测闸,行为与旧版逐字节一致)。 */
65
+ /** 注入实测闸钩子(由装配层设置;null=关闭实测闸,行为与旧版逐字节一致)。 */
66
66
  setGateHook(hook) {
67
67
  this.gateHook = hook;
68
68
  }
69
- /** T22:订阅持久化后的看板事件;返回解除订阅函数。listener 异常不影响已落盘状态。 */
69
+ /** 订阅持久化后的看板事件;返回解除订阅函数。listener 异常不影响已落盘状态。 */
70
70
  subscribe(listener) {
71
71
  this.listeners.add(listener);
72
72
  return () => { this.listeners.delete(listener); };
73
73
  }
74
- /** T22:返回 seq >= 入参 的事件(与 EventStore.readSince 同为 inclusive 语义)。 */
74
+ /** 返回 seq >= 入参 的事件(与 EventStore.readSince 同为 inclusive 语义)。 */
75
75
  async eventsSince(seq) {
76
76
  return this.store.readSince(seq);
77
77
  }
@@ -146,6 +146,14 @@ export class KanbanService {
146
146
  throw new Error('blockChain requires executing chain, got: ' + chain.status);
147
147
  await this.emit({ chainId, taskId: null, kind: 'chain/blocked', payload: { reason }, author: 'system', at: Date.now() });
148
148
  }
149
+ /** IM 投递失败留痕(仅 system 机械记账):非状态转换注记事件,仅落盘供取证/GUI 观察。
150
+ * 投递是旁路通知,失败绝不 block 链(评审决议);留痕满足「禁止静默 skip」红线。 */
151
+ async noteImDeliveryFailed(chainId, detail, actor) {
152
+ if (actor !== 'system')
153
+ throw new Error('permission denied: only system may note im delivery failure');
154
+ await this.chainOf(chainId);
155
+ return this.emit({ chainId, taskId: null, kind: 'chain/im-delivery-failed', payload: { detail }, author: actor, at: Date.now() });
156
+ }
149
157
  async createTask(input, actor) {
150
158
  if (!can('create-task', actor, null))
151
159
  throw new Error('permission denied');
@@ -154,7 +162,7 @@ export class KanbanService {
154
162
  throw new Error('title required');
155
163
  const chain = await this.chainOf(input.chainId);
156
164
  const explicitParents = input.parents && input.parents.length > 0;
157
- // P0 硬拦(防御纵深):调用方未显式指定 parents(V 建全新阶段卡)时,按语义依赖查上游非终态——
165
+ // 硬拦(防御纵深):调用方未显式指定 parents(V 建全新阶段卡)时,按语义依赖查上游非终态——
158
166
  // 上游卡还卡着/未走完则拒绝建卡(防「P blocked 后仍建 PT」类事故)。显式 parents 的调用
159
167
  // (复审卡 parents=[rework.id]、createReworkTask)由调用方决定接链,不在此拦。
160
168
  if (!explicitParents) {
@@ -164,7 +172,7 @@ export class KanbanService {
164
172
  throw new Error(`create-task rejected: 上游 ${b} 未终态,下游 ${input.assignee}/${input.mode} 卡不能创建`);
165
173
  }
166
174
  }
167
- // 语义 parents 兜底:调用方(V 建卡)未显式指定 parents 时,按 R20 依赖自动接链上终态父任务,
175
+ // 语义 parents 兜底:调用方(V 建卡)未显式指定 parents 时,按语义依赖自动接链上终态父任务,
168
176
  // 保证「父交接注入」通道闭合(如 w2 接 P、dt 接 d)。兜底对已显式传 parents 的
169
177
  // 调用(复审卡 parents=[rework.id]、createReworkTask)不生效。
170
178
  const parents = input.parents && input.parents.length > 0
@@ -195,13 +203,13 @@ export class KanbanService {
195
203
  throw new Error('permission denied');
196
204
  if (!handoff.summary.trim())
197
205
  throw new Error('handoff summary required');
198
- // C2:D(execute) 完成必须带 git 产物证据(changed_files + commit/push 至少其一)——
199
- // 执行者语义硬校验;human 为信任锚(GUI 强制收尾)可豁免,但 C1 链完成门禁仍会拦截无证据链。
206
+ // D(execute) 完成必须带 git 产物证据(changed_files + commit/push 至少其一)——
207
+ // 执行者语义硬校验;human 为信任锚(GUI 强制收尾)可豁免,但链完成门禁仍会拦截无证据链。
200
208
  if (t.assignee === 'd' && t.mode === 'execute' && actor !== 'human' && !hasDeliveryEvidence(handoff)) {
201
209
  throw new Error('delivery evidence required: D(execute) complete must carry changed_files + (commit_hash|push)');
202
210
  }
203
211
  // TDD 声明闸(2026-08-26):D(execute) 完成必须带 tdd 声明(test_files 非空 或 skipped.reason 非空),
204
- // 否则抛错——防止 D 完全不声明测试;顺序真伪由 DT 评审闸兜底。human 信任锚可豁免(同 C2)。
212
+ // 否则抛错——防止 D 完全不声明测试;顺序真伪由 DT 评审闸兜底。human 信任锚可豁免(同上)。
205
213
  if (t.assignee === 'd' && t.mode === 'execute' && actor !== 'human' && !hasTddDeclaration(handoff)) {
206
214
  throw new Error('tdd declaration required: D(execute) complete must carry tdd = { test_files: [...] } or tdd = { skipped: { reason } }');
207
215
  }
@@ -230,7 +238,7 @@ export class KanbanService {
230
238
  }
231
239
  // v2 断代:w:file 交付键随旧 w1 预取阶段移除,manifest 校验块同步删除
232
240
  // (validatePrefetchManifest 仍保留于 prefetch-manifest.ts,供清单 schema 校验复用)。
233
- // 实测闸(P1):hook 由装配层注入(gate-policy 派生 + gate-runner 实测执行 + 分支核对),
241
+ // 实测闸:hook 由装配层注入(gate-policy 派生 + gate-runner 实测执行 + 分支核对),
234
242
  // hook 返回 null=跳过(未启用/非 D/旧卡/分支不一致)。**无 human 豁免**(2026-09-02 决议收紧);
235
243
  // ok=false → gate-failed 事件 + 拒绝 complete(卡留 running,throw 经工具边界回 D 会话)。
236
244
  if (this.gateHook) {
@@ -243,7 +251,7 @@ export class KanbanService {
243
251
  }
244
252
  }
245
253
  await this.emit({ chainId: t.chainId, taskId, kind: 'task/completed', payload: { ...handoff }, author: actor, at: Date.now() });
246
- // Q3&5:W2/W3 完成 → 调度层互链登记(拿 page_path 机械写三方互链)。
254
+ // W2/W3 完成 → 调度层互链登记(拿 page_path 机械写三方互链)。
247
255
  // 仅 w:kb 触发(P/D/PT/DT 不涉 KB 页互链);钩子内异常不阻断 completeTask(登记失败仅记 warning)。
248
256
  if (t.assignee === 'w' && t.mode === 'kb' && this.onTaskCompletedHook) {
249
257
  try {
@@ -253,11 +261,11 @@ export class KanbanService {
253
261
  console.error('[dsh-swarm] task completion linkage hook failed: ' + String(error));
254
262
  }
255
263
  }
256
- // P0-3 链完成机械规则:仅当「最后完成的执行任务是 W3(w/kb)且链上 D(execute) 已 done 且
264
+ // 链完成机械规则:仅当「最后完成的执行任务是 W3(w/kb)且链上 D(execute) 已 done 且
257
265
  // 交付物证据满足(changed_files + commit/push)」且无未终态任务时 → 链 completed(不靠 agent 自觉)。
258
266
  // 收紧判据:中间阶段(如 P done 后 W2 尚未创建)无未终态任务也不得误收链。
259
- // C1:D(execute) 无产物证据 → 不判链完成(human 强制 complete 的 D 卡同样被拦截,防漂移被掩盖)。
260
- // 'align' 为旧链路兼容(只读对齐/校验语义已废弃,R20 起新卡一律 'execute')。
267
+ // D(execute) 无产物证据 → 不判链完成(human 强制 complete 的 D 卡同样被拦截,防漂移被掩盖)。
268
+ // 'align' 为旧链路兼容(只读对齐/校验语义已废弃,新卡一律 'execute')。
261
269
  const terminal = ['done', 'archived'];
262
270
  const chain = this.state.chains.get(t.chainId);
263
271
  const chainTasks = [...this.state.tasks.values()].filter((x) => x.chainId === t.chainId);
@@ -270,7 +278,7 @@ export class KanbanService {
270
278
  const w3Done = !!lastTask && lastTask.assignee === 'w' && lastTask.mode === 'kb' && dDone && dEvidenceOk;
271
279
  if (chain && chain.status === 'executing' && openTasks.length === 0 && w3Done) {
272
280
  await this.emit({ chainId: t.chainId, taskId: null, kind: 'chain/completed', payload: {}, author: 'system', at: Date.now() });
273
- // D23:链完成 → 调度层验收核对(主会话越权写产物 → chain/audit-warning)。
281
+ // 链完成 → 调度层验收核对(主会话越权写产物 → chain/audit-warning)。
274
282
  // 钩子内异常不阻断 completeTask 本身(核对失败仅记录,链路仍 completed)。
275
283
  if (this.onChainCompletedHook) {
276
284
  try {
@@ -283,14 +291,14 @@ export class KanbanService {
283
291
  }
284
292
  return this.state.tasks.get(taskId);
285
293
  }
286
- /** D23:链完成验收核对发警告(仅 system/dispatcher 可发)。Chain 状态保持 completed。 */
294
+ /** 链完成验收核对发警告(仅 system/dispatcher 可发)。Chain 状态保持 completed。 */
287
295
  async auditWarning(chainId, evidence, actor) {
288
296
  if (actor !== 'system')
289
297
  throw new Error('permission denied: only dispatcher may raise audit warnings');
290
298
  await this.chainOf(chainId);
291
299
  return this.emit({ chainId, taskId: null, kind: 'chain/audit-warning', payload: { evidence }, author: actor, at: Date.now() });
292
300
  }
293
- /** D23:用户确认产物归属(仅 human,GUI confirm-audit action)。放行最终汇报。 */
301
+ /** 用户确认产物归属(仅 human,GUI confirm-audit action)。放行最终汇报。 */
294
302
  async confirmAudit(chainId, actor) {
295
303
  if (!can('audit-confirm', actor, null))
296
304
  throw new Error('permission denied');
@@ -300,7 +308,7 @@ export class KanbanService {
300
308
  throw new Error('no audit warning for chain: ' + chainId);
301
309
  return this.emit({ chainId, taskId: null, kind: 'chain/audit-confirmed', payload: {}, author: actor, at: Date.now() });
302
310
  }
303
- /** T7:链标题改名(仅 human,GUI)。发 chain/title-updated 事件(非状态转换)。 */
311
+ /** 链标题改名(仅 human,GUI)。发 chain/title-updated 事件(非状态转换)。 */
304
312
  async updateChainTitle(chainId, title, actor) {
305
313
  if (!can('update-title', actor, null))
306
314
  throw new Error('permission denied');
@@ -393,7 +401,7 @@ export class KanbanService {
393
401
  await this.emit({ chainId: t.chainId, taskId, kind: 'task/archived', payload: {}, author: actor, at: Date.now() });
394
402
  return this.state.tasks.get(taskId);
395
403
  }
396
- /** T10.5:仅 draft 规格卡可挂附件(V 挂清单附件(/openspec: 建链)/ human GUI 上传)。 */
404
+ /** draft 规格卡可挂附件(V 挂清单附件(/openspec: 建链)/ human GUI 上传)。 */
397
405
  async addSpecCardAttachment(cardId, attachment, actor) {
398
406
  const card = this.state.specCards.get(cardId);
399
407
  if (!card)
@@ -454,7 +462,7 @@ export class KanbanService {
454
462
  return task;
455
463
  }
456
464
  async snapshot() {
457
- // P0-3:重投影为权威(事件日志是唯一事实源;非法转换在此抛错)
465
+ // 重投影为权威(事件日志是唯一事实源;非法转换在此抛错)
458
466
  this.state = project(await this.store.readAll());
459
467
  return this.state;
460
468
  }
@@ -5,7 +5,11 @@ export interface LearningEntry {
5
5
  evidence: string;
6
6
  tags: string[];
7
7
  }
8
+ /** 五类有效经验类别标签(0.3.1 判据):tags 必含其一,防「为出经验而出」的假沉淀。 */
9
+ export declare const LEARNING_CATEGORY_TAGS: readonly ["mistake", "reusable", "env-trap", "collab-contract", "efficiency"];
8
10
  export declare function validateLearning(raw: unknown): string[];
11
+ /** A 类犯错教训机械信号累计(评审失败/任务阻塞/审计警告事件 + 返工卡)。brief 统计行与 save 硬闸共用同一口径。 */
12
+ export declare function countLearningSignals(state: BoardState, chainId: string): number;
9
13
  export declare function formatLearningBody(entry: LearningEntry, created?: Date): string;
10
14
  export interface MemoryIndexEntry {
11
15
  kind: 'learning' | 'doc';
@@ -16,7 +20,8 @@ export declare function buildMemoryIndexBlock(entries: MemoryIndexEntry[]): stri
16
20
  export declare function weightedRank<T>(items: T[], scoreOf: (t: T) => number, timeOf: (t: T) => number): T[];
17
21
  export declare function buildRepoSlug(workspaceDir: string): string;
18
22
  export type { BoardState };
19
- /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。 */
23
+ /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。
24
+ * 头部带信号统计(判据 A 准入:累计 ≥2 次才可沉淀犯错教训,模型不用自己数)。 */
20
25
  export declare function buildLearningBrief(state: BoardState, chainId: string): string;
21
26
  /** /learning rest → 链解析:空→最近链;精确 id→命中;子串匹配→单命中或候选列表(≤3);无→null。 */
22
27
  export declare function resolveLearningChainId(state: BoardState, rest: string): {
@@ -1,4 +1,6 @@
1
1
  import { buildChecklistSlug } from '../wiki/page-path.js';
2
+ /** 五类有效经验类别标签(0.3.1 判据):tags 必含其一,防「为出经验而出」的假沉淀。 */
3
+ export const LEARNING_CATEGORY_TAGS = ['mistake', 'reusable', 'env-trap', 'collab-contract', 'efficiency'];
2
4
  export function validateLearning(raw) {
3
5
  const errors = [];
4
6
  if (typeof raw !== 'object' || raw === null)
@@ -14,8 +16,18 @@ export function validateLearning(raw) {
14
16
  errors.push('learning.tags must be an array of strings');
15
17
  if (Array.isArray(l['tags']) && l['tags'].some((v) => typeof v !== 'string'))
16
18
  errors.push('learning.tags must be an array of strings');
19
+ // 类别硬闸(报错文案即教学):tags 必含五类之一;样式/文案/字段名等一次性平凡变更不构成经验
20
+ if (Array.isArray(l['tags']) && l['tags'].every((v) => typeof v !== 'string' || !LEARNING_CATEGORY_TAGS.includes(v))) {
21
+ errors.push('learning.tags 必须含类别标签之一:mistake(犯错教训:链上阻塞/返工/评审失败/审计警告累计≥2) / reusable(可复用模式:同类流程或行为重复≥2) / env-trap(环境陷阱:踩坑1次+报错diff等硬证据) / collab-contract(协作契约:用户偏好≥2次确认) / efficiency(效率模式:量化省力依据);样式/文案/字段名等一次性平凡变更不构成经验,五类均不满足应回复「无新经验」');
22
+ }
17
23
  return errors;
18
24
  }
25
+ /** A 类犯错教训机械信号累计(评审失败/任务阻塞/审计警告事件 + 返工卡)。brief 统计行与 save 硬闸共用同一口径。 */
26
+ export function countLearningSignals(state, chainId) {
27
+ const chainEvents = state.events.filter((e) => e.chainId === chainId);
28
+ const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
29
+ return chainEvents.filter((e) => e.kind === 'review/failed' || e.kind === 'task/blocked' || e.kind === 'chain/audit-warning').length + reworks.length;
30
+ }
19
31
  export function formatLearningBody(entry, created = new Date()) {
20
32
  const date = created.toISOString().slice(0, 10);
21
33
  const lines = [
@@ -65,7 +77,7 @@ export function buildRepoSlug(workspaceDir) {
65
77
  const base = workspaceDir.replace(/\/+$/, '').split('/').pop() ?? '';
66
78
  return buildChecklistSlug(base);
67
79
  }
68
- // ── Task 4:证据包 + 链解析 ──────────────────────────────────────────
80
+ // ── 证据包 + 链解析 ──────────────────────────────────────────
69
81
  const truncate = (s, n) => (s.length > n ? s.slice(0, n) + '…' : s);
70
82
  /** 链上下文头:链标题 + 规格卡 problem 首行。 */
71
83
  function chainContext(state, chainId) {
@@ -76,12 +88,22 @@ function chainContext(state, chainId) {
76
88
  const problem = spec?.sections.problem ? spec.sections.problem.split('\n')[0].slice(0, 200) : '';
77
89
  return [chain.title, problem].filter(Boolean).join(' — ');
78
90
  }
79
- /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。 */
91
+ /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。
92
+ * 头部带信号统计(判据 A 准入:累计 ≥2 次才可沉淀犯错教训,模型不用自己数)。 */
80
93
  export function buildLearningBrief(state, chainId) {
81
94
  const header = ['## 链上下文', chainContext(state, chainId), ''];
82
- const sections = [];
83
95
  const chainEvents = state.events.filter((e) => e.chainId === chainId);
84
96
  const reviewFailed = chainEvents.filter((e) => e.kind === 'review/failed');
97
+ const blocked = chainEvents.filter((e) => e.kind === 'task/blocked');
98
+ const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
99
+ const audit = chainEvents.filter((e) => e.kind === 'chain/audit-warning');
100
+ if (countLearningSignals(state, chainId) === 0) {
101
+ return [...header, '(无机械信号。A 类犯错教训判据不满足;B/D 需重复出现 ≥2 次,C 需报错/diff 等硬证据,E 需量化省力依据,均不满足则回复「无新经验」)'].join('\n');
102
+ }
103
+ const total = countLearningSignals(state, chainId);
104
+ const sections = [
105
+ ['### 信号统计(A 类准入:累计 ≥2 次)', `评审失败 ${reviewFailed.length} / 任务阻塞 ${blocked.length} / 返工卡 ${reworks.length} / 审计警告 ${audit.length},累计 ${total} 次`].join('\n'),
106
+ ];
85
107
  if (reviewFailed.length > 0) {
86
108
  const lines = ['### 评审失败'];
87
109
  for (const e of reviewFailed.slice(-5)) {
@@ -91,7 +113,6 @@ export function buildLearningBrief(state, chainId) {
91
113
  }
92
114
  sections.push(lines.join('\n'));
93
115
  }
94
- const blocked = chainEvents.filter((e) => e.kind === 'task/blocked');
95
116
  if (blocked.length > 0) {
96
117
  const lines = ['### 任务阻塞'];
97
118
  for (const e of blocked.slice(-5)) {
@@ -101,14 +122,12 @@ export function buildLearningBrief(state, chainId) {
101
122
  }
102
123
  sections.push(lines.join('\n'));
103
124
  }
104
- const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
105
125
  if (reworks.length > 0) {
106
126
  const lines = ['### 返工卡'];
107
127
  for (const t of reworks.slice(-5))
108
128
  lines.push(`- [返工×${t.reviewAttempt}] ${t.title}(原卡 ${t.reworkOfTaskId})`);
109
129
  sections.push(lines.join('\n'));
110
130
  }
111
- const audit = chainEvents.filter((e) => e.kind === 'chain/audit-warning');
112
131
  if (audit.length > 0) {
113
132
  const lines = ['### 审计警告'];
114
133
  for (const e of audit.slice(-5)) {
@@ -118,8 +137,6 @@ export function buildLearningBrief(state, chainId) {
118
137
  }
119
138
  sections.push(lines.join('\n'));
120
139
  }
121
- if (sections.length === 0)
122
- return [...header, '(无机械信号,可基于对话观察蒸馏)'].join('\n');
123
140
  return [...header, ...sections].join('\n');
124
141
  }
125
142
  /** /learning rest → 链解析:空→最近链;精确 id→命中;子串匹配→单命中或候选列表(≤3);无→null。 */
@@ -0,0 +1,47 @@
1
+ /** OCR 委派子命令类型:预览 / 规则 / 托管评审。 */
2
+ export type OcrSub = 'preview' | 'rule' | 'managed';
3
+ /** OCR 调用可选参数集合。 */
4
+ export interface OcrArgsInput {
5
+ repo?: string;
6
+ from?: string;
7
+ to?: string;
8
+ commit?: string;
9
+ paths?: string[];
10
+ }
11
+ /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review 并恒带 JSON 输出。 */
12
+ export declare function buildOcrArgs(sub: OcrSub, a: OcrArgsInput): string[];
13
+ /** 预览结果归一化结构。 */
14
+ export interface PreviewResult {
15
+ mode: string;
16
+ files: {
17
+ path: string;
18
+ status: string;
19
+ }[];
20
+ excluded: {
21
+ path: string;
22
+ reason: string;
23
+ }[];
24
+ mergeBase: string | null;
25
+ }
26
+ /** 解析 delegate preview 的 JSON 输出,失败或结构异常时回退 unknown。 */
27
+ export declare function parsePreviewJson(stdout: string): PreviewResult;
28
+ /** 托管评审结果归一化结构。 */
29
+ export interface ManagedResult {
30
+ status: string;
31
+ comments: {
32
+ path: string | null;
33
+ line: number | null;
34
+ severity: string | null;
35
+ message: string;
36
+ }[];
37
+ }
38
+ /** 解析 managed review 的 JSON 输出,失败时回退 unknown。 */
39
+ export declare function parseManagedJson(stdout: string): ManagedResult;
40
+ /** 建议切换托管评审的文件数阈值。 */
41
+ export declare const SUGGEST_MANAGED_FILES = 50;
42
+ /** 文件数严格超过阈值时建议托管评审。 */
43
+ export declare function shouldSuggestManaged(fileCount: number): boolean;
44
+ /** 生成独立评审页命名空间路径:projects/<slug>/reviews/<topic>-<ymd>/。 */
45
+ export declare function reviewPagePath(repoSlug: string, topic: string, ymd: string): string;
46
+ /** 判定路径是否为合法的独立评审页命名空间路径。 */
47
+ export declare function isStandaloneReviewNamespacePath(pagePath: string): boolean;