@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
@@ -6,6 +6,7 @@ import { buildKanbanTools } from './kanban-tools.js';
6
6
  import { buildSpecCardTools } from './spec-card-tools.js';
7
7
  import { buildPlanningTools } from './planning-tools.js';
8
8
  import { handlePlanRoute, handleOpenspecRoute, handleLearningRoute } from '../routes/prefix-router.js';
9
+ import { sendChainReport, createSender, parseSendRequest, resolveReportChainId, DSH_IM_MISSING_PREFIX, DSH_IM_MISSING_GUIDANCE } from '../services/im-delivery.js';
9
10
  import { recallMemoryIndex, searchChecklists } from '../wiki/memory-recall.js';
10
11
  import { buildPlanningGuidance } from '../routes/planning-driver.js';
11
12
  import { attachSessionToWorkspace, resolveOrCreateWorkspace } from '../dispatcher/workspace-attach.js';
@@ -15,16 +16,25 @@ import { LocalWikiClient } from '../wiki/local-kb-client.js';
15
16
  import { ensureLocalKbRoot } from '../wiki/local-kb.js';
16
17
  import { LOCAL_CHECKLIST_PREFIX } from '../wiki/page-path.js';
17
18
  export const planningBySession = new Map();
18
- export const KANBAN_HANDOFF_RULE = (routes) => `
19
+ export const KANBAN_HANDOFF_RULE = (routes, opts) => {
20
+ const confirmLine = opts?.swarm
21
+ ? `- 澄清期:调 planning_prefetch(只读子代理)采集仓库事实 → 逐问用户收敛 → 调 planning_checklist_save 存需求澄清清单。
22
+ - 确认闸:清单落库后向用户征求确认;仅当用户回复含明确肯定语义(确认/开干/开跑/开始/go 等)才调 kanban_route{intent:'openspec'} 建链;模糊、岔开话题、只提修改意见 = 未确认,继续澄清。禁止未确认建链。`
23
+ : `- 澄清期:调 planning_prefetch(只读子代理)采集仓库事实 → 逐问用户收敛 → 调 planning_checklist_save 存需求澄清清单 → 提醒用户 ${routes.openspec} 确认。`;
24
+ const nextLine = opts?.swarm
25
+ ? `- 用户确认后你调 kanban_route{intent:'openspec'};链路进入 executing,V 自动串行建卡 p→(pt)→w2→d→dt→w3;你不要自己执行。`
26
+ : `- ${routes.openspec} 后链路进入 executing,V 自动串行建卡 p→(pt)→w2→d→dt→w3;你不要自己执行。`;
27
+ return `
19
28
  ## 主 agent 铁律(看板工作流 v2)
20
29
  - 你是计划者:只做需求澄清(grill-me)与最终收尾汇报;绝不执行任务本身。
21
30
  - 最高护栏:只读仓库——禁止 git 操作、禁止 write/edit 任何仓库源码;只允许写 KB(planning_checklist_save)与临时目录兜底。
22
- - 澄清期:调 planning_prefetch(只读子代理)采集仓库事实 → 逐问用户收敛 → 调 planning_checklist_save 存需求澄清清单 → 提醒用户 ${routes.openspec} 确认。
23
- - ${routes.openspec} 后链路进入 executing,V 自动串行建卡 p→(pt)→w2→d→dt→w3;你不要自己执行。
31
+ ${confirmLine}
32
+ ${nextLine}
24
33
  - 用 kanban_show / kanban_list / spec_card_view 观察进度,链完成后向用户汇报产物链接与轨迹入口。
25
34
  - 经验沉淀:链路完成后,用户可发 ${routes.learning}(或 ${routes.learning} <chainId>)沉淀本链经验;主 agent 消化机械证据包后调 planning_learning_save 入库。
26
35
  - 叙述铁律:回复中的链号/卡号(ch_/sc_/t_ 编号)只能逐字复制自工具结果;建链成败以工具结果字段为准,禁止编造或沿用旧对话里的编号。
