@joekytc/dsh-swarm 0.3.2 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +14 -7
  2. package/README.zh-CN.md +10 -5
  3. package/client/KanbanBoard.tsx +3 -1
  4. package/client/TaskDrawer.tsx +74 -2
  5. package/client/WorkflowRail.tsx +84 -1
  6. package/client/config-store.ts +3 -1
  7. package/client/kanban.css +28 -0
  8. package/client/timeline-model.ts +4 -0
  9. package/client/workflow-model.ts +20 -11
  10. package/lib/client.js +1100 -22
  11. package/lib/config.d.ts +5 -1
  12. package/lib/config.js +2 -1
  13. package/lib/dispatcher/agent-runner.js +25 -9
  14. package/lib/dispatcher/event-waker.js +4 -1
  15. package/lib/dispatcher/v-orchestrator.d.ts +2 -0
  16. package/lib/dispatcher/v-orchestrator.js +58 -9
  17. package/lib/domain/config-override.d.ts +6 -0
  18. package/lib/domain/config-override.js +16 -1
  19. package/lib/domain/im-message.d.ts +21 -2
  20. package/lib/domain/im-message.js +84 -4
  21. package/lib/domain/kanban-service.d.ts +8 -1
  22. package/lib/domain/kanban-service.js +67 -1
  23. package/lib/domain/permissions.d.ts +1 -1
  24. package/lib/domain/permissions.js +4 -0
  25. package/lib/domain/projection.js +8 -3
  26. package/lib/domain/review-evidence.js +9 -0
  27. package/lib/domain/review-target.d.ts +10 -0
  28. package/lib/domain/review-target.js +24 -0
  29. package/lib/domain/state-machine.js +1 -1
  30. package/lib/domain/types.d.ts +16 -3
  31. package/lib/routes/kanban-http.js +52 -2
  32. package/lib/services/config-provider.js +2 -0
  33. package/lib/services/im-bot-probe.d.ts +36 -0
  34. package/lib/services/im-bot-probe.js +128 -0
  35. package/lib/services/im-delivery.d.ts +69 -7
  36. package/lib/services/im-delivery.js +220 -34
  37. package/lib/tools/kanban-tools.js +38 -1
  38. package/lib/tools/main-session-tools.js +34 -9
  39. package/package.json +1 -1
  40. package/personas/kanban-p/agent.cordis.yml +16 -0
  41. package/personas/kanban-pt/agent.cordis.yml +19 -1
  42. package/personas/kanban-w/agent.cordis.yml +15 -0
  43. package/personas/persona-p.md +4 -0
  44. package/personas/persona-pt.md +4 -1
  45. package/personas/persona-w.md +1 -0
  46. package/personas/swarm/agent.cordis.yml +11 -0
@@ -2,8 +2,13 @@ import { homedir } from 'node:os';
2
2
  import { writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { buildCompletionMessage, buildBlockMessage } from '../domain/im-message.js';
5
+ import { createBotProbe, probeDepsFromCtx } from './im-bot-probe.js';
5
6
  const RETRYABLE_CODES = new Set(['bot-not-connected', 'delivery-failed']);
6
7
  const DEFAULT_RETRY_DELAYS_MS = [5_000, 10_000, 15_000];
8
+ /** 机器人交互选择等待上限(5 分钟):到点 abort 信号 + 竞速超时,按「未选择」fail-closed 不投。 */
9
+ const ASK_TIMEOUT_MS = 300_000;
10
+ /** 「设为默认」选项文案:提问与应答解析共用一处,改文案两处同步(字符串比对,无 id 兜底)。 */
11
+ const ASK_SET_DEFAULT_LABEL = '设为默认';
7
12
  /** dsh-im 未安装的可识别错误前缀(0.3.1:缺插件属环境问题不重试,直接友好提醒安装)。 */
8
13
  export const DSH_IM_MISSING_PREFIX = 'dsh-im-not-installed';
9
14
  const DSH_IM_MISSING_ERROR = `${DSH_IM_MISSING_PREFIX}: 未检测到 dsh-im 插件服务,请先安装并启用 @xmanrui/dsh-im(安装后重启 dsh 生效)`;
@@ -23,35 +28,115 @@ function resolveDshIm(ctx, log) {
23
28
  }
24
29
  return svc;
25
30
  }
