@joekytc/dsh-swarm 0.3.0 → 0.3.2

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.
package/README.md CHANGED
@@ -20,6 +20,7 @@ Swarm mode turns your main session into a **team lead**: **you state the require
20
20
 
21
21
  - **No commands to memorize** — just state your requirement; no `/plan:` or `/openspec:` prefixes needed.
22
22
  - **Automatic intent recognition** — development requests → clarify/plan and build a chain; lessons & retrospectives → persist to memory; group notifications → deliver to WeCom; Q&A / chit-chat → answered directly.
23
+ - **Free delivery** — `/sms <intent>` (e.g. "post current progress to the group"): facts are grounded via kanban lookup, then the body is composed per intent and delivered; `-s` or wording like "private chat" targets the DM. Group and private-chat targets must each be exactly one (0 or 2+ targets error out; clean up in dsh-im settings or set `imDelivery.dmTargetId`).
23
24
  - **Confirmation gate against accidental chains** — after the checklist is saved, a chain is only built once you reply with an explicit affirmative (`确认` / `开干` / `开跑` / `开始` / `go`, etc.); vague replies, topic switches, or edit-only feedback count as *not confirmed*.
24
25
  - **The lead is read-only** — the main session cannot write/edit repo sources, nor run git mutations (push/commit/checkout…); writing code is done by the executor (D) in an isolated workspace by design.
25
26
  - **Progress is always actually queried** — ask "how is it going?" anytime and the lead reports from real kanban lookups, never fabricated.