27
36
  `;
37
+ };
28
38
  /** 防线③:/openspec: 成功后的逐链确定性叙述规则(2026-09-04 mtmgp81q:模型口播 ch_1_mtjrhkrf
29
39
  * 与工具结果 ch_1_mtmgp81q 脱节)。逐字引用锚点 + firstCard 成败结论;整包缓存回放仍可能
30
40
  * 绕过 prompt 层——最终防线是链级看门狗(防线①)。 */
@@ -108,20 +118,33 @@ export function buildSpawnPrefetch(ctx) {
108
118
  }
109
119
  };
110
120
  }
111
- /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 /openspec: 用清单建链。
121
+ /** 自由投递 guidance:三步教学(实查基准 生成正文sms_send 投递)+ 红线分界。 */
122
+ function buildFreeSendGuidance(routes, query, dm) {
123
+ return [
124
+ '## 自由投递指令(' + routes.send + ')',
125
+ '用户意图:' + query + ';投递目标:' + (dm ? '私聊' : '群聊') + '。',
126
+ '1. 先 kanban_show / kanban_list 实查看板与相关链状态,取得事实基准;禁止编造数据、禁止虚构进度。',
127
+ '2. 按用户意图撰写中文正文(≤4000 字符,先结论后细节)。本自由投递正文由你撰写(显式豁免);但链完成/阻塞汇报正文由系统渲染,你始终不撰写、不复述、不改写。',
128
+ '3. ' + (dm
129
+ ? '调 sms_send{text: <正文>, dm: true} 投递(目标为私聊,已由 -s/私聊语义确定)。'
130
+ : '调 sms_send{text: <正文>, dm: false} 投递;若用户消息含「私聊/私信/单聊」语义,改传 dm: true(目标为私聊)。'),
131
+ '投递结果只向用户确认成功与目标,不复述正文;失败时把 error/guidance 原样转告,勿编造原因。',
132
+ ].join('\n');
133
+ }
134
+ /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
112
135
  * 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
113
136
  * 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
114
137
  export function registerMainSessionTools(ctx, configProvider) {
115
138
  const registry = ctx.get('tools');
116
139
  if (!registry)
117
- return; // 测试裸 Context 无 tools 服务(P1-9 无 inject 依赖),跳过注册
140
+ return; // 测试裸 Context 无 tools 服务,跳过注册
118
141
  const provider = ctx.get('kanban');
119
142
  if (!provider)
120
143
  return;
121
144
  const service = provider.service;
122
145
  // 生产 wiring(src/index.ts)仅保证 tools+kanban 可用,无 wiki 服务 → 经 configProvider 自建
123
146
  //(与 dispatcher 构造同源,getEffective() 调用时热读取 baseUrl/pagePrefix);测试经 ctx.get('wiki') 注入 mock 客户端。
124
- // D3 双模式:local 模式走 LocalWikiClient 本地检索(不发 HTTP),remote 用配置的 WikiVaultClient。
147
+ // 双模式:local 模式走 LocalWikiClient 本地检索(不发 HTTP),remote 用配置的 WikiVaultClient。
125
148
  const kbMode = configProvider.mode;
126
149
  const wiki = kbMode === 'local'
127
150
  ? new LocalWikiClient(ensureLocalKbRoot())
@@ -141,15 +164,16 @@ export function registerMainSessionTools(ctx, configProvider) {
141
164
  }
142
165
  // planning 工具(清单落库 + 只读预取)——spawnPrefetch 由模块级 buildSpawnPrefetch 提供(可单测)
143
166
  for (const tool of buildPlanningTools({
144
- service, wiki: wiki, // local 模式为 LocalWikiClient(write/read/search 同面,D3 双模式客户端)
167
+ service, wiki: wiki, // local 模式为 LocalWikiClient(write/read/search 同面,双模式客户端)
145
168
  getCaller: caller,
146
169
  spawnPrefetch: buildSpawnPrefetch(ctx),
147
170
  tempDir: () => `${tmpdir()}/dsh-swarm-checklists`, // KB 不可达时的临时兜底,放系统临时目录(不落插件源码/核心存储目录)
148
171
  pagePrefix: configProvider.getEffective().wikiVault?.pagePrefix ?? 'projects/', // 生成的清单页路径保持在该客户端配置的命名空间内(避免 kb-rejected)
149
- kbMode: configProvider.mode, // D3 双模式:local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/)
172
+ kbMode: configProvider.mode, // 双模式:local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/)
150
173
  prefixRoutes: configProvider.getEffective().prefixRoutes,
151
174
  memoryEnabled: configProvider.getEffective().memory?.enabled ?? true,
152
175
  resolveWorkspaceDir: () => planningBySession.get('session_main')?.workspaceDir ?? null,
176
+ flowMode: () => planningBySession.get('session_main')?.mode ?? null,
153
177
  ownerSessionId: 'session_main',
154
178
  onChecklistSaved({ ref, source, checklist }) {
155
179
  const cur = planningBySession.get('session_main') ?? { workspaceDir: null, sessionId: 'session_main', checklist: null, checklistRef: null, checklistSource: null, requirementName: null };
@@ -157,23 +181,34 @@ export function registerMainSessionTools(ctx, configProvider) {
157
181
  },
158
182
  }))
159
183
  registry.register(tool);
160
- // kanban_route:/plan: 捕获规划上下文;/openspec: 用清单建链(前缀路由注册时快照,用于工具描述)
161
- const { plan, openspec, learning } = configProvider.getEffective().prefixRoutes;
184
+ // kanban_route:/plan: 捕获规划上下文;/openspec: 用清单建链;/sms 手动投递链报告(前缀路由注册时快照,用于工具描述)
185
+ const { plan, openspec, learning, send } = configProvider.getEffective().prefixRoutes;
162
186
  registry.register(defineTool({
163
187
  name: 'kanban_route',
164
- description: `MUST be called when the human message starts with ${plan}, ${openspec}, or ${learning}. This is dsh-swarm planning, NOT the built-in /plan plan mode. ${plan} = zero side-effect + start grill-me (+ auto KB memory index); ${openspec} = create chain from saved checklist; ${learning} = distill experience from a chain (evidence pack + planning_learning_save).`,
165
- parameters: { message: { type: 'string', required: true } },
188
+ description: `Route hub for dsh-swarm kanban workflow (NOT the built-in /plan plan mode). Two trigger forms. (1) PREFIX form — MUST be called when the human message starts with ${plan}, ${openspec}, ${learning}, or ${send}; omit intent. (2) INTENT form (swarm preset sessions) — MUST be called with intent when the human expresses: a hands-on development requirement → intent='plan'; explicit approval to start the workflow after the checklist was saved → intent='openspec'; distill/retrospect lessons from a chain → intent='learning' (message = chainId or title words, may be empty = latest chain); deliver a chain report to the WeCom group, or free-form delivery of user-intent content → intent='send' (block notice: prefix message with 'blocked '). When intent is set, message = the user's raw words (no prefix). Ambiguous intent → do NOT call, ask the user instead. Semantics: ${plan} = zero side-effect + start grill-me (+ auto KB memory index); ${openspec} = create chain from saved checklist; ${learning} = distill experience from a chain (evidence pack + planning_learning_save); ${send} = manually deliver a chain report to the WeCom group (bare = latest completed chain; '${send} blocked [chainId]' = resend block notice; bypasses imDelivery.enabled; the message body is composed by system code — never compose or repeat it, only relay the delivery status; any other non-empty message = free-form delivery: compose a fact-grounded body per the returned guidance and call sms_send (append ' -s' to the message or mention 私聊 for private chat)).`,
189
+ parameters: { message: { type: 'string', required: true }, intent: { type: 'string', description: "swarm preset sessions: 'plan' | 'openspec' | 'learning' | 'send' — model-judged intent; omit for prefix-triggered calls" } },
166
190
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
167
191
  async execute(args, exec) {
168
- // M2(Q5)+归组:仅 /plan: 分支捕获主 agent 工作空间并可能询问注册——/openspec:/none 不触发,
169
- // 避免死代码副作用多弹一次 ask(/openspec: 实际用的是 planningBySession 已存的 workspaceDir)。
170
- // header.cwd 缺失或未注册时询问用户注册工作区;仍不可得保持 null(链任务随后 block 'workspace-unknown')。
171
- const plan = await handlePlanRoute(args.message, service, configProvider.getEffective().prefixRoutes, 'session_main');
192
+ // intent 路径(蜂群模式):handler 内部会重 parsePrefix(message),无前缀 message
193
+ // 会判成 none —— 因此 intent 命中时合成「前缀 + rest」消息再进 handler;handler 零改动、
194
+ // 前缀路径零感知。intent 优先于 message 前缀;非法 intent 回退 parsePrefix(none 兜底)。
195
+ // 简报矛盾修正:intent 命中时 message 若自带已知前缀(模型契约
196
+ // 违例,description 明文 raw words no prefix),原样拼接会使 rest 带旧前缀 → handler 内必
197
+ // chain-not-found → brief 断言不可能绿;故丢弃载荷只保留 intent 裸前缀
198
+ //(learning 裸 = 最近链,与既有 '/learning' 语义一致)。raw words 正常路径与简报逐字节一致。
199
+ const routes = configProvider.getEffective().prefixRoutes;
200
+ const INTENTS = ['plan', 'openspec', 'learning', 'send'];
201
+ const intent = INTENTS.includes(args.intent ?? '') ? args.intent : null;
202
+ const trimmed = args.message.trim();
203
+ const routeMessage = intent
204
+ ? `${routes[intent]}${trimmed && !INTENTS.some((k) => trimmed.startsWith(routes[k])) ? ' ' + trimmed : ''}`
205
+ : args.message;
206
+ const plan = await handlePlanRoute(routeMessage, service, routes, 'session_main');
172
207
  if (plan.kind === 'plan') {
173
208
  const headerCwd = exec?.agent?.session?.header?.cwd ?? null;
174
209
  const workspaceDir = await resolveOrCreateWorkspace(ctx, headerCwd, '主 agent 会话');
175
- planningBySession.set('session_main', { workspaceDir, sessionId: 'session_main', checklist: null, checklistRef: null, checklistSource: null, requirementName: plan.rest });
176
- let guidance = buildPlanningGuidance(configProvider.getEffective().prefixRoutes) + KANBAN_HANDOFF_RULE(configProvider.getEffective().prefixRoutes);
210
+ planningBySession.set('session_main', { workspaceDir, sessionId: 'session_main', checklist: null, checklistRef: null, checklistSource: null, requirementName: plan.rest, mode: intent ? 'swarm' : 'prefix' });
211
+ let guidance = buildPlanningGuidance(configProvider.getEffective().prefixRoutes, { swarm: intent === 'plan' }) + KANBAN_HANDOFF_RULE(configProvider.getEffective().prefixRoutes, { swarm: intent === 'plan' });
177
212
  if ((configProvider.getEffective().memory?.enabled ?? true) && workspaceDir) {
178
213
  const idx = await recallMemoryIndex(wiki, {
179
214
  requirementName: plan.rest || null,
@@ -187,11 +222,41 @@ export function registerMainSessionTools(ctx, configProvider) {
187
222
  return { kind: 'plan', guidance };
188
223
  }
189
224
  if (plan.kind === 'learning') {
190
- const r = await handleLearningRoute(args.message, service, configProvider.getEffective().prefixRoutes, 'session_main');
225
+ const r = await handleLearningRoute(routeMessage, service, configProvider.getEffective().prefixRoutes, 'session_main');
191
226
  if (r.error)
192
227
  return { kind: 'learning', error: r.error, guidance: r.guidance };
193
228
  return { kind: 'learning', chainId: r.chainId, brief: r.brief, guidance: r.guidance };
194
229
  }
230
+ if (plan.kind === 'send') {
231
+ // /sms 三岔:先链后自由。blocked 前缀/空 rest/链 id 命中 → 既有机械汇报(正文系统渲染,红线不变);
232
+ // 其余非空 rest → 自由投递(正文由主 agent 按意图生成,显式豁免;事实基准=看板实查)。
233
+ // -s 独立 token 由 parseSendRequest 剥离置 dm;manual 路径零重试。
234
+ const parsed = parseSendRequest(plan.rest);
235
+ const targetOpts = { retryDelaysMs: [], manual: true, targetKind: (parsed.dm ? 'user' : 'group') };
236
+ const deliver = async (variant, query) => {
237
+ const r = await sendChainReport(ctx, service, configProvider, targetOpts, variant, query);
238
+ if (r.ok) {
239
+ const noun = variant === 'blocked' ? '阻塞通知' : '完成汇报';
240
+ const dest = parsed.dm ? '私聊' : '群聊';
241
+ return {
242
+ kind: 'send', chainId: r.chainId, botId: r.botId, targetId: r.targetId,
243
+ guidance: `${noun}已投递${dest}(/sms 手动触发)。请仅向用户确认投递成功与目标,勿复述消息正文。`,
244
+ };
245
+ }
246
+ return { kind: 'send', error: r.error, guidance: r.guidance ?? '请将 error 字段原样转告用户,勿复述消息正文。' };
247
+ };
248
+ if (parsed.variant !== 'free')
249
+ return deliver(parsed.variant === 'blocked' ? 'blocked' : 'completion', parsed.query);
250
+ // free 候选复判链:显式 id 命中/歧义/判据不满足都归链汇报路径,仅 chain-not-found 才自由
251
+ const state = await service.snapshot();
252
+ const resolved = resolveReportChainId(state, 'completion', parsed.query);
253
+ if (resolved.ok || resolved.error !== 'chain-not-found')
254
+ return deliver('completion', parsed.query);
255
+ return {
256
+ kind: 'send', mode: 'free', dm: parsed.dm,
257
+ guidance: buildFreeSendGuidance(configProvider.getEffective().prefixRoutes, parsed.query, parsed.dm),
258
+ };
259
+ }
195
260
  if (plan.kind === 'none')
196
261
  return { kind: 'none' };
197
262
  // 路由1(内存):planningBySession 命中 → 直接建链
@@ -218,14 +283,14 @@ export function registerMainSessionTools(ctx, configProvider) {
218
283
  };
219
284
  }
220
285
  const input = { workspaceDir: pctx.workspaceDir, checklist: pctx.checklist, checklistRef: pctx.checklistRef, requirementName: pctx.requirementName };
221
- const r = await handleOpenspecRoute(args.message, service, configProvider.getEffective().prefixRoutes, input, 'session_main');
286
+ const r = await handleOpenspecRoute(routeMessage, service, configProvider.getEffective().prefixRoutes, input, 'session_main');
222
287
  // 护栏拦截(如 workspace-unknown):透传失败原因与恢复指引,不得伪装成建链成功
223
288
  if (r.approved === false || !r.chainId || !r.specCardId) {
224
289
  return { kind: 'openspec', approved: false, reason: r.reason ?? 'unknown', guidance: r.guidance };
225
290
  }
226
291
  return {
227
292
  kind: 'openspec', chainId: r.chainId, specCardId: r.specCardId, approved: true, firstCard: r.firstCard,
228
- guidance: KANBAN_HANDOFF_RULE(configProvider.getEffective().prefixRoutes) + '\n\n' + buildOpenspecNarrationRule({ chainId: r.chainId, specCardId: r.specCardId, firstCard: r.firstCard }),
293
+ guidance: KANBAN_HANDOFF_RULE(configProvider.getEffective().prefixRoutes, { swarm: planningBySession.get('session_main')?.mode === 'swarm' }) + '\n\n' + buildOpenspecNarrationRule({ chainId: r.chainId, specCardId: r.specCardId, firstCard: r.firstCard }),
229
294
  };
230
295
  }
231
296
  // 路由2(知识库):内存丢失(插件重启)→ 搜 KB 候选清单页供 LLM 读页重建;搜不到/不可达 → 两条路皆空
@@ -253,5 +318,39 @@ export function registerMainSessionTools(ctx, configProvider) {
253
318
  };
254
319
  },
255
320
  }));
