@joekytc/dsh-swarm 0.3.10 → 0.3.12
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 +8 -0
- package/README.zh-CN.md +8 -0
- package/lib/dispatcher/agent-runner.js +106 -38
- package/lib/dispatcher/session-events.d.ts +14 -0
- package/lib/dispatcher/session-events.js +34 -0
- package/lib/dispatcher/v-orchestrator.js +2 -2
- package/lib/domain/review-evidence.js +3 -2
- package/lib/routes/kanban-http.js +23 -1
- package/lib/services/llm-catalog.d.ts +18 -0
- package/lib/services/llm-catalog.js +41 -0
- package/package.json +1 -1
- package/personas/kanban-dt/agent.cordis.yml +4 -0
- package/personas/persona-dt.md +1 -0
package/README.md
CHANGED
|
@@ -133,6 +133,10 @@ plan plan rev. KB impl impl rev. KB wrap-up
|
|
|
133
133
|
|
|
134
134
|
All keys are optional; schema lives in `src/config.ts`. **Most users only need the first three** — keep the rest at their defaults.
|
|
135
135
|
|
|
136
|
+
The 「模型链」 (model chain) card in the config panel: **every role gets its own model** — pick provider, model and reasoning effort per role; roles you leave untouched show 「继承」 (inherit) and follow the default.
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
136
140
|
| Key | Default | Description |
|
|
137
141
|
|---|---|---|
|
|
138
142
|
| `storageDir` | `$DSH_HOME/storages/kanban` | Event log (`events.jsonl`), orchestration state, per-task workspaces, `dispatcher.log`. Value must use the unquoted `!!js dshHomePath("storages/kanban")` form — quoting degrades it into a literal string |
|
|
@@ -185,6 +189,10 @@ web config panel under 「Swarm 配置 → 评审引擎(ocr)」 (Swarm confi
|
|
|
185
189
|
| **Delegate** (default) | ocr only outputs the review scope and rules; DT reviews each file with its own model | Zero API keys, works out of the box |
|
|
186
190
|
| **Managed** | ocr runs the full review with your chosen provider/model and returns normalized findings in one shot | For large change sets; delegate mode hints at switching past 50 files (a hint only, never auto-switched) |
|
|
187
191
|
|
|
192
|
+
The 「评审引擎(ocr)」 (review engine) card in the config panel: with ocr installed, choose the mode (managed / delegate) plus provider and model, then click 「应用到 ocr」 (Apply to ocr) to wire it up.
|
|
193
|
+
|
|
194
|
+

|
|
195
|
+
|
|
188
196
|
### Install
|
|
189
197
|
|
|
190
198
|
- When ocr is missing, the config panel shows a red banner — click 「安装 ocr」 (Install ocr) for a one-click global install (async, cancellable);
|
package/README.zh-CN.md
CHANGED
|
@@ -135,6 +135,10 @@ p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
|
|
|
135
135
|
|
|
136
136
|
所有键均可选;schema 见 `src/config.ts`。**多数使用者只需关心前三项**,其余保持默认即可。
|
|
137
137
|
|
|
138
|
+
配置面板的「模型链」卡:**每个角色都能单独配模型**——逐个选提供方、模型与推理强度;未单独设置的角色显示「继承」,沿用默认配置。
|
|
139
|
+
|
|
140
|
+

|
|
141
|
+
|
|
138
142
|
| 键 | 默认值 | 说明 |
|
|
139
143
|
|---|---|---|
|
|
140
144
|
| `storageDir` | `$DSH_HOME/storages/kanban` | 事件日志(`events.jsonl`)、编排状态、每任务工作区、`dispatcher.log`。取值须用不加引号的 `!!js dshHomePath("storages/kanban")` 写法,加引号会退化成字面量字符串 |
|
|
@@ -186,6 +190,10 @@ p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
|
|
|
186
190
|
| **委托**(默认) | ocr 只输出评审范围与规则,由 DT 自己的模型逐文件深入评审 | 零 API key,开箱即用 |
|
|
187
191
|
| **托管** | ocr 调用你选定的提供方/模型跑完整评审,一次返回归一化 findings | 适合大变更集;委托模式下超 50 文件时会提示可切换(仅提醒,不自动切换) |
|
|
188
192
|
|
|
193
|
+
配置面板的「评审引擎(ocr)」卡:装好 ocr 后选模式(托管/委托)与提供方、模型,点「应用到 ocr」即完成接入。
|
|
194
|
+
|
|
195
|
+

|
|
196
|
+
|
|
189
197
|
### 安装
|
|
190
198
|
|
|
191
199
|
- 未安装时配置面板出现红横幅,点「安装 ocr」一键全局安装(异步执行,可取消);
|
|
@@ -7,7 +7,8 @@ import { ensureLocalKbRoot } from '../wiki/local-kb.js';
|
|
|
7
7
|
import { installCleanFsTools } from '../roles/clean-fs-tools.js';
|
|
8
8
|
import { mountOrRecompose } from '../roles/preset-installer.js';
|
|
9
9
|
import { buildModelCandidates, isModelUnavailableError } from './model-candidates.js';
|
|
10
|
-
import { toolName, isGuardSynthesizedReply } from './session-events.js';
|
|
10
|
+
import { toolName, isGuardSynthesizedReply, lastTurnEnd } from './session-events.js';
|
|
11
|
+
import { filterCandidatesByCatalog } from '../services/llm-catalog.js';
|
|
11
12
|
import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
|
|
12
13
|
import { isPathInside, resolveTargetRepoDir } from './target-repo.js';
|
|
13
14
|
import { injectGitCredentials, resolveGitPatFromCtx } from './git-credentials.js';
|
|
@@ -21,6 +22,18 @@ const GOAL_MODE_KEYWORDS = ['/goal', '目标模式', 'goal mode'];
|
|
|
21
22
|
function isInfraError(err) {
|
|
22
23
|
return /cannot prepare session|while it is live|timeout|ETIMEDOUT|ECONNREFUSED|ECONNRESET|socket/i.test(String(err));
|
|
23
24
|
}
|
|
25
|
+
/** turn 级错误分类(环境中止 ≠ 角色不收敛):
|
|
26
|
+
* - 'config':provider/model/effort 组合性永久错误(重派必撞同一错误)→ 终态 block(model-unavailable) 等人工修配置;
|
|
27
|
+
* - 'transient':配额/限流/网络类瞬时错误 → failTask({infra:true})(不计 attempts,靠调度 tick 重派);
|
|
28
|
+
* - 'unknown':未识别错误 → failTask 计入 attempts(有界重试,防未知错误无限重派)。
|
|
29
|
+
* 三种去向都不写 protocol_violation、不烧 PV 护栏预算——环境错误不是角色协议违规。 */
|
|
30
|
+
function classifyTurnError(code, message) {
|
|
31
|
+
if (code === 'UNKNOWN_MODEL' || code === 'UNSUPPORTED_REASONING_EFFORT' || code === 'INVALID_REQUEST')
|
|
32
|
+
return 'config';
|
|
33
|
+
if (code === 'QUOTA' || /quota|rate.?limit|\b429\b/i.test(message) || isInfraError(message))
|
|
34
|
+
return 'transient';
|
|
35
|
+
return 'unknown';
|
|
36
|
+
}
|
|
24
37
|
/** 关键事件追加落盘 storageDir/dispatcher.log(与 dispatcher.ts 同一文件、同一行格式)——
|
|
25
38
|
* agent-runner 未注入 logFile,按 dispatcher 同款方式经 configProvider 派生 storageDir;
|
|
26
39
|
* 写失败静默忽略(日志只是观测面,事件日志才是事实源)。 */
|
|
@@ -380,7 +393,35 @@ ${task.body}`);
|
|
|
380
393
|
context = this.buildContext(task, state, hasRunHistory);
|
|
381
394
|
// 模型候选链:primary + fallbacks,model/provider 不可用时静默切换下一候选;
|
|
382
395
|
// 全部候选不可用 → block(model-unavailable) 抛给用户;非 model 错误 → failTask(原逻辑)。
|
|
383
|
-
const
|
|
396
|
+
const builtCandidates = buildModelCandidates(this.configProvider.getEffective(), task.assignee, this.defaultModel);
|
|
397
|
+
// 派发前预校验(LLM 目录):不存在的 provider/model 组合、模型未声明的 reasoningEffort
|
|
398
|
+
// 直接剔除,不再打进网络白烧会话(UNKNOWN_MODEL / UNSUPPORTED_REASONING_EFFORT 秒退实证形态——
|
|
399
|
+
// 候选 fallback 只护 create/resume 抛错,turn 期错误不触发切换,必须前置拦截)。
|
|
400
|
+
// fail-open:llm 服务未就绪/目录探测失败 → 全放行,不阻断派发。
|
|
401
|
+
// 全部候选被拒 → block(model-unavailable) 终态(与 spawn 全候选失败同语义,等人工修配置)。
|
|
402
|
+
let candidates = builtCandidates;
|
|
403
|
+
if (builtCandidates.length > 0) {
|
|
404
|
+
// llm 服务可能尚未接线(runner 只依赖 agents+kanban 就绪)——缺失/异常一律 fail-open。
|
|
405
|
+
let llmRuntime;
|
|
406
|
+
try {
|
|
407
|
+
llmRuntime = this.ctx.get('llm');
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
llmRuntime = undefined;
|
|
411
|
+
}
|
|
412
|
+
const precheck = await filterCandidatesByCatalog(llmRuntime, builtCandidates);
|
|
413
|
+
if (precheck.rejected.length > 0) {
|
|
414
|
+
const detail = precheck.rejected.map((r) => `${r.candidate.provider}/${r.candidate.model}: ${r.reason}`).join(';');
|
|
415
|
+
console.error('[dsh-swarm][debug] model precheck rejected task=' + taskId + ': ' + detail);
|
|
416
|
+
logToDispatcherLog(this.configProvider, '[model-precheck] task=' + taskId + ' rejected: ' + detail);
|
|
417
|
+
}
|
|
418
|
+
if (precheck.ok.length === 0) {
|
|
419
|
+
await this.kanban.blockTask(taskId, 'model-unavailable: 配置的模型候选与 LLM 目录不匹配 —— '
|
|
420
|
+
+ precheck.rejected.map((r) => `${r.candidate.provider}/${r.candidate.model}: ${r.reason}`).join(';'), 'system');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
candidates = precheck.ok;
|
|
424
|
+
}
|
|
384
425
|
if (candidates.length === 0) {
|
|
385
426
|
// 无任何候选配置:不传 agentOptions(用部署默认),单次尝试
|
|
386
427
|
agent = hasRunHistory
|
|
@@ -488,43 +529,70 @@ ${task.body}`);
|
|
|
488
529
|
console.error('[dsh-swarm][debug] runner skip block ' + taskId + ' status=' + (cur ? cur.status : 'gone'));
|
|
489
530
|
}
|
|
490
531
|
else {
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
532
|
+
// 环境中止识别:读本轮最后一轮 turn/end(拒答重试会产生第二轮,取 seq 最大者)。
|
|
533
|
+
// whenIdle resolve 只说明会话空闲,不区分「正常收敛」与「模型/网关错误秒退」——
|
|
534
|
+
// 后者按原判据会误记 protocol_violation(UNKNOWN_MODEL 误判实证:claim→block 1.3s、
|
|
535
|
+
// 0 次模型调用、0 token)。turn 以 error 结束 → 按 classifyTurnError 分流
|
|
536
|
+
// (config → block(model-unavailable) 终态;transient → failTask(infra) 靠 tick 重派;
|
|
537
|
+
// unknown → failTask 计预算有界重试),一律不写 protocol_violation、不烧 PV 预算。
|
|
538
|
+
// 只有 turn 正常 completed(或无 turn 事件的测试桩/宿主变体兜底)才进入下方协议违规
|
|
539
|
+
// 判据——PV 从此只剩真实语义:模型正常收敛但未调 terminal 工具(comment-only 等)。
|
|
540
|
+
const finalTurnEvents = typeof sessionLike.snapshotEvents === 'function'
|
|
541
|
+
? sessionLike.snapshotEvents(eventsBase)
|
|
542
|
+
: (sessionLike.events ?? []);
|
|
543
|
+
const turnInfo = lastTurnEnd(finalTurnEvents, eventsBase);
|
|
544
|
+
if (turnInfo?.kind === 'error') {
|
|
545
|
+
const cls = classifyTurnError(turnInfo.code, turnInfo.message ?? '');
|
|
546
|
+
const detail = (turnInfo.code ?? 'UNKNOWN') + ': ' + (turnInfo.message ?? 'unknown turn error');
|
|
547
|
+
console.error('[dsh-swarm][debug] turn abort(' + cls + ') ' + taskId + ': ' + detail);
|
|
548
|
+
logToDispatcherLog(this.configProvider, '[turn-abort] task=' + taskId + ' ' + cls
|
|
549
|
+
+ (cls === 'config' ? ' → blocked model-unavailable: ' : ' → failed(infra=' + (cls === 'transient') + '): ') + detail);
|
|
550
|
+
if (cls === 'config') {
|
|
551
|
+
await this.kanban.blockTask(taskId, 'model-unavailable: turn aborted (' + detail + ')', 'system');
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
await this.kanban.failTask(taskId, 'turn-abort: ' + detail, 'system', { infra: cls === 'transient' });
|
|
555
|
+
}
|
|
508
556
|
}
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
557
|
+
else {
|
|
558
|
+
// 协议违规护栏:连续 protocol_violation 阻塞 ≥ maxProtocolViolations(默认 2)后,
|
|
559
|
+
// 下一次违规直接 gave_up(不再恢复,走 [blocked-final] 证据链抛给主 agent)。任意角色(含 pt/dt)统一。
|
|
560
|
+
const maxPV = this.configProvider.getEffective().dispatcher?.maxProtocolViolations ?? 2;
|
|
561
|
+
const priorViolations = fresh.events.filter((e) => e.taskId === taskId && e.kind === 'task/blocked' &&
|
|
562
|
+
String(e.payload['reason'] ?? '').startsWith('protocol_violation')).length;
|
|
563
|
+
const finalBlock = priorViolations >= maxPV;
|
|
564
|
+
const reason = finalBlock
|
|
565
|
+
? 'gave_up: protocol_violation after ' + maxPV + ' review cycles without complete/block'
|
|
566
|
+
: 'protocol_violation: idle without complete/block';
|
|
567
|
+
await this.kanban.blockTask(taskId, reason, 'system');
|
|
568
|
+
// comment-only 收尾显形——放 block 之后(保证 block 一定先发生,
|
|
569
|
+
// 告警只追加在已阻塞卡上,不影响终态判据与 [blocked-final] 证据链快照)。
|
|
570
|
+
// 本轮增量事件命中 kanban_comment(经 toolName 兼容读取落盘/live 两形态)且未成功提交
|
|
571
|
+
// (能走到这里即终态判据已确认非确定态)→ 交付内容很可能滞留 comments,显形告警。
|
|
572
|
+
const commentOnly = agent.session.snapshotEvents(eventsBase).some((e) => toolName(e) === 'kanban_comment');
|
|
573
|
+
if (commentOnly) {
|
|
574
|
+
await this.kanban.comment(taskId, '[comment-only-closeout] 本轮仅用 kanban_comment 交付(无 complete/block)。交付内容可能滞留 comments(如角色工具缺失/会话被错误复用);请人工核对 comments 与产物后解除阻塞重派。', 'system');
|
|
575
|
+
}
|
|
576
|
+
if (finalBlock) {
|
|
577
|
+
// [blocked-final] 证据链:block 时间线 + 复核/评论时间线 + 最终 reason(system 确定性写入)
|
|
578
|
+
const evs = fresh.events.filter((e) => e.taskId === taskId);
|
|
579
|
+
const blockTimeline = evs
|
|
580
|
+
.filter((e) => e.kind === 'task/blocked')
|
|
581
|
+
.map((e) => ` - seq=${e.seq} at=${e.at} author=${e.author} reason=${String(e.payload['reason'] ?? '')}`)
|
|
582
|
+
.join('\n');
|
|
583
|
+
const reviewTimeline = evs
|
|
584
|
+
.filter((e) => e.kind === 'task/commented')
|
|
585
|
+
.map((e) => ` - seq=${e.seq} at=${e.at} author=${e.author}: ${String(e.payload['body'] ?? '')}`)
|
|
586
|
+
.join('\n');
|
|
587
|
+
await this.kanban.comment(taskId, [
|
|
588
|
+
'[blocked-final] 协议违规超护栏,任务不再自动恢复(人工解除后仍按 gave_up 终态处理)。',
|
|
589
|
+
'## block 时间线',
|
|
590
|
+
blockTimeline,
|
|
591
|
+
'## 复核/评论时间线',
|
|
592
|
+
reviewTimeline || ' - (无复核评论)',
|
|
593
|
+
'最终原因: ' + reason,
|
|
594
|
+
].join('\n'), 'system');
|
|
595
|
+
}
|
|
528
596
|
}
|
|
529
597
|
}
|
|
530
598
|
}
|
|
@@ -30,3 +30,17 @@ export declare function replayModel(e: unknown): string | null;
|
|
|
30
30
|
export declare const GUARD_SYNTH_REPLAY_MODELS: ReadonlySet<string>;
|
|
31
31
|
/** 该事件是否为网关合成拒答/缓存回放的 assistant 消息(判定与标记集单一事实源)。 */
|
|
32
32
|
export declare function isGuardSynthesizedReply(e: unknown): boolean;
|
|
33
|
+
/** turn/end 结束原因(区分「环境错误秒退」与「角色正常收敛」的单一读取点):
|
|
34
|
+
* 落盘形态 {"type":"turn/end","seq":N,"data":{"turn":T,"reason":{"kind":"completed"} |
|
|
35
|
+
* {"kind":"error","error":{"code":"UNKNOWN_MODEL","message":"…"}}}};
|
|
36
|
+
* live 顶层展开形态经顶层 turn/reason 兜底兼容。非 turn/end 或 reason 不可读 → null。 */
|
|
37
|
+
export interface TurnEndInfo {
|
|
38
|
+
turn: number;
|
|
39
|
+
kind: string;
|
|
40
|
+
code: string | null;
|
|
41
|
+
message: string | null;
|
|
42
|
+
}
|
|
43
|
+
export declare function turnEndOf(e: unknown): TurnEndInfo | null;
|
|
44
|
+
/** 本轮增量(seq > fromSeq)内最后一轮 turn/end——拒答重试会产生第二轮,取 seq 最大者。
|
|
45
|
+
* 无 turn/end(测试桩/宿主变体)→ null,调用方按「无轮信息」回退原判据,不改变既有行为。 */
|
|
46
|
+
export declare function lastTurnEnd(events: ReadonlyArray<unknown>, fromSeq: number): TurnEndInfo | null;
|
|
@@ -55,3 +55,37 @@ export function isGuardSynthesizedReply(e) {
|
|
|
55
55
|
const m = replayModel(e);
|
|
56
56
|
return m !== null && GUARD_SYNTH_REPLAY_MODELS.has(m);
|
|
57
57
|
}
|
|
58
|
+
export function turnEndOf(e) {
|
|
59
|
+
const rec = e;
|
|
60
|
+
const t = rec?.type ?? rec?.data?.type;
|
|
61
|
+
if (t !== 'turn/end')
|
|
62
|
+
return null;
|
|
63
|
+
const reason = rec?.data?.reason ?? rec?.reason;
|
|
64
|
+
const kind = typeof reason?.kind === 'string' ? reason.kind : null;
|
|
65
|
+
if (!kind)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
turn: Number(rec?.data?.turn ?? rec?.turn) || 0,
|
|
69
|
+
kind,
|
|
70
|
+
code: typeof reason?.error?.code === 'string' ? reason.error.code : null,
|
|
71
|
+
message: typeof reason?.error?.message === 'string' ? reason.error.message : null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** 本轮增量(seq > fromSeq)内最后一轮 turn/end——拒答重试会产生第二轮,取 seq 最大者。
|
|
75
|
+
* 无 turn/end(测试桩/宿主变体)→ null,调用方按「无轮信息」回退原判据,不改变既有行为。 */
|
|
76
|
+
export function lastTurnEnd(events, fromSeq) {
|
|
77
|
+
let best = null;
|
|
78
|
+
let bestSeq = -1;
|
|
79
|
+
for (const e of events) {
|
|
80
|
+
const rec = e;
|
|
81
|
+
const seq = Number(rec?.seq ?? rec?.data?.seq) || 0;
|
|
82
|
+
if (seq <= fromSeq)
|
|
83
|
+
continue;
|
|
84
|
+
const info = turnEndOf(e);
|
|
85
|
+
if (info && seq >= bestSeq) {
|
|
86
|
+
best = info;
|
|
87
|
+
bestSeq = seq;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return best;
|
|
91
|
+
}
|
|
@@ -64,7 +64,7 @@ export const PHASE_INSTRUCTIONS = {
|
|
|
64
64
|
].join('\n'),
|
|
65
65
|
dt: [
|
|
66
66
|
'## DT 阶段任务体要求(实现校验+评审,只读护栏)',
|
|
67
|
-
'body 写入实现校验指令:对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审目标为 D 交接 metadata.branch 指向的 feature 分支,而非 TARGET_BRANCH。评审引擎:用 ocr_review 工具按配置引擎评审——委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block(\'review-tool-unavailable\') 并注明可在 GUI 配置面板安装。ocr_review 的 repo 参数缺省 = 会话工作目录;评审目标不在其内(D 的 worktree / 其他仓库)时须显式传仓库绝对路径(取 D 交接 metadata.worktree_dir 或规格卡 file-prefetch 附件 ref)。',
|
|
67
|
+
'body 写入实现校验指令:对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。review_evidence.test 必须带两键:exit(数字 0,键名 exit 非 exit_code)与 runner(固定 \'vitest\',与 D 卡 gate 实测口径一致)——评审闸机械校验,键名/取值错一处即拒绝 complete。评审目标为 D 交接 metadata.branch 指向的 feature 分支,而非 TARGET_BRANCH。评审引擎:用 ocr_review 工具按配置引擎评审——委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block(\'review-tool-unavailable\') 并注明可在 GUI 配置面板安装。ocr_review 的 repo 参数缺省 = 会话工作目录;评审目标不在其内(D 的 worktree / 其他仓库)时须显式传仓库绝对路径(取 D 交接 metadata.worktree_dir 或规格卡 file-prefetch 附件 ref)。',
|
|
68
68
|
'铁律:DT 是只读校验+评审角色,绝不修改源码/产物;校验经 ToolGuard 硬性只读护栏;不注入 git 凭据。',
|
|
69
69
|
].join('\n'),
|
|
70
70
|
w3: [
|
|
@@ -89,7 +89,7 @@ export function buildPhaseInstruction(phase, ctx, kbMode) {
|
|
|
89
89
|
case 'd':
|
|
90
90
|
return '先读父任务交接(W2)里的 page_path(本地库 wiki/sources/ 下相对路径,库根见运行时上下文注入——Task 8 已注入 D),直接 fs 读实施计划原文;如需 KB 经验召回,经 skill 工具加载 llm-wiki query(本地库;kanban-d preset 已挂 tool-skill)——再按计划执行规格卡 solution/testing —— git worktree/branch → 改代码/README → git commit → git push(仅 feature 分支,可选)→ 自检并附产物证据(changed_files/commit_hash)。其余 TDD/TARGET_REPO/TARGET_BRANCH 等要求与 remote 版一致。';
|
|
91
91
|
case 'dt':
|
|
92
|
-
return `对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。评审引擎:用 ocr_review 工具按配置引擎评审(委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block('review-tool-unavailable') 并注明可在 GUI 配置面板安装)。ocr_review 的 repo 缺省 = 会话工作目录,评审目标不在其内(D 的 worktree / 其他仓库)时显式传仓库绝对路径(D 交接 metadata.worktree_dir / 规格卡 file-prefetch ref)。评审页经 fs 写本地库 <库根>/wiki/queries/${ctx.chainId}/review/<name>.md(库根已由系统注入 DT 运行时上下文);库根外一律只读,其余铁律与 remote 版一致。`;
|
|
92
|
+
return `对 D 产物实证校验(test/build/typecheck/diff/git 证据 + open-code-review 评审),输出 verdict+issues 入交接 metadata.review_evidence。review_evidence.test 必须带两键:exit(数字 0,键名 exit 非 exit_code)与 runner(固定 'vitest')——评审闸机械校验,键名/取值错一处即拒绝 complete。评审引擎:用 ocr_review 工具按配置引擎评审(委托 = preview→rule→git diff 自评;托管 = managed 一次出 findings;托管未配置时按工具指引静默改走委托;ocr 未安装则 kanban_block('review-tool-unavailable') 并注明可在 GUI 配置面板安装)。ocr_review 的 repo 缺省 = 会话工作目录,评审目标不在其内(D 的 worktree / 其他仓库)时显式传仓库绝对路径(D 交接 metadata.worktree_dir / 规格卡 file-prefetch ref)。评审页经 fs 写本地库 <库根>/wiki/queries/${ctx.chainId}/review/<name>.md(库根已由系统注入 DT 运行时上下文);库根外一律只读,其余铁律与 remote 版一致。`;
|
|
93
93
|
default:
|
|
94
94
|
return PHASE_INSTRUCTIONS[phase] ?? '';
|
|
95
95
|
}
|
|
@@ -39,8 +39,9 @@ export function validateReviewEvidence(role, handoff) {
|
|
|
39
39
|
// DT:实证校验证据(fail 评审同样必须已实证——test 字段必须存在;仅 pass 要求 exit 0)
|
|
40
40
|
if (!ev.test || typeof ev.test !== 'object')
|
|
41
41
|
missing.push('review_evidence.test');
|
|
42
|
+
// 报错文案带明确键名:agent 不读源码也能一轮自纠(exit_code 等别名一律不收)
|
|
42
43
|
else if (ev.verdict === 'pass' && ev.test['exit'] !== 0)
|
|
43
|
-
missing.push('review_evidence.test
|
|
44
|
+
missing.push('review_evidence.test.exit 必须为数字 0(键名 exit,非 exit_code;pass 门槛)');
|
|
44
45
|
const buildOk = ev.build && typeof ev.build === 'object';
|
|
45
46
|
const tcOk = ev.typecheck && typeof ev.typecheck === 'object';
|
|
46
47
|
if (!buildOk && !tcOk)
|
|
@@ -76,7 +77,7 @@ export function validateReviewEvidence(role, handoff) {
|
|
|
76
77
|
missing.push('review_evidence.tdd (test_files)');
|
|
77
78
|
}
|
|
78
79
|
if (ev.test && typeof ev.test === 'object' && ev.test['runner'] !== 'vitest') {
|
|
79
|
-
missing.push(
|
|
80
|
+
missing.push("review_evidence.test.runner 必须为 'vitest'(键名 runner,与 D 卡 gate 实测口径一致)");
|
|
80
81
|
}
|
|
81
82
|
if (ev.verdict === 'pass' && tdd.test_first !== true) {
|
|
82
83
|
missing.push('review_evidence.tdd (test_first=true)');
|
|
@@ -2,7 +2,8 @@ import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { buildLlmCatalog } from '../services/llm-catalog.js';
|
|
5
|
+
import { buildLlmCatalog, filterCandidatesByCatalog } from '../services/llm-catalog.js';
|
|
6
|
+
import { ROLES } from '../domain/config-override.js';
|
|
6
7
|
import { INSTALL_GUIDANCE, OCR_PACKAGE, managedProviderReady, probeOcr, wireManagedProvider } from '../services/ocr-cli.js';
|
|
7
8
|
import { serveKanbanEvents } from './kanban-sse.js';
|
|
8
9
|
import { resolveReviewTarget } from '../domain/review-target.js';
|
|
@@ -323,6 +324,27 @@ export function registerKanbanHttp(ctx, provider, configProvider, llm, config, o
|
|
|
323
324
|
},
|
|
324
325
|
imDelivery: { fallbackBotId: raw.imDelivery?.fallbackBotId ?? cur.imDelivery?.fallbackBotId ?? '' },
|
|
325
326
|
};
|
|
327
|
+
// 模型配置保存口预检(堵源头):角色模型与 managed 评审组合 (provider, model, effort)
|
|
328
|
+
// 与 LLM 目录比对,确认不匹配 → 400 附明细。与派发前预检(agent-runner 消费同一目录)
|
|
329
|
+
// 同口径——跨层拼接的死组合(provider/model 来自不同层)在保存时即被拦下,不再流入派发。
|
|
330
|
+
// fail-open:llm 运行时缺失/目录探测失败放行(网络抖动 ≠ 配置错误),不阻断无关字段保存。
|
|
331
|
+
const toProbe = [];
|
|
332
|
+
for (const role of ROLES) {
|
|
333
|
+
const m = snapshot.roles.models[role];
|
|
334
|
+
if (m?.provider?.trim() && m?.model?.trim())
|
|
335
|
+
toProbe.push({ provider: m.provider, model: m.model, reasoningEffort: m.reasoningEffort });
|
|
336
|
+
}
|
|
337
|
+
if (snapshot.reviewEngine.mode === 'managed' && snapshot.reviewEngine.managed.provider.trim() && snapshot.reviewEngine.managed.model.trim()) {
|
|
338
|
+
toProbe.push({ provider: snapshot.reviewEngine.managed.provider, model: snapshot.reviewEngine.managed.model });
|
|
339
|
+
}
|
|
340
|
+
const catalogProbe = await filterCandidatesByCatalog(llm, toProbe);
|
|
341
|
+
if (catalogProbe.rejected.length > 0) {
|
|
342
|
+
json(res, 400, {
|
|
343
|
+
error: 'validation failed',
|
|
344
|
+
fields: catalogProbe.rejected.map((r) => `${r.candidate.provider}/${r.candidate.model}: ${r.reason}`),
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
326
348
|
const r = configProvider.applyOverride(snapshot);
|
|
327
349
|
if (!r.ok) {
|
|
328
350
|
json(res, 400, { error: 'validation failed', fields: r.errors });
|
|
@@ -33,3 +33,21 @@ export interface LlmRuntimeLike {
|
|
|
33
33
|
}>;
|
|
34
34
|
}
|
|
35
35
|
export declare function buildLlmCatalog(llm: LlmRuntimeLike): Promise<LlmCatalog>;
|
|
36
|
+
/** 候选模型(与 dispatcher AgentModelOptions 结构同形;不 import 防环)。 */
|
|
37
|
+
export interface ModelCandidateLike {
|
|
38
|
+
provider: string;
|
|
39
|
+
model: string;
|
|
40
|
+
reasoningEffort?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface CandidateReject {
|
|
43
|
+
candidate: ModelCandidateLike;
|
|
44
|
+
reason: string;
|
|
45
|
+
}
|
|
46
|
+
/** 派发/保存前候选预校验:provider 已配置、model 属该 provider、reasoningEffort ∈ 模型声明集。
|
|
47
|
+
* fail-open 原则:运行时缺失、provider 枚举失败、单候选探测异常一律放行——只拦「确实查到
|
|
48
|
+
* 不匹配」的组合,目录不可达(网络抖动)≠ 配置错误,不应阻断派发/保存。
|
|
49
|
+
* effort 声明集为空(resolveModelInfo 降级)时跳过 effort 把关。 */
|
|
50
|
+
export declare function filterCandidatesByCatalog(runtime: LlmRuntimeLike | undefined, candidates: readonly ModelCandidateLike[]): Promise<{
|
|
51
|
+
ok: ModelCandidateLike[];
|
|
52
|
+
rejected: CandidateReject[];
|
|
53
|
+
}>;
|
|
@@ -17,3 +17,44 @@ export async function buildLlmCatalog(llm) {
|
|
|
17
17
|
}
|
|
18
18
|
return { providers, models };
|
|
19
19
|
}
|
|
20
|
+
/** 派发/保存前候选预校验:provider 已配置、model 属该 provider、reasoningEffort ∈ 模型声明集。
|
|
21
|
+
* fail-open 原则:运行时缺失、provider 枚举失败、单候选探测异常一律放行——只拦「确实查到
|
|
22
|
+
* 不匹配」的组合,目录不可达(网络抖动)≠ 配置错误,不应阻断派发/保存。
|
|
23
|
+
* effort 声明集为空(resolveModelInfo 降级)时跳过 effort 把关。 */
|
|
24
|
+
export async function filterCandidatesByCatalog(runtime, candidates) {
|
|
25
|
+
if (!runtime || candidates.length === 0)
|
|
26
|
+
return { ok: [...candidates], rejected: [] };
|
|
27
|
+
let providerIds;
|
|
28
|
+
try {
|
|
29
|
+
providerIds = new Set(runtime.listProviders().map((p) => p.id));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: [...candidates], rejected: [] };
|
|
33
|
+
}
|
|
34
|
+
const ok = [];
|
|
35
|
+
const rejected = [];
|
|
36
|
+
for (const c of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
if (!providerIds.has(c.provider)) {
|
|
39
|
+
rejected.push({ candidate: c, reason: `provider "${c.provider}" 未配置` });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const models = await runtime.listModels(c.provider);
|
|
43
|
+
if (!models.some((m) => m.id === c.model)) {
|
|
44
|
+
rejected.push({ candidate: c, reason: `provider "${c.provider}" 无模型 "${c.model}"` });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const info = await runtime.resolveModelInfo(c.provider, c.model).catch(() => undefined);
|
|
48
|
+
const efforts = (info?.reasoning?.efforts ?? []).map((e) => e.id);
|
|
49
|
+
if (efforts.length > 0 && c.reasoningEffort && !efforts.includes(c.reasoningEffort)) {
|
|
50
|
+
rejected.push({ candidate: c, reason: `模型 "${c.model}" 不支持 reasoningEffort "${c.reasoningEffort}"(声明:${efforts.join('/')})` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
ok.push(c);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
ok.push(c); // 单候选探测异常 fail-open
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { ok, rejected };
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joekytc/dsh-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
{ verdict: 'pass'|'fail', issues: [...], test/build/typecheck/lint/diff/git/
|
|
60
60
|
openCodeReview/reviewPage }: pass = all six checks pass (system advances to W3);
|
|
61
61
|
fail = critical/high unresolved (system creates a D rework task + fresh review card).
|
|
62
|
+
test must carry two mechanically-gated keys (wrong key name or value rejects the
|
|
63
|
+
complete call): test = { exit: 0, runner: 'vitest', targeted/full } — the key is
|
|
64
|
+
exit (a number, never exit_code) and runner is fixed to 'vitest' (same runner the
|
|
65
|
+
D-side gate actually runs).
|
|
62
66
|
6. Never call kanban_create; only complete/block/comment your own bound task.
|
|
63
67
|
|
|
64
68
|
- id: agent-instructions
|
package/personas/persona-dt.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
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
|
+
- test 必须带两键(评审闸机械校验,键名/取值错一处即拒绝 complete):test = { exit: 0, runner: 'vitest', targeted/full }——键名是 `exit`(数字,非 exit_code),`runner` 固定 'vitest'(与 D 卡 gate 实测口径一致)。
|
|
12
13
|
- pass = 六项校验全过 → 系统推进 W3;
|
|
13
14
|
- fail = critical/high 未处置 → 系统 createReworkTask 让 D 返工 + 新建复审卡。
|
|
14
15
|
6. 不得调用 kanban_create;只可 complete/block/comment 本任务(会话绑定)。
|