package/README.zh-CN.md CHANGED
@@ -24,6 +24,7 @@ SSE 把全部状态流式同步到浏览器。设计灵感源自 [Hermes Agent k
24
24
 
25
25
  - **免记命令** —— 直接说需求即可,不需要 `/plan:`、`/openspec:` 前缀。
26
26
  - **意图自动识别** —— 开发需求 → 进入澄清规划并建链;沉淀经验/复盘 → 记入记忆库;发通知到群 → 投递企微;问答/闲聊 → 直接回答。
27
+ - **自由投递** —— `/sms <意图>`(如「把当前进度发到群」):先实查看板取事实基准,再按意图生成正文投递;`-s` 或自然语言「私聊」→ 发送私聊。群聊/私聊目标各自仅且只有一个(0 目标或多个目标均报错,到 dsh-im 设置清理或配置 `imDelivery.dmTargetId`)。
27
28
  - **确认闸防误建** —— 规划清单落库后,必须等你明确回复「确认 / 开干 / 开跑 / 开始 / go」等肯定语义才会建链;模糊、岔开话题、只提修改意见 = 未确认。
28
29
  - **领队只读** —— 主会话不能写/改仓库源码,也不能执行 git 变更(push/commit/checkout 等);写代码由流水线中的执行者(D)在隔离工作区完成,这是设计使然。
29
30
  - **进度实查** —— 任何时刻问「进度怎么样」,领队都以看板实查结果播报,绝不虚构。
package/lib/config.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface KanbanConfig {
64
64
  enabled: boolean;
65
65
  botId: string;
66
66
  targetId: string;
67
+ /** 私聊目标(自由投递 /sms -s):留空=自动发现唯一 kind:'user' 已保存目标,仅且只有一个。 */
68
+ dmTargetId: string;
67
69
  };
68
70
  /** 评审引擎双模:delegate=沿用各角色自有模型评审;managed=统一经 dsh「模型链」评审。
69
71
  * managed.provider/model = dsh「模型链」llm-catalog 的 provider/model id,wire 时写成 ocr 自定义 provider(dsh-managed);key 不落本配置。 */
package/lib/config.js CHANGED
@@ -61,7 +61,8 @@ export const Config = Schema.object({
61
61
  enabled: Schema.boolean().default(false),
62
62
  botId: Schema.string().default(''),
63
63
  targetId: Schema.string().default(''),
64
- }).default({ enabled: false, botId: '', targetId: '' }),
64
+ dmTargetId: Schema.string().default(''),
65
+ }).default({ enabled: false, botId: '', targetId: '', dmTargetId: '' }),
65
66
  reviewEngine: Schema.object({
66
67
  mode: Schema.union(['delegate', 'managed']).default('delegate'),
67
68
  managed: Schema.object({
@@ -5,7 +5,11 @@ export interface LearningEntry {
5
5
  evidence: string;
6
6
  tags: string[];
7
7
  }
8
+ /** 五类有效经验类别标签(0.3.1 判据):tags 必含其一,防「为出经验而出」的假沉淀。 */
9
+ export declare const LEARNING_CATEGORY_TAGS: readonly ["mistake", "reusable", "env-trap", "collab-contract", "efficiency"];
8
10
  export declare function validateLearning(raw: unknown): string[];
11
+ /** A 类犯错教训机械信号累计(评审失败/任务阻塞/审计警告事件 + 返工卡)。brief 统计行与 save 硬闸共用同一口径。 */
12
+ export declare function countLearningSignals(state: BoardState, chainId: string): number;
9
13
  export declare function formatLearningBody(entry: LearningEntry, created?: Date): string;
10
14
  export interface MemoryIndexEntry {
11
15
  kind: 'learning' | 'doc';
@@ -16,7 +20,8 @@ export declare function buildMemoryIndexBlock(entries: MemoryIndexEntry[]): stri
16
20
  export declare function weightedRank<T>(items: T[], scoreOf: (t: T) => number, timeOf: (t: T) => number): T[];
17
21
  export declare function buildRepoSlug(workspaceDir: string): string;
18
22
  export type { BoardState };
19
- /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。 */
23
+ /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。
24
+ * 头部带信号统计(判据 A 准入:累计 ≥2 次才可沉淀犯错教训,模型不用自己数)。 */
20
25
  export declare function buildLearningBrief(state: BoardState, chainId: string): string;
21
26
  /** /learning rest → 链解析:空→最近链;精确 id→命中;子串匹配→单命中或候选列表(≤3);无→null。 */
22
27
  export declare function resolveLearningChainId(state: BoardState, rest: string): {
@@ -1,4 +1,6 @@
1
1
  import { buildChecklistSlug } from '../wiki/page-path.js';
2
+ /** 五类有效经验类别标签(0.3.1 判据):tags 必含其一,防「为出经验而出」的假沉淀。 */
3
+ export const LEARNING_CATEGORY_TAGS = ['mistake', 'reusable', 'env-trap', 'collab-contract', 'efficiency'];
2
4
  export function validateLearning(raw) {
3
5
  const errors = [];
4
6
  if (typeof raw !== 'object' || raw === null)
@@ -14,8 +16,18 @@ export function validateLearning(raw) {
14
16
  errors.push('learning.tags must be an array of strings');
15
17
  if (Array.isArray(l['tags']) && l['tags'].some((v) => typeof v !== 'string'))
16
18
  errors.push('learning.tags must be an array of strings');
19
+ // 类别硬闸(报错文案即教学):tags 必含五类之一;样式/文案/字段名等一次性平凡变更不构成经验
20
+ if (Array.isArray(l['tags']) && l['tags'].every((v) => typeof v !== 'string' || !LEARNING_CATEGORY_TAGS.includes(v))) {
21
+ errors.push('learning.tags 必须含类别标签之一:mistake(犯错教训:链上阻塞/返工/评审失败/审计警告累计≥2) / reusable(可复用模式:同类流程或行为重复≥2) / env-trap(环境陷阱:踩坑1次+报错diff等硬证据) / collab-contract(协作契约:用户偏好≥2次确认) / efficiency(效率模式:量化省力依据);样式/文案/字段名等一次性平凡变更不构成经验,五类均不满足应回复「无新经验」');
22
+ }
17
23
  return errors;
18
24
  }
25
+ /** A 类犯错教训机械信号累计(评审失败/任务阻塞/审计警告事件 + 返工卡)。brief 统计行与 save 硬闸共用同一口径。 */
26
+ export function countLearningSignals(state, chainId) {
27
+ const chainEvents = state.events.filter((e) => e.chainId === chainId);
28
+ const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
29
+ return chainEvents.filter((e) => e.kind === 'review/failed' || e.kind === 'task/blocked' || e.kind === 'chain/audit-warning').length + reworks.length;
30
+ }
19
31
  export function formatLearningBody(entry, created = new Date()) {
20
32
  const date = created.toISOString().slice(0, 10);
21
33
  const lines = [
@@ -76,12 +88,22 @@ function chainContext(state, chainId) {
76
88
  const problem = spec?.sections.problem ? spec.sections.problem.split('\n')[0].slice(0, 200) : '';
77
89
  return [chain.title, problem].filter(Boolean).join(' — ');
78
90
  }
79
- /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。 */
91
+ /** 机械提取四类信号(事件流/投影,禁 LLM 猜测),渲染紧凑 markdown。
92
+ * 头部带信号统计(判据 A 准入:累计 ≥2 次才可沉淀犯错教训,模型不用自己数)。 */
80
93
  export function buildLearningBrief(state, chainId) {
81
94
  const header = ['## 链上下文', chainContext(state, chainId), ''];
82
- const sections = [];
83
95
  const chainEvents = state.events.filter((e) => e.chainId === chainId);
84
96
  const reviewFailed = chainEvents.filter((e) => e.kind === 'review/failed');
97
+ const blocked = chainEvents.filter((e) => e.kind === 'task/blocked');
98
+ const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
99
+ const audit = chainEvents.filter((e) => e.kind === 'chain/audit-warning');
100
+ if (countLearningSignals(state, chainId) === 0) {
101
+ return [...header, '(无机械信号。A 类犯错教训判据不满足;B/D 需重复出现 ≥2 次,C 需报错/diff 等硬证据,E 需量化省力依据,均不满足则回复「无新经验」)'].join('\n');
102
+ }
103
+ const total = countLearningSignals(state, chainId);
104
+ const sections = [
105
+ ['### 信号统计(A 类准入:累计 ≥2 次)', `评审失败 ${reviewFailed.length} / 任务阻塞 ${blocked.length} / 返工卡 ${reworks.length} / 审计警告 ${audit.length},累计 ${total} 次`].join('\n'),
106
+ ];
85
107
  if (reviewFailed.length > 0) {
86
108
  const lines = ['### 评审失败'];
87
109
  for (const e of reviewFailed.slice(-5)) {
@@ -91,7 +113,6 @@ export function buildLearningBrief(state, chainId) {
91
113
  }
92
114
  sections.push(lines.join('\n'));
93
115
  }
94
- const blocked = chainEvents.filter((e) => e.kind === 'task/blocked');
95
116
  if (blocked.length > 0) {
96
117
  const lines = ['### 任务阻塞'];
97
118
  for (const e of blocked.slice(-5)) {
@@ -101,14 +122,12 @@ export function buildLearningBrief(state, chainId) {
101
122
  }
102
123
  sections.push(lines.join('\n'));
103
124
  }
104
- const reworks = [...state.tasks.values()].filter((t) => t.chainId === chainId && t.reworkOfTaskId !== null);
105
125
  if (reworks.length > 0) {
106
126
  const lines = ['### 返工卡'];
107
127
  for (const t of reworks.slice(-5))
108
128
  lines.push(`- [返工×${t.reviewAttempt}] ${t.title}(原卡 ${t.reworkOfTaskId})`);
109
129
  sections.push(lines.join('\n'));
110
130
  }
111
- const audit = chainEvents.filter((e) => e.kind === 'chain/audit-warning');
112
131
  if (audit.length > 0) {
113
132
  const lines = ['### 审计警告'];
114
133
  for (const e of audit.slice(-5)) {
@@ -118,8 +137,6 @@ export function buildLearningBrief(state, chainId) {
118
137
  }
119
138
  sections.push(lines.join('\n'));
120
139
  }
121
- if (sections.length === 0)
122
- return [...header, '(无机械信号,可基于对话观察蒸馏)'].join('\n');
123
140
  return [...header, ...sections].join('\n');
124
141
  }
125
142
  /** /learning rest → 链解析:空→最近链;精确 id→命中;子串匹配→单命中或候选列表(≤3);无→null。 */
@@ -7,8 +7,12 @@ export interface OcrArgsInput {
7
7
  to?: string;
8
8
  commit?: string;
9
9
  paths?: string[];
10
+ /** 业务上下文,托管评审时提升评审质量(仅 managed 分支透传)。 */
11
+ background?: string;
10
12
  }
11
- /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review 并恒带 JSON 输出。 */
13
+ /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review
14
+ * preview 默认输出 text,必须显式 --format json 才能拿到可解析输出;
15
+ * managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。 */
12
16
  export declare function buildOcrArgs(sub: OcrSub, a: OcrArgsInput): string[];
13
17
  /** 预览结果归一化结构。 */
14
18
  export interface PreviewResult {
@@ -23,7 +27,7 @@ export interface PreviewResult {
23
27
  }[];
24
28
  mergeBase: string | null;
25
29
  }
26
- /** 解析 delegate preview 的 JSON 输出,失败或结构异常时回退 unknown。 */
30
+ /** 解析 delegate preview 的 JSON 输出(官方字段 reviewable_files/excluded_files/exclude_reason),失败或结构异常时回退 unknown。 */
27
31
  export declare function parsePreviewJson(stdout: string): PreviewResult;
28
32
  /** 托管评审结果归一化结构。 */
29
33
  export interface ManagedResult {
@@ -1,11 +1,17 @@
1
- /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review 并恒带 JSON 输出。 */
1
+ /** 构造 OCR CLI 参数:preview/rule 走 delegate,managed 走 review
2
+ * preview 默认输出 text,必须显式 --format json 才能拿到可解析输出;
3
+ * managed 恒带 --audience agent(agent 场景标准参数,抑制 progress 输出)。 */
2
4
  export function buildOcrArgs(sub, a) {
3
5
  if (sub === 'preview') {
4
- const args = ['delegate', 'preview'];
5
- if (a.from)
6
- args.push('--from', a.from);
7
- if (a.to)
8
- args.push('--to', a.to);
6
+ const args = ['delegate', 'preview', '--format', 'json'];
7
+ if (a.commit)
8
+ args.push('--commit', a.commit);
9
+ else if (a.from || a.to) {
10
+ if (a.from)
11
+ args.push('--from', a.from);
12
+ if (a.to)
13
+ args.push('--to', a.to);
14
+ }
9
15
  if (a.repo)
10
16
  args.push('--repo', a.repo);
11
17
  return args;
@@ -22,7 +28,9 @@ export function buildOcrArgs(sub, a) {
22
28
  if (a.to)
23
29
  args.push('--to', a.to);
24
30
  }
25
- args.push('--format', 'json');
31
+ args.push('--format', 'json', '--audience', 'agent');
32
+ if (a.background)
33
+ args.push('--background', a.background);
26
34
  return args;
27
35
  }
28
36
  /** 解析失败的兜底结果。 */
@@ -37,7 +45,7 @@ function pathOf(el) {
37
45
  function asArray(v) {
38
46
  return Array.isArray(v) ? v.filter(isRecord) : [];
39
47
  }
40
- /** 解析 delegate preview 的 JSON 输出,失败或结构异常时回退 unknown。 */
48
+ /** 解析 delegate preview 的 JSON 输出(官方字段 reviewable_files/excluded_files/exclude_reason),失败或结构异常时回退 unknown。 */
41
49
  export function parsePreviewJson(stdout) {
42
50
  let obj;
43
51
  try {
@@ -51,8 +59,8 @@ export function parsePreviewJson(stdout) {
51
59
  const mergeBase = obj.merge_base ?? obj.mergeBase;
52
60
  return {
53
61
  mode: String(obj.mode ?? 'unknown'),
54
- files: asArray(obj.files).map((el) => ({ path: pathOf(el), status: String(el.status ?? '') })),
55
- excluded: asArray(obj.excluded).map((el) => ({ path: pathOf(el), reason: String(el.reason ?? '') })),
62
+ files: asArray(obj.reviewable_files).map((el) => ({ path: pathOf(el), status: String(el.status ?? '') })),
63
+ excluded: asArray(obj.excluded_files).map((el) => ({ path: pathOf(el), reason: String(el.exclude_reason ?? '') })),
56
64
  mergeBase: mergeBase == null ? null : String(mergeBase),
57
65
  };
58
66
  }
@@ -68,7 +76,8 @@ export function parseManagedJson(stdout) {
68
76
  if (!isRecord(obj))
69
77
  return { status: 'unknown', comments: [] };
70
78
  const comments = asArray(obj.comments).map((el) => {
71
- const rawLine = el.line ?? el.startLine;
79
+ // 官方 schema 行号字段为 snake_case start_line,line/startLine 仅作容错别名
80
+ const rawLine = el.start_line ?? el.line ?? el.startLine;
72
81
  const line = Number(rawLine);
73
82
  const message = String(el.content ?? el.message ?? el.body ?? '');
74
83
  return {
@@ -1,3 +1,4 @@
1
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
1
2
  import { execFile } from 'node:child_process';
2
3
  import { writeFileSync } from 'node:fs';
3
4
  import { join } from 'node:path';
@@ -72,13 +73,17 @@ const OCR_WIRE_DEGRADED = '未能从 dsh 解析该提供方的接入信息(bas
72
73
  * 从宿主设置解析所选提供方的接入信息(baseUrl/协议/apiKey)。
73
74
  * 事实核查结论(实现期探查):llm 服务的公开 API 不暴露连接事实,但同进程可经
74
75
  * settings 服务的 describe() 读到模型适配器的 provider profile——形如
75
- * { providers: { <id>: { baseURL, api, apiKey | apiKeyEnv } } }(apiKeyEnv 指向进程环境变量名)。
76
+ * { providers: { <id>: { baseURL, api, apiKey | apiKeyEnv } } }(apiKeyEnv 指向凭据引用名)。
77
+ * 踩坑:apiKeyEnv 名下的 key 不一定在进程 env——Models 页写入的凭据存在
78
+ * credentials 服务的 managed store(ctx.credentials)里;解析顺序须先进程 env、
79
+ * 再 ctx.credentials.resolve(env → managed store → $DSH_HOME/.env,per-call 不缓存)。
76
80
  * 命中 profile 但字段不全时跳过该 descriptor 继续尝试下一个(多 descriptor 场景勿误降级),
77
81
  * 绝不回传半套配置;全部不合才返回 null 走降级。apiKey 只透传给 ocr config,不落日志。
78
82
  */
79
- function resolveDshProviderAccess(ctx, providerId) {
83
+ async function resolveDshProviderAccess(ctx, providerId) {
80
84
  try {
81
85
  const settings = ctx.get('settings');
86
+ const creds = ctx.get('credentials');
82
87
  const descriptors = settings?.describe?.() ?? [];
83
88
  for (const d of descriptors) {
84
89
  const providers = d.value?.providers;
@@ -89,8 +94,13 @@ function resolveDshProviderAccess(ctx, providerId) {
89
94
  const api = typeof profile.api === 'string' ? profile.api : '';
90
95
  const protocol = api.startsWith('openai') ? 'openai' : api.includes('anthropic') ? 'anthropic' : '';
91
96
  let apiKey = typeof profile.apiKey === 'string' ? profile.apiKey : '';
92
- if (!apiKey && typeof profile.apiKeyEnv === 'string')
97
+ if (!apiKey && typeof profile.apiKeyEnv === 'string') {
93
98
  apiKey = process.env[profile.apiKeyEnv] ?? '';
99
+ if (!apiKey && creds?.resolve) {
100
+ const r = await creds.resolve(credentialRef(profile.apiKeyEnv));
101
+ apiKey = r?.value ?? '';
102
+ }
103
+ }
94
104
  if (baseUrl && protocol && apiKey)
95
105
  return { baseUrl, protocol, apiKey };
96
106
  continue; // 字段不全:换下一个 descriptor,勿在此误降级
@@ -330,7 +340,7 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config, o
330
340
  json(res, 200, { ok: false, log: INSTALL_GUIDANCE });
331
341
  return;
332
342
  }
333
- const access = resolveDshProviderAccess(ctx, providerId);
343
+ const access = await resolveDshProviderAccess(ctx, providerId);
334
344
  if (!access) {
335
345
  json(res, 200, { ok: false, log: OCR_WIRE_DEGRADED });
336
346
  return;
@@ -38,7 +38,9 @@ export interface OpenspecPlanningInput {
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 派生,歧义/未找到时注入主 agent。 */
41
+ /** /learning 零副作用引导文案:命令串从 config 派生,歧义/未找到时注入主 agent。
42
+ * 五类准入判据(0.3.1):A 犯错教训 / B 可复用模式 / C 环境陷阱 / D 协作契约 / E 效率模式,
43
+ * 全不满足 → 「无新经验」;样式/文案/字段名等一次性平凡变更明确排除。 */
42
44
  export declare function buildLearningGuidance(routes: PrefixRoutes): string;
43
45
  /** v2:/learning 零副作用——不建链建卡,仅机械提取证据包供主 agent 蒸馏。歧义返回候选列表,链不存在返回错误文本(不 throw)。 */
44
46
  export declare function handleLearningRoute(message: string, service: KanbanService, cfg: PrefixRoutes, _ownerSessionId: string): Promise<PrefixRouteResult>;
@@ -66,14 +66,21 @@ async function waitFirstCard(service, chainId) {
66
66
  await new Promise((r) => setTimeout(r, Math.min(OPENSPEC_FIRST_CARD.pollIntervalMs, Math.max(1, deadline - Date.now()))));
67
67
  }
68
68
  }
69
- /** /learning 零副作用引导文案:命令串从 config 派生,歧义/未找到时注入主 agent。 */
69
+ /** /learning 零副作用引导文案:命令串从 config 派生,歧义/未找到时注入主 agent。
70
+ * 五类准入判据(0.3.1):A 犯错教训 / B 可复用模式 / C 环境陷阱 / D 协作契约 / E 效率模式,
71
+ * 全不满足 → 「无新经验」;样式/文案/字段名等一次性平凡变更明确排除。 */
70
72
  export function buildLearningGuidance(routes) {
71
73
  return [
72
74
  '## 经验蒸馏指令(' + routes.learning + ')',
73
- '消化上方「链上下文 + 机械信号证据包」,蒸馏 1-3 条可复用经验(返工根因 / 阻塞原因 / 审计教训)。',
74
- '每条约成 LearningEntry(title 一句话≤80 字符;lesson 教训;evidence 必须填本链 chain id 作机械证据;tags 自由标签)。',
75
- ' planning_learning_save:scope=chain 存需求级 projects/<repoSlug>/<chainId>/learnings/;仓库通用经验用 scope=project(projects/<repoSlug>/learnings/,repoSlug 由链 workspaceDir 派生)。',
76
- '无值得沉淀的经验时,明确回复「无新经验」,不要硬凑。',
75
+ '消化上方「链上下文 + 机械信号证据包」,逐条对照以下准入判据,只沉淀满足其一的经验;全部不满足 明确回复「无新经验」,禁止硬凑。',
76
+ 'A【犯错教训】链上 阻塞/返工/评审失败/审计警告 累计 ≥2 次(见信号统计):lesson 写根因 + 下次如何避免,并引用证据包中的具体信号(哪张卡/哪个环节)。tags mistake。',
77
+ 'B【可复用模式】同类动作/流程/逻辑重复出现 ≥2 次:lesson 列出重复出现的具体步骤或行为模式,达到照着做就能复现的可复制程度。tags reusable。',
78
+ 'C【环境陷阱】环境/工具的客观事实坑(配置语法/依赖版本/构建链路/生效条件)踩 1 次即可沉淀:lesson 写触发条件 + 正确做法,evidence 附报错原文/diff/命令输出等机械痕迹。tags 加 env-trap。',
79
+ 'D【协作契约】用户反复确认的流程偏好与验收口径(≥2 次明确表达或被执行):lesson 写成可执行规则(何时、做什么、算完成)。tags 加 collab-contract。',
80
+ 'E【效率模式】实测省轮次/省时间的动作序列:必须给量化依据(省几轮/避免几次返工/少多少排查),无量化不沉淀。tags 加 efficiency。',
81
+ '排除:样式/文案/字段名等一次性平凡变更不构成经验(除非引发 A 类信号);只出现 1 次且无失败信号、又不满足 C/E 类证据要求的观察不沉淀。',
82
+ '每条 LearningEntry:title 一句话≤80 字符;evidence 必须填本链 chain id;tags 按上述类别标记。调 planning_learning_save 入库。',
83
+ 'scope=chain 存需求级 projects/<repoSlug>/<chainId>/learnings/;仓库通用经验用 scope=project(projects/<repoSlug>/learnings/,repoSlug 由链 workspaceDir 派生)。',
77
84
  ].join('\n');
78
85
  }
79
86
  /** v2:/learning 零副作用——不建链建卡,仅机械提取证据包供主 agent 蒸馏。歧义返回候选列表,链不存在返回错误文本(不 throw)。 */
@@ -31,14 +31,20 @@ export interface ImDeliveryOptions {
31
31
  retryDelaysMs?: number[];
32
32
  /** 手动投递路径(/sms):失败仅 dispatcher.log 留痕,不写 chain/im-delivery-failed 链事件(用户同步可见错误)。 */
33
33
  manual?: boolean;
34
+ /** 投递目标类型(0.3.x 自由投递):group=群聊;user=私聊(仅且只有一个已保存目标)。缺省 group。 */
35
+ targetKind?: 'group' | 'user';
34
36
  }
37
+ /** dsh-im 未安装的可识别错误前缀(0.3.1:缺插件属环境问题不重试,直接友好提醒安装)。 */
38
+ export declare const DSH_IM_MISSING_PREFIX = "dsh-im-not-installed";
39
+ export declare const DSH_IM_MISSING_GUIDANCE = "\u672A\u68C0\u6D4B\u5230 dsh-im \u63D2\u4EF6\uFF0C\u65E0\u6CD5\u6295\u9012\u4F01\u5FAE\u6D88\u606F\u3002\u8BF7\u5148\u5B89\u88C5\u5E76\u542F\u7528 @xmanrui/dsh-im \u63D2\u4EF6\uFF08\u5B89\u88C5\u540E\u91CD\u542F dsh \u751F\u6548\uFF09\uFF0C\u518D\u91CD\u8BD5\u6295\u9012\u3002";
35
40
  export declare function isDshImLike(svc: unknown): svc is DshImLike;
36
- /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存群目标;
37
- * 发现异常返回 error(调用方留痕不投,fail-closed——投错群比不投更糟)。 */
41
+ /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存目标(按 kind)。
42
+ * 群/私聊各自仅且只有一个——发现异常返回 error(调用方留痕不投,fail-closed——投错对象比不投更糟)。 */
38
43
  export declare function resolveTarget(im: DshImLike, cfg: {
39
44
  botId: string;
40
45
  targetId: string;
41
- }): Promise<{
46
+ dmTargetId?: string;
47
+ }, kind: 'group' | 'user'): Promise<{
42
48
  botId: string;
43
49
  targetId: string;
44
50
  } | {
@@ -76,6 +82,15 @@ export declare function resolveReportChainId(state: BoardState, variant: ReportV
76
82
  title: string;
77
83
  }>;
78
84
  };
85
+ export type ParsedSendRequest = {
86
+ variant: 'blocked' | 'completion' | 'free';
87
+ query: string;
88
+ dm: boolean;
89
+ };
90
+ /** /sms rest 三岔判定(纯函数,不查看板状态):先剥独立 '-s' token('-sx' 粘连不算,防误伤正文);
91
+ * blocked 前缀 → 链阻塞汇报(词边界:'blockedx' 等粘连 token 是正文不算);空 → 最近完成链;
92
+ * 其余非空 → free(是否真指链由调用方 resolveReportChainId 复判——显式 id 是强信号,先链后自由)。 */
93
+ export declare function parseSendRequest(rest: string): ParsedSendRequest;
79
94
  /** /sms 手动投递:解析链 → 领域函数渲染正文(红线:正文只出自 buildCompletionMessage/buildBlockMessage,
80
95
  * 绝不返回给模型)→ createSender 发送。不受 imDelivery.enabled 门控(显式人工调用即意图),
81
96
  * 但仍要求 dshIm 服务在位且形状合法、目标可解析(同 auto 路径 fail-closed 规则)。 */
@@ -4,6 +4,10 @@ import { join } from 'node:path';
4
4
  import { buildCompletionMessage, buildBlockMessage } from '../domain/im-message.js';
5
5
  const RETRYABLE_CODES = new Set(['bot-not-connected', 'delivery-failed']);
6
6
  const DEFAULT_RETRY_DELAYS_MS = [5_000, 10_000, 15_000];
7
+ /** dsh-im 未安装的可识别错误前缀(0.3.1:缺插件属环境问题不重试,直接友好提醒安装)。 */
8
+ export const DSH_IM_MISSING_PREFIX = 'dsh-im-not-installed';
9
+ const DSH_IM_MISSING_ERROR = `${DSH_IM_MISSING_PREFIX}: 未检测到 dsh-im 插件服务,请先安装并启用 @xmanrui/dsh-im(安装后重启 dsh 生效)`;
10
+ export const DSH_IM_MISSING_GUIDANCE = '未检测到 dsh-im 插件,无法投递企微消息。请先安装并启用 @xmanrui/dsh-im 插件(安装后重启 dsh 生效),再重试投递。';
7
11
  export function isDshImLike(svc) {
8
12
  const s = svc;
9
13
  return typeof s === 'object' && s !== null
@@ -19,9 +23,9 @@ function resolveDshIm(ctx, log) {
19
23
  }
20
24
  return svc;
21
25
  }
22
- /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存群目标;
23
- * 发现异常返回 error(调用方留痕不投,fail-closed——投错群比不投更糟)。 */
24
- export async function resolveTarget(im, cfg) {
26
+ /** botId/targetId 解析(评审决议):配置显式指定优先;留空自动发现唯一 wecom bot + 唯一已保存目标(按 kind)。
27
+ * 群/私聊各自仅且只有一个——发现异常返回 error(调用方留痕不投,fail-closed——投错对象比不投更糟)。 */
28
+ export async function resolveTarget(im, cfg, kind) {
25
29
  let botId = cfg.botId.trim();
26
30
  if (!botId) {
27
31
  const bots = await im.listBots();
@@ -30,17 +34,22 @@ export async function resolveTarget(im, cfg) {
30
34
  return { error: `企微机器人数量=${wecom.length}(期望 1),请在插件配置 imDelivery.botId 显式指定` };
31
35
  botId = wecom[0].botId;
32
36
  }
33
- let targetId = cfg.targetId.trim();
37
+ const explicit = (kind === 'group' ? cfg.targetId : cfg.dmTargetId ?? '').trim();
38
+ let targetId = explicit;
34
39
  if (!targetId) {
35
40
  const raw = await im.listTargets(botId);
36
41
  // 宿主同 Host 服务返回裸数组(PROACTIVE_DELIVERY.md:176);防御兼容 Connection RPC `target.list` 信封形状。
37
42
  const targets = Array.isArray(raw) ? raw : Array.isArray(raw?.targets)
38
43
  ? raw.targets
39
44
  : [];
40
- const groups = targets.filter((x) => x.kind === 'group');
41
- if (groups.length !== 1)
42
- return { error: `已保存群目标数量=${groups.length}(期望 1),请到 dsh-im 设置→IM机器人 新建目标或在插件配置 imDelivery.targetId 显式指定` };
43
- targetId = groups[0].targetId;
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;
44
53
  }
45
54
  return { botId, targetId };
46
55
  }
@@ -85,12 +94,24 @@ function makeDefaultLog(storageDir) {
85
94
  export function createSender(ctx, kanban, configProvider, opts = {}) {
86
95
  const log = opts.log ?? makeDefaultLog(deriveStorageDir(configProvider));
87
96
  const delays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
97
+ const targetKind = opts.targetKind ?? 'group';
88
98
  return async (chainId, text) => {
89
99
  const cfg = configProvider.getEffective().imDelivery;
90
100
  const im = resolveDshIm(ctx, log);
91
- if (!im)
92
- return { ok: false, error: 'dshIm 服务缺失或形状不符(需 @xmanrui/dsh-im 宿主服务)' };
93
- const t = await resolveTarget(im, cfg);
101
+ if (!im) {
102
+ // 缺插件=环境问题(0.3.1):不进重试,友好提醒安装。auto 路径写链事件让 GUI 可见;manual 路径错误同步返回。
103
+ log(`[im-delivery] FAILED chain=${chainId}: ${DSH_IM_MISSING_ERROR}`);
104
+ if (!opts.manual) {
105
+ try {
106
+ await kanban.noteImDeliveryFailed(chainId, `企微投递未生效:${DSH_IM_MISSING_GUIDANCE}`, 'system');
107
+ }
108
+ catch (err) {
109
+ log(`[im-delivery] noteImDeliveryFailed failed chain=${chainId}: ` + String(err));
110
+ }
111
+ }
112
+ return { ok: false, error: DSH_IM_MISSING_ERROR };
113
+ }
114
+ const t = await resolveTarget(im, cfg, targetKind);
94
115
  if ('error' in t) {
95
116
  log(`[im-delivery] target resolve failed chain=${chainId}: ${t.error}`);
96
117
  return { ok: false, error: t.error };
@@ -189,6 +210,19 @@ export function resolveReportChainId(state, variant, query) {
189
210
  return { ok: false, error: 'completion-not-met' };
190
211
  return { ok: true, chainId: resolved.chainId };
191
212
  }
213
+ /** /sms rest 三岔判定(纯函数,不查看板状态):先剥独立 '-s' token('-sx' 粘连不算,防误伤正文);
214
+ * blocked 前缀 → 链阻塞汇报(词边界:'blockedx' 等粘连 token 是正文不算);空 → 最近完成链;
215
+ * 其余非空 → free(是否真指链由调用方 resolveReportChainId 复判——显式 id 是强信号,先链后自由)。 */
216
+ export function parseSendRequest(rest) {
217
+ const tokens = rest.trim().split(/\s+/).filter(Boolean);
218
+ const dm = tokens.includes('-s');
219
+ const query = tokens.filter((t) => t !== '-s').join(' ');
220
+ if (/^blocked(\s|$)/.test(query))
221
+ return { variant: 'blocked', query: query.slice('blocked'.length).trim(), dm };
222
+ if (!query)
223
+ return { variant: 'completion', query: '', dm };
224
+ return { variant: 'free', query, dm };
225
+ }
192
226
  /** /sms 失败 guidance(主会话模型原样转述给用户;绝不生成消息正文)。 */
193
227
  function reportGuidance(error, candidates, sendCmd) {
194
228
  if (error === 'chain-ambiguous' && candidates?.length) {
@@ -226,8 +260,12 @@ export async function sendChainReport(ctx, kanban, configProvider, opts, variant
226
260
  text = buildBlockMessage(state, chainId, String(reason ?? ''), deriveStorageDir(configProvider));
227
261
  }
228
262
  const r = await createSender(ctx, kanban, configProvider, { ...opts, manual: true })(chainId, text);
229
- if (!r.ok)
230
- return { ok: false, error: r.error };
263
+ if (!r.ok) {
264
+ // 缺插件错误附安装指引,主 agent 原样转告用户(0.3.1 友好提醒)。
265
+ return r.error.startsWith(DSH_IM_MISSING_PREFIX)
266
+ ? { ok: false, error: r.error, guidance: DSH_IM_MISSING_GUIDANCE }
267
+ : { ok: false, error: r.error };
268
+ }
231
269
  return { ok: true, chainId, botId: r.botId, targetId: r.targetId };
232
270
  }
233
271
  /** 接线:订阅看板事件多播通道(不动 setOnTaskCompleted 单消费者钩子)。
@@ -33,7 +33,7 @@ export declare function buildOpenspecNarrationRule(r: {
33
33
  };
34
34
  }): string;
35
35
  export declare function buildSpawnPrefetch(ctx: Context): PlanningToolDeps['spawnPrefetch'] | undefined;
36
- /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 用清单建链。
36
+ /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
37
37
  * 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
38
38
  * 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
39
39
  export declare function registerMainSessionTools(ctx: Context, configProvider: ConfigProvider): void;
@@ -6,7 +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
+ import { sendChainReport, createSender, parseSendRequest, resolveReportChainId, DSH_IM_MISSING_PREFIX, DSH_IM_MISSING_GUIDANCE } from '../services/im-delivery.js';
10
10
  import { recallMemoryIndex, searchChecklists } from '../wiki/memory-recall.js';
11
11
  import { buildPlanningGuidance } from '../routes/planning-driver.js';
12
12
  import { attachSessionToWorkspace, resolveOrCreateWorkspace } from '../dispatcher/workspace-attach.js';
@@ -118,7 +118,20 @@ export function buildSpawnPrefetch(ctx) {
118
118
  }
119
119
  };
120
120
  }
121
- /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 /openspec: 用清单建链。
121
+ /** 自由投递 guidance:三步教学(实查基准 生成正文sms_send 投递)+ 红线分界。 */
122
+ function buildFreeSendGuidance(routes, query, dm) {
123
+ return [
124
+ '## 自由投递指令(' + routes.send + ')',
125
+ '用户意图:' + query + ';投递目标:' + (dm ? '私聊' : '群聊') + '。',
126
+ '1. 先 kanban_show / kanban_list 实查看板与相关链状态,取得事实基准;禁止编造数据、禁止虚构进度。',
127
+ '2. 按用户意图撰写中文正文(≤4000 字符,先结论后细节)。本自由投递正文由你撰写(显式豁免);但链完成/阻塞汇报正文由系统渲染,你始终不撰写、不复述、不改写。',
128
+ '3. ' + (dm
129
+ ? '调 sms_send{text: <正文>, dm: true} 投递(目标为私聊,已由 -s/私聊语义确定)。'
130
+ : '调 sms_send{text: <正文>, dm: false} 投递;若用户消息含「私聊/私信/单聊」语义,改传 dm: true(目标为私聊)。'),
131
+ '投递结果只向用户确认成功与目标,不复述正文;失败时把 error/guidance 原样转告,勿编造原因。',
132
+ ].join('\n');
133
+ }
134
+ /** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
122
135
  * 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
123
136
  * 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
124
137
  export function registerMainSessionTools(ctx, configProvider) {
@@ -172,7 +185,7 @@ export function registerMainSessionTools(ctx, configProvider) {
172
185
  const { plan, openspec, learning, send } = configProvider.getEffective().prefixRoutes;
173
186
  registry.register(defineTool({
174
187
  name: 'kanban_route',
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).`,
188
+ description: `Route hub for dsh-swarm kanban workflow (NOT the built-in /plan plan mode). Two trigger forms. (1) PREFIX form — MUST be called when the human message starts with ${plan}, ${openspec}, ${learning}, or ${send}; omit intent. (2) INTENT form (swarm preset sessions) — MUST be called with intent when the human expresses: a hands-on development requirement → intent='plan'; explicit approval to start the workflow after the checklist was saved → intent='openspec'; distill/retrospect lessons from a chain → intent='learning' (message = chainId or title words, may be empty = latest chain); deliver a chain report to the WeCom group, or free-form delivery of user-intent content → intent='send' (block notice: prefix message with 'blocked '). When intent is set, message = the user's raw words (no prefix). Ambiguous intent → do NOT call, ask the user instead. Semantics: ${plan} = zero side-effect + start grill-me (+ auto KB memory index); ${openspec} = create chain from saved checklist; ${learning} = distill experience from a chain (evidence pack + planning_learning_save); ${send} = manually deliver a chain report to the WeCom group (bare = latest completed chain; '${send} blocked [chainId]' = resend block notice; bypasses imDelivery.enabled; the message body is composed by system code — never compose or repeat it, only relay the delivery status; any other non-empty message = free-form delivery: compose a fact-grounded body per the returned guidance and call sms_send (append ' -s' to the message or mention 私聊 for private chat)).`,
176
189
  parameters: { message: { type: 'string', required: true }, intent: { type: 'string', description: "swarm preset sessions: 'plan' | 'openspec' | 'learning' | 'send' — model-judged intent; omit for prefix-triggered calls" } },
177
190
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
178
191
  async execute(args, exec) {
@@ -215,20 +228,34 @@ export function registerMainSessionTools(ctx, configProvider) {
215
228
  return { kind: 'learning', chainId: r.chainId, brief: r.brief, guidance: r.guidance };
216
229
  }
217
230
  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 字段原样转告用户,勿复述消息正文。' };
231
+ // /sms 三岔:先链后自由。blocked 前缀/空 rest/链 id 命中 既有机械汇报(正文系统渲染,红线不变);
232
+ // 其余非空 rest → 自由投递(正文由主 agent 按意图生成,显式豁免;事实基准=看板实查)。
233
+ // -s 独立 token 由 parseSendRequest 剥离置 dm;manual 路径零重试。
234
+ const parsed = parseSendRequest(plan.rest);
235
+ const targetOpts = { retryDelaysMs: [], manual: true, targetKind: (parsed.dm ? 'user' : 'group') };
236
+ const deliver = async (variant, query) => {
237
+ const r = await sendChainReport(ctx, service, configProvider, targetOpts, variant, query);
238
+ if (r.ok) {
239
+ const noun = variant === 'blocked' ? '阻塞通知' : '完成汇报';
240
+ const dest = parsed.dm ? '私聊' : '群聊';
241
+ return {
242
+ kind: 'send', chainId: r.chainId, botId: r.botId, targetId: r.targetId,
243
+ guidance: `${noun}已投递${dest}(/sms 手动触发)。请仅向用户确认投递成功与目标,勿复述消息正文。`,
244
+ };
245
+ }
246
+ return { kind: 'send', error: r.error, guidance: r.guidance ?? '请将 error 字段原样转告用户,勿复述消息正文。' };
247
+ };
248
+ if (parsed.variant !== 'free')
249
+ return deliver(parsed.variant === 'blocked' ? 'blocked' : 'completion', parsed.query);
250
+ // free 候选复判链:显式 id 命中/歧义/判据不满足都归链汇报路径,仅 chain-not-found 才自由
251
+ const state = await service.snapshot();
252
+ const resolved = resolveReportChainId(state, 'completion', parsed.query);
253
+ if (resolved.ok || resolved.error !== 'chain-not-found')
254
+ return deliver('completion', parsed.query);
255
+ return {
256
+ kind: 'send', mode: 'free', dm: parsed.dm,
257
+ guidance: buildFreeSendGuidance(configProvider.getEffective().prefixRoutes, parsed.query, parsed.dm),
258
+ };
232
259
  }
233
260
  if (plan.kind === 'none')
234
261
  return { kind: 'none' };
@@ -291,5 +318,39 @@ export function registerMainSessionTools(ctx, configProvider) {
291
318
  };
292
319
  },
293
320
  }));
321
+ // sms_send:自由投递出口(/sms 非链意图 → guidance 教学 → 此工具实际发送)。manual 路径零重试;
322
+ // 缺 dsh-im 插件走 DSH_IM_MISSING_PREFIX/GUIDANCE 友好提醒;text 硬上限 4000 字符、空白拒绝。
323
+ registry.register(defineTool({
324
+ name: 'sms_send',
325
+ description: 'Deliver a free-form message via dsh-im (WeCom). text = body composed per the free-delivery guidance returned by kanban_route (fact-grounded via kanban_show; <=4000 chars). dm=false -> the single saved group target; dm=true -> the single saved private-chat target (kind=user; exactly one must exist). Manual path: no retry on failure; missing dsh-im plugin returns install guidance.',
326
+ parameters: {
327
+ text: { type: 'string', required: true, description: 'Message body (1..4000 chars) composed per the free-delivery guidance' },
328
+ dm: { type: 'boolean', description: 'true = private chat; omit/false = group' },
329
+ },
330
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
331
+ async execute(args) {
332
+ // actor 边界与 planning 工具同构:主会话工具面仅限人工触发,非 human caller 一律拒绝
333
+ if (caller().actor !== 'human')
334
+ throw new Error('permission denied: sms_send');
335
+ const text = typeof args.text === 'string' ? args.text.trim() : '';
336
+ if (!text)
337
+ throw new Error('text required(正文必填,按自由投递 guidance 生成)');
338
+ if (text.length > 4000)
339
+ throw new Error(`text too long: ${text.length} > 4000 chars`);
340
+ const dm = args.dm === true;
341
+ const deliver = createSender(ctx, service, configProvider, { retryDelaysMs: [], manual: true, targetKind: dm ? 'user' : 'group' });
342
+ const r = await deliver('free-send', text);
343
+ if (!r.ok) {
344
+ return {
345
+ kind: 'send', mode: 'free', dm, error: r.error,
346
+ guidance: r.error.startsWith(DSH_IM_MISSING_PREFIX) ? DSH_IM_MISSING_GUIDANCE : undefined,
347
+ };
348
+ }
349
+ return {
350
+ kind: 'send', mode: 'free', dm, botId: r.botId, targetId: r.targetId,
351
+ guidance: '已投递(自由投递)。请仅向用户确认投递成功与目标(群聊/私聊),勿复述正文。',
352
+ };
353
+ },
354
+ }));
294
355
  console.info('[dsh-swarm] main-session tools registered (v2: kanban_route + planning + spec view + read-only kanban)');
295
356
  }
@@ -28,6 +28,7 @@ export function buildOcrReviewTool(deps) {
28
28
  to: { type: 'string', description: '目标分支/引用(range 模式,默认 HEAD)' },
29
29
  commit: { type: 'string', description: '单次提交审查' },
30
30
  paths: { type: 'array', items: { type: 'string' }, description: 'rule 子命令的文件路径列表' },
31
+ background: { type: 'string', description: '业务上下文,托管评审时提升评审质量' },
31
32
  },
32
33
  output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String(v) }] },
33
34
  async execute(args) {
@@ -42,18 +43,24 @@ export function buildOcrReviewTool(deps) {
42
43
  throw new Error("ocr_review: sub='rule' requires non-empty paths (file path list)");
43
44
  if (args.sub === 'managed' && !args.commit && !args.from)
44
45
  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());
46
+ // 托管评审走 ocr 自带 LLM(官方预算 15min×2 rounds),超时须远大于 preview/rule 的本地 git 操作
47
+ const timeoutMs = args.sub === 'managed' ? 2_400_000 : 600_000;
48
+ const res = await runOcrFn(buildOcrArgs(args.sub, { repo: args.repo, from: args.from, to: args.to, commit: args.commit, paths: args.paths, background: args.background }), { cwd: deps.cwd?.() ?? process.cwd(), timeoutMs });
49
+ // 失败不抛错:结果附 error 摘要(error + stderr 前 500 字符),模型拿得到部分结果与原因后自行降级/重试
50
+ const errSummary = res.error ? `${res.error} ${res.stderr.slice(0, 500)}`.trim() : '';
48
51
  if (args.sub === 'preview') {
49
52
  const preview = parsePreviewJson(res.stdout);
53
+ const errorPart = errSummary ? { error: errSummary } : {};
50
54
  if (shouldSuggestManaged(preview.files.length)) {
51
- return JSON.stringify({ ...preview, suggestion: `文件较多(N>${SUGGEST_MANAGED_FILES}),可在配置面板切换托管模式` });
55
+ return JSON.stringify({ ...preview, ...errorPart, suggestion: `文件较多(N>${SUGGEST_MANAGED_FILES}),可在配置面板切换托管模式` });
52
56
  }
53
- return JSON.stringify(preview);
57
+ return JSON.stringify({ ...preview, ...errorPart });
54
58
  }
55
- if (args.sub === 'rule')
59
+ if (args.sub === 'rule') {
60
+ if (errSummary)
61
+ return JSON.stringify({ error: errSummary });
56
62
  return res.stdout.slice(0, 8000);
63
+ }
57
64
  const report = parseManagedJson(res.stdout);
58
65
  if (report.status !== 'completed' && res.error)
59
66
  return JSON.stringify({ ...report, message: res.error });
@@ -4,7 +4,7 @@ import {} from '@deepseek-ai/dsh-util-values';
4
4
  import { validatePlanningChecklist, formatChecklistBody } from '../domain/planning-checklist.js';
5
5
  import { validatePrefetchManifest } from '../domain/prefetch-manifest.js';
6
6
  import { buildChecklistSlug, KB_PAGE_NAMESPACES_HINT, LOCAL_CHECKLIST_PREFIX, LOCAL_LEARNING_BASE, assertAllowedWikiPagePath, assertLocalKbPagePath } from '../wiki/page-path.js';
7
- import { validateLearning, formatLearningBody, buildRepoSlug } from '../domain/memory.js';
7
+ import { validateLearning, formatLearningBody, buildRepoSlug, countLearningSignals } from '../domain/memory.js';
8
8
  const isWikiError = (e) => e instanceof Error && e.code === 'kb-unreachable';
9
9
  /** 主 agent 规划期工具:需求澄清清单落库(KB 优先/临时目录兜底)+ 只读仓库预取(子代理)。 */
10
10
  export function buildPlanningTools(deps) {
@@ -102,7 +102,7 @@ export function buildPlanningTools(deps) {
102
102
  name: 'planning_learning_save',
103
103
  description: 'Save a distilled learning (experience) to the knowledge base. Remote KB: scope=chain → projects/<repoSlug>/<chainId>/learnings/ (requirement-level); scope=project → projects/<repoSlug>/learnings/ (repo-level). repoSlug is derived from the chain workspaceDir; both scopes require chain.workspaceDir. Local KB: both scopes → wiki/synthesis/learnings/<chainId|repoSlug>/. Returns ref. Soft-fails {ok:false,reason:"kb-unreachable"} when KB is unreachable (no temp fallback).',
104
104
  parameters: {
105
- learning: { type: 'json', required: true, description: 'LearningEntry: { title (≤80 chars), lesson, evidence (mechanical chain/task id — required), tags: string[] }' },
105
+ learning: { type: 'json', required: true, description: 'LearningEntry: { title (≤80 chars), lesson, evidence (mechanical chain/task id — required), tags: string[] — must include one of mistake/reusable/env-trap/collab-contract/efficiency (category criteria enforced) }' },
106
106
  scope: { type: 'string', enum: ['chain', 'project'], required: true, description: '"chain" (requirement-level) | "project" (repo-level)' },
107
107
  chainId: { type: 'string', required: true, description: 'The chain this learning is distilled from; must exist' },
108
108
  },
@@ -123,6 +123,12 @@ export function buildPlanningTools(deps) {
123
123
  if (!chain)
124
124
  throw new Error('unknown chain: ' + args.chainId);
125
125
  const entry = args.learning;
126
+ // A 类硬复核(0.3.1):mistake 标签须通过机械信号复核(累计 ≥2),不复信模型自报
127
+ if (Array.isArray(entry.tags) && entry.tags.includes('mistake')) {
128
+ const total = countLearningSignals(state, args.chainId);
129
+ if (total < 2)
130
+ throw new Error(`[mistake-gate] 标记 mistake 需链上 阻塞/返工/评审失败/审计警告 累计 ≥2 次,当前链累计 ${total} 次。请核对证据包;不满足时改用其他类别标签(reusable/env-trap/collab-contract/efficiency)或回复「无新经验」`);
131
+ }
126
132
  let prefix;
127
133
  if (local) {
128
134
  // local:scope=chain 直接挂 chainId;scope=project 挂 repoSlug(需 workspaceDir)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joekytc/dsh-swarm",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",
@@ -48,6 +48,10 @@
48
48
  },
49
49
  "./package.json": "./package.json"
50
50
  },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
51
55
  "scripts": {
52
56
  "build": "tsc -p tsconfig.build.json && npm run build:client",
53
57
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -59,6 +63,7 @@
59
63
  "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
60
64
  "@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
61
65
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
66
+ "@deepseek-ai/dsh-credentials": "^0.1.2-rc.1",
62
67
  "@deepseek-ai/dsh-persona": "^0.1.2-rc.1",
63
68
  "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
64
69
  "@deepseek-ai/dsh-tool-bash": "^0.1.2-rc.1",
@@ -7,6 +7,7 @@
7
7
  # 子代理继承 D 权限与沙箱,产物归属 D 的 feature 分支 git 证据)与 goal(条件启用:
8
8
  # spec/实施计划提及 /goal 目标模式关键词时由系统注入目标模式指令)。
9
9
  # 仍裁剪:workflow/ralph(编排循环归链路编排职责)、plan-mode(planning)、web。
10
+ # MCP(2026-09-09):文件尾留 dsh-mcp-client 模板行(默认注释,接入 server 时填参启用)。
10
11
 
11
12
  - id: persona
12
13
  name: '@deepseek-ai/dsh-persona'
@@ -33,7 +34,8 @@
33
34
  subagents must equally never approve specs, create tasks, or wiki_write. Do not run
34
35
  sub-workflows or ralph loops inside a card — orchestration belongs to the chain.
35
36
  5. Use kanban_* + wiki_read/wiki_search (read-only KB) + spec_card_view + bash/fs/run_code
36
- + subagent tools + goal (provided by base).
37
+ + subagent tools + goal (provided by base). If mcp__* tools are present in this session
38
+ (MCP servers configured at preset level), you may use them for external integrations.
37
39
  6. Execution methodology (HARD):
38
40
  a. TDD is mandatory (tool-gated by DT). For JS/TS/JSX/Vue targets, tests MUST use vitest ONLY:
39
41
  write the failing test first (RED), then implement to pass (GREEN), refactor (REFACTOR).
@@ -42,15 +44,18 @@
42
44
  Non-code-only diffs (docs/config/README) must declare tdd.skipped={reason} instead. On complete,
43
45
  metadata MUST carry tdd: { test_files: [...], test_first: bool, skipped?: { reason } }
44
46
  (skipped XOR test_files; the tool gate rejects otherwise).
45
- b. Delegate with the DSH-native subagent tool (subagent / subagent_fork, one-shot) when a task is
46
- large or multi-module: split implementation into independent, well-bounded subtasks and run them
47
- SERIALLY (one at a time; verify each subtask before starting the next). Each subagent inherits
48
- your permission and sandbox and MUST commit onto YOUR feature branch so git evidence attributes
49
- to you. Forbid subagents from approving specs, creating tasks, or wiki_write. Do NOT run
50
- sub-workflows or ralph loops inside a card.
47
+ b. Task-package decomposition is YOUR call (the orchestrator does NOT tell you how many
48
+ packages exist): after reading the implementation plan, assess how many independently
49
+ deliverable, well-bounded implementation units (task packages) it contains and how to split
50
+ them. If MORE THAN ONE package: you MUST drive execution via the DSH-native subagent tool
51
+ (subagent / subagent_fork, one-shot) one subagent per package, each running the full loop
52
+ implement → self-review its diff verify (tests/build/typecheck), executed SERIALLY (finish
53
+ and verify one package before starting the next). If exactly one package, implement it
54
+ yourself. Each subagent inherits your permission and sandbox and MUST commit onto YOUR
55
+ feature branch so git evidence attributes to you. Forbid subagents from approving specs,
56
+ creating tasks, or wiki_write. Do NOT run sub-workflows or ralph loops inside a card.
51
57
  c. Verify before claiming done (verification-before-completion): run `npx vitest run` + build +
52
58
  typecheck, and confirm your diff, before complete. Use using-git-worktrees for isolation.
53
- d. Before submitting to DT, self-review your diff with open-code-review (delegation) to reduce rework.
54
59
  7. Commit convention: `<type>: [AI-GEN] <one-line concise description>` (type in
55
60
  feat/fix/chore/docs/refactor/test/perf/ci...). Workflow: worktree isolated branch →
56
61
  implement + verify → [AI-GEN] commit → (optionally push the feature branch). Do NOT merge
@@ -146,6 +151,21 @@
146
151
  - id: tool-goal
147
152
  name: '@deepseek-ai/dsh-tool-goal'
148
153
 
154
+ # ── MCP(模板,默认注释——接入具体 server 时取消注释并填参)─────────────────
155
+ # 宿主已装 @deepseek-ai/dsh-mcp-client(MCP client bridge):每 server 一个插件实例,
156
+ # 工具以 mcp__<serverName>__<rawName> 注册到 ctx.tools,对 D 会话可见。stdio 与
157
+ # streamable-http 二选一;serverName 全局唯一([A-Za-z0-9_-]{1,32})。
158
+ # - id: mcp-<serverName>
159
+ # name: '@deepseek-ai/dsh-mcp-client'
160
+ # config:
161
+ # serverName: <serverName>
162
+ # transport: stdio # 或 streamable-http
163
+ # command: npx # stdio:可执行文件
164
+ # args: ['-y', '<mcp-server-package>']
165
+ # env: {} # stdio:额外环境变量(可选)
166
+ # # url: http://host:port/mcp # streamable-http:服务地址
167
+ # # headers: {} # streamable-http:额外请求头(可选)
168
+
149
169
  # ── presentation (run_code) ─────────────────────────────────────────────────
150
170
  # mode 由 code 改为 both —— code 模式下注册表把「直接调用 bash/kanban_* 等工具」解析为
151
171
  # UNKNOWN_TOOL(仅 run_code 可模型直呼),导致 D 无法直接 kanban_complete/kanban_block 收尾;
@@ -33,9 +33,10 @@
33
33
  yourself → classify findings by severity (Critical/High must be reported, Medium with
34
34
  context, Low dropped by default). Managed = call ocr_review{sub:'managed',
35
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
36
+ feature branch from the parent handoff metadata.branch; you may pass background with a
37
+ one-line business context distilled from the spec card / task description); silently
38
+ fall back to the delegate flow when status is not completed or the tool returns
39
+ managed-not-configured guidance. When ocr is not installed the tool returns install guidance (installable
39
40
  from the GUI config panel); in chain scenarios kanban_block('review-tool-unavailable')
40
41
  and note the GUI install option in the reason. Standalone review mode (no bound chain
41
42
  task): review user-specified local dirs, branch ranges (--from/--to), single commits,
@@ -1,11 +1,13 @@
1
1
  # kanban-w — W(wiki-bridge) 裁剪 agent preset(2026-08-15)。
2
2
  #
3
3
  # 官方 agent-presets 无 include/patch 语义,本文件为手写最小组合(取舍见 git 提交历史)。
4
- # 基座同 kanban-p:persona/instructions + bash + fs + fs-search;双模式(local/remote)另挂 skill。
4
+ # 基座同 kanban-p:persona/instructions + bash + fs + fs-search + skill + web;双模式(local/remote)另挂角色工具。
5
5
  # 角色差异(wiki/prefetch/spec/kanban 工具)由 installRoleTools 按角色注入,不在此文件。
6
6
  # 裁剪(最小能力面):无 run_code、jobs、goal、plan-mode、compaction、delegation、
7
- # ask-user、todo、webbash 保留给只读命令(cat/git show);无 readonly 模式的 API
8
- # 局限与 P 相同:只读纪律 = workspace-write sandbox + 角色上下文提示词(软约束)。
7
+ # ask-user、todo。2026-09-09 开放 web(web_search/web_fetch):W 联网收集外部事实供
8
+ # prefetch_external 落盘,并可用本地联网收集类 skill(aihot 等)。bash 保留给只读命令
9
+ # (cat/git show);无 readonly 模式的 API 局限与 P 相同:只读纪律 = workspace-write
10
+ # sandbox + 角色上下文提示词(软约束)。
9
11
 
10
12
  - id: persona
11
13
  name: '@deepseek-ai/dsh-persona'
@@ -25,8 +27,12 @@
25
27
  page_path = a wiki/** relative path inside the KB root, and kb_url = "" (empty string).
26
28
  3. KB retrieval ranks relevance first (7) then freshness (3).
27
29
  4. Read-only repo prefetch (file/external/kb modes) lands verbatim in the task workspace —
28
- no compression/distillation. In local KB mode prefetch_kb is unavailable: use the skill
29
- tool with llm-wiki instead. Never ingest external URLs in local KB mode.
30
+ no compression/distillation. For external-research prefetch, gather facts first with
31
+ web_search/web_fetch (network read-only), then register the artifact via prefetch_external
32
+ verbatim; the skill tool also exposes network-collection skills (e.g. aihot for AI news and
33
+ daily digests) — prefer them for aggregated briefings. In local KB mode prefetch_kb is
34
+ unavailable: use the skill tool with llm-wiki instead. Never ingest external URLs in local
35
+ KB mode (llm-wiki ingest discipline unchanged).
30
36
  5. You are NOT an executor: no git/code/build/push, no dependency install, no repo writes
31
37
  (bash limited to read-only commands like cat/git show for repo facts; your only writable
32
38
  area in local KB mode is the local KB root — direct cp/mv/mkdir tools are rejected by the
@@ -36,8 +42,8 @@
36
42
  complete/block your own bound task).
37
43
  7. If the KB (either mode) is unavailable, kanban_block(reason=kb-unreachable) and wait
38
44
  for a human; never hand in an empty complete.
39
- 8. Use kanban_* + (remote: wiki_search/wiki_read/wiki_write | local: skill) +
40
- prefetch_file/prefetch_external/prefetch_kb + spec_card_view (read-only).
45
+ 8. Use kanban_* + web_search/web_fetch + (remote: wiki_search/wiki_read/wiki_write | local:
46
+ skill) + prefetch_file/prefetch_external/prefetch_kb + spec_card_view (read-only).
41
47
 
42
48
  - id: agent-instructions
43
49
  name: '@deepseek-ai/dsh-agent-instructions'
@@ -64,3 +70,7 @@
64
70
 
65
71
  - id: tool-skill
66
72
  name: '@deepseek-ai/dsh-tool-skill'
73
+
74
+ # ── web(2026-09-09:联网检索外部事实;web_search/web_fetch 走宿主 ctx.web seam)──
75
+ - id: tool-web
76
+ name: '@deepseek-ai/dsh-tool-web'
@@ -8,7 +8,7 @@
8
8
  2. 知识库只读(wiki_read/wiki_search),不得 wiki_write;不得只读校验/对齐后交差——必须产生真实代码变更。
9
9
  3. 交接 metadata 必须带 git 产物证据:changed_files(数组)+ commit_hash 与 push 至少其一(+verification/kb_url 如适用),summary 非空;无证据 kanban_complete 会被拒绝、链路不收尾。
10
10
  4. 不得创建任务、不得批准规格卡;只可 complete/block 本任务(会话绑定)。可派子代理(spawn/fork,继承你的权限与沙箱,one-shot):子代理提交必须落在你的 feature 分支(git 证据归你);子代理同样禁规格批准/建卡/wiki_write。卡内禁跑子工作流/ralph 循环——编排归链路。
11
- 5. 使用 kanban_* + wiki_read/wiki_search(只读 KB)+ spec_card_view + bash/fs/run_code + subagent 工具 + goal(条件启用:spec/计划提及 /goal 目标模式时按目标模式执行)(base 提供)。
11
+ 5. 使用 kanban_* + wiki_read/wiki_search(只读 KB)+ spec_card_view + bash/fs/run_code + subagent 工具 + goal(条件启用:spec/计划提及 /goal 目标模式时按目标模式执行)(base 提供);会话中如有 mcp__* 工具(preset 层接入 MCP server 后出现)可用于外部集成。
12
12
  6. 执行方法论(硬性):
13
13
  a. TDD 强制(DT 工具闸兜底)。JS/TS/JSX/Vue 项目测试一律只用 vitest:先写会失败的测试(RED),
14
14
  再实现使其通过(GREEN),然后重构(REFACTOR)。测试与实现允许同一提交,但每个测试文件进入
@@ -16,11 +16,12 @@
16
16
  纯非代码变更(文档/配置/README)须改声明 tdd.skipped={reason}。complete 时 metadata 必须携带
17
17
  tdd = { test_files: [...], test_first: bool, skipped?: { reason } }(skipped 与 test_files 二选一,
18
18
  否则工具闸拒绝)。
19
- b. 任务大或跨多模块时,用 DSH 原生 subagent 工具(subagent / subagent_fork,one-shot)委派:
20
- 把实现拆成独立、边界清晰的子任务,并串行执行(一次一个,验完上一个再开下一个)。每个子代理
21
- 继承你的权限与沙箱,提交必须落在你的 feature 分支(git 证据归你)。禁止子代理批准规格/建卡/
22
- wiki_write。卡内禁止跑子工作流或 ralph 循环。
19
+ b. 任务包拆分由你自主判定(编排方不会告诉你有几个任务包):读完实施计划后,评估其中包含多少个
20
+ 可独立交付、边界清晰的实现单元(任务包)以及如何拆分。任务包 >1:必须走 DSH 原生 subagent
21
+ 工具(subagent / subagent_fork,one-shot)驱动执行——每个任务包派一个子代理,跑完整闭环
22
+ 实施 自审 diff → 验证(test/build/typecheck),串行执行(一包验证过再开下一包)。任务包
23
+ =1:可亲自实现。每个子代理继承你的权限与沙箱,提交必须落在你的 feature 分支(git 证据归你)。
24
+ 禁止子代理批准规格/建卡/wiki_write。卡内禁止跑子工作流或 ralph 循环。
23
25
  c. 完成前先验证(verification-before-completion):complete 前跑 `npx vitest run` + build +
24
26
  typecheck,并核对你的 diff。用 using-git-worktrees 隔离工作区。
25
- d. 提交 DT 前,用 open-code-review(delegation)自审 diff,减少返工轮次。
26
27
  7. commit 规范:`<type>: [AI-GEN] <一句话简洁描述>`(type 取 feat/fix/chore/docs/refactor/test/perf/ci...)。工作流:worktree 隔离分支 → 实现+验证 → [AI-GEN] commit →(可选推 feature 分支)。禁止合并回 TARGET_BRANCH / 推 TARGET_BRANCH——由 DT 通过后 system 合入。
@@ -6,7 +6,7 @@
6
6
 
7
7
  1. 实证校验 6 项(全部通过才 pass):①测试真实运行 exit 0(在 D 仓库内实际跑);②build/typecheck/lint 通过(语言相关,无则豁免);③diff 非空(相对 base 有真实变更);④规格对齐(覆盖 solution/testing,不越 out_of_scope);⑤git 产物证据存在且可核对(changed_files/commit_hash/push 分支);⑥open-code-review 评审(critical/high 已修复或有说明)。
8
8
  2. 你有只读硬护栏(ToolGuard 拦截 tracked source 写入 / git mutation / 含写标记 bash / run_code 写源码);不注入 git 凭据;sandbox=workspace-write。绝不改源码;验证命令(npm test/build、tsc --noEmit、eslint、git show/log、ocr review)放行。
9
- 3. 评审引擎由配置面板 reviewEngine.mode 决定(默认委托):①委托 = 调 ocr_review{sub:'preview'} 获取评审范围 → ocr_review{sub:'rule'} 获取各文件评审规则 → 自行 git diff 逐文件深入评审 → 按严重级归类(Critical/High 必报、Medium 带上下文、Low 默认丢弃);②托管 = 调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>} 一次出归一化 findings(branch 取 D 交接 metadata.branch),status 非 completed 或返回托管未配置指引时静默改走委托流程。
9
+ 3. 评审引擎由配置面板 reviewEngine.mode 决定(默认委托):①委托 = 调 ocr_review{sub:'preview'} 获取评审范围 → ocr_review{sub:'rule'} 获取各文件评审规则 → 自行 git diff 逐文件深入评审 → 按严重级归类(Critical/High 必报、Medium 带上下文、Low 默认丢弃);②托管 = 调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>} 一次出归一化 findings(branch 取 D 交接 metadata.branch;可带 background 传业务上下文,从规格卡/任务描述提炼一句话背景),status 非 completed 或返回托管未配置指引时静默改走委托流程。
10
10
  4. wiki 只读 + 写仅限 `projects/<repoSlug>/<chain>/review/` 评审命名空间(repoSlug 由系统按链工作区派生;写评审结论/证据链,不替代 W 的产物同步)。
11
11
  5. 评审结论写进 kanban_complete 的交接 metadata.review_evidence = { verdict: 'pass'|'fail', issues: [...], test/build/typecheck/lint/diff/git/openCodeReview/reviewPage }:
12
12
  - pass = 六项校验全过 → 系统推进 W3;
@@ -15,7 +15,7 @@
15
15
 
16
16
  ## open-code-review(ocr)评审引擎(双模)
17
17
  - 评审引擎由配置面板 reviewEngine.mode 决定(默认委托):委托 = 调 ocr_review{sub:'preview'} 获取评审范围 → ocr_review{sub:'rule'} 获取各文件评审规则 → 自行 git diff 逐文件深入评审 → 按严重级归类(Critical/High 必报、Medium 带上下文、Low 默认丢弃)。
18
- - 托管 = 调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>}(branch 取 D 交接 metadata.branch)一次出归一化 findings;status 非 completed 或返回托管未配置指引时静默改走委托流程。
18
+ - 托管 = 调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>}(branch 取 D 交接 metadata.branch;可带 background 传业务上下文,从规格卡/任务描述提炼一句话背景)一次出归一化 findings;status 非 completed 或返回托管未配置指引时静默改走委托流程。
19
19
  - ocr 未安装时工具自动返回中文安装指引(可在 GUI 配置面板安装);链上场景按规则 kanban_block('review-tool-unavailable') 并在 reason 注明 GUI 可安装。
20
20
 
21
21
  ## 独立评审模式
@@ -7,8 +7,8 @@
7
7
  1. 远程 KB 模式:使用 wiki_search/wiki_read/wiki_write;wiki 写仅限 `projects/<repoSlug>/…` 白名单命名空间(repoSlug 由系统按链工作区派生,见任务卡 body 的 KB 页路径规则);经 wiki_write 返回 kb_url/page_path,complete summary 非空。
8
8
  2. 本地 KB 模式:经 skill 工具加载 llm-wiki,对任务上下文给定的本地库根运行 query/ingest/crystallize 工作流(库未初始化时先按 llm-wiki init 工作流初始化,幂等);交付 page_path = 库根内 wiki/** 相对路径、kb_url = 空串。
9
9
  3. KB 检索排序:相关性优先(7),新鲜度次之(3)。
10
- 4. 只读仓库预取(file/external/kb 模式)原样落任务工作区——不压缩、不蒸馏;本地模式 prefetch_kb 不可用:改用 skill 工具 + llm-wiki;本地模式绝不 ingest 外部 URL
10
+ 4. 只读仓库预取(file/external/kb 模式)原样落任务工作区——不压缩、不蒸馏。外部研究(external 模式):先用 web_search/web_fetch 联网收集事实,再经 prefetch_external 原样登记落盘;skill 工具亦暴露本地联网收集类 skill(如 aihot 的 AI 资讯/日报聚合)——聚合简报类需求优先用 skill。本地模式 prefetch_kb 不可用:改用 skill 工具 + llm-wiki;本地模式绝不 ingest 外部 URL(llm-wiki ingest 纪律不变)。
11
11
  5. 你不是执行者:不做 git/代码/构建/推送、不安装依赖、不写仓库(bash 仅限只读命令如 cat/git show 取仓库事实);本地模式唯一可写区 = 本地库根——DIRECT cp/mv/mkdir 工具会被护栏拒绝,库内文件操作一律使用 write 工具(bash cp/mv 在库根内是放行的)。
12
12
  6. 不得创建任务、不得批准/编辑规格卡;不得越权操作其他任务(只可 complete/block 本任务,会话绑定)。
13
13
  7. KB(任一模式)不可达时 kanban_block(reason=kb-unreachable) 等人工;绝不放行空 complete。
14
- 8. 工具面按模式分支:kanban_* +(远程:wiki_search/wiki_read/wiki_write | 本地:skill)+ prefetch_file/prefetch_external/prefetch_kb + spec_card_view(只读)。
14
+ 8. 工具面按模式分支:kanban_* + web_search/web_fetch(联网检索外部事实)+(远程:wiki_search/wiki_read/wiki_write | 本地:skill)+ prefetch_file/prefetch_external/prefetch_kb + spec_card_view(只读)。
@@ -26,11 +26,12 @@
26
26
  ## 记忆与经验
27
27
  - 规划起手收到「KB 记忆索引」→ 先 planning_memory_recall 深读相关 learnings 再澄清
28
28
  - 用户问历史/类似需求 → planning_memory_recall{query} 或看板实查;引用必须来自实查
29
- - 蒸馏:消化 learning 响应的证据包,提炼 1-3 条经验调 planning_learning_save;evidence 必填链 ID;需求特有→scope:'chain',仓库通用→scope:'project';无值得沉淀的明确说「无新经验」,不硬凑
29
+ - 蒸馏:消化 learning 响应的证据包,对照五类判据只沉淀满足其一的经验,调 planning_learning_save;tags 必含类别标签之一——mistake(链上阻塞/返工/评审失败/审计警告累计≥2)/reusable(同类流程或行为重复≥2)/env-trap(环境坑踩1次+报错或diff硬证据)/collab-contract(用户偏好≥2次确认)/efficiency(量化省力依据);evidence 必填链 ID;需求特有→scope:'chain',仓库通用→scope:'project';五类均不满足(含改样式/文案/字段名等一次性平凡变更)明确说「无新经验」,不硬凑
30
30
 
31
31
  ## 群投递
32
32
  - 完成汇报:intent:'send',query 传链 ID 或标题词;阻塞通知 query 前加 'blocked '
33
- - 正文由系统渲染:你不撰写、不复述、不改写正文,仅确认投递结果与目标群
33
+ - 自由投递:intent:'send' 传自然语言意图(如「把当前进度发到群」)→ 按返回 guidance 先 kanban_show 实查事实基准再生成正文(≤4000 字),调 sms_send 投递;用户要私聊或消息带 -s → dm:true
34
+ - 正文边界:链完成/阻塞汇报正文由系统渲染,你不撰写不复述;自由投递正文由你按意图撰写,但必须以看板实查为事实基准,禁编造
34
35
 
35
36
  ## 风格
36
37
  - 中文简报式:先结论后细节;不虚构、不硬凑