321
+ // sms_send:自由投递出口(/sms 非链意图 → guidance 教学 → 此工具实际发送)。manual 路径零重试;
322
+ // 缺 dsh-im 插件走 DSH_IM_MISSING_PREFIX/GUIDANCE 友好提醒;text 硬上限 4000 字符、空白拒绝。
323
+ registry.register(defineTool({
324
+ name: 'sms_send',
325
+ description: 'Deliver a free-form message via dsh-im (WeCom). text = body composed per the free-delivery guidance returned by kanban_route (fact-grounded via kanban_show; <=4000 chars). dm=false -> the single saved group target; dm=true -> the single saved private-chat target (kind=user; exactly one must exist). Manual path: no retry on failure; missing dsh-im plugin returns install guidance.',
326
+ parameters: {
327
+ text: { type: 'string', required: true, description: 'Message body (1..4000 chars) composed per the free-delivery guidance' },
328
+ dm: { type: 'boolean', description: 'true = private chat; omit/false = group' },
329
+ },
330
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
331
+ async execute(args) {
332
+ // actor 边界与 planning 工具同构:主会话工具面仅限人工触发,非 human caller 一律拒绝
333
+ if (caller().actor !== 'human')
334
+ throw new Error('permission denied: sms_send');
335
+ const text = typeof args.text === 'string' ? args.text.trim() : '';
336
+ if (!text)
337
+ throw new Error('text required(正文必填,按自由投递 guidance 生成)');
338
+ if (text.length > 4000)
339
+ throw new Error(`text too long: ${text.length} > 4000 chars`);
340
+ const dm = args.dm === true;
341
+ const deliver = createSender(ctx, service, configProvider, { retryDelaysMs: [], manual: true, targetKind: dm ? 'user' : 'group' });
342
+ const r = await deliver('free-send', text);
343
+ if (!r.ok) {
344
+ return {
345
+ kind: 'send', mode: 'free', dm, error: r.error,
346
+ guidance: r.error.startsWith(DSH_IM_MISSING_PREFIX) ? DSH_IM_MISSING_GUIDANCE : undefined,
347
+ };
348
+ }
349
+ return {
350
+ kind: 'send', mode: 'free', dm, botId: r.botId, targetId: r.targetId,
351
+ guidance: '已投递(自由投递)。请仅向用户确认投递成功与目标(群聊/私聊),勿复述正文。',
352
+ };
353
+ },
354
+ }));
256
355
  console.info('[dsh-swarm] main-session tools registered (v2: kanban_route + planning + spec view + read-only kanban)');
