@joekytc/dsh-swarm 0.2.0 → 0.3.0

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 (103) hide show
  1. package/README.md +176 -139
  2. package/README.zh-CN.md +172 -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 +18 -1
  11. package/lib/config.js +14 -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.js +1 -1
  44. package/lib/domain/ocr-review.d.ts +47 -0
  45. package/lib/domain/ocr-review.js +105 -0
  46. package/lib/domain/permissions.js +5 -5
  47. package/lib/domain/projection.js +6 -6
  48. package/lib/domain/state-machine.js +1 -1
  49. package/lib/domain/task-parents.js +1 -1
  50. package/lib/domain/types.d.ts +4 -4
  51. package/lib/index.js +14 -9
  52. package/lib/roles/clean-fs-tools.js +3 -4
  53. package/lib/roles/preset-installer.js +3 -3
  54. package/lib/roles/skill-installer.js +1 -1
  55. package/lib/roles/toolsets.d.ts +60 -16
  56. package/lib/roles/toolsets.js +265 -47
  57. package/lib/roles/wiki-worker.js +3 -3
  58. package/lib/routes/kanban-http.d.ts +25 -2
  59. package/lib/routes/kanban-http.js +171 -11
  60. package/lib/routes/kanban-sse.d.ts +1 -1
  61. package/lib/routes/kanban-sse.js +1 -1
  62. package/lib/routes/planning-driver.d.ts +4 -2
  63. package/lib/routes/planning-driver.js +7 -3
  64. package/lib/routes/prefix-router.d.ts +3 -3
  65. package/lib/routes/prefix-router.js +3 -1
  66. package/lib/services/config-provider.d.ts +0 -4
  67. package/lib/services/config-provider.js +7 -9
  68. package/lib/services/im-delivery.d.ts +94 -0
  69. package/lib/services/im-delivery.js +263 -0
  70. package/lib/services/kanban-provider.d.ts +1 -1
  71. package/lib/services/kanban-provider.js +2 -2
  72. package/lib/services/ocr-cli.d.ts +35 -0
  73. package/lib/services/ocr-cli.js +128 -0
  74. package/lib/tools/kanban-tools.d.ts +2 -2
  75. package/lib/tools/kanban-tools.js +1 -1
  76. package/lib/tools/main-session-tools.d.ts +6 -2
  77. package/lib/tools/main-session-tools.js +58 -20
  78. package/lib/tools/ocr-review-tools.d.ts +11 -0
  79. package/lib/tools/ocr-review-tools.js +63 -0
  80. package/lib/tools/planning-tools.d.ts +5 -2
  81. package/lib/tools/planning-tools.js +11 -9
  82. package/lib/tools/wiki-tools.js +2 -2
  83. package/lib/wiki/kb-linkage.d.ts +1 -1
  84. package/lib/wiki/local-kb-client.js +3 -3
  85. package/lib/wiki/local-kb.d.ts +1 -1
  86. package/lib/wiki/local-kb.js +2 -2
  87. package/lib/wiki/page-path.d.ts +1 -1
  88. package/lib/wiki/page-path.js +6 -3
  89. package/lib/wiki/wiki-vault-client.d.ts +1 -1
  90. package/lib/wiki/wiki-vault-client.js +1 -1
  91. package/package.json +1 -1
  92. package/personas/kanban-d/agent.cordis.yml +5 -5
  93. package/personas/kanban-dt/agent.cordis.yml +23 -5
  94. package/personas/kanban-p/agent.cordis.yml +3 -3
  95. package/personas/kanban-pt/agent.cordis.yml +1 -1
  96. package/personas/kanban-v/agent.cordis.yml +3 -3
  97. package/personas/kanban-w/agent.cordis.yml +5 -5
  98. package/personas/persona-d.md +1 -1
  99. package/personas/persona-dt.md +12 -4
  100. package/personas/persona-p.md +1 -1
  101. package/personas/persona-v.md +2 -2
  102. package/personas/swarm/agent.cordis.yml +43 -0
  103. package/personas/swarm/preset.yml +5 -0
@@ -1,4 +1,8 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
1
4
  import { buildLlmCatalog } from '../services/llm-catalog.js';
5
+ import { INSTALL_GUIDANCE, OCR_PACKAGE, managedProviderReady, probeOcr, wireManagedProvider } from '../services/ocr-cli.js';
2
6
  import { serveKanbanEvents } from './kanban-sse.js';