26
- /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存目标(按 kind)。
27
- * 群/私聊各自仅且只有一个——发现异常返回 error(调用方留痕不投,fail-closed——投错对象比不投更糟)。 */
28
- export async function resolveTarget(im, cfg, kind) {
29
- let botId = cfg.botId.trim();
30
- if (!botId) {
31
- const bots = await im.listBots();
32
- const wecom = bots.filter((b) => b.channel === 'wecom');
33
- if (wecom.length !== 1)
34
- return { error: `企微机器人数量=${wecom.length}(期望 1),请在插件配置 imDelivery.botId 显式指定` };
35
- botId = wecom[0].botId;
31
+ /** 交互选项展示文案:脱敏名 + id(id 是唯一可靠标识,展示名可能重名)。 */
32
+ function askLabel(o) {
33
+ return o.label ? `${o.label}(${o.botId})` : o.botId;
34
+ }
35
+ /** 多机器人且无法自动决策时的 fail-closed 报错:列候选(有探针数据则带模式与连接态)。 */
36
+ function botCandidatesError(ids, probed) {
37
+ const byId = new Map(probed.map((b) => [b.botId, b]));
38
+ const list = ids.map((id) => {
39
+ const c = byId.get(id);
40
+ if (!c)
41
+ return id;
42
+ return `${id}(${c.label || '未命名'}·${c.agentPreset || '未设模式'}${c.connected ? '' : '·离线'})`;
43
+ }).join(' / ');
44
+ return `企微机器人数量=${ids.length}(期望 1),无法自动确定投递对象。候选:${list}。请在插件配置 imDelivery.botId 显式指定,或在可交互的手动投递会话重试以选择`;
45
+ }
46
+ /** 机器人决策链:显式配置 → 唯一机器人 → 会话 preset 命中 → 默认机器人 → 交互选择。
47
+ * 每环都 fail-closed(宁可不投也不投错对象);探针不可用只失去自动匹配能力,不阻断投递。 */
48
+ export async function chooseBot(im, cfg, deps) {
49
+ const explicit = cfg.botId.trim();
50
+ if (explicit)
51
+ return { ok: true, botId: explicit, via: 'explicit' };
52
+ const wecom = (await im.listBots()).filter((b) => b.channel === 'wecom');
53
+ if (wecom.length === 0) {
54
+ return { ok: false, error: '未检测到企微机器人接入:请先在 dsh-im 设置→IM机器人 接入机器人后重试' };
36
55
  }
56
+ const ids = wecom.map((b) => b.botId);
57
+ if (ids.length === 1)
58
+ return { ok: true, botId: ids[0], via: 'single' };
59
+ const fallback = (cfg.fallbackBotId ?? '').trim();
60
+ const preset = (deps.sessionPreset ?? '').trim();
61
+ // 只有「能模式匹配」或「能交互」时才值得探针:纯自动路径 + 无默认 → 直接给候选报错,省一次握手。
62
+ const probed = (preset || deps.ask) ? await deps.probe() : { ok: false, error: 'probe-skipped' };
63
+ if (!probed.ok && (preset || deps.ask))
64
+ deps.log(`[im-delivery] bot probe unavailable: ${probed.error}`);
65
+ const candidates = probed.ok ? probed.bots.filter((b) => ids.includes(b.botId)) : [];
66
+ if (preset && candidates.length) {
67
+ const hit = candidates.filter((b) => b.connected && b.agentPreset === preset);
68
+ if (hit.length === 1)
69
+ return { ok: true, botId: hit[0].botId, via: 'preset' };
70
+ deps.log(hit.length > 1
71
+ ? `[im-delivery] preset ${preset} 命中 ${hit.length} 个已连接机器人,不唯一,转默认/交互`
72
+ : `[im-delivery] preset ${preset} 未命中已连接机器人(候选模式:${candidates.map((b) => b.agentPreset || '未设').join(', ') || '无元数据'}),转默认/交互`);
73
+ }
74
+ if (fallback && ids.includes(fallback))
75
+ return { ok: true, botId: fallback, via: 'fallback' };
76
+ if (fallback)
77
+ deps.log(`[im-delivery] 默认机器人 ${fallback} 不在接入列表,转交互`);
78
+ if (!deps.ask)
79
+ return { ok: false, error: botCandidatesError(ids, candidates) };
80
+ const options = ids.map((id) => {
81
+ const c = candidates.find((b) => b.botId === id);
82
+ if (!c)
83
+ return { botId: id, label: '' };
84
+ return { botId: id, label: `${c.label || '未命名'}·${c.agentPreset || '未设模式'}${c.connected ? '' : '·离线'}` };
85
+ });
86
+ const answer = await deps.ask(options);
87
+ if (!answer)
88
+ return { ok: false, error: '未选择投递机器人(交互取消或超时),本次不投递' };
89
+ if (!answer.setDefault || !deps.setFallback)
90
+ return { ok: true, botId: answer.botId, via: 'interactive' };
91
+ const saved = await deps.setFallback(answer.botId);
92
+ if (!saved)
93
+ deps.log('[im-delivery] 默认机器人写入失败(配置未更新),本次仍按所选机器人投递');
94
+ return { ok: true, botId: answer.botId, via: 'interactive', fallbackSaved: saved };
95
+ }
96
+ /** dsh-im 目标列表(宿主同 Host 服务返回裸数组;防御兼容 Connection RPC `target.list` 信封形状)。 */
97
+ async function readTargets(im, botId) {
98
+ const raw = await im.listTargets(botId);
99
+ if (Array.isArray(raw))
100
+ return raw;
101
+ const enveloped = raw?.targets;
102
+ return Array.isArray(enveloped) ? enveloped : [];
103
+ }
104
+ /** targetId 解析:配置显式指定优先;留空自动发现该机器人下唯一已保存目标(按 kind)。
105
+ * 群/私聊各自仅且只有一个——发现异常返回 error(调用方留痕不投,fail-closed)。
106
+ * 机器人非显式指定(preset/默认/交互选出)而配置又填死了 targetId 时先校验归属:
107
+ * targetId 是 per-bot 作用域,填错机器人只会落成 dsh-im 的 unknown-target,这里提前给可行动报错。 */
108
+ export async function resolveTargetId(im, botId, botIdExplicit, cfg, kind) {
37
109
  const explicit = (kind === 'group' ? cfg.targetId : cfg.dmTargetId ?? '').trim();
38
- let targetId = explicit;
39
- if (!targetId) {
40
- const raw = await im.listTargets(botId);
41
- // 宿主同 Host 服务返回裸数组(PROACTIVE_DELIVERY.md:176);防御兼容 Connection RPC `target.list` 信封形状。
42
- const targets = Array.isArray(raw) ? raw : Array.isArray(raw?.targets)
43
- ? raw.targets
44
- : [];
45
- const matched = targets.filter((x) => x.kind === kind);
46
- const label = kind === 'group' ? '群目标' : '私聊目标';
47
- const cfgHint = kind === 'group' ? 'imDelivery.targetId' : 'imDelivery.dmTargetId';
48
- if (matched.length !== 1) {
49
- const candidates = matched.length > 1 ? ',候选:' + matched.map((x) => `${x.targetId}${x.name ? `(${x.name})` : ''}`).join(' / ') : '';
50
- return { error: `已保存${label}数量=${matched.length}(期望 1,仅且只有一个)${candidates}。请到 dsh-im 设置→IM机器人 ${matched.length > 1 ? '清理多余目标' : '新建目标'}或在插件配置 ${cfgHint} 显式指定` };
51
- }
52
- targetId = matched[0].targetId;
110
+ const label = kind === 'group' ? '群目标' : '私聊目标';
111
+ const cfgHint = kind === 'group' ? 'imDelivery.targetId' : 'imDelivery.dmTargetId';
112
+ if (explicit) {
113
+ if (botIdExplicit)
114
+ return { targetId: explicit };
115
+ const targets = await readTargets(im, botId);
116
+ const owned = targets.some((x) => x.targetId === explicit);
117
+ if (owned)
118
+ return { targetId: explicit };
119
+ return { error: `${cfgHint}=${explicit} 不属于本轮选定的机器人 ${botId}(targetId 按机器人隔离)。请清空该字段改由自动发现,或显式指定 imDelivery.botId 固定机器人` };
120
+ }
121
+ const targets = await readTargets(im, botId);
122
+ const matched = targets.filter((x) => x.kind === kind);
123
+ if (matched.length !== 1) {
124
+ const candidates = matched.length > 1 ? ',候选:' + matched.map((x) => `${x.targetId}${x.name ? `(${x.name})` : ''}`).join(' / ') : '';
125
+ return { error: `已保存${label}数量=${matched.length}(期望 1,仅且只有一个)${candidates}。请到 dsh-im 设置→IM机器人 ${matched.length > 1 ? '清理多余目标' : '新建目标'}或在插件配置 ${cfgHint} 显式指定` };
53
126
  }
54
- return { botId, targetId };
127
+ return { targetId: matched[0].targetId };
128
+ }
129
+ /** botId+targetId 一步解析(无交互/无探针的简化入口,保留给单机器人场景与单测)。 */
130
+ export async function resolveTarget(im, cfg, kind) {
131
+ const choice = await chooseBot(im, cfg, {
132
+ sessionPreset: null, probe: async () => ({ ok: false, error: 'probe-skipped' }), ask: null, setFallback: null, log: () => { },
133
+ });
134
+ if (!choice.ok)
135
+ return { error: choice.error };
136
+ const t = await resolveTargetId(im, choice.botId, cfg.botId.trim() !== '', cfg, kind);
137
+ if ('error' in t)
138
+ return t;
139
+ return { botId: choice.botId, targetId: t.targetId };
55
140
  }
56
141
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
57
142
  /** 发送 + 退避重试(仅可重试错误码;{sent:true} 即停——dsh-im 无幂等,成功后绝不重发)。 */
@@ -88,13 +173,90 @@ function makeDefaultLog(storageDir) {
88
173
  catch { /* 忽略写失败 */ }
89
174
  };
90
175
  }
91
- /** 发送器工厂:resolveDshIm → resolveTarget(fail-closed)→ sendWithRetry → 留痕。
176
+ /** 交互选择接线(仅手动投递路径):两问一次弹——选机器人 + 是否设为默认。
177
+ * agent 必传(exec.agent 的 live 实例):GUI 弹窗按 agent 路由;无询问通道返回 null(调用方 fail-closed 列候选),
178
+ * 取消/超时/服务端拒绝同样返回 null(不投),失败原因进日志(只带错误码,不含任何敏感载荷)。 */
179
+ function buildBotAsk(ctx, agent, log) {
180
+ const uq = ctx.get?.('userQuestions');
181
+ if (typeof uq?.ask !== 'function') {
182
+ log('[im-delivery] 机器人交互不可用:无 userQuestions 服务');
183
+ return null;
184
+ }
185
+ return async (options) => {
186
+ const controller = new AbortController();
187
+ const timer = setTimeout(() => { controller.abort(); }, ASK_TIMEOUT_MS);
188
+ try {
189
+ const ans = await Promise.race([
190
+ uq.ask({
191
+ questions: [
192
+ {
193
+ id: 'im-bot',
194
+ header: '投递机器人',
195
+ question: '本轮企微投递用哪个机器人?(当前会话模式未匹配到任何机器人)',
196
+ detail: '展示名·默认模式;离线机器人不保证能发出。选择只作用于本次投递。',
197
+ options: options.map((o) => ({ label: askLabel(o), description: o.botId })),
198
+ },
199
+ {
200
+ id: 'im-bot-default',
201
+ header: '设为默认',
202
+ question: '是否把所选机器人设为默认机器人?(之后模式未命中时直接使用,不再询问)',
203
+ detail: '写入插件配置 imDelivery.fallbackBotId(可在设置面板看到来源标记)。',
204
+ options: [{ label: ASK_SET_DEFAULT_LABEL }, { label: '仅本次' }],
205
+ },
206
+ ],
207
+ ...(agent === undefined ? {} : { agent }),
208
+ signal: controller.signal,
209
+ }),
210
+ new Promise((_, reject) => setTimeout(() => reject(new Error('im-bot-ask-timeout')), ASK_TIMEOUT_MS)),
211
+ ]);
212
+ const pick = ans.answers?.find((a) => a.id === 'im-bot');
213
+ const typed = pick?.custom?.trim() ?? '';
214
+ const selected = pick?.selected?.[0]?.trim() ?? '';
215
+ // 自由文本也接受,但必须是候选之一——防手写错 id 投到未知对象。
216
+ const botId = options.find((o) => askLabel(o) === selected || o.botId === selected || o.botId === typed)?.botId ?? '';
217
+ if (!botId) {
218
+ log('[im-delivery] 机器人交互已应答但未映射到候选(selected 与候选不符)');
219
+ return null;
220
+ }
221
+ const setDefault = (ans.answers?.find((a) => a.id === 'im-bot-default')?.selected ?? []).includes(ASK_SET_DEFAULT_LABEL);
222
+ return { botId, setDefault };
223
+ }
224
+ catch (err) {
225
+ const code = err.code;
226
+ const name = err.name;
227
+ log(`[im-delivery] 机器人交互未完成: ${typeof code === 'string' && code ? code : typeof name === 'string' && name ? name : 'unknown'}`);
228
+ return null;
229
+ }
230
+ finally {
231
+ clearTimeout(timer);
232
+ }
233
+ };
234
+ }
235
+ /** 写默认机器人接线:经 ConfigProvider.applyOverride 落 config-override.json + 审计留痕。 */
236
+ function buildSetFallback(configProvider) {
237
+ return async (botId) => {
238
+ try {
239
+ const snap = configProvider.snapshot().effective;
240
+ return configProvider.applyOverride({ ...snap, imDelivery: { fallbackBotId: botId } }).ok;
241
+ }
242
+ catch {
243
+ return false;
244
+ }
245
+ };
246
+ }
247
+ /** 发送器工厂:resolveDshIm → chooseBot(fail-closed)→ resolveTargetId → sendWithRetry → 留痕。
92
248
  * auto 路径(wireImDelivery)失败额外写 chain/im-delivery-failed 链事件;
93
249
  * manual 路径(/sms,opts.manual)仅 dispatcher.log 留痕,错误同步返回给调用方。 */
94
250
  export function createSender(ctx, kanban, configProvider, opts = {}) {
95
251
  const log = opts.log ?? makeDefaultLog(deriveStorageDir(configProvider));
96
252
  const delays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
97
253
  const targetKind = opts.targetKind ?? 'group';
254
+ // 探针单飞缓存挂在发送器上(连发 /sms 不重复握手);交互与写默认只在手动路径接线(后台 auto 路径无人应答)。
255
+ const probe = opts.botChoice?.probe ?? createBotProbe(probeDepsFromCtx(ctx));
256
+ const ask = opts.botChoice?.ask !== undefined ? opts.botChoice.ask
257
+ : (opts.manual ? buildBotAsk(ctx, opts.agent, log) : null);
258
+ const setFallback = opts.botChoice?.setFallback !== undefined ? opts.botChoice.setFallback
259
+ : (opts.manual ? buildSetFallback(configProvider) : null);
98
260
  return async (chainId, text) => {
99
261
  const cfg = configProvider.getEffective().imDelivery;
100
262
  const im = resolveDshIm(ctx, log);
@@ -111,15 +273,22 @@ export function createSender(ctx, kanban, configProvider, opts = {}) {
111
273
  }
112
274
  return { ok: false, error: DSH_IM_MISSING_ERROR };
113
275
  }
114
- const t = await resolveTarget(im, cfg, targetKind);
276
+ const choice = await chooseBot(im, cfg, {
277
+ sessionPreset: opts.sessionPreset ?? null, probe, ask, setFallback, log,
278
+ });
279
+ if (!choice.ok) {
280
+ log(`[im-delivery] bot resolve failed chain=${chainId}: ${choice.error}`);
281
+ return { ok: false, error: choice.error };
282
+ }
283
+ const t = await resolveTargetId(im, choice.botId, cfg.botId.trim() !== '', cfg, targetKind);
115
284
  if ('error' in t) {
116
285
  log(`[im-delivery] target resolve failed chain=${chainId}: ${t.error}`);
117
286
  return { ok: false, error: t.error };
118
287
  }
119
- const r = await sendWithRetry(im, t.botId, t.targetId, text, delays, log);
288
+ const r = await sendWithRetry(im, choice.botId, t.targetId, text, delays, log);
120
289
  if (r.ok) {
121
- log(`[im-delivery] delivered chain=${chainId} bot=${t.botId} target=${t.targetId}`);
122
- return { ok: true, botId: t.botId, targetId: t.targetId };
290
+ log(`[im-delivery] delivered chain=${chainId} bot=${choice.botId} target=${t.targetId} via=${choice.via}`);
291
+ return { ok: true, botId: choice.botId, targetId: t.targetId, via: choice.via, ...(choice.fallbackSaved === undefined ? {} : { fallbackSaved: choice.fallbackSaved }) };
123
292
  }
124
293
  // 超限显形(评审决议):dispatcher.log 留痕;auto 路径追加 events.jsonl 双留痕;投递失败绝不 block 链。
125
294
  log(`[im-delivery] FAILED chain=${chainId}: ${r.error}`);
@@ -266,7 +435,7 @@ export async function sendChainReport(ctx, kanban, configProvider, opts, variant
266
435
  ? { ok: false, error: r.error, guidance: DSH_IM_MISSING_GUIDANCE }
267
436
  : { ok: false, error: r.error };
268
437
  }
269
- return { ok: true, chainId, botId: r.botId, targetId: r.targetId };
438
+ return { ok: true, chainId, botId: r.botId, targetId: r.targetId, via: r.via, ...(r.fallbackSaved === undefined ? {} : { fallbackSaved: r.fallbackSaved }) };
270
439
  }
271
440
  /** 接线:订阅看板事件多播通道(不动 setOnTaskCompleted 单消费者钩子)。
272
441
  * listener 同步返回,投递全程 fire-and-forget 自兜异常(publish 在 emit 队列内同步调用,不得拖慢落盘)。 */
@@ -279,8 +448,9 @@ export function wireImDelivery(ctx, kanban, configProvider, opts = {}) {
279
448
  const cfg = configProvider.getEffective().imDelivery;
280
449
  if (!cfg?.enabled)
281
450
  return; // 功能关闭:零开销早退(非异常,不留痕)
282
- // 仅 task/completed(带 taskId)与 chain/blocked 关心事件流;其余早退,避免每个无关事件触发 snapshot 全量重放
283
- if (!((ev.kind === 'task/completed' && ev.taskId) || ev.kind === 'chain/blocked'))
451
+ // 仅 task/completed(带 taskId)/ chain/blocked / review/gave-up 关心事件流;其余早退,避免每个无关事件触发 snapshot 全量重放。
452
+ // 2026-09-15:+ review/gave-up(评审超限待裁决)——不再静默等看门狗 block,人工在 gave-up 时刻即获通知与两条出口。
453
+ if (!((ev.kind === 'task/completed' && ev.taskId) || ev.kind === 'chain/blocked' || ev.kind === 'review/gave-up'))
284
454
  return;
285
455
  const state = await kanban.snapshot();
286
456
  if (ev.kind === 'task/completed' && ev.taskId) {
@@ -295,6 +465,22 @@ export function wireImDelivery(ctx, kanban, configProvider, opts = {}) {
295
465
  const msg = buildBlockMessage(state, ev.chainId, String(ev.payload['reason'] ?? ''), storageDir);
296
466
  await deliver(ev.chainId, msg);
297
467
  }
468
+ else if (ev.kind === 'review/gave-up') {
469
+ const chain = state.chains.get(ev.chainId);
470
+ if (!chain)
471
+ return;
472
+ const reviewTaskId = String(ev.payload['reviewTaskId'] ?? '');
473
+ const targetTaskId = String(ev.payload['targetTaskId'] ?? '');
474
+ const msg = [
475
+ `【评审超限待裁决】${chain.title || ev.chainId} 的评审卡 ${reviewTaskId} 超限放弃(${String(ev.payload['reason'] ?? '')})。`,
476
+ `被评审卡:${targetTaskId}`,
477
+ '两条出口:',
478
+ '① 人工豁免评审 → 计划评审按通过继续(GUI 任务详情「豁免评审」按钮 / 主会话 kanban_waive_review);',
479
+ '② 人工恢复链后按需返工(GUI 链头「人工恢复」按钮 / 主会话 kanban_reopen_chain)。',
480
+ '若无人处置,看门狗将在链持续无进展后把链置 blocked(届时可人工恢复)。',
481
+ ].join('\n');
482
+ await deliver(ev.chainId, msg);
483
+ }
298
484
  })().catch((err) => log('[im-delivery] handler failed ev=' + ev.kind + ': ' + String(err)));
299
485
  };
300
486
  return kanban.subscribe(handle);
@@ -2,6 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import {} from '@deepseek-ai/dsh-util-values';
3
3
  import { KanbanService } from '../domain/kanban-service.js';
4
4
  import { can } from '../domain/permissions.js';
5
+ import { resolveReviewTarget } from '../domain/review-target.js';
5
6
  function guard(action, caller, task = null) {
6
7
  if (!can(action, caller.actor, task))
7
8
  throw new Error('permission denied: ' + action);
@@ -92,7 +93,7 @@ export function buildKanbanTools(service, getCaller) {
92
93
  parameters: {
93
94
  taskId: { type: 'string', required: true },
94
95
  summary: { type: 'string', required: true, description: 'Human-readable completion summary' },
95
- metadata: { type: 'json', description: 'Machine-readable handoff: changed_files/verification/kb_url...' },
96
+ metadata: { type: 'json', description: 'Machine-readable handoff: changed_files/verification/kb_url... W3/kb may add report = { requirement, status, branch, tasks: [{ text, done }], acceptance, verification, leftovers?, todos? } (delivery-report fields; tasks mirrors the OpenSpec plan checklist openspec/changes/<id>/tasks.md verbatim, done = checked)' },
96
97
  },
97
98
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
98
99
  async execute(args) {
@@ -166,5 +167,41 @@ export function buildKanbanTools(service, getCaller) {
166
167
  return task;
167
168
  },
168
169
  }),
170
+ // 人工恢复能力(2026-09-15):仅 human(caller.actor 由主会话/角色装配注入;角色工具面不暴露这两个工具)。
171
+ defineTool({
172
+ name: 'kanban_reopen_chain',
173
+ description: 'Reopen a blocked chain back to executing (human only; reason required).',
174
+ parameters: {
175
+ chainId: { type: 'string', required: true, description: 'Chain id (ch_xxx)' },
176
+ reason: { type: 'string', required: true, description: 'Recovery reason (audited)' },
177
+ },
178
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
179
+ async execute(args) {
180
+ const caller = getCaller();
181
+ const ev = await service.reopenChain(args.chainId, args.reason, caller.actor);
182
+ return ev;
183
+ },
184
+ }),
185
+ defineTool({
186
+ name: 'kanban_waive_review',
187
+ description: 'Waive a failed/gave-up review card for its target task (human only; reason required).',
188
+ parameters: {
189
+ taskId: { type: 'string', required: true, description: 'Review card id (pt/dt card)' },
190
+ reason: { type: 'string', required: true, description: 'Waive reason (audited)' },
191
+ },
192
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
193
+ async execute(args) {
194
+ const caller = getCaller();
195
+ const state = await service.snapshot();
196
+ const reviewTask = state.tasks.get(args.taskId);
197
+ if (!reviewTask)
198
+ throw new Error('unknown review task: ' + args.taskId);
199
+ const target = resolveReviewTarget(state, reviewTask, reviewTask.chainId);
200
+ if (!target)
201
+ throw new Error('review target not resolvable for ' + args.taskId);
202
+ const ev = await service.waiveReview(args.taskId, target.root.id, args.reason, caller.actor);
203
+ return ev;
204
+ },
205
+ }),
169
206
  ];
170
207
  }
