@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
@@ -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 } 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 层——最终防线是链级看门狗(防线①)。 */
@@ -114,14 +124,14 @@ export function buildSpawnPrefetch(ctx) {
114
124
  export function registerMainSessionTools(ctx, configProvider) {
115
125
  const registry = ctx.get('tools');
116
126
  if (!registry)
117
- return; // 测试裸 Context 无 tools 服务(P1-9 无 inject 依赖),跳过注册
127
+ return; // 测试裸 Context 无 tools 服务,跳过注册
118
128
  const provider = ctx.get('kanban');
119
129
  if (!provider)
120
130
  return;
121
131
  const service = provider.service;
122
132
  // 生产 wiring(src/index.ts)仅保证 tools+kanban 可用,无 wiki 服务 → 经 configProvider 自建
123
133
  //(与 dispatcher 构造同源,getEffective() 调用时热读取 baseUrl/pagePrefix);测试经 ctx.get('wiki') 注入 mock 客户端。
124
- // D3 双模式:local 模式走 LocalWikiClient 本地检索(不发 HTTP),remote 用配置的 WikiVaultClient。
134
+ // 双模式:local 模式走 LocalWikiClient 本地检索(不发 HTTP),remote 用配置的 WikiVaultClient。
125
135
  const kbMode = configProvider.mode;
126
136
  const wiki = kbMode === 'local'
127
137
  ? new LocalWikiClient(ensureLocalKbRoot())
@@ -141,15 +151,16 @@ export function registerMainSessionTools(ctx, configProvider) {
141
151
  }
142
152
  // planning 工具(清单落库 + 只读预取)——spawnPrefetch 由模块级 buildSpawnPrefetch 提供(可单测)
143
153
  for (const tool of buildPlanningTools({
144
- service, wiki: wiki, // local 模式为 LocalWikiClient(write/read/search 同面,D3 双模式客户端)
154
+ service, wiki: wiki, // local 模式为 LocalWikiClient(write/read/search 同面,双模式客户端)
145
155
  getCaller: caller,
146
156
  spawnPrefetch: buildSpawnPrefetch(ctx),
147
157
  tempDir: () => `${tmpdir()}/dsh-swarm-checklists`, // KB 不可达时的临时兜底,放系统临时目录(不落插件源码/核心存储目录)
148
158
  pagePrefix: configProvider.getEffective().wikiVault?.pagePrefix ?? 'projects/', // 生成的清单页路径保持在该客户端配置的命名空间内(避免 kb-rejected)
149
- kbMode: configProvider.mode, // D3 双模式:local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/)
159
+ kbMode: configProvider.mode, // 双模式:local 时 checklist/learning 落本地库命名空间(wiki/queries/checklists/、wiki/synthesis/learnings/)
150
160
  prefixRoutes: configProvider.getEffective().prefixRoutes,
151
161
  memoryEnabled: configProvider.getEffective().memory?.enabled ?? true,
152
162
  resolveWorkspaceDir: () => planningBySession.get('session_main')?.workspaceDir ?? null,
163
+ flowMode: () => planningBySession.get('session_main')?.mode ?? null,
153
164
  ownerSessionId: 'session_main',
154
165
  onChecklistSaved({ ref, source, checklist }) {
155
166
  const cur = planningBySession.get('session_main') ?? { workspaceDir: null, sessionId: 'session_main', checklist: null, checklistRef: null, checklistSource: null, requirementName: null };
@@ -157,23 +168,34 @@ export function registerMainSessionTools(ctx, configProvider) {
157
168
  },
158
169
  }))
159
170
  registry.register(tool);
160
- // kanban_route:/plan: 捕获规划上下文;/openspec: 用清单建链(前缀路由注册时快照,用于工具描述)
161
- const { plan, openspec, learning } = configProvider.getEffective().prefixRoutes;
171
+ // kanban_route:/plan: 捕获规划上下文;/openspec: 用清单建链;/sms 手动投递链报告(前缀路由注册时快照,用于工具描述)
172
+ const { plan, openspec, learning, send } = configProvider.getEffective().prefixRoutes;
162
173
  registry.register(defineTool({
163
174
  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 } },
175
+ 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 → 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).`,
176
+ 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
177
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
167
178
  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');
179
+ // intent 路径(蜂群模式):handler 内部会重 parsePrefix(message),无前缀 message
180
+ // 会判成 none —— 因此 intent 命中时合成「前缀 + rest」消息再进 handler;handler 零改动、
181
+ // 前缀路径零感知。intent 优先于 message 前缀;非法 intent 回退 parsePrefix(none 兜底)。
182
+ // 简报矛盾修正:intent 命中时 message 若自带已知前缀(模型契约
183
+ // 违例,description 明文 raw words no prefix),原样拼接会使 rest 带旧前缀 → handler 内必
184
+ // chain-not-found → brief 断言不可能绿;故丢弃载荷只保留 intent 裸前缀
185
+ //(learning 裸 = 最近链,与既有 '/learning' 语义一致)。raw words 正常路径与简报逐字节一致。
186
+ const routes = configProvider.getEffective().prefixRoutes;
187
+ const INTENTS = ['plan', 'openspec', 'learning', 'send'];
188
+ const intent = INTENTS.includes(args.intent ?? '') ? args.intent : null;
189
+ const trimmed = args.message.trim();
190
+ const routeMessage = intent
191
+ ? `${routes[intent]}${trimmed && !INTENTS.some((k) => trimmed.startsWith(routes[k])) ? ' ' + trimmed : ''}`
192
+ : args.message;
193
+ const plan = await handlePlanRoute(routeMessage, service, routes, 'session_main');
172
194
  if (plan.kind === 'plan') {
173
195
  const headerCwd = exec?.agent?.session?.header?.cwd ?? null;
174
196
  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);
197
+ planningBySession.set('session_main', { workspaceDir, sessionId: 'session_main', checklist: null, checklistRef: null, checklistSource: null, requirementName: plan.rest, mode: intent ? 'swarm' : 'prefix' });
198
+ let guidance = buildPlanningGuidance(configProvider.getEffective().prefixRoutes, { swarm: intent === 'plan' }) + KANBAN_HANDOFF_RULE(configProvider.getEffective().prefixRoutes, { swarm: intent === 'plan' });
177
199
  if ((configProvider.getEffective().memory?.enabled ?? true) && workspaceDir) {
178
200
  const idx = await recallMemoryIndex(wiki, {
179
201
  requirementName: plan.rest || null,
@@ -187,11 +209,27 @@ export function registerMainSessionTools(ctx, configProvider) {
187
209
  return { kind: 'plan', guidance };
188
210
  }
189
211
  if (plan.kind === 'learning') {
190
- const r = await handleLearningRoute(args.message, service, configProvider.getEffective().prefixRoutes, 'session_main');
212
+ const r = await handleLearningRoute(routeMessage, service, configProvider.getEffective().prefixRoutes, 'session_main');
191
213
  if (r.error)
192
214
  return { kind: 'learning', error: r.error, guidance: r.guidance };
193
215
  return { kind: 'learning', chainId: r.chainId, brief: r.brief, guidance: r.guidance };
194
216
  }
217
+ if (plan.kind === 'send') {
218
+ // /sms 手动投递:rest 'blocked [chainId]' → 阻塞通知;其余 → 完成汇报(query = rest)。
219
+ // 红线:消息正文由 sendChainReport 内的领域函数渲染并发送,本分支只返回投递状态(绝不含正文)。
220
+ const rest = plan.rest;
221
+ const variant = rest.startsWith('blocked') ? 'blocked' : 'completion';
222
+ const query = variant === 'blocked' ? rest.slice('blocked'.length).trim() : rest;
223
+ const r = await sendChainReport(ctx, service, configProvider, { retryDelaysMs: [] }, variant, query);
224
+ if (r.ok) {
225
+ const noun = variant === 'blocked' ? '阻塞通知' : '完成汇报';
226
+ return {
227
+ kind: 'send', chainId: r.chainId, botId: r.botId, targetId: r.targetId,
228
+ guidance: `${noun}已投递企微群(/sms 手动触发)。请仅向用户确认投递成功与目标群,勿复述消息正文。`,
229
+ };
230
+ }
231
+ return { kind: 'send', error: r.error, guidance: r.guidance ?? '请将 error 字段原样转告用户,勿复述消息正文。' };
232
+ }
195
233
  if (plan.kind === 'none')
196
234
  return { kind: 'none' };
197
235
  // 路由1(内存):planningBySession 命中 → 直接建链
@@ -218,14 +256,14 @@ export function registerMainSessionTools(ctx, configProvider) {
218
256
  };
219
257
  }
220
258
  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');
259
+ const r = await handleOpenspecRoute(routeMessage, service, configProvider.getEffective().prefixRoutes, input, 'session_main');
222
260
  // 护栏拦截(如 workspace-unknown):透传失败原因与恢复指引,不得伪装成建链成功
223
261
  if (r.approved === false || !r.chainId || !r.specCardId) {
224
262
  return { kind: 'openspec', approved: false, reason: r.reason ?? 'unknown', guidance: r.guidance };
225
263
  }
226
264
  return {
227
265
  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 }),
266
+ 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
267
  };
230
268
  }
231
269
  // 路由2(知识库):内存丢失(插件重启)→ 搜 KB 候选清单页供 LLM 读页重建;搜不到/不可达 → 两条路皆空
@@ -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[];
@@ -9,11 +9,13 @@ 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
  }),
@@ -123,7 +125,7 @@ export function buildPlanningTools(deps) {
123
125
  const entry = args.learning;
124
126
  let prefix;
125
127
  if (local) {
126
- // local(D5):scope=chain 直接挂 chainId;scope=project 挂 repoSlug(需 workspaceDir)
128
+ // localscope=chain 直接挂 chainId;scope=project 挂 repoSlug(需 workspaceDir)
127
129
  if (args.scope === 'chain') {
128
130
  prefix = `${LOCAL_LEARNING_BASE}${args.chainId}/`;
129
131
  }
@@ -134,7 +136,7 @@ export function buildPlanningTools(deps) {
134
136
  }
135
137
  }
136
138
  else {
137
- // remote(workspace 计划 T3):两 scope 均要求 chain.workspaceDir,统一挂 projects/<repoSlug>/
139
+ // remote:两 scope 均要求 chain.workspaceDir,统一挂 projects/<repoSlug>/
138
140
  if (!chain.workspaceDir)
139
141
  throw new Error('learning save requires chain.workspaceDir (target repo) — chain has none');
140
142
  const wsRoot = `${pagePrefix}${buildRepoSlug(chain.workspaceDir)}/`;
@@ -171,8 +173,8 @@ export function buildPlanningTools(deps) {
171
173
  if (hasPath === hasQuery)
172
174
  throw new Error('provide exactly one of path|query');
173
175
  if (hasPath) {
174
- // path 白名单按 KB 双模式分支(D3/D9):remote → projects/** 命名空间;local → wiki/**(
175
- // Task 11 后 local 的 checklist/learning 全落 wiki/ 前缀,用 projects/ 白名单会把 local 读腿全拒)
176
+ // path 白名单按 KB 双模式分支:remote → projects/** 命名空间;local → wiki/**(
177
+ // local 的 checklist/learning 全落 wiki/ 前缀,用 projects/ 白名单会把 local 读腿全拒)
176
178
  if (local)
177
179
  assertLocalKbPagePath(args.path);
178
180
  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.0",
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",
@@ -1,12 +1,12 @@
1
- # kanban-d — D(fullstack-dev) 裁剪 agent preset(D22,2026-08-15)。
1
+ # kanban-d — D(fullstack-dev) 裁剪 agent preset(2026-08-15)。
2
2
  #
3
- # 官方 agent-presets 无 include/patch 语义,本文件为手写最小组合(取舍见 Task 33 提交)。
3
+ # 官方 agent-presets 无 include/patch 语义,本文件为手写最小组合(取舍见 git 提交历史)。
4
4
  # 保留完整开发能力:persona/instructions + bash/pwsh + fs + fs-search + jobs + skill
5
5
  # + compaction + ask-user + todo + run_code(tool-presentation)。
6
6
  # 0.1.0 调整(2026-08-21):开放 delegation(spawn/fork + control/list-agents,one-shot;
7
7
  # 子代理继承 D 权限与沙箱,产物归属 D 的 feature 分支 git 证据)与 goal(条件启用:
8
8
  # spec/实施计划提及 /goal 目标模式关键词时由系统注入目标模式指令)。
9
- # 仍裁剪:workflow/ralph(编排循环归 R20 链路职责)、plan-mode(planning)、web。
9
+ # 仍裁剪:workflow/ralph(编排循环归链路编排职责)、plan-mode(planning)、web。
10
10
 
11
11
  - id: persona
12
12
  name: '@deepseek-ai/dsh-persona'
@@ -147,10 +147,10 @@
147
147
  name: '@deepseek-ai/dsh-tool-goal'
148
148
 
149
149
  # ── presentation (run_code) ─────────────────────────────────────────────────
150
- # B1:mode 由 code 改为 both —— code 模式下注册表把「直接调用 bash/kanban_* 等工具」解析为
150
+ # mode 由 code 改为 both —— code 模式下注册表把「直接调用 bash/kanban_* 等工具」解析为
151
151
  # UNKNOWN_TOOL(仅 run_code 可模型直呼),导致 D 无法直接 kanban_complete/kanban_block 收尾;
152
152
  # native 又隐藏 run_code。both = 原生工具 schema + run_code 并存:bash/fs/kanban_* 可直接调用,
153
- # run_code(tools.* 路由)亦可用,正好是 D=执行者的工具面(见 checklist B1/B5)。
153
+ # run_code(tools.* 路由)亦可用,正好是 D=执行者的工具面。
154
154
  - id: tool-presentation
155
155
  name: '@deepseek-ai/dsh-agent-tool-presentation'
156
156
  config:
@@ -1,4 +1,4 @@
1
- # kanban-dt — DT(impl verify+review) 裁剪 agent preset(交付质量链 Task 9)。
1
+ # kanban-dt — DT(impl verify+review) 裁剪 agent preset(交付质量链)。
2
2
  #
3
3
  # DT = 只读实现校验+评审角色:kanban 任务工具 + spec_card_view + wiki 只读(评审区写由 ToolGuard 收窄)。
4
4
  # 组合:persona + instructions + bash + fs + fs-search + run_code(验证命令用,ToolGuard 拦源码写)。
@@ -27,10 +27,28 @@
27
27
  You may spawn read-only check subagents (spawn, one-shot) to parallelize the
28
28
  verification; they are hard-locked read-only by the global subagent guard — do
29
29
  not attempt writes through them.
30
- 3. Review engine priority: open-code-review (ocr review --from <TARGET_BRANCH> --to
31
- <branch>, delegation mode; <branch> = D's feature branch from the parent handoff
32
- metadata.branch) fallback superpowers code-review when ocr is unavailable
33
- kanban_block('review-tool-unavailable') only when both are unavailable.
30
+ 3. The review engine is decided by the config panel reviewEngine.mode (default:
31
+ delegate). Delegate = call ocr_review{sub:'preview'} for the review scope
32
+ ocr_review{sub:'rule'} for per-file review rules → review the git diff file by file
33
+ yourself → classify findings by severity (Critical/High must be reported, Medium with
34
+ context, Low dropped by default). Managed = call ocr_review{sub:'managed',
35
+ from:<TARGET_BRANCH>, to:<branch>} for normalized findings in one shot (<branch> = D's
36
+ feature branch from the parent handoff metadata.branch); silently fall back to the
37
+ delegate flow when status is not completed or the tool returns managed-not-configured
38
+ guidance. When ocr is not installed the tool returns install guidance (installable
39
+ from the GUI config panel); in chain scenarios kanban_block('review-tool-unavailable')
40
+ and note the GUI install option in the reason. Standalone review mode (no bound chain
41
+ task): review user-specified local dirs, branch ranges (--from/--to), single commits,
42
+ uncommitted diffs or public repo URLs (git clone to a temp dir, discard after
43
+ review); git queries (status/log/show/diff/rev-list/merge-base/blame etc.), clone/fetch
44
+ and bare checkout/switch are allowed while mutations (push/merge/rebase/reset/commit/
45
+ add/clean/restore etc.) are always rejected by the guard — you are a read-only
46
+ reviewer; never pass the sandbox_permissions/justification escalation params on
47
+ bash/write/edit (read-only sessions have no escalation use case; passing them
48
+ triggers the host sandbox validation error); output the full report in conversation,
49
+ then wiki_write to
50
+ projects/<repoSlug>/reviews/<topic>-<date>/ after user confirmation; no kanban tools;
51
+ never modify reviewed code.
34
52
  4. Wiki writes only under the whitelisted projects/<repoSlug>/<chain>/review/ namespace
35
53
  (repoSlug is derived from the chain workspaceDir).
36
54
  5. Write the review conclusion into kanban_complete metadata review_evidence =
@@ -1,9 +1,9 @@
1
- # kanban-p — P(planner) 裁剪 agent preset(D22,2026-08-15)。
1
+ # kanban-p — P(planner) 裁剪 agent preset(2026-08-15)。
2
2
  #
3
3
  # 组合方式:官方 agent-presets 无 include/patch 语义(cordis/code 均为 standard 的
4
- # 整目录副本),故本文件为手写最小组合(取舍已记入 Task 33 提交信息)。
4
+ # 整目录副本),故本文件为手写最小组合(取舍已记入 git 提交历史)。
5
5
  # 仅保留规划所需基座:persona/instructions + bash + fs + fs-search。
6
- # 裁剪(设计 §4):无 run_code(tool-presentation)、jobs、skill、goal、plan-mode(planning)、
6
+ # 裁剪(最小能力面):无 run_code(tool-presentation)、jobs、skill、goal、plan-mode(planning)、
7
7
  # compaction、delegation(subagent/fork/workflow/ralph)、ask-user、todo、web。
8
8
  # 局限:dsh-tool-bash 无 readonly 模式,P 的"只读命令(cat/git show)"纪律由
9
9
  # runner 设置的 danger-full-access sandbox + 工具级硬护栏(buildPlanWriteGuard:写仅允许
@@ -1,4 +1,4 @@
1
- # kanban-pt — PT(plan review) 裁剪 agent preset(交付质量链 Task 8)。
1
+ # kanban-pt — PT(plan review) 裁剪 agent preset(交付质量链)。
2
2
  #
3
3
  # PT = 只读计划评审角色:kanban 任务工具 + spec_card_view(无 create/wiki/执行)。
4
4
  # 组合:persona + instructions + bash + fs + fs-search(与 P/W 同类基座,读/找用)。
@@ -1,6 +1,6 @@
1
- # kanban-v — V(orchestrator) 裁剪 agent preset(2026-08-17,R21 对齐)。
1
+ # kanban-v — V(orchestrator) 裁剪 agent preset(2026-08-17)。
2
2
  #
3
- # V = butler·orchestrator:唯一可 kanban_create、零执行能力(R21「disabled: browser/file/
3
+ # V = butler·orchestrator:唯一可 kanban_create、零执行能力(「disabled: browser/file/
4
4
  # web/search/delegation/code_execution;只路由」)。组合仅 persona + agent-instructions
5
5
  # (+ installRoleTools 注入的 kanban/spec 工具)——不含 bash/fs/fs-search/run_code/jobs/
6
6
  # skill/goal/plan-mode/compaction/delegation/web/todo 等任何执行/探索/派单工具,
@@ -18,7 +18,7 @@
18
18
  Rules:
19
19
  1. You have NO execution or exploration tools (no bash/fs/run_code/web/search/delegation);
20
20
  facts come only from kanban_show/kanban_list/spec_card_view and the injected context.
21
- 2. R20 phase-by-phase creation: create the next phase's task only after the previous
21
+ 2. Phase-by-phase creation: create the next phase's task only after the previous
22
22
  phase's completion event; no cross-phase parallelism. Chain: P(openspec) → (PT plan
23
23
  review, on demand) → W2(kb sync) → D(execute, the ONLY executor) → DT(impl verify+review)
24
24
  → W3(kb sync) → summary. Create a PT card only when P(openspec) completes with