3
7
  function json(res, status, body) {
4
8
  res.statusCode = status;
@@ -13,10 +17,95 @@ function readBody(req) {
13
17
  req.on('error', reject);
14
18
  });
15
19
  }
20
+ const defaultOcrInstaller = (onSpawn) => new Promise((resolve) => {
21
+ const child = execFile('npm', ['install', '-g', OCR_PACKAGE], { timeout: 600_000 }, (err, stdout, stderr) => {
22
+ if (err)
23
+ resolve({ ok: false, log: `${err.message}\n${String(stderr ?? '')}`.trim() });
24
+ else
25
+ resolve({ ok: true, log: String(stdout ?? '') });
26
+ });
27
+ onSpawn?.(child);
28
+ });
29
+ /** 模块级单飞安装任务态:同一插件进程同时只允许一个 npm 全局安装在跑。 */
30
+ let installJob = null;
31
+ /** 安装关键事件落盘(与 dispatcher.log 同文件同格式,写失败静默——日志只是观测面)。 */
32
+ function logOcrEvent(configProvider, msg) {
33
+ try {
34
+ const storageDir = configProvider.getEffective().storageDir.replace('$DSH_HOME', process.env.DSH_HOME ?? process.cwd());
35
+ writeFileSync(join(storageDir, 'dispatcher.log'), new Date().toISOString() + ' ' + msg + '\n', { flag: 'a' });
36
+ }
37
+ catch { /* 忽略写失败 */ }
38
+ }
39
+ async function runOcrInstall(deps, configProvider) {
40
+ const job = installJob;
41
+ if (!job)
42
+ return;
43
+ try {
44
+ const r = await (deps?.installer ?? defaultOcrInstaller)((child) => { job.child = child; });
45
+ if (!job.running)
46
+ return; // 已取消:cancelled 是终态,迟到的安装结果不覆盖
47
+ job.running = false;
48
+ job.log = r.log;
49
+ if (r.ok) {
50
+ job.result = 'ok';
51
+ const probe = await (deps?.probeFn ?? probeOcr)();
52
+ job.version = probe.version;
53
+ logOcrEvent(configProvider, '[ocr-install] ok' + (probe.version ? ' version=' + probe.version : ''));
54
+ }
55
+ else {
56
+ job.result = 'failed';
57
+ logOcrEvent(configProvider, '[ocr-install] failed');
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (!job.running)
62
+ return;
63
+ job.running = false;
64
+ job.result = 'failed';
65
+ job.log = String(err);
66
+ logOcrEvent(configProvider, '[ocr-install] failed: ' + String(err));
67
+ }
68
+ }
69
+ /** wire 降级文案:解析不到该提供方接入信息时,引导终端手动配置(不写半套 ocr 配置)。 */
70
+ const OCR_WIRE_DEGRADED = '未能从 dsh 解析该提供方的接入信息(baseUrl/apiKey)——请在终端手动执行 ocr config provider 完成托管配置;委托模式不受影响';
71
+ /**
72
+ * 从宿主设置解析所选提供方的接入信息(baseUrl/协议/apiKey)。
73
+ * 事实核查结论(实现期探查):llm 服务的公开 API 不暴露连接事实,但同进程可经
74
+ * settings 服务的 describe() 读到模型适配器的 provider profile——形如
75
+ * { providers: { <id>: { baseURL, api, apiKey | apiKeyEnv } } }(apiKeyEnv 指向进程环境变量名)。
76
+ * 命中 profile 但字段不全时跳过该 descriptor 继续尝试下一个(多 descriptor 场景勿误降级),
77
+ * 绝不回传半套配置;全部不合才返回 null 走降级。apiKey 只透传给 ocr config,不落日志。
78
+ */
79
+ function resolveDshProviderAccess(ctx, providerId) {
80
+ try {
81
+ const settings = ctx.get('settings');
82
+ const descriptors = settings?.describe?.() ?? [];
83
+ for (const d of descriptors) {
84
+ const providers = d.value?.providers;
85
+ const profile = providers?.[providerId];
86
+ if (!profile || typeof profile !== 'object')
87
+ continue;
88
+ const baseUrl = typeof profile.baseURL === 'string' ? profile.baseURL : '';
89
+ const api = typeof profile.api === 'string' ? profile.api : '';
90
+ const protocol = api.startsWith('openai') ? 'openai' : api.includes('anthropic') ? 'anthropic' : '';
91
+ let apiKey = typeof profile.apiKey === 'string' ? profile.apiKey : '';
92
+ if (!apiKey && typeof profile.apiKeyEnv === 'string')
93
+ apiKey = process.env[profile.apiKeyEnv] ?? '';
94
+ if (baseUrl && protocol && apiKey)
95
+ return { baseUrl, protocol, apiKey };
96
+ continue; // 字段不全:换下一个 descriptor,勿在此误降级
97
+ }
98
+ return null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
16
104
  /** 看板 HTTP 桥(Web GUI 浏览器半消费):GET /kanban/board 读快照;POST /kanban/action 执行状态操作;
17
- * GET/PUT /kanban/config + POST /kanban/config/reset 配置读写;GET /kanban/llm-catalog 模型目录。
105
+ * GET/PUT /kanban/config 配置读写(无 reset:各用户模型配置不同,无公共默认值);GET /kanban/llm-catalog 模型目录;
106
+ * GET /kanban/ocr/status + POST /kanban/ocr/install(/cancel) + GET install/state + POST /kanban/ocr/wire ocr 评审引擎运维。
18
107
  * 仅在 webServer 服务存在时挂载(CLI/headless/测试裸 Context 不挂)。 */
19
- export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
108
+ export function registerKanbanHttp(ctx, provider, configProvider, llm, config, ocrDeps) {
20
109
  // 可选服务:经 ctx.get 读取(cordis 4 直接属性读取需 inject;get 不需要)
21
110
  const webServer = ctx.get('webServer');
22
111
  if (!webServer)
@@ -46,7 +135,7 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
46
135
  }
47
136
  if (req.method === 'POST' && req.url?.startsWith('/kanban/action')) {
48
137
  const body = JSON.parse((await readBody(req)) || '{}');
49
- // D23:confirm-audit 是链级 action(无 taskId),提前分流处理
138
+ // confirm-audit 是链级 action(无 taskId),提前分流处理
50
139
  if (body.type === 'confirm-audit') {
51
140
  const chainId = String(body.chainId ?? '').trim();
52
141
  if (!chainId) {
@@ -71,7 +160,7 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
71
160
  json(res, 200, { ok: true });
72
161
  return;
73
162
  }
74
- // T7:rename 是链级或任务级 action(chainId 或 taskId 二选一),在 taskId 守卫前分流
163
+ // rename 是链级或任务级 action(chainId 或 taskId 二选一),在 taskId 守卫前分流
75
164
  if (body.type === 'rename') {
76
165
  const title = String(body.title ?? '').trim();
77
166
  if (!title) {
@@ -108,7 +197,7 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
108
197
  await provider.service.unblockTask(t, 'human');
109
198
  break;
110
199
  case 'retry': {
111
- // T32 fix:retry 走 runner(failed→claim→spawn/resume),而非只 claim 造成 running 悬挂
200
+ // retry 走 runner(failed→claim→spawn/resume),而非只 claim 造成 running 悬挂
112
201
  const state = await provider.service.snapshot();
113
202
  const task = state.tasks.get(t);
114
203
  if (!task) {
@@ -154,21 +243,25 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
154
243
  json(res, 200, { ok: true });
155
244
  return;
156
245
  }
157
- // 配置读写:reset 是 POST,与 GET/PUT config 经 method 区分(reset 分支前置,防前缀遮蔽)
158
- if (req.method === 'POST' && req.url?.startsWith('/kanban/config/reset')) {
159
- json(res, 200, configProvider.reset());
160
- return;
161
- }
162
246
  if (req.method === 'GET' && req.url?.startsWith('/kanban/config')) {
163
247
  json(res, 200, configProvider.snapshot());
164
248
  return;
165
249
  }
166
250
  if (req.method === 'PUT' && req.url?.startsWith('/kanban/config')) {
167
251
  const raw = JSON.parse((await readBody(req)) || '{}');
168
- // 归一化缺省字段,保证缺字段走 400 校验失败而非 500
252
+ // 归一化缺省字段,保证缺字段走 400 校验失败而非 500
253
+ // reviewEngine 缺省时回退当前 effective(再兜底默认):旧客户端 bundle 不带该字段时保存不清空既有评审引擎配置。
254
+ const fb = configProvider.getEffective().reviewEngine;
169
255
  const snapshot = {
170
256
  wikiVault: { baseUrl: raw.wikiVault?.baseUrl ?? '', pagePrefix: raw.wikiVault?.pagePrefix ?? '' },
171
257
  roles: { models: raw.roles?.models ?? {} },
258
+ reviewEngine: {
259
+ mode: raw.reviewEngine?.mode ?? fb?.mode ?? 'delegate',
260
+ managed: {
261
+ provider: raw.reviewEngine?.managed?.provider ?? fb?.managed?.provider ?? '',
262
+ model: raw.reviewEngine?.managed?.model ?? fb?.managed?.model ?? '',
263
+ },
264
+ },
172
265
  };
173
266
  const r = configProvider.applyOverride(snapshot);
174
267
  if (!r.ok) {
@@ -182,6 +275,73 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config) {
182
275
  json(res, 200, await buildLlmCatalog(llm));
183
276
  return;
184
277
  }
278
+ // ocr 运维面:cancel 前置于 install(POST 前缀匹配防遮蔽),install/state 与 install 以 method 区分
279
+ if (req.method === 'POST' && req.url?.startsWith('/kanban/ocr/install/cancel')) {
280
+ if (!installJob?.running) {
281
+ json(res, 200, { ok: false, error: 'no-install-running' });
282
+ return;
283
+ }
284
+ installJob.child?.kill('SIGTERM');
285
+ installJob.running = false;
286
+ installJob.result = 'cancelled';
287
+ logOcrEvent(configProvider, '[ocr-install] cancelled');
288
+ json(res, 200, { ok: true });
289
+ return;
290
+ }
291
+ if (req.method === 'POST' && req.url?.startsWith('/kanban/ocr/install')) {
292
+ if (installJob?.running) {
293
+ json(res, 409, { error: 'install-in-progress' });
294
+ return;
295
+ }
296
+ installJob = { id: 'ocr-install-' + Date.now(), running: true, child: null, log: '' };
297
+ logOcrEvent(configProvider, '[ocr-install] start');
298
+ void runOcrInstall(ocrDeps, configProvider);
299
+ json(res, 200, { ok: true, id: installJob.id });
300
+ return;
301
+ }
302
+ if (req.method === 'GET' && req.url?.startsWith('/kanban/ocr/install/state')) {
303
+ json(res, 200, installJob
304
+ ? { running: installJob.running, result: installJob.result, version: installJob.version, log: installJob.log }
305
+ : { running: false, log: '' });
306
+ return;
307
+ }
308
+ if (req.method === 'GET' && req.url?.startsWith('/kanban/ocr/status')) {
309
+ const probe = await (ocrDeps?.probeFn ?? probeOcr)();
310
+ json(res, 200, {
311
+ installed: probe.installed,
312
+ version: probe.version,
313
+ mode: configProvider.getEffective().reviewEngine.mode,
314
+ // kbMode 随状态端点透出:前端据此显示 local 模式下独立评审不落 wiki 的提示
315
+ kbMode: configProvider.mode,
316
+ managedReady: ocrDeps?.managedReadyFn ? ocrDeps.managedReadyFn() : managedProviderReady(),
317
+ });
318
+ return;
319
+ }
320
+ if (req.method === 'POST' && req.url?.startsWith('/kanban/ocr/wire')) {
321
+ const body = JSON.parse((await readBody(req)) || '{}');
322
+ const providerId = String(body.provider ?? '').trim();
323
+ const model = String(body.model ?? '').trim();
324
+ if (!providerId || !model) {
325
+ json(res, 400, { error: 'provider and model required' });
326
+ return;
327
+ }
328
+ const probe = await (ocrDeps?.probeFn ?? probeOcr)();
329
+ if (!probe.installed) {
330
+ json(res, 200, { ok: false, log: INSTALL_GUIDANCE });
331
+ return;
332
+ }
333
+ const access = resolveDshProviderAccess(ctx, providerId);
334
+ if (!access) {
335
+ json(res, 200, { ok: false, log: OCR_WIRE_DEGRADED });
336
+ return;
337
+ }
338
+ const r = ocrDeps?.wirer
339
+ ? await ocrDeps.wirer({ provider: providerId, model })
340
+ : await wireManagedProvider({ baseUrl: access.baseUrl, protocol: access.protocol, apiKey: access.apiKey, model });
341
+ logOcrEvent(configProvider, '[ocr-wire] ' + (r.ok ? 'ok' : 'fail'));
342
+ json(res, 200, { ok: r.ok, log: r.log });
343
+ return;
344
+ }
185
345
  json(res, 404, { error: 'not found' });
186
346
  }
187
347
  catch (err) {
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { KanbanService } from '../domain/kanban-service.js';
3
3
  import type { KanbanEvent } from '../domain/types.js';
4
4
  export declare function writeSseEvent(res: ServerResponse, event: KanbanEvent): void;
5
- /** T23:SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
5
+ /** SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
6
6
  export declare function serveKanbanEvents(req: IncomingMessage, res: ServerResponse, service: Pick<KanbanService, 'subscribe' | 'eventsSince'>, options: {
7
7
  heartbeatMs: number;
8
8
  }): Promise<void>;
@@ -10,7 +10,7 @@ function lastSeen(req) {
10
10
  const header = Number(headerValue ?? -1);
11
11
  return Math.max(Number.isFinite(query) ? query : -1, Number.isFinite(header) ? header : -1);
12
12
  }
13
- /** T23:SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
13
+ /** SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
14
14
  export async function serveKanbanEvents(req, res, service, options) {
15
15
  res.statusCode = 200;
16
16
  res.setHeader('content-type', 'text/event-stream; charset=utf-8');
@@ -1,10 +1,12 @@
1
1
  import { KanbanService } from '../domain/kanban-service.js';
2
2
  import type { SpecCard, SpecCardAttachment } from '../domain/types.js';
3
3
  import { type PrefixRoutes } from '../config.js';
4
- /** 阶段 0 规划引导:命令串从 config 派生(决策12),/openspec: 改名时文案自动跟随。
4
+ /** 阶段 0 规划引导:命令串从 config 派生,/openspec: 改名时文案自动跟随。
5
5
  * 提问节奏与 skills/grill-me/SKILL.md 对齐:官方 frontier 分轮制(整批问 + 推荐答案 + 答复后解锁下一轮);
6
6
  * 增强项:人话硬规则 + 过程风险登记;收敛产物仍为 planning_checklist_save(KB 清单是建链数据源)。 */
7
- export declare function buildPlanningGuidance(routes: PrefixRoutes): string;
7
+ export declare function buildPlanningGuidance(routes: PrefixRoutes, opts?: {
8
+ swarm?: boolean;
9
+ }): string;
8
10
  export declare function validateSpecCardForApproval(card: SpecCard): string[];
9
11
  export declare function buildPlanningContext(chainId: string, card: SpecCard, attachments: SpecCardAttachment[], routes?: PrefixRoutes): string;
10
12
  export declare function approveIfReady(message: string, service: KanbanService, cfg: PrefixRoutes, chainId: string, specCardId: string): Promise<{
@@ -1,10 +1,14 @@
1
1
  import { KanbanService } from '../domain/kanban-service.js';
2
2
  import { parsePrefix } from './prefix-router.js';
3
3
  import { DEFAULT_PREFIX_ROUTES } from '../config.js';
4
- /** 阶段 0 规划引导:命令串从 config 派生(决策12),/openspec: 改名时文案自动跟随。
4
+ /** 阶段 0 规划引导:命令串从 config 派生,/openspec: 改名时文案自动跟随。
5
5
  * 提问节奏与 skills/grill-me/SKILL.md 对齐:官方 frontier 分轮制(整批问 + 推荐答案 + 答复后解锁下一轮);
6
6
  * 增强项:人话硬规则 + 过程风险登记;收敛产物仍为 planning_checklist_save(KB 清单是建链数据源)。 */
7
- export function buildPlanningGuidance(routes) {
7
+ export function buildPlanningGuidance(routes, opts) {
8
+ // 第 6 条按形态分叉:前缀 = 教学旧命令收尾(逐字节不变);swarm = 确认闸语义(不提旧命令,与 KANBAN_HANDOFF_RULE swarm 确认闸对齐)。
9
+ const finish = opts?.swarm
10
+ ? `6. 收尾(确认闸):清单落库后向用户征求确认;仅当用户回复含明确肯定语义(确认/开干/开跑/开始/go 等)才调 kanban_route{intent:'openspec'} 建链开跑;模糊回复视为未确认,继续澄清。`
11
+ : `6. 收尾:提醒用户以 ${routes.openspec} 确认执行结束规划阶段——${routes.openspec} 会从清单建链并自动串行执行。`;
8
12
  return `
9
13
  # 阶段 0 规划对话(v4:grill-me frontier 分轮,与 skills/grill-me 对齐)
10
14
  1. 需求澄清(grill-me,frontier 分轮):把计划映射为设计决策树——每轮只问当前前提已就绪的问题(frontier),整批问出:编号 + 每问附推荐答案;等用户全部答复后重算 frontier(已定决定向外推进、解锁依赖它的问题)再发下一轮,本轮答不了的依赖问题留到后续轮。先澄清目的、约束、成功标准,逐支拷问直至用户"没有任何疑问"。
@@ -12,7 +16,7 @@ export function buildPlanningGuidance(routes) {
12
16
  3. 仓库事实(planning_prefetch):事实自查是规划者的职责——调只读子代理采集目标仓库/资料/知识库事实(本地路径/分支/目标文件基线/既有实现),不凭空假设、不拿可自查的问题问用户;只把真正的决定留给用户。
13
17
  4. 风险登记:每轮答复中若发现真实风险或非阻塞关注点,随手记一行(描述 + 来源决定 + 缓解思路),收敛时并入澄清清单;没有就不写,禁止硬凑。
14
18
  5. 收敛(planning_checklist_save):把结论写成结构化需求澄清清单(spec 六段 + manifest repo.files + 澄清问答 + 疑问点 + 风险点(若有))存入 KB(KB 不可达自动兜底临时目录)。
15
- 6. 收尾:提醒用户以 ${routes.openspec} 确认执行结束规划阶段——${routes.openspec} 会从清单建链并自动串行执行。
19
+ ${finish}
16
20
  护栏:规划期只读仓库,禁止任何 git/源码写入;只写 KB 与临时目录。
17
21
  `;
18
22
  }
@@ -8,7 +8,7 @@ export declare const OPENSPEC_FIRST_CARD: {
8
8
  pollIntervalMs: number;
9
9
  };
10
10
  export interface PrefixRouteResult {
11
- kind: 'plan' | 'openspec' | 'learning' | 'none';
11
+ kind: 'plan' | 'openspec' | 'learning' | 'send' | 'none';
12
12
  chainId?: string;
13
13
  specCardId?: string;
14
14
  rest: string;
@@ -33,12 +33,12 @@ export interface OpenspecPlanningInput {
33
33
  workspaceDir: string | null;
34
34
  checklist: PlanningChecklist;
35
35
  checklistRef: string;
36
- /** T7:/plan: rest 原始需求描述;null=无 /plan: 捕获(回退 checklist.problem 首句/未命名需求)。 */
36
+ /** /plan: rest 原始需求描述;null=无 /plan: 捕获(回退 checklist.problem 首句/未命名需求)。 */
37
37
  requirementName?: string | null;
38
38
  }
39
39
  /** v2:/openspec: 建链——从清单机械映射规格卡六段 → 挂 file-prefetch(仓库 localPath)+kb(清单页) → 批准 → executing。 */
40
40
  export declare function handleOpenspecRoute(message: string, service: KanbanService, cfg: PrefixRoutes, planning: OpenspecPlanningInput, ownerSessionId: string): Promise<PrefixRouteResult>;
41
- /** /learning 零副作用引导文案:命令串从 config 派生(决策12),歧义/未找到时注入主 agent。 */
41
+ /** /learning 零副作用引导文案:命令串从 config 派生,歧义/未找到时注入主 agent。 */
42
42
  export declare function buildLearningGuidance(routes: PrefixRoutes): string;
43
43
  /** v2:/learning 零副作用——不建链建卡,仅机械提取证据包供主 agent 蒸馏。歧义返回候选列表,链不存在返回错误文本(不 throw)。 */
44
44
  export declare function handleLearningRoute(message: string, service: KanbanService, cfg: PrefixRoutes, _ownerSessionId: string): Promise<PrefixRouteResult>;
@@ -11,6 +11,8 @@ export function parsePrefix(message, cfg) {
11
11
  return { kind: 'openspec', rest: trimmed.slice(cfg.openspec.length).trim() };
12
12
  if (trimmed.startsWith(cfg.learning))
13
13
  return { kind: 'learning', rest: trimmed.slice(cfg.learning.length).trim() };
14
+ if (trimmed.startsWith(cfg.send))
15
+ return { kind: 'send', rest: trimmed.slice(cfg.send.length).trim() };
14
16
  return { kind: 'none', rest: trimmed };
15
17
  }
16
18
  /** v2:/plan: 零副作用——不建链/规格卡/任务卡,仅返回路由结果(workspaceDir/sessionId 由 main-session-tools 捕获)。 */
@@ -64,7 +66,7 @@ async function waitFirstCard(service, chainId) {
64
66
  await new Promise((r) => setTimeout(r, Math.min(OPENSPEC_FIRST_CARD.pollIntervalMs, Math.max(1, deadline - Date.now()))));
65
67
  }
66
68
  }
67
- /** /learning 零副作用引导文案:命令串从 config 派生(决策12),歧义/未找到时注入主 agent。 */
69
+ /** /learning 零副作用引导文案:命令串从 config 派生,歧义/未找到时注入主 agent。 */
68
70
  export function buildLearningGuidance(routes) {
69
71
  return [
70
72
  '## 经验蒸馏指令(' + routes.learning + ')',
@@ -31,10 +31,6 @@ export declare class ConfigProvider extends Service {
31
31
  sources: SourceMap;
32
32
  };
33
33
  applyOverride(snapshot: EditableSnapshot): ApplyResult;
34
- reset(): {
35
- effective: EditableSnapshot;
36
- sources: SourceMap;
37
- };
38
34
  private readOverride;
39
35
  private writeOverride;
40
36
  private appendAudit;
@@ -20,7 +20,7 @@ export class ConfigProvider extends Service {
20
20
  }
21
21
  getEffective() { return this.effective; }
22
22
  getSources() { return this.sources; }
23
- // mode 派生自 effective.wikiVault.baseUrl;运行期改 baseUrl 需重启 dsh 使客户端注册面同步(D11 不可热切换):
23
+ // mode 派生自 effective.wikiVault.baseUrl;运行期改 baseUrl 需重启 dsh 使客户端注册面同步(不可热切换):
24
24
  // 注册期冻结(dispatcher、main-session-tools 客户端构造)与运行期活读(agent-runner guard、kanban-provider 交付闸)
25
25
  // 分属两类读取点,中途 applyOverride 改 baseUrl 会产生混合态(后果 fail-closed 可见,不静默)。
26
26
  get mode() { return (this.effective.wikiVault?.baseUrl ?? '').trim() ? 'remote' : 'local'; }
@@ -41,14 +41,6 @@ export class ConfigProvider extends Service {
41
41
  this.sources = computeSources(next);
42
42
  return { ok: true, effective: projectEditable(this.effective), sources: this.sources, changed };
43
43
  }
44
- reset() {
45
- const next = {};
46
- this.writeOverride(next);
47
- this.override = next;
48
- this.effective = mergeConfig(this.baseline, next);
49
- this.sources = computeSources(next);
50
- return { effective: projectEditable(this.effective), sources: this.sources };
51
- }
52
44
  readOverride() {
53
45
  try {
54
46
  if (!existsSync(this.overrideFile))
@@ -89,6 +81,12 @@ export class ConfigProvider extends Service {
89
81
  keys.add(`roles.models.${role}.${f}`);
90
82
  }
91
83
  }
84
+ if (prev.reviewEngine?.mode !== next.reviewEngine?.mode)
85
+ keys.add('reviewEngine.mode');
86
+ for (const f of ['provider', 'model']) {
87
+ if (prev.reviewEngine?.managed?.[f] !== next.reviewEngine?.managed?.[f])
88
+ keys.add('reviewEngine.managed.' + f);
89
+ }
92
90
  return [...keys];
93
91
  }
94
92
  }
@@ -0,0 +1,94 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { KanbanService } from '../domain/kanban-service.js';
3
+ import type { BoardState } from '../domain/types.js';
4
+ import type { ConfigProvider } from './config-provider.js';
5
+ /** dsh-im 宿主服务结构接口(形状以 @xmanrui/dsh-im PROACTIVE_DELIVERY.md 为准,运行时守卫校验)。
6
+ * listTargets 证据(2026-09-07):PROACTIVE_DELIVERY.md:176 `const targets = await ctx.dshIm.listTargets(botId);
7
+ * // [{ targetId, name?, kind, route }, ...]`;宿主 lib/index.js 实现为
8
+ * `listTargets: async Y => (await H.listTargets(Y)).targets`(同 Host 服务返回裸数组)。
9
+ * `{ botId, channel, targets }` 是 Connection RPC `target.list` 的信封形状,与同 Host 服务不同。 */
10
+ export interface DshImLike {
11
+ send(botId: string, targetId: string, text: string, opts?: {
12
+ signal?: AbortSignal;
13
+ }): Promise<{
14
+ sent?: boolean;
15
+ }>;
16
+ listBots(): Promise<Array<{
17
+ botId: string;
18
+ channel: string;
19
+ }>>;
20
+ listTargets(botId: string): Promise<Array<{
21
+ targetId: string;
22
+ name?: string;
23
+ kind: string;
24
+ route: Record<string, string>;
25
+ }>>;
26
+ }
27
+ export interface ImDeliveryOptions {
28
+ /** 缺省写 storageDir/dispatcher.log([im-delivery] 前缀;评审决议落盘位置)。 */
29
+ log?: (msg: string) => void;
30
+ /** 重试退避间隔(ms);测试传 [0,0,0]。 */
31
+ retryDelaysMs?: number[];
32
+ /** 手动投递路径(/sms):失败仅 dispatcher.log 留痕,不写 chain/im-delivery-failed 链事件(用户同步可见错误)。 */
33
+ manual?: boolean;
34
+ }
35
+ export declare function isDshImLike(svc: unknown): svc is DshImLike;
36
+ /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存群目标;
37
+ * 发现异常返回 error(调用方留痕不投,fail-closed——投错群比不投更糟)。 */
38
+ export declare function resolveTarget(im: DshImLike, cfg: {
39
+ botId: string;
40
+ targetId: string;
41
+ }): Promise<{
42
+ botId: string;
43
+ targetId: string;
44
+ } | {
45
+ error: string;
46
+ }>;
47
+ /** 发送 + 退避重试(仅可重试错误码;{sent:true} 即停——dsh-im 无幂等,成功后绝不重发)。 */
48
+ export declare function sendWithRetry(im: DshImLike, botId: string, targetId: string, text: string, delays: number[], log: (m: string) => void): Promise<{
49
+ ok: true;
50
+ } | {
51
+ ok: false;
52
+ error: string;
53
+ }>;
54
+ /** 发送器工厂:resolveDshIm → resolveTarget(fail-closed)→ sendWithRetry → 留痕。
55
+ * auto 路径(wireImDelivery)失败额外写 chain/im-delivery-failed 链事件;
56
+ * manual 路径(/sms,opts.manual)仅 dispatcher.log 留痕,错误同步返回给调用方。 */
57
+ export declare function createSender(ctx: Context, kanban: KanbanService, configProvider: ConfigProvider, opts?: ImDeliveryOptions): (chainId: string, text: string) => Promise<{
58
+ ok: true;
59
+ botId: string;
60
+ targetId: string;
61
+ } | {
62
+ ok: false;
63
+ error: string;
64
+ }>;
65
+ export type ReportVariant = 'completion' | 'blocked';
66
+ /** /sms 链解析(纯函数)。空 query:completion=最近满足 W3 完成判据的链(最后完成事件 at 最大);
67
+ * blocked=最近阻塞的链(chain/blocked 事件 at 最大)。显式 query:全量链精确/后缀解析后再验判据。 */
68
+ export declare function resolveReportChainId(state: BoardState, variant: ReportVariant, query: string): {
69
+ ok: true;
70
+ chainId: string;
71
+ } | {
72
+ ok: false;
73
+ error: string;
74
+ candidates?: Array<{
75
+ chainId: string;
76
+ title: string;
77
+ }>;
78
+ };
79
+ /** /sms 手动投递:解析链 → 领域函数渲染正文(红线:正文只出自 buildCompletionMessage/buildBlockMessage,
80
+ * 绝不返回给模型)→ createSender 发送。不受 imDelivery.enabled 门控(显式人工调用即意图),
81
+ * 但仍要求 dshIm 服务在位且形状合法、目标可解析(同 auto 路径 fail-closed 规则)。 */
82
+ export declare function sendChainReport(ctx: Context, kanban: KanbanService, configProvider: ConfigProvider, opts: ImDeliveryOptions, variant: ReportVariant, query: string): Promise<{
83
+ ok: true;
84
+ chainId: string;
85
+ botId: string;
86
+ targetId: string;
87
+ } | {
88
+ ok: false;
89
+ error: string;
90
+ guidance?: string;
91
+ }>;
92
+ /** 接线:订阅看板事件多播通道(不动 setOnTaskCompleted 单消费者钩子)。
93
+ * listener 同步返回,投递全程 fire-and-forget 自兜异常(publish 在 emit 队列内同步调用,不得拖慢落盘)。 */
94
+ export declare function wireImDelivery(ctx: Context, kanban: KanbanService, configProvider: ConfigProvider, opts?: ImDeliveryOptions): () => void;