@@ -131,6 +131,16 @@ function buildFreeSendGuidance(routes, query, dm) {
131
131
  '投递结果只向用户确认成功与目标,不复述正文;失败时把 error/guidance 原样转告,勿编造原因。',
132
132
  ].join('\n');
133
133
  }
134
+ /** 当前会话 preset(多机器人投递按它匹配机器人的默认 dsh 模式)。
135
+ * header.agentPreset 缺省(用默认模式创建的会话可能不落该字段)→ 回落到 agentPresets.defaultId;
136
+ * 两者都拿不到返回空串,调用方按「无模式匹配」处理(走默认机器人/交互,绝不猜)。 */
137
+ function sessionPresetOf(ctx, exec) {
138
+ const fromHeader = exec?.agent?.session?.header?.agentPreset;
139
+ if (typeof fromHeader === 'string' && fromHeader.trim())
140
+ return fromHeader.trim();
141
+ const presets = ctx.get?.('agentPresets');
142
+ return typeof presets?.defaultId === 'string' ? presets.defaultId : '';
143
+ }
134
144
  /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
135
145
  * 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
136
146
  * 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
@@ -150,8 +160,10 @@ export function registerMainSessionTools(ctx, configProvider) {
150
160
  ? new LocalWikiClient(ensureLocalKbRoot())
151
161
  : (ctx.get('wiki') ?? new WikiVaultClient(() => configProvider.getEffective().wikiVault));
152
162
  const caller = () => ({ actor: 'human' });
153
- // 只读 kanban 子集(无 create/complete/block)
154
- const readOnly = new Set(['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_comment']);
163
+ // 主会话 kanban 工具子集(无 create/complete/block)。2026-09-15 恢复能力:+ 人工恢复双工具
164
+ //(kanban_reopen_chain / kanban_waive_review)——主会话 caller 恒为 human(服务层再校验 can()),
165
+ // 角色 toolsets 名单不动(角色工具面零暴露)。
166
+ const readOnly = new Set(['kanban_show', 'kanban_chain', 'kanban_list', 'kanban_comment', 'kanban_reopen_chain', 'kanban_waive_review']);
155
167
  for (const tool of buildKanbanTools(service, caller)) {
156
168
  const name = tool.name;
157
169
  if (name && readOnly.has(name))
@@ -232,15 +244,23 @@ export function registerMainSessionTools(ctx, configProvider) {
232
244
  // 其余非空 rest → 自由投递(正文由主 agent 按意图生成,显式豁免;事实基准=看板实查)。
233
245
  // -s 独立 token 由 parseSendRequest 剥离置 dm;manual 路径零重试。
234
246
  const parsed = parseSendRequest(plan.rest);
235
- const targetOpts = { retryDelaysMs: [], manual: true, targetKind: (parsed.dm ? 'user' : 'group') };
247
+ const targetOpts = {
248
+ retryDelaysMs: [], manual: true,
249
+ targetKind: (parsed.dm ? 'user' : 'group'),
250
+ sessionPreset: sessionPresetOf(ctx, exec),
251
+ agent: exec?.agent,
252
+ };
236
253
  const deliver = async (variant, query) => {
237
254
  const r = await sendChainReport(ctx, service, configProvider, targetOpts, variant, query);
238
255
  if (r.ok) {
239
256
  const noun = variant === 'blocked' ? '阻塞通知' : '完成汇报';
240
257
  const dest = parsed.dm ? '私聊' : '群聊';
258
+ const via = r.via === 'interactive' ? `(本次交互选定${r.fallbackSaved ? ',已设为默认' : ''})`
259
+ : r.via === 'fallback' ? '(未命中会话模式,走默认机器人)'
260
+ : r.via === 'preset' ? '(按会话模式匹配机器人)' : '';
241
261
  return {
242
- kind: 'send', chainId: r.chainId, botId: r.botId, targetId: r.targetId,
243
- guidance: `${noun}已投递${dest}(/sms 手动触发)。请仅向用户确认投递成功与目标,勿复述消息正文。`,
262
+ kind: 'send', chainId: r.chainId, botId: r.botId, targetId: r.targetId, via: r.via,
263
+ guidance: `${noun}已投递${dest}${via}(/sms 手动触发)。请仅向用户确认投递成功与目标,勿复述消息正文。`,
244
264
  };
245
265
  }
246
266
  return { kind: 'send', error: r.error, guidance: r.guidance ?? '请将 error 字段原样转告用户,勿复述消息正文。' };
@@ -328,7 +348,7 @@ export function registerMainSessionTools(ctx, configProvider) {
328
348
  dm: { type: 'boolean', description: 'true = private chat; omit/false = group' },
329
349
  },
330
350
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
331
- async execute(args) {
351
+ async execute(args, exec) {
332
352
  // actor 边界与 planning 工具同构:主会话工具面仅限人工触发,非 human caller 一律拒绝
333
353
  if (caller().actor !== 'human')
334
354
  throw new Error('permission denied: sms_send');
@@ -338,7 +358,10 @@ export function registerMainSessionTools(ctx, configProvider) {
338
358
  if (text.length > 4000)
339
359
  throw new Error(`text too long: ${text.length} > 4000 chars`);
340
360
  const dm = args.dm === true;
341
- const deliver = createSender(ctx, service, configProvider, { retryDelaysMs: [], manual: true, targetKind: dm ? 'user' : 'group' });
361
+ const deliver = createSender(ctx, service, configProvider, {
362
+ retryDelaysMs: [], manual: true, targetKind: dm ? 'user' : 'group',
363
+ sessionPreset: sessionPresetOf(ctx, exec), agent: exec?.agent,
364
+ });
342
365
  const r = await deliver('free-send', text);
343
366
  if (!r.ok) {
344
367
  return {
@@ -347,8 +370,10 @@ export function registerMainSessionTools(ctx, configProvider) {
347
370
  };
348
371
  }
349
372
  return {
350
- kind: 'send', mode: 'free', dm, botId: r.botId, targetId: r.targetId,
351
- guidance: '已投递(自由投递)。请仅向用户确认投递成功与目标(群聊/私聊),勿复述正文。',
373
+ kind: 'send', mode: 'free', dm, botId: r.botId, targetId: r.targetId, via: r.via,
374
+ guidance: '已投递(自由投递)。请仅向用户确认投递成功与目标(群聊/私聊),勿复述正文。'
375
+ + (r.fallbackSaved === true ? '已按你的选择把该机器人设为默认机器人(后续模式未命中时直接使用)。' : '')
376
+ + (r.fallbackSaved === false ? '注意:设为默认未写入成功(配置未更新),下次仍需重新选择。' : ''),
352
377
  };
353
378
  },
354
379
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joekytc/dsh-swarm",
3
- "version": "0.3.2",
3
+ "version": "0.3.6",
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",
@@ -60,6 +60,22 @@
60
60
  explicit protocol name) and how the plan complies with it; if no protocol is declared,
61
61
  write 「无」 in that section. Never reference protocol standards beyond the spec card
62
62
  (read the declared files themselves, never secondhand summaries).
63
+ 10. Requirement alignment (granularity): every task item / task point / note in the
64
+ clarification checklist / spec card must map BOTH ways to plan tasks — nothing missing;
65
+ anything beyond scope must trace back to a clarification answer.
66
+ 11. Logical/interaction consistency: the plan must not contradict itself; slice/task
67
+ dependency order must be executable. When impl_decisions offer multiple options, converge
68
+ to a single path inside the plan — no left-open branches (e.g. "render per the current
69
+ page data source"); declared boundaries/error handling must have task mappings.
70
+ 12. Structural readiness — stage-free criteria: a done criterion MUST describe a single
71
+ terminal state after implementation is complete. Do NOT write execution-layer staged
72
+ criteria (RED/GREEN/REFACTOR, "fail first then pass") — those belong to the D-phase
73
+ execution protocol (see the D-phase instruction template); process notes may stay as
74
+ non-acceptance remarks only.
75
+ 13. Rework rounds (2026-09-15): when the task body contains a 「本轮修复清单」 (fix-this-round)
76
+ section, KEEP the existing task numbering and fix ONLY the listed items — never renumber
77
+ tasks and never rewrite/expand unrelated parts of the plan (each rewrite grows the review
78
+ surface and blocks convergence).
63
79
 
64
80
  - id: agent-instructions
65
81
  name: '@deepseek-ai/dsh-agent-instructions'
@@ -32,7 +32,16 @@
32
32
  - Engineering-protocol consistency: only reconcile against the 「上游协议遵循说明」
33
33
  section of proposal.md; the only judge reference is the file declared upstream; you have
34
34
  no authority to introduce undeclared protocols (never scan repo skills as review
35
- standards).
35
+ standards). TDD (RED/GREEN/REFACTOR) is a pipeline-built-in execution-layer protocol
36
+ (D-phase requirement) and is NOT in your protocol-reconciliation scope — when the plan
37
+ contains staged criteria, ask for final-state criteria per rule 4 (stable terminal
38
+ state); never fail it as an "undeclared upstream protocol".
39
+ - Review-source positivity (2026-09-15): your review source is the spec-card functional
40
+ surface (problem/solution/user_stories/testing plus explicit server-side constraints)
41
+ plus the plan text. Environment/tooling/credential/permission/API-source matters (e.g.
42
+ "a task must prove no reference to project X") are OUTSIDE the requirement and must NOT
43
+ be blocking criteria — put such findings in non-blocking suggestions for the execution
44
+ phase (D API integration / joint retest) to verify.
36
45
  2. A read-only ToolGuard blocks tracked-source writes, git mutations, and write-marker
37
46
  bash targeting the repo — do not attempt them; use read-only commands (cat/git show/glob).
38
47
  3. Write the review conclusion into kanban_complete metadata review_evidence =
@@ -53,6 +62,15 @@
53
62
  carried over as-is / partially fixed resolved=false with the remaining part in detail), then
54
63
  raise new issues; unfixed prior issues MUST stay in the issues list. Never skip
55
64
  reconciliation and only raise new issues.
65
+ Legacy marking (2026-09-15 convergence gate): reconciliation entries MUST carry legacy: true;
66
+ newly raised issues carry legacy: false (or omit). The system uses this to detect whether the
67
+ legacy backlog is cleared — while it is not, rework continues; once cleared, only unresolved
68
+ CRITICAL findings may keep the verdict at fail — all other new findings are downgraded into
69
+ downstream non-blocking suggestions (this round counts as passed).
70
+ Convergence expectation: do not keep failing with finer-grained NEW dimensions in the same
71
+ round where the legacy backlog was fixed (e.g. from "one criterion per protocol" down to
72
+ import-level, or from behavior splits down to symbol-level). New issues must fall inside the
73
+ five existing elements and trace back to an upstream declaration or an internal contradiction.
56
74
  5. Never call kanban_create, never write the wiki, never edit/approve spec cards; only
57
75
  complete/block/comment your own bound task.
58
76
  6. Use kanban_show/kanban_list/kanban_complete/kanban_block/kanban_heartbeat/
@@ -44,6 +44,21 @@
44
44
  for a human; never hand in an empty complete.
45
45
  8. Use kanban_* + web_search/web_fetch + (remote: wiki_search/wiki_read/wiki_write | local:
46
46
  skill) + prefetch_file/prefetch_external/prefetch_kb + spec_card_view (read-only).
47
+ 9. Sandbox discipline (2026-09-15): this session may already run at danger-full-access (the
48
+ permission ceiling — e.g. after the session permission was switched to 完全权限, or after a
49
+ one-shot escalation was approved). NEVER attach `sandbox_permissions` or `justification` to
50
+ ANY tool call (bash/write/edit/run_code/...): when the current mode is already the ceiling,
51
+ any value is rejected by the host with "sandbox escalation ... is not strictly wider than
52
+ this call's current ... mode". If a call was rejected that way, DROP the parameter and retry
53
+ with bare arguments — never retry with the parameter attached (it keeps failing).
54
+ 10. Chain closing report (W3/kb only): when the repo carries an OpenSpec plan
55
+ (openspec/changes/*/tasks.md), read its checkbox state and attach metadata.report to
56
+ kanban_complete: { requirement, status, branch, tasks: [{ text, done }], acceptance,
57
+ verification, leftovers?, todos? }. tasks mirrors the plan checklist verbatim (done =
58
+ checked; unchecked items stay false — failures belong in leftovers, never flipped to done).
59
+ requirement/status/branch/acceptance/verification are one-line factual strings (branch from
60
+ the D handoff, status from the chain outcome). These fields render verbatim into the
61
+ delivered WeCom report — keep them factual; omit report entirely when no OpenSpec plan exists.
47
62
 
48
63
  - id: agent-instructions
49
64
  name: '@deepseek-ai/dsh-agent-instructions'
@@ -14,3 +14,7 @@
14
14
  8. complete 时 metadata 必须带 `pt_decision`:`{ needed: boolean, reason?: string }`(needed=true 时 reason 必填)。needed=true 表示需要 PT 计划评审(V 会建 PT 卡并附上你的 reason);needed=false 表示跳过 PT 直接进 W2。
15
15
  9. tasks.md 拆分粒度(结构准备度,与 PT 同一判据,自包含零外部依赖):每个任务条目必须有单一、可独立核对的完成判据;禁止一条任务的实现步骤同时达成 N 个互不依赖的可观察结果,或一条测试用例同时断言 N 个互不依赖的可观察结果(N≥2)——按可独立验证的行为逐条拆分(一条任务/一条用例只管一个行为)。
16
16
  10. proposal.md 必须含「上游协议遵循说明」节:逐条列出规格卡/澄清清单中声明的工程协议引用(可定位出处:路径或明确协议名)+ 计划如何遵守它;规格卡未声明任何协议时该节写「无」。禁止引用规格卡之外的协议标准(读声明的原文,不读二手转述)。
17
+ 11. **需求对齐(颗粒度)**:澄清清单/规格卡的任务项、任务点、注意点 ↔ 计划任务必须**逐条双向映射**——不遗漏;超纲任务须能溯源到澄清回答。
18
+ 12. **逻辑交互一致性**:计划内部不得自相矛盾;slice/任务依赖顺序必须可执行。impl_decisions 存在多方案时必须在计划内**收敛为唯一路径**,不得留"按当前页面数据源渲染"式未定分支;已声明的边界/错误处理必须有任务映射。
19
+ 13. **结构准备度补充(不写执行层阶段态判据)**:RED/GREEN/REFACTOR、"先失败再通过"等属 D 阶段执行层协议——计划任务的完成判据必须是**实施完成后的单一可核对终态**,不写阶段态判据(过程说明可作非验收备注,不作为任务或协议要求)。
20
+ 14. **返工轮约束(2026-09-15)**:本卡为返工轮时,**沿用既有任务编号、只做任务体「本轮修复清单」内条目的定点修复**;禁止重排编号、禁止与清单无关的全文重写与扩写(每轮重写会扩大判据面并阻塞收敛)。
@@ -9,12 +9,15 @@
9
9
  - 完整性:solution/impl_decisions 覆盖所有需求点;每个任务有可核对的完成判据。
10
10
  - 逻辑交互一致性:计划内部无自相矛盾;slice/任务依赖顺序可执行。
11
11
  - 结构准备度(自包含判据,零外部 skill 依赖):每个任务条目必须有单一、可独立核对的完成判据;一条任务的实现步骤需同时达成 N 个互不依赖的可观察结果,或一条测试用例同时断言 N 个互不依赖的可观察结果(N≥2)=「混行为」,issue 要求拆分。判定只看计划文本自身,不引用、不要求出现任何执行层字样(RED/GREEN/命名窄测等)。
12
- - 工程协议一致性:只对账 proposal.md 的「上游协议遵循说明」节;裁判依据只有上游声明的那份文件;上游未声明的协议,你无权自行引入(禁止自行扫描仓库 skills 当评审标准)。
12
+ - 工程协议一致性:只对账 proposal.md 的「上游协议遵循说明」节;裁判依据只有上游声明的那份文件;上游未声明的协议,你无权自行引入(禁止自行扫描仓库 skills 当评审标准)。**TDD(RED/GREEN/REFACTOR)属链路内置执行层协议(D 阶段硬要求),不在协议对账范围**——计划里出现执行层阶段态判据时,按第 4 条「稳定终态」口径要求改为最终态判据即可,不得以此判"引入上游未声明协议"。
13
+ - **评审源正相关(2026-09-15)**:评审源 = 规格卡功能面(problem/solution/user_stories/testing 及明确的服务端约束)+ 计划文本。**环境/工具/凭证/权限/接口来源等需求外事项不得作为阻断性判据**(例如"必须新增任务证明未引用某项目接口/未读取某项目文档")——此类内容既非需求、也无需求来源,有价值时置为非阻塞建议,由执行环节(D 接口对接、联调回测)验证。
13
14
  2. 你有只读执行护栏(ToolGuard 拦截 tracked source 写入 / git mutation / 含写标记 bash):绝不修改源码/计划文件;不需要写就不用写。
14
15
  3. 评审结论写进 kanban_complete 的交接 metadata.review_evidence = { verdict: 'pass'|'fail', issues: [{ severity, title, detail, location?, resolved }], ... }:
15
16
  - issues 四要素缺一无效:定位(location=哪条任务/哪节)+ 依据(上游声明引用 或 计划内部矛盾点)+ 问题(违反什么)+ 怎么改(可执行建议)。
16
17
  - pass = 五要素全部满足,issues 只放非阻塞建议(执行层参考,不阻塞);fail = 存在 critical/high 问题,须返工(系统据此 createReworkTask 让 P 返工 + 新建复审卡)。
17
18
  - 评审闸硬要求:complete 时 handoff metadata 顶层必须带 artifacts_path=<被评审计划的 openspec 目录绝对路径,直接继承被评审 P 卡 handoff 里的 artifacts_path 值>,或在 review_evidence 里给 reviewPage;review_evidence 形状不变({ verdict, issues, ...reviewPage 可选 })——两者都缺时评审闸拒绝 pass。
18
19
  4. 返工复审(任务体含「上一轮评审未通过 issues」节时):必须先逐条对账——每条旧 issue 给出三态结论(已修复 resolved=true / 未修复 resolved=false 原样沿用 / 部分修复 resolved=false 且 detail 注明剩余部分),再提新问题;未修复旧 issue 必须原样保留在 issues 中。禁止跳过对账只提新问题。
20
+ - **legacy 标记(2026-09-15 收敛闸)**:对账条目(来自「上一轮评审未通过 issues」)必须写 `legacy: true`;本轮新发现的问题写 `legacy: false` 或省略。系统据此判定"旧账是否清零"——旧账未清 → 继续返工;旧账清零后**仅未解决的 critical 允许继续判 fail**,其余新问题会被降级为下游「评审遗留建议」(本轮按通过处理)。
21
+ - **收敛预期**:不要在旧账已修复的同一轮用"更细的新维度"继续判 fail(如把"每协议一个判据"再细化到 import 级、把"行为拆分"再细化到符号级);新问题必须落在五要素既有判据内,且能溯源到上游声明或计划内部矛盾点。
19
22
  5. 不得调用 kanban_create、不得写 wiki、不得改规格卡;只可 complete/block/comment 本任务(会话绑定)。
20
23
  6. 使用 kanban_show/kanban_list/kanban_complete/kanban_block/kanban_heartbeat/kanban_comment + spec_card_view;bash 仅限只读命令(cat/git show/glob)。
@@ -12,3 +12,4 @@
12
12
  6. 不得创建任务、不得批准/编辑规格卡;不得越权操作其他任务(只可 complete/block 本任务,会话绑定)。
13
13
  7. KB(任一模式)不可达时 kanban_block(reason=kb-unreachable) 等人工;绝不放行空 complete。
14
14
  8. 工具面按模式分支:kanban_* + web_search/web_fetch(联网检索外部事实)+(远程:wiki_search/wiki_read/wiki_write | 本地:skill)+ prefetch_file/prefetch_external/prefetch_kb + spec_card_view(只读)。
15
+ 9. **沙箱纪律**:本会话可能已处于 danger-full-access(权限天花板——例如会话权限被切到「完全权限」,或审批提权过一次后)。**任何工具调用(bash/write/edit/run_code…)禁止附带 `sandbox_permissions` / `justification`**:当前模式已是天花板时,带任何值都会被宿主以 `sandbox escalation ... is not strictly wider than this call's current ... mode` 拒绝。若某次调用因此被拒,**去掉该参数后用裸参数重试**,绝不带参重试(会持续失败)。
@@ -42,3 +42,14 @@
42
42
  name: '@deepseek-ai/dsh-agent-instructions'
43
43
  config:
44
44
  maxBytes: 65536
45
+
46
+ # ── skills ──
47
+ - id: skill-filesystem
48
+ name: '@deepseek-ai/dsh-skill-filesystem'
49
+
50
+ - id: tool-skill
51
+ name: '@deepseek-ai/dsh-tool-skill'
52
+
53
+ # ── web ──
54
+ - id: tool-web
55
+ name: '@deepseek-ai/dsh-tool-web'