@joekytc/dsh-swarm 0.3.6 → 0.3.8
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 +165 -116
- package/README.zh-CN.md +143 -96
- package/client/timeline-model.ts +14 -1
- package/lib/client.js +20 -2
- package/lib/config.d.ts +17 -0
- package/lib/config.js +11 -0
- package/lib/dispatcher/agent-runner.js +3 -7
- package/lib/dispatcher/chain-auditor.d.ts +7 -1
- package/lib/dispatcher/chain-auditor.js +7 -4
- package/lib/dispatcher/context-dedup.d.ts +1 -0
- package/lib/dispatcher/context-dedup.js +24 -0
- package/lib/dispatcher/dispatcher.js +19 -7
- package/lib/dispatcher/session-preset.d.ts +36 -0
- package/lib/dispatcher/session-preset.js +67 -0
- package/lib/domain/evidence-check.d.ts +17 -0
- package/lib/domain/evidence-check.js +44 -0
- package/lib/domain/gate-policy.d.ts +33 -9
- package/lib/domain/gate-policy.js +60 -19
- package/lib/domain/kanban-service.d.ts +27 -4
- package/lib/domain/kanban-service.js +45 -7
- package/lib/domain/review-evidence.js +3 -1
- package/lib/domain/types.d.ts +7 -1
- package/lib/roles/toolsets.d.ts +33 -10
- package/lib/roles/toolsets.js +41 -15
- package/lib/services/evidence-replay.d.ts +28 -0
- package/lib/services/evidence-replay.js +79 -0
- package/lib/services/gate-evidence.d.ts +2 -0
- package/lib/services/gate-evidence.js +21 -0
- package/lib/services/im-delivery.d.ts +4 -0
- package/lib/services/im-delivery.js +31 -3
- package/lib/services/kanban-provider.d.ts +1 -0
- package/lib/services/kanban-provider.js +92 -19
- package/lib/tools/main-session-tools.js +6 -13
- package/package.json +1 -1
- package/personas/persona-dt.md +3 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Service } from '@deepseek-ai/cordis';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
4
5
|
import { KanbanService } from '../domain/kanban-service.js';
|
|
5
6
|
import { FileEventStore } from '../domain/event-store.js';
|
|
6
|
-
import { deriveGatePlan,
|
|
7
|
+
import { deriveGatePlan, branchMatches, resolveDiffBase } from '../domain/gate-policy.js';
|
|
7
8
|
import { runGateCommands } from './gate-runner.js';
|
|
9
|
+
import { writeGateLog } from './gate-evidence.js';
|
|
10
|
+
import { checkIssueEvidence, resolveTargetWorktree } from './evidence-replay.js';
|
|
8
11
|
export class KanbanProvider extends Service {
|
|
9
12
|
service;
|
|
10
13
|
/** GUI retry 的任务执行器(由 startDispatcher 装配后注入;webServer 先于 agents 就绪时可为 null)。 */
|
|
@@ -13,34 +16,49 @@ export class KanbanProvider extends Service {
|
|
|
13
16
|
* purge 物理重排 events seq,若不同步则删链后新建链的可唤醒事件被运行中实例永久跳过。 */
|
|
14
17
|
onChainDeleted = null;
|
|
15
18
|
// 经 configProvider getter 读 effective 配置——配置面板改 wikiVault.baseUrl 后 kb_url 前缀校验热生效。
|
|
19
|
+
gateLogDir;
|
|
16
20
|
constructor(ctx, config, configProvider) {
|
|
17
21
|
super(ctx, 'kanban');
|
|
18
22
|
const dir = config.storageDir.replace('$DSH_HOME', process.env.DSH_HOME ?? homedir());
|
|
23
|
+
this.gateLogDir = dir;
|
|
19
24
|
this.service = new KanbanService(new FileEventStore(dir), () => configProvider.getEffective().wikiVault?.baseUrl);
|
|
20
|
-
// P1 实测闸装配(组合根):complete 前派生
|
|
21
|
-
// hook 绝不 throw:意外异常记日志并返回 null
|
|
25
|
+
// P1 实测闸装配(组合根):complete 前派生 GateOutcome → 分支核对 → 实测执行/打回/警报。
|
|
26
|
+
// hook 绝不 throw:意外异常记日志并返回 null(基建故障静默,既有决议)。
|
|
27
|
+
// diff 实况:base=D 卡 body 的 TARGET_BRANCH(gate-policy resolveDiffBase);git 失败或无 base → null(派生层保守放行)。
|
|
22
28
|
this.service.setGateHook(async (task, handoff) => {
|
|
23
29
|
try {
|
|
24
30
|
// 热读取(同 wikiVault.baseUrl 先例,getEffective):配置面板改 gates 后无需重建即生效。
|
|
25
31
|
const cfg = configProvider.getEffective().gates ?? { enabled: true, timeoutMs: 600000, forbidden: ['rm -rf /', 'git push'] };
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let out = '';
|
|
34
|
-
child.stdout.on('data', (c) => { out += c.toString('utf8'); });
|
|
35
|
-
child.on('error', () => resolve(null));
|
|
36
|
-
child.on('close', (code) => resolve(code === 0 ? out.trim() || null : null));
|
|
32
|
+
const metadata = (handoff.metadata ?? {});
|
|
33
|
+
const wt = typeof metadata['worktree_dir'] === 'string' ? metadata['worktree_dir'] : '';
|
|
34
|
+
const base = resolveDiffBase(task.body ?? '');
|
|
35
|
+
const diffFiles = wt.trim() && base ? await gitDiffNames(wt, base) : null;
|
|
36
|
+
const outcome = deriveGatePlan({
|
|
37
|
+
assignee: task.assignee, mode: task.mode, handoff,
|
|
38
|
+
config: { enabled: cfg.enabled }, diffFiles,
|
|
37
39
|
});
|
|
38
|
-
if (
|
|
39
|
-
return null;
|
|
40
|
-
|
|
40
|
+
if (outcome.kind === 'silent-skip')
|
|
41
|
+
return null; // 不适用:零事件(旧语义)
|
|
42
|
+
if (outcome.kind === 'alarm-skip')
|
|
43
|
+
return { skipped: true, reason: outcome.reason };
|
|
44
|
+
if (outcome.kind === 'bounce')
|
|
45
|
+
return { ok: false, detail: outcome.reason };
|
|
46
|
+
// run:分支一致性核对(⑦ 不一致由静默 skip 升级为 bounce 打回)
|
|
47
|
+
const current = await gitCurrentBranch(outcome.commands[0].cwd);
|
|
48
|
+
if (!branchMatches(current, metadata['branch']))
|
|
49
|
+
return { ok: false, detail: `分支不一致:声明 ${String(metadata['branch'] ?? '')},实际 ${current ?? 'unknown'}——先同步分支再重交` };
|
|
50
|
+
const report = await runGateCommands(outcome.commands, { timeoutMs: cfg.timeoutMs, forbidden: cfg.forbidden });
|
|
51
|
+
// 落盘失败不吞实测结果(评审 Minor):测量已做,结论必须保住——仅降级 evidence 路径
|
|
52
|
+
let evidencePath = '(log write failed)';
|
|
53
|
+
try {
|
|
54
|
+
evidencePath = await writeGateLog(this.gateLogDir, task.id, report);
|
|
55
|
+
}
|
|
56
|
+
catch (logError) {
|
|
57
|
+
console.error('[dsh-swarm] gate log write failed (result kept): ' + String(logError));
|
|
58
|
+
}
|
|
41
59
|
const detail = report.ok
|
|
42
|
-
?
|
|
43
|
-
: `${report.failure.code}: ${report.failure.command} — ${report.failure.detail}\n${(report.results.at(-1)?.output ?? '').slice(-2000)}`;
|
|
60
|
+
? outcome.commands.map((c) => c.command).join(' && ') + ' exit 0\nevidence: ' + evidencePath
|
|
61
|
+
: `${report.failure.code}: ${report.failure.command} — ${report.failure.detail}\n${(report.results.at(-1)?.output ?? '').slice(-2000)}\nevidence: ${evidencePath}`;
|
|
44
62
|
return { ok: report.ok, detail };
|
|
45
63
|
}
|
|
46
64
|
catch (error) {
|
|
@@ -48,5 +66,60 @@ export class KanbanProvider extends Service {
|
|
|
48
66
|
return null;
|
|
49
67
|
}
|
|
50
68
|
});
|
|
69
|
+
// PR2 评审证据核验装配:DT 卡 complete 时三级核验(缺证标记/纸面核对/白名单重放)。
|
|
70
|
+
// 非阻塞:结果只发 review/evidence-check 事件;重放开关默认关(真执行 AI 命令=非沙箱)。
|
|
71
|
+
this.service.setEvidenceCheckHook(async (task, handoff) => {
|
|
72
|
+
try {
|
|
73
|
+
const cfg = configProvider.getEffective().evidenceReplay;
|
|
74
|
+
const state = await this.service.snapshot();
|
|
75
|
+
const parentTasks = task.parents
|
|
76
|
+
.map((pid) => state.tasks.get(pid))
|
|
77
|
+
.filter((t) => Boolean(t));
|
|
78
|
+
const parentsWithMeta = parentTasks.map((t) => ({
|
|
79
|
+
assignee: t.assignee, mode: t.mode,
|
|
80
|
+
metadata: (state.handoffs.get(t.id)?.metadata ?? {}),
|
|
81
|
+
}));
|
|
82
|
+
const issues = handoff.metadata?.['review_evidence']?.issues;
|
|
83
|
+
if (!Array.isArray(issues) || issues.length === 0)
|
|
84
|
+
return null;
|
|
85
|
+
// worktree 解析边界(评审 Minor 固化):一层 parents 解析覆盖现网全部建卡路径
|
|
86
|
+
//(首评卡 parents=d:execute;复审卡 parents=[rework 卡] 直接命中);
|
|
87
|
+
// rework source 为 DT 卡等未命中场景 fail-safe → could-not-replay 不执行。
|
|
88
|
+
return { results: await checkIssueEvidence({
|
|
89
|
+
issues,
|
|
90
|
+
replayEnabled: cfg.enabled, timeoutMs: cfg.timeoutMs, allowPrefixes: cfg.allowPrefixes,
|
|
91
|
+
forbidden: configProvider.getEffective().gates?.forbidden,
|
|
92
|
+
worktreeDir: resolveTargetWorktree(parentsWithMeta),
|
|
93
|
+
readFile: (p) => readFile(p, 'utf8').catch(() => null),
|
|
94
|
+
}) };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error('[dsh-swarm] evidence check hook failed (non-blocking): ' + String(error));
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
51
101
|
}
|
|
52
102
|
}
|
|
103
|
+
function gitCurrentBranch(wt) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const child = spawn('git', ['-C', wt, 'rev-parse', '--abbrev-ref', 'HEAD'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
106
|
+
let out = '';
|
|
107
|
+
child.stdout.on('data', (c) => { out += c.toString('utf8'); });
|
|
108
|
+
child.on('error', () => resolve(null));
|
|
109
|
+
child.on('close', (code) => resolve(code === 0 ? out.trim() || null : null));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** git diff --name-only base...HEAD;任何失败 → null(gate-policy 保守放行)。 */
|
|
113
|
+
function gitDiffNames(wt, base) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const child = spawn('git', ['-C', wt, 'diff', '--name-only', `${base}...HEAD`], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
116
|
+
let out = '';
|
|
117
|
+
child.stdout.on('data', (c) => { out += c.toString('utf8'); });
|
|
118
|
+
child.on('error', () => resolve(null));
|
|
119
|
+
child.on('close', (code) => {
|
|
120
|
+
if (code !== 0)
|
|
121
|
+
return resolve(null);
|
|
122
|
+
resolve(out.split('\n').map((s) => s.trim()).filter(Boolean));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -10,6 +10,7 @@ import { sendChainReport, createSender, parseSendRequest, resolveReportChainId,
|
|
|
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';
|
|
13
|
+
import { sessionPresetOf } from '../dispatcher/session-preset.js';
|
|
13
14
|
import { PREFETCH_MANIFEST_SCHEMA } from '../domain/prefetch-manifest.js';
|
|
14
15
|
import { WikiVaultClient } from '../wiki/wiki-vault-client.js';
|
|
15
16
|
import { LocalWikiClient } from '../wiki/local-kb-client.js';
|
|
@@ -131,16 +132,6 @@ function buildFreeSendGuidance(routes, query, dm) {
|
|
|
131
132
|
'投递结果只向用户确认成功与目标,不复述正文;失败时把 error/guidance 原样转告,勿编造原因。',
|
|
132
133
|
].join('\n');
|
|
133
134
|
}
|
|
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
|
-
}
|
|
144
135
|
/** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 建链。
|
|
145
136
|
* 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
|
|
146
137
|
* 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
|
|
@@ -247,7 +238,7 @@ export function registerMainSessionTools(ctx, configProvider) {
|
|
|
247
238
|
const targetOpts = {
|
|
248
239
|
retryDelaysMs: [], manual: true,
|
|
249
240
|
targetKind: (parsed.dm ? 'user' : 'group'),
|
|
250
|
-
sessionPreset: sessionPresetOf(ctx, exec),
|
|
241
|
+
sessionPreset: sessionPresetOf(ctx, exec?.agent),
|
|
251
242
|
agent: exec?.agent,
|
|
252
243
|
};
|
|
253
244
|
const deliver = async (variant, query) => {
|
|
@@ -360,13 +351,15 @@ export function registerMainSessionTools(ctx, configProvider) {
|
|
|
360
351
|
const dm = args.dm === true;
|
|
361
352
|
const deliver = createSender(ctx, service, configProvider, {
|
|
362
353
|
retryDelaysMs: [], manual: true, targetKind: dm ? 'user' : 'group',
|
|
363
|
-
sessionPreset: sessionPresetOf(ctx, exec), agent: exec?.agent,
|
|
354
|
+
sessionPreset: sessionPresetOf(ctx, exec?.agent), agent: exec?.agent,
|
|
364
355
|
});
|
|
365
356
|
const r = await deliver('free-send', text);
|
|
366
357
|
if (!r.ok) {
|
|
358
|
+
// 注意:返回对象不得带 undefined 值——工具输出要过 lossless-JSON 校验,undefined 会让模型只看到
|
|
359
|
+
// 「value is not lossless JSON」而看不到真实错误(实测踩过)。
|
|
367
360
|
return {
|
|
368
361
|
kind: 'send', mode: 'free', dm, error: r.error,
|
|
369
|
-
|
|
362
|
+
...(r.error.startsWith(DSH_IM_MISSING_PREFIX) ? { guidance: DSH_IM_MISSING_GUIDANCE } : {}),
|
|
370
363
|
};
|
|
371
364
|
}
|
|
372
365
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joekytc/dsh-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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",
|
package/personas/persona-dt.md
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
- fail = critical/high 未处置 → 系统 createReworkTask 让 D 返工 + 新建复审卡。
|
|
14
14
|
6. 不得调用 kanban_create;只可 complete/block/comment 本任务(会话绑定)。
|
|
15
15
|
|
|
16
|
+
## 问题取证(PR2,critical/high 必附)
|
|
17
|
+
报告 critical/high 问题时,每条 issue 附 `evidence = { file, command, exit }`:`command` 为你实际执行过的验证命令;把该命令的**逐字原始输出**写入存档文件(`file` = 绝对路径,可写在链工作区内),文件首行必须为 `[exit code: N]`(N 为实际退出码);`exit` = 该实际退出码。medium/low 建议同样附证据,缺省则标记为未提供证据。存档必须来自真实执行——系统始终纸面核对首行退出码与声明是否一致;配置开启证据重放且命令合规时,伪造/不自洽的存档会被重放 `differs` 实锤转人工(默认关闭时仅纸面核对)。
|
|
18
|
+
|
|
16
19
|
## open-code-review(ocr)评审引擎(双模)
|
|
17
20
|
- 评审引擎由配置面板 reviewEngine.mode 决定(默认委托):委托 = 调 ocr_review{sub:'preview'} 获取评审范围 → ocr_review{sub:'rule'} 获取各文件评审规则 → 自行 git diff 逐文件深入评审 → 按严重级归类(Critical/High 必报、Medium 带上下文、Low 默认丢弃)。
|
|
18
21
|
- 托管 = 调 ocr_review{sub:'managed', from:<TARGET_BRANCH>, to:<branch>}(branch 取 D 交接 metadata.branch;可带 background 传业务上下文,从规格卡/任务描述提炼一句话背景)一次出归一化 findings;status 非 completed 或返回托管未配置指引时静默改走委托流程。
|