257
356
  }
@@ -0,0 +1,11 @@
1
+ /** ocr_review 工具:把 ocr CLI 三子命令(preview/rule/managed)封装为 cordis 工具,DT 评审角色工具面注册。
2
+ * 依赖全注入可换 fake(tests),缺省用 ocr-cli 真实现。 */
3
+ import { type ToolDefinition as ToolDef } from '@deepseek-ai/dsh-tools';
4
+ import { managedProviderReady, probeOcr, runOcr } from '../services/ocr-cli.js';
5
+ /** 构造 ocr_review 工具定义(defineTool 返回形态,与 kanban-tools 一致)。 */
6
+ export declare function buildOcrReviewTool(deps: {
7
+ runOcrFn?: typeof runOcr;
8
+ probeFn?: typeof probeOcr;
9
+ managedReadyFn?: typeof managedProviderReady;
10
+ cwd?: () => string;
11
+ }): ToolDef;
@@ -0,0 +1,63 @@
1
+ // src/tools/ocr-review-tools.ts
2
+ /** ocr_review 工具:把 ocr CLI 三子命令(preview/rule/managed)封装为 cordis 工具,DT 评审角色工具面注册。
3
+ * 依赖全注入可换 fake(tests),缺省用 ocr-cli 真实现。 */
4
+ import { defineTool } from '@deepseek-ai/dsh-tools';
5
+ import { INSTALL_GUIDANCE, managedProviderReady, probeOcr, runOcr } from '../services/ocr-cli.js';
6
+ import { buildOcrArgs, parseManagedJson, parsePreviewJson, shouldSuggestManaged, SUGGEST_MANAGED_FILES } from '../domain/ocr-review.js';
7
+ /** 托管未配置时的降级引导:不改道执行,交还调用方决策。 */
8
+ const MANAGED_FALLBACK_TEXT = '托管模式未配置 LLM——本次按委托模式执行:改调 sub=\'preview\' 获取评审范围后自行评审';
9
+ const SUBS = ['preview', 'rule', 'managed'];
10
+ /** 构造 ocr_review 工具定义(defineTool 返回形态,与 kanban-tools 一致)。 */
11
+ export function buildOcrReviewTool(deps) {
12
+ const runOcrFn = deps.runOcrFn ?? runOcr;
13
+ const probeFn = deps.probeFn ?? probeOcr;
14
+ const managedReadyFn = deps.managedReadyFn ?? managedProviderReady;
15
+ return defineTool({
16
+ name: 'ocr_review',
17
+ description: 'Review code changes via the ocr CLI. Three subcommands: '
18
+ + "'preview' — first step of delegated review; lists reviewable files and branch metadata "
19
+ + '(JSON output: mode/files/excluded/mergeBase) so you can review them yourself; '
20
+ + "'rule' — batch-fetch review rule groups for the file paths returned by preview; "
21
+ + "'managed' — full hosted review where ocr uses its own configured LLM and returns normalized findings JSON "
22
+ + "(requires prior wiring: ocr config provider=dsh-managed). "
23
+ + "Start with 'preview' to scope the review; prefer 'managed' for large change sets when the managed provider is configured.",
24
+ parameters: {
25
+ sub: { type: 'string', required: true, description: "'preview' | 'rule' | 'managed'" },
26
+ repo: { type: 'string', description: '仓库根绝对路径;缺省用当前工作目录' },
27
+ from: { type: 'string', description: 'base 分支/引用(range 模式)' },
28
+ to: { type: 'string', description: '目标分支/引用(range 模式,默认 HEAD)' },
29
+ commit: { type: 'string', description: '单次提交审查' },
30
+ paths: { type: 'array', items: { type: 'string' }, description: 'rule 子命令的文件路径列表' },
31
+ },
32
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String(v) }] },
33
+ async execute(args) {
34
+ const probe = await probeFn();
35
+ if (!probe.installed)
36
+ return INSTALL_GUIDANCE;
37
+ if (args.sub === 'managed' && !managedReadyFn())
38
+ return MANAGED_FALLBACK_TEXT;
39
+ if (!SUBS.includes(args.sub))
40
+ throw new Error(`ocr_review: invalid sub '${args.sub}' (expected 'preview' | 'rule' | 'managed')`);
41
+ if (args.sub === 'rule' && (!args.paths || args.paths.length === 0))
42
+ throw new Error("ocr_review: sub='rule' requires non-empty paths (file path list)");
43
+ if (args.sub === 'managed' && !args.commit && !args.from)
44
+ throw new Error("ocr_review: sub='managed' requires commit or from (base ref)");
45
+ const res = await runOcrFn(buildOcrArgs(args.sub, { repo: args.repo, from: args.from, to: args.to, commit: args.commit, paths: args.paths }), { cwd: deps.cwd?.() ?? process.cwd(), timeoutMs: 600_000 });
46
+ if (res.error && !res.stdout)
47
+ throw new Error(`ocr_review: ${res.error} ${res.stderr.slice(0, 500)}`.trim());
48
+ if (args.sub === 'preview') {
49
+ const preview = parsePreviewJson(res.stdout);
50
+ if (shouldSuggestManaged(preview.files.length)) {
51
+ return JSON.stringify({ ...preview, suggestion: `文件较多(N>${SUGGEST_MANAGED_FILES}),可在配置面板切换托管模式` });
52
+ }
53
+ return JSON.stringify(preview);
54
+ }
55
+ if (args.sub === 'rule')
56
+ return res.stdout.slice(0, 8000);
57
+ const report = parseManagedJson(res.stdout);
58
+ if (report.status !== 'completed' && res.error)
59
+ return JSON.stringify({ ...report, message: res.error });
60
+ return JSON.stringify(report);
61
+ },
62
+ });
63
+ }
@@ -20,10 +20,10 @@ export interface PlanningToolDeps {
20
20
  spawnPrefetch?(prompt: string, workspaceDir: string, parentAgent?: Agent, signal?: AbortSignal): Promise<string>;
21
21
  tempDir(): string;
22
22
  pagePrefix?: string;
23
- /** KB 双模式(D3):local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/),缺省 remote。 */
23
+ /** KB 双模式:local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/),缺省 remote。 */
24
24
  kbMode?: 'remote' | 'local';
25
25
  ownerSessionId?: string;
26
- /** 斜杠命令前缀路由(决策12 单一事实源),用于 description 文案派生。 */
26
+ /** 斜杠命令前缀路由(单一事实源),用于 description 文案派生。 */
27
27
  prefixRoutes: PrefixRoutes;
28
28
  defaultModel?: AgentModelOptions;
29
29
  /** 清单落库成功回调(kb 与 temp 两分支各调一次),供 main-session-tools 回写 planningBySession。 */
@@ -36,6 +36,9 @@ export interface PlanningToolDeps {
36
36
  memoryEnabled?: boolean;
37
37
  /** 闸1:/plan: 捕获的当前工作区(main-session-tools 注入);非 null 且与清单 localPath 不一致 → 阻断落库。 */
38
38
  resolveWorkspaceDir?: () => string | null;
39
+ /** 蜂群模式分叉:main-session-tools 注入 () => planningBySession.get('session_main')?.mode ?? null。
40
+ * 'swarm' 时 checklist_save 成功返回体附 nextStep 确认闸指引;前缀模式返回体不变。 */
41
+ flowMode?: () => 'swarm' | 'prefix' | null;
39
42
  }
40
43
  /** 主 agent 规划期工具:需求澄清清单落库(KB 优先/临时目录兜底)+ 只读仓库预取(子代理)。 */
41
44
  export declare function buildPlanningTools(deps: PlanningToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition[];
@@ -4,16 +4,18 @@ import {} from '@deepseek-ai/dsh-util-values';
4
4
  import { validatePlanningChecklist, formatChecklistBody } from '../domain/planning-checklist.js';
5
5
  import { validatePrefetchManifest } from '../domain/prefetch-manifest.js';
6
6
  import { buildChecklistSlug, KB_PAGE_NAMESPACES_HINT, LOCAL_CHECKLIST_PREFIX, LOCAL_LEARNING_BASE, assertAllowedWikiPagePath, assertLocalKbPagePath } from '../wiki/page-path.js';
7
- import { validateLearning, formatLearningBody, buildRepoSlug } from '../domain/memory.js';
7
+ import { validateLearning, formatLearningBody, buildRepoSlug, countLearningSignals } from '../domain/memory.js';
8
8
  const isWikiError = (e) => e instanceof Error && e.code === 'kb-unreachable';
9
9
  /** 主 agent 规划期工具:需求澄清清单落库(KB 优先/临时目录兜底)+ 只读仓库预取(子代理)。 */
10
10
  export function buildPlanningTools(deps) {
11
11
  const local = deps.kbMode === 'local';
12
- // checklist 前缀双模式(D3/D5):remote → projects/<repoSlug>/checklists/(repoSlug 段在 pagePath 拼接,workspace 计划 T3);
13
- // local → wiki/queries/checklists/(local 时 LocalWikiClient.write 仅接受 wiki/** 相对路径,projects/ 会被 kb-rejected——I1 修复)
12
+ // checklist 前缀双模式:remote → projects/<repoSlug>/checklists/(repoSlug 段在 pagePath 拼接);
13
+ // local → wiki/queries/checklists/(local 时 LocalWikiClient.write 仅接受 wiki/** 相对路径,projects/ 会被 kb-rejected
14
14
  const pagePrefix = deps.pagePrefix ?? 'projects/';
15
15
  const checklistPrefix = local ? LOCAL_CHECKLIST_PREFIX : pagePrefix;
16
16
  const session = deps.ownerSessionId ?? 'session_main';
17
+ const SWARM_NEXT_STEP = '向用户征求确认;仅当用户回复含明确肯定语义(确认/开干/开跑/开始/go 等)才调 kanban_route{intent:\'openspec\'} 建链;模糊、岔开话题、只提修改意见 = 未确认,继续澄清。禁止未确认建链。';
18
+ const swarmNext = () => (deps.flowMode?.() === 'swarm' ? { nextStep: SWARM_NEXT_STEP } : {});
17
19
  return [
18
20
  defineTool({
19
21
  name: 'planning_checklist_save',
@@ -38,7 +40,7 @@ export function buildPlanningTools(deps) {
38
40
  try {
39
41
  await deps.wiki.write(args.restoreRef, body);
40
42
  deps.onChecklistSaved?.({ ref: args.restoreRef, source: 'kb', checklist });
41
- return { ok: true, ref: args.restoreRef, source: 'kb', repoPath: checklist.manifest.repo.localPath };
43
+ return { ok: true, ref: args.restoreRef, source: 'kb', repoPath: checklist.manifest.repo.localPath, ...swarmNext() };
42
44
  }
43
45
  catch (err) {
44
46
  if (!isWikiError(err))
@@ -53,7 +55,7 @@ export function buildPlanningTools(deps) {
53
55
  try {
54
56
  await deps.wiki.write(pagePath, body);
55
57
  deps.onChecklistSaved?.({ ref: pagePath, source: 'kb', checklist });
56
- return { ok: true, ref: pagePath, source: 'kb', repoPath: checklist.manifest.repo.localPath };
58
+ return { ok: true, ref: pagePath, source: 'kb', repoPath: checklist.manifest.repo.localPath, ...swarmNext() };
57
59
  }
58
60
  catch (err) {
59
61
  if (!isWikiError(err))
@@ -64,7 +66,7 @@ export function buildPlanningTools(deps) {
64
66
  mkdirSync(deps.tempDir(), { recursive: true });
65
67
  writeFileSync(local, body, 'utf8');
66
68
  deps.onChecklistSaved?.({ ref: local, source: 'temp', checklist });
67
- return { ok: true, ref: local, source: 'temp', repoPath: checklist.manifest.repo.localPath };
69
+ return { ok: true, ref: local, source: 'temp', repoPath: checklist.manifest.repo.localPath, ...swarmNext() };
68
70
  }
69
71
  },
70
72
  }),
@@ -100,7 +102,7 @@ export function buildPlanningTools(deps) {
100
102
  name: 'planning_learning_save',
101
103
  description: 'Save a distilled learning (experience) to the knowledge base. Remote KB: scope=chain → projects/<repoSlug>/<chainId>/learnings/ (requirement-level); scope=project → projects/<repoSlug>/learnings/ (repo-level). repoSlug is derived from the chain workspaceDir; both scopes require chain.workspaceDir. Local KB: both scopes → wiki/synthesis/learnings/<chainId|repoSlug>/. Returns ref. Soft-fails {ok:false,reason:"kb-unreachable"} when KB is unreachable (no temp fallback).',
102
104
  parameters: {
103
- learning: { type: 'json', required: true, description: 'LearningEntry: { title (≤80 chars), lesson, evidence (mechanical chain/task id — required), tags: string[] }' },
105
+ learning: { type: 'json', required: true, description: 'LearningEntry: { title (≤80 chars), lesson, evidence (mechanical chain/task id — required), tags: string[] — must include one of mistake/reusable/env-trap/collab-contract/efficiency (category criteria enforced) }' },
104
106
  scope: { type: 'string', enum: ['chain', 'project'], required: true, description: '"chain" (requirement-level) | "project" (repo-level)' },
105
107
  chainId: { type: 'string', required: true, description: 'The chain this learning is distilled from; must exist' },
106
108
  },
@@ -121,9 +123,15 @@ export function buildPlanningTools(deps) {
121
123
  if (!chain)
122
124
  throw new Error('unknown chain: ' + args.chainId);
123
125
  const entry = args.learning;
126
+ // A 类硬复核(0.3.1):mistake 标签须通过机械信号复核(累计 ≥2),不复信模型自报
127
+ if (Array.isArray(entry.tags) && entry.tags.includes('mistake')) {
128
+ const total = countLearningSignals(state, args.chainId);
129
+ if (total < 2)
130
+ throw new Error(`[mistake-gate] 标记 mistake 需链上 阻塞/返工/评审失败/审计警告 累计 ≥2 次,当前链累计 ${total} 次。请核对证据包;不满足时改用其他类别标签(reusable/env-trap/collab-contract/efficiency)或回复「无新经验」`);
131
+ }
124
132
  let prefix;
125
133
  if (local) {
126
- // local(D5):scope=chain 直接挂 chainId;scope=project 挂 repoSlug(需 workspaceDir)
134
+ // localscope=chain 直接挂 chainId;scope=project 挂 repoSlug(需 workspaceDir)
127
135
  if (args.scope === 'chain') {
128
136
  prefix = `${LOCAL_LEARNING_BASE}${args.chainId}/`;
129
137
  }
@@ -134,7 +142,7 @@ export function buildPlanningTools(deps) {
134
142
  }
135
143
  }
136
144
  else {
137
- // remote(workspace 计划 T3):两 scope 均要求 chain.workspaceDir,统一挂 projects/<repoSlug>/
145
+ // remote:两 scope 均要求 chain.workspaceDir,统一挂 projects/<repoSlug>/
138
146
  if (!chain.workspaceDir)
139
147
  throw new Error('learning save requires chain.workspaceDir (target repo) — chain has none');
140
148
  const wsRoot = `${pagePrefix}${buildRepoSlug(chain.workspaceDir)}/`;
@@ -171,8 +179,8 @@ export function buildPlanningTools(deps) {
171
179
  if (hasPath === hasQuery)
172
180
  throw new Error('provide exactly one of path|query');
173
181
  if (hasPath) {
174
- // path 白名单按 KB 双模式分支(D3/D9):remote → projects/** 命名空间;local → wiki/**(
175
- // Task 11 后 local 的 checklist/learning 全落 wiki/ 前缀,用 projects/ 白名单会把 local 读腿全拒)
182
+ // path 白名单按 KB 双模式分支:remote → projects/** 命名空间;local → wiki/**(
183
+ // local 的 checklist/learning 全落 wiki/ 前缀,用 projects/ 白名单会把 local 读腿全拒)
176
184
  if (local)
177
185
  assertLocalKbPagePath(args.path);
178
186
  else
@@ -44,11 +44,11 @@ export function buildWikiTools(wiki, getCaller) {
44
44
  async execute(args) {
45
45
  const caller = getCaller();
46
46
  guard('wiki-write', caller);
47
- // Q3&5:工具边界强校验——只允许 projects/<repoSlug>/ 白名单命名空间(KB_PAGE_NAMESPACES_HINT 五类,
47
+ // 工具边界强校验——只允许 projects/<repoSlug>/ 白名单命名空间(KB_PAGE_NAMESPACES_HINT 五类,
48
48
  // 见 page-path.ts),杜绝 LLM 自造路径/拼错层级导致 kb_url 无法跳转。
49
49
  assertAllowedWikiPagePath(args.pagePath);
50
50
  const out = await wiki.write(args.pagePath, args.content);
51
- // Q4:工具直接拼完整 kb_url(host 用 config.wikiVault.baseUrl,杜绝 LLM 手写错域名)。
51
+ // 工具直接拼完整 kb_url(host 用 config.wikiVault.baseUrl,杜绝 LLM 手写错域名)。
52
52
  // W 角色交付闸(kanban_complete w:kb)同时校验 host 前缀,双保险。
53
53
  const base = wiki.baseUrl.replace(/\/$/, '');
54
54
  return { path: out.path, kb_url: `${base}/#/page/${args.pagePath}` };
@@ -1,5 +1,5 @@
1
1
  import type { BoardState } from '../domain/types.js';
2
- /** kb-linkage 所需最小 wiki 面(WikiVaultClient / LocalWikiClient 均满足,D2 双模式客户端注入)。 */
2
+ /** kb-linkage 所需最小 wiki 面(WikiVaultClient / LocalWikiClient 均满足,双模式客户端注入)。 */
3
3
  type KbWikiClient = {
4
4
  read(pagePath: string): Promise<{
5
5
  rawMd: string;
@@ -1,7 +1,7 @@
1
1
  // src/wiki/local-kb-client.ts
2
2
  // 本地 KB 客户端(llm-wiki 目录结构):与 WikiVaultClient 同构的 search/read/write 面,
3
3
  // local 模式下替换注入(planning 工具 / kb-linkage / memory-recall 复用既有逻辑,零分支扩散)。
4
- // 排序原则 D12:相关性优先 7 : 新鲜度 3(0.7*scoreNorm + 0.3*mtimeNorm)。
4
+ // 排序原则:相关性优先 7 : 新鲜度 3(0.7*scoreNorm + 0.3*mtimeNorm)。
5
5
  import { readdirSync, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from 'node:fs';
6
6
  import { join, dirname, resolve, sep } from 'node:path';
7
7
  import { WikiError } from './wiki-vault-client.js';
@@ -12,7 +12,7 @@ export class LocalWikiClient {
12
12
  root;
13
13
  constructor(root) { this.root = root; }
14
14
  abs(pagePath) {
15
- assertLocalKbPagePath(pagePath); // Task 1 白名单单一来源(wiki/**、拒 .. 与绝对路径),勿内联重复
15
+ assertLocalKbPagePath(pagePath); // 白名单单一来源(wiki/**、拒 .. 与绝对路径),勿内联重复
16
16
  const t = resolve(this.root, pagePath);
17
17
  if (!isInsideKbRoot(this.root, t))
18
18
  throw new WikiError('kb-rejected', undefined, `kb-rejected: path escapes local KB root: ${pagePath}`);
@@ -25,7 +25,7 @@ export class LocalWikiClient {
25
25
  writeFileSync(t, content, 'utf8');
26
26
  }
27
27
  catch (err) {
28
- // §9 降级链依赖:fs 失败归一 kb-unreachable(planning 工具按 code 判定 temp 兜底/软失败,审查 M5)
28
+ // 降级链依赖:fs 失败归一 kb-unreachable(planning 工具按 code 判定 temp 兜底/软失败)
29
29
  throw new WikiError('kb-unreachable', undefined, `kb-unreachable: local KB write failed: ${String(err)}`);
30
30
  }
31
31
  return { path: pagePath };
@@ -1,5 +1,5 @@
1
1
  export declare function localKbRoot(): string;
2
- /** 尽力创建库根(幂等);失败由调用方告警 + block(kb-unreachable)(设计 §9)。 */
2
+ /** 尽力创建库根(幂等);失败由调用方告警 + block(kb-unreachable) */
3
3
  export declare function ensureLocalKbRoot(root?: string): string;
4
4
  /** target 须为绝对路径且解析后落在 root 内(resolve 折叠重复斜杠;相对路径 fail-closed,
5
5
  * 因为 bash/fs 里的相对目标语义属会话 cwd 而非库根,护栏不可猜测)。 */
@@ -1,5 +1,5 @@
1
1
  // src/wiki/local-kb.ts
2
- // 本地 KB(llm-wiki)库根工具(设计 D4:独立 <DSH_HOME>/storages/kanban/wiki/,隔离用户个人 ~/.llm-wiki-path)。
2
+ // 本地 KB(llm-wiki)库根工具(设计:独立 <DSH_HOME>/storages/kanban/wiki/,隔离用户个人 ~/.llm-wiki-path)。
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve, sep } from 'node:path';
5
5
  import { mkdirSync } from 'node:fs';
@@ -7,7 +7,7 @@ export function localKbRoot() {
7
7
  const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
8
8
  return join(dshHome, 'storages', 'kanban', 'wiki');
9
9
  }
10
- /** 尽力创建库根(幂等);失败由调用方告警 + block(kb-unreachable)(设计 §9)。 */
10
+ /** 尽力创建库根(幂等);失败由调用方告警 + block(kb-unreachable) */
11
11
  export function ensureLocalKbRoot(root = localKbRoot()) {
12
12
  mkdirSync(root, { recursive: true });
13
13
  return root;
@@ -1,5 +1,5 @@
1
1
  /** 命名空间提示(报错/工具 description 文案单一来源,禁止散落硬编码)。 */
2
- export declare const KB_PAGE_NAMESPACES_HINT = "projects/<repoSlug>/checklists/\u3001projects/<repoSlug>/learnings/\u3001projects/<repoSlug>/ch_*/learnings/\u3001projects/<repoSlug>/ch_*/t_*.md\u3001projects/<repoSlug>/ch_*/review/";
2
+ export declare const KB_PAGE_NAMESPACES_HINT = "projects/<repoSlug>/checklists/\u3001projects/<repoSlug>/learnings/\u3001projects/<repoSlug>/ch_*/learnings/\u3001projects/<repoSlug>/ch_*/t_*.md\u3001projects/<repoSlug>/ch_*/review/\u3001projects/<repoSlug>/reviews/<topic>-<ymd>/";
3
3
  /** 从需求名(回退 problem)派生 URL 安全 slug:ASCII 化、空格/特殊字符→-、限长 40;全非 ASCII(如中文)兜底 'req'。 */
4
4
  export declare function buildChecklistSlug(name: string): string;
5
5
  export declare function isAllowedWikiPagePath(pagePath: string): boolean;
@@ -4,10 +4,12 @@
4
4
  // 需求清单 projects/<repoSlug>/checklists/<slug>-<ts>.md (planning_checklist_save,代码强制)
5
5
  // 实施计划/结果 projects/<repoSlug>/ch_<id>/t_<id>.md (W2/W3 wiki_write,工具边界强制)
6
6
  // DT 评审页 projects/<repoSlug>/ch_<id>/review/<name>.md (DT wiki_write + 写护栏强制)
7
+ // 独立评审报告 projects/<repoSlug>/reviews/<topic>-<ymd>/ (独立 DT 模式 wiki_write,形态由 domain/ocr-review 判定)
7
8
  // 经验 projects/<repoSlug>/learnings/、projects/<repoSlug>/ch_<id>/learnings/
8
9
  import { WikiError } from './wiki-vault-client.js';
10
+ import { isStandaloneReviewNamespacePath } from '../domain/ocr-review.js';
9
11
  /** 命名空间提示(报错/工具 description 文案单一来源,禁止散落硬编码)。 */
10
- export const KB_PAGE_NAMESPACES_HINT = 'projects/<repoSlug>/checklists/、projects/<repoSlug>/learnings/、projects/<repoSlug>/ch_*/learnings/、projects/<repoSlug>/ch_*/t_*.md、projects/<repoSlug>/ch_*/review/';
12
+ export const KB_PAGE_NAMESPACES_HINT = 'projects/<repoSlug>/checklists/、projects/<repoSlug>/learnings/、projects/<repoSlug>/ch_*/learnings/、projects/<repoSlug>/ch_*/t_*.md、projects/<repoSlug>/ch_*/review/、projects/<repoSlug>/reviews/<topic>-<ymd>/';
11
13
  /** 白名单:projects/<repoSlug>/ 下五类命名空间(repoSlug=[a-z0-9-]+,杜绝 LLM 自造路径)。 */
12
14
  const KB_PAGE_PATH_RE = /^projects\/[a-z0-9-]+\/(?:checklists\/|learnings\/|ch_[0-9a-z_]+\/(?:t_[0-9a-z_]+\.md|review\/|learnings\/))/;
13
15
  /** 从需求名(回退 problem)派生 URL 安全 slug:ASCII 化、空格/特殊字符→-、限长 40;全非 ASCII(如中文)兜底 'req'。 */
@@ -21,7 +23,8 @@ export function buildChecklistSlug(name) {
21
23
  return slug || 'req';
22
24
  }
23
25
  export function isAllowedWikiPagePath(pagePath) {
24
- return KB_PAGE_PATH_RE.test(pagePath);
26
+ // 独立评审命名空间(projects/<repoSlug>/reviews/<topic>-<ymd>/)复用 domain/ocr-review 判定,保持单一来源。
27
+ return KB_PAGE_PATH_RE.test(pagePath) || isStandaloneReviewNamespacePath(pagePath);
25
28
  }
26
29
  /** learnings 路径谓词:项目级 / 需求级(均在 repoSlug 下)。 */
27
30
  export function isLearningsPath(pagePath) {
@@ -33,7 +36,7 @@ export function assertAllowedWikiPagePath(pagePath) {
33
36
  throw new WikiError('kb-rejected', undefined, `page path outside allowed namespaces (${KB_PAGE_NAMESPACES_HINT}): ${pagePath}`);
34
37
  }
35
38
  }
36
- // ── 本地 KB(llm-wiki)路径规则(双模式 D5/D9)──────────────────────────────
39
+ // ── 本地 KB(llm-wiki)路径规则(双模式)──────────────────────────────
37
40
  export const LOCAL_CHECKLIST_PREFIX = 'wiki/queries/checklists/';
38
41
  export const LOCAL_LEARNING_BASE = 'wiki/synthesis/learnings/';
39
42
  /** 本地模式交付白名单:KB 库根下 wiki/** 相对路径(拒绝对象路径、.. 穿越、空串)。 */
@@ -15,7 +15,7 @@ export declare class WikiVaultClient {
15
15
  baseUrl: string;
16
16
  pagePrefix: string;
17
17
  });
18
- /** P2:暴露 baseUrl getter(下游 WikiWorker 拼 kb_url 用),不挖私有字段;经 getCfg 每次取最新配置(热生效)。 */
18
+ /** 暴露 baseUrl getter(下游 WikiWorker 拼 kb_url 用),不挖私有字段;经 getCfg 每次取最新配置(热生效)。 */
19
19
  get baseUrl(): string;
20
20
  private request;
21
21
  search(q: string): Promise<WikiSearchResult[]>;
@@ -11,7 +11,7 @@ export class WikiError extends Error {
11
11
  export class WikiVaultClient {
12
12
  getCfg;
13
13
  constructor(getCfg) { this.getCfg = getCfg; }
14
- /** P2:暴露 baseUrl getter(下游 WikiWorker 拼 kb_url 用),不挖私有字段;经 getCfg 每次取最新配置(热生效)。 */
14
+ /** 暴露 baseUrl getter(下游 WikiWorker 拼 kb_url 用),不挖私有字段;经 getCfg 每次取最新配置(热生效)。 */
15
15
  get baseUrl() { return this.getCfg().baseUrl; }
16
16
  async request(method, path, body) {
17
17
  const url = this.getCfg().baseUrl.replace(/\/$/, '') + path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joekytc/dsh-swarm",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A governed swarm of six specialist DSH agents (orchestrator, planner, knowledge-base bridge, developer and two reviewers) that turns a requirement into a strict phase pipeline with machine-verified delivery evidence, review-gated merges, a full audit-log event stream and a live kanban tab; design inspired by the Hermes Agent kanban",
5
5
  "license": "MIT",
6
6
  "author": "joekytc",