@joekytc/dsh-swarm 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -139
- package/README.zh-CN.md +172 -125
- package/client/ConfigSection.tsx +96 -5
- package/client/ConfigSelect.tsx +3 -1
- package/client/TaskDrawer.tsx +2 -0
- package/client/config-store.ts +48 -7
- package/client/kanban.css +69 -0
- package/client/timeline-model.ts +2 -0
- package/lib/client.js +172 -14
- package/lib/config.d.ts +18 -1
- package/lib/config.js +14 -0
- package/lib/dispatcher/agent-runner.d.ts +13 -5
- package/lib/dispatcher/agent-runner.js +78 -38
- package/lib/dispatcher/chain-auditor.d.ts +4 -4
- package/lib/dispatcher/chain-auditor.js +10 -8
- package/lib/dispatcher/dispatcher.d.ts +18 -18
- package/lib/dispatcher/dispatcher.js +59 -43
- package/lib/dispatcher/event-waker.d.ts +1 -1
- package/lib/dispatcher/event-waker.js +2 -2
- package/lib/dispatcher/git-credentials.d.ts +2 -2
- package/lib/dispatcher/git-credentials.js +2 -2
- package/lib/dispatcher/model-candidates.d.ts +1 -1
- package/lib/dispatcher/model-candidates.js +1 -1
- package/lib/dispatcher/session-events.d.ts +1 -1
- package/lib/dispatcher/session-events.js +1 -1
- package/lib/dispatcher/target-repo.d.ts +2 -2
- package/lib/dispatcher/target-repo.js +3 -3
- package/lib/dispatcher/v-orchestrator.d.ts +8 -8
- package/lib/dispatcher/v-orchestrator.js +41 -41
- package/lib/dispatcher/watchdog.d.ts +1 -1
- package/lib/dispatcher/watchdog.js +2 -2
- package/lib/domain/config-override.d.ts +14 -0
- package/lib/domain/config-override.js +38 -1
- package/lib/domain/delivery-contract.d.ts +2 -2
- package/lib/domain/delivery-contract.js +5 -5
- package/lib/domain/delivery-evidence.d.ts +1 -1
- package/lib/domain/delivery-evidence.js +1 -1
- package/lib/domain/event-store.js +1 -1
- package/lib/domain/im-message.d.ts +15 -0
- package/lib/domain/im-message.js +149 -0
- package/lib/domain/kanban-service.d.ts +13 -10
- package/lib/domain/kanban-service.js +36 -28
- package/lib/domain/memory.js +1 -1
- package/lib/domain/ocr-review.d.ts +47 -0
- package/lib/domain/ocr-review.js +105 -0
- package/lib/domain/permissions.js +5 -5
- package/lib/domain/projection.js +6 -6
- package/lib/domain/state-machine.js +1 -1
- package/lib/domain/task-parents.js +1 -1
- package/lib/domain/types.d.ts +4 -4
- package/lib/index.js +14 -9
- package/lib/roles/clean-fs-tools.js +3 -4
- package/lib/roles/preset-installer.js +3 -3
- package/lib/roles/skill-installer.js +1 -1
- package/lib/roles/toolsets.d.ts +60 -16
- package/lib/roles/toolsets.js +265 -47
- package/lib/roles/wiki-worker.js +3 -3
- package/lib/routes/kanban-http.d.ts +25 -2
- package/lib/routes/kanban-http.js +171 -11
- package/lib/routes/kanban-sse.d.ts +1 -1
- package/lib/routes/kanban-sse.js +1 -1
- package/lib/routes/planning-driver.d.ts +4 -2
- package/lib/routes/planning-driver.js +7 -3
- package/lib/routes/prefix-router.d.ts +3 -3
- package/lib/routes/prefix-router.js +3 -1
- package/lib/services/config-provider.d.ts +0 -4
- package/lib/services/config-provider.js +7 -9
- package/lib/services/im-delivery.d.ts +94 -0
- package/lib/services/im-delivery.js +263 -0
- package/lib/services/kanban-provider.d.ts +1 -1
- package/lib/services/kanban-provider.js +2 -2
- package/lib/services/ocr-cli.d.ts +35 -0
- package/lib/services/ocr-cli.js +128 -0
- package/lib/tools/kanban-tools.d.ts +2 -2
- package/lib/tools/kanban-tools.js +1 -1
- package/lib/tools/main-session-tools.d.ts +6 -2
- package/lib/tools/main-session-tools.js +58 -20
- package/lib/tools/ocr-review-tools.d.ts +11 -0
- package/lib/tools/ocr-review-tools.js +63 -0
- package/lib/tools/planning-tools.d.ts +5 -2
- package/lib/tools/planning-tools.js +11 -9
- package/lib/tools/wiki-tools.js +2 -2
- package/lib/wiki/kb-linkage.d.ts +1 -1
- package/lib/wiki/local-kb-client.js +3 -3
- package/lib/wiki/local-kb.d.ts +1 -1
- package/lib/wiki/local-kb.js +2 -2
- package/lib/wiki/page-path.d.ts +1 -1
- package/lib/wiki/page-path.js +6 -3
- package/lib/wiki/wiki-vault-client.d.ts +1 -1
- package/lib/wiki/wiki-vault-client.js +1 -1
- package/package.json +1 -1
- package/personas/kanban-d/agent.cordis.yml +5 -5
- package/personas/kanban-dt/agent.cordis.yml +23 -5
- package/personas/kanban-p/agent.cordis.yml +3 -3
- package/personas/kanban-pt/agent.cordis.yml +1 -1
- package/personas/kanban-v/agent.cordis.yml +3 -3
- package/personas/kanban-w/agent.cordis.yml +5 -5
- package/personas/persona-d.md +1 -1
- package/personas/persona-dt.md +12 -4
- package/personas/persona-p.md +1 -1
- package/personas/persona-v.md +2 -2
- package/personas/swarm/agent.cordis.yml +43 -0
- package/personas/swarm/preset.yml +5 -0
|
@@ -10,7 +10,7 @@ import { AgentRunner } from './agent-runner.js';
|
|
|
10
10
|
import { Watchdog } from './watchdog.js';
|
|
11
11
|
import { ChainAuditor } from './chain-auditor.js';
|
|
12
12
|
import { mergeDAfterReview } from './merge-gate.js';
|
|
13
|
-
import { buildSubagentTreeGuard } from '../roles/toolsets.js';
|
|
13
|
+
import { buildSubagentTreeGuard, buildSwarmSessionGuard, buildStandaloneDtGuard, registerStandaloneReviewerTools } from '../roles/toolsets.js';
|
|
14
14
|
import { syncKbLinks } from '../wiki/kb-linkage.js';
|
|
15
15
|
/** 读取部署默认模型(settings 的 agent-default-model 优先,其次 dsh-agent-default-model),角色未单独配置时回退使用。 */
|
|
16
16
|
export function resolveDefaultModel(ctx) {
|
|
@@ -41,7 +41,7 @@ function parentsDone(task, state) {
|
|
|
41
41
|
return parent !== undefined && (parent.status === 'done' || parent.status === 'archived');
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/** 从状态文件恢复 lastSeq;无文件时回退到事件日志尾行(不重放旧事件重复唤醒 V)。 */
|
|
45
45
|
function loadLastSeq(stateFile) {
|
|
46
46
|
try {
|
|
47
47
|
const raw = JSON.parse(readFileSync(stateFile, 'utf8'));
|
|
@@ -57,7 +57,7 @@ function saveLastSeq(stateFile, lastSeq) {
|
|
|
57
57
|
}
|
|
58
58
|
catch { /* 忽略写失败:事件日志仍是事实源 */ }
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
60
|
+
/** 把 [dsh-swarm] 关键事件追加到 storageDir/dispatcher.log,便于无控制台时观测调度器状态。 */
|
|
61
61
|
function logToFile(file, msg) {
|
|
62
62
|
try {
|
|
63
63
|
writeFileSync(file, new Date().toISOString() + ' ' + msg + '\n', { flag: 'a' });
|
|
@@ -79,22 +79,22 @@ export function makeWakeImpl(wakeV, logFile, onSettled) {
|
|
|
79
79
|
onSettled();
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
82
|
+
/** 单次异步操作加超时护栏——一个挂起的 V 编排会话不得卡死整个调度器 tick。 */
|
|
83
83
|
function withTimeout(p, ms, label) {
|
|
84
84
|
return Promise.race([
|
|
85
85
|
p,
|
|
86
86
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout: ' + label)), ms)),
|
|
87
87
|
]);
|
|
88
88
|
}
|
|
89
|
-
/** 防线①:链级进度看门狗阈值。tick=2000ms × 45 ticks = 90s
|
|
89
|
+
/** 防线①:链级进度看门狗阈值。tick=2000ms × 45 ticks = 90s 无进展即重唤醒。 */
|
|
90
90
|
export const STALL_WATCHDOG_TICKS = 45;
|
|
91
|
-
/**
|
|
91
|
+
/** 防线①:链级重唤醒上限(超限 [create-failed] + chain/blocked)。 */
|
|
92
92
|
export const STALL_WATCHDOG_REWAKE_LIMIT = 3;
|
|
93
|
-
/** 调度器:事件唤醒 V
|
|
94
|
-
* -
|
|
93
|
+
/** 调度器:事件唤醒 V(逐阶段建卡)+ 每任务一次性角色 agent + 心跳看门狗。
|
|
94
|
+
* - failed 且 attempts<maxRetries 的任务重派(claim→running,AgentRunner resume 同一会话);
|
|
95
95
|
* attempts≥maxRetries 熔断 blocked(gave_up)。
|
|
96
|
-
* -
|
|
97
|
-
* -
|
|
96
|
+
* - lastSeq 持久化,重启后仅唤醒 lastSeq 之后的事件。
|
|
97
|
+
* - inFlight 互斥,防止慢 tick 与下一轮并发重复派发同一任务。 */
|
|
98
98
|
export class Dispatcher {
|
|
99
99
|
kanban;
|
|
100
100
|
runner;
|
|
@@ -103,10 +103,10 @@ export class Dispatcher {
|
|
|
103
103
|
maxRetries;
|
|
104
104
|
stateFile;
|
|
105
105
|
logFile;
|
|
106
|
-
agents; //
|
|
106
|
+
agents; // 宿主 agents 注册表(热重载豁免判据)
|
|
107
107
|
stallProbe;
|
|
108
108
|
lastSeq = null; // null=尚未加载(首轮 tick 从状态文件/事件日志尾行恢复)
|
|
109
|
-
orphanReconciled = false; // 启动 reconcile
|
|
109
|
+
orphanReconciled = false; // 启动 reconcile 一次性闸:仅首轮 tick 执行孤儿收敛
|
|
110
110
|
inFlight = false;
|
|
111
111
|
stallState = new Map();
|
|
112
112
|
timer = null;
|
|
@@ -124,14 +124,14 @@ export class Dispatcher {
|
|
|
124
124
|
async ensureLastSeq(state) {
|
|
125
125
|
if (this.lastSeq !== null)
|
|
126
126
|
return;
|
|
127
|
-
//
|
|
128
|
-
// chain/created / spec-card/approved 等可唤醒事件导致 V
|
|
129
|
-
// 重放安全:VOrchestrator.wakeV
|
|
127
|
+
// 无状态文件(首次启动)时从 -1 起处理全部事件,避免跳过已存在的
|
|
128
|
+
// chain/created / spec-card/approved 等可唤醒事件导致 V 永不建卡。
|
|
129
|
+
// 重放安全:VOrchestrator.wakeV 的幂等(已有匹配卡则跳过)保证不重复建卡。
|
|
130
130
|
let loaded = loadLastSeq(this.stateFile) ?? -1;
|
|
131
131
|
// 游标自愈:purge/renumber(整链硬删除物理重排 events.jsonl seq)或外部修复会把
|
|
132
132
|
// 新事件 seq 压到旧游标之下,若不钳回,spec-card/approved 等可唤醒事件将被永久
|
|
133
133
|
// 跳过 → V 永不建卡(2026-09-02 实测事故)。游标超前即视为状态文件失效,回退
|
|
134
|
-
// 全量重放;重复唤醒由 wakeV
|
|
134
|
+
// 全量重放;重复唤醒由 wakeV 幂等与 done 卡不可变兜底。
|
|
135
135
|
const maxSeq = state.events.length > 0 ? state.events[state.events.length - 1].seq : -1;
|
|
136
136
|
if (loaded > maxSeq) {
|
|
137
137
|
logToFile(this.logFile, '[tick] lastSeq=' + loaded + ' > maxSeq=' + maxSeq + ' (post-purge cursor skew) → rewind to -1 and replay');
|
|
@@ -142,12 +142,12 @@ export class Dispatcher {
|
|
|
142
142
|
}
|
|
143
143
|
async tick() {
|
|
144
144
|
if (this.inFlight)
|
|
145
|
-
return; //
|
|
145
|
+
return; // 防重叠 tick 并发派发同一任务
|
|
146
146
|
this.inFlight = true;
|
|
147
147
|
try {
|
|
148
148
|
const state = await this.kanban.snapshot();
|
|
149
149
|
await this.ensureLastSeq(state);
|
|
150
|
-
// 启动 reconcile
|
|
150
|
+
// 启动 reconcile:仅首轮执行,位置在游标自愈之后、正常事件消费之前——
|
|
151
151
|
// 游标 rewind 可能全量重放旧事件,必须先把上次进程遗留的 running 孤儿卡收敛为 blocked,
|
|
152
152
|
// 消除「进程重启 → whenIdle 协程死亡 → 卡 running 悬挂到看门狗 4h」的口子。
|
|
153
153
|
if (!this.orphanReconciled) {
|
|
@@ -176,11 +176,11 @@ export class Dispatcher {
|
|
|
176
176
|
await this.runner.runTask(t.id);
|
|
177
177
|
}
|
|
178
178
|
else if (t.status === 'failed' && t.attempts < this.maxRetries) {
|
|
179
|
-
//
|
|
179
|
+
// failed 重派——AgentRunner 内 claim→running + resume 同一会话
|
|
180
180
|
await this.runner.runTask(t.id);
|
|
181
181
|
}
|
|
182
182
|
else if (t.status === 'failed') {
|
|
183
|
-
//
|
|
183
|
+
// attempts≥maxRetries 熔断 blocked(gave_up),人工介入
|
|
184
184
|
await this.kanban.blockTask(t.id, 'gave_up: max retries', 'system');
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -282,8 +282,8 @@ export class Dispatcher {
|
|
|
282
282
|
this.stop();
|
|
283
283
|
this.timer = setInterval(() => { void this.tick(); }, intervalMs);
|
|
284
284
|
}
|
|
285
|
-
/**
|
|
286
|
-
* 否则删链后新建链的可唤醒事件(seq <
|
|
285
|
+
/** 整链硬删除联动:purge 物理重排 events.jsonl seq,游标必须同步钳到当前 maxSeq。
|
|
286
|
+
* 否则删链后新建链的可唤醒事件(seq < 旧内存游标)被运行中实例永久跳过——启动时自愈,
|
|
287
287
|
* 覆盖不了运行中删链场景(2026-09-02 残留审计结论)。 */
|
|
288
288
|
async onPurge() {
|
|
289
289
|
const state = await this.kanban.snapshot();
|
|
@@ -299,10 +299,10 @@ export class Dispatcher {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
-
/** 调度层装配:事件唤醒 V
|
|
302
|
+
/** 调度层装配:事件唤醒 V(逐阶段建卡)+ 每任务一次性角色 agent + 心跳看门狗。
|
|
303
303
|
* 仅在 agents 与 kanban 服务同时可用时由插件入口调用(不依赖可能已错过的 ready 事件)。
|
|
304
|
-
*
|
|
305
|
-
/** 启动 reconcile
|
|
304
|
+
* 收 ConfigProvider——storageDir 取启动时快照;wiki/模型链经 getEffective() 调用时热读取。 */
|
|
305
|
+
/** 启动 reconcile:剔除事件流中已不存在的链的编排 entry(历史残留/外部 purge)。
|
|
306
306
|
* 原地删除并返回被移除的 chainId 列表(调用方负责持久化与日志)。 */
|
|
307
307
|
export function reconcileOrchestrations(orch, chains) {
|
|
308
308
|
const removed = [...orch.keys()].filter((k) => !chains.has(k));
|
|
@@ -310,14 +310,14 @@ export function reconcileOrchestrations(orch, chains) {
|
|
|
310
310
|
orch.delete(k);
|
|
311
311
|
return removed;
|
|
312
312
|
}
|
|
313
|
-
/** 启动 reconcile
|
|
313
|
+
/** 启动 reconcile:进程重启会杀死 runner 的 whenIdle 协程,上次遗留的 running 卡无人收尾,
|
|
314
314
|
* 看门狗默认 4h(staleTimeoutSeconds=14400)才回收——重启后立即把 running 孤儿卡收敛为 blocked
|
|
315
315
|
* (system comment + blockTask),中断显形且可重派续跑(重派将 resume 同一会话,进度保留)。
|
|
316
316
|
* 状态机注:TaskStatus 无独立 'claimed' 态——claimTask 发 task/claimed 事件后投影即为 running,
|
|
317
317
|
* 扫描 running 即覆盖「claimed 未收尾」;todo/ready/triage 从未派发,done/blocked/failed/archived
|
|
318
|
-
* 已有归属或终态(且 failed
|
|
319
|
-
*
|
|
320
|
-
* ctx.get('agents').get('kbn-<taskId>')(session id 构造同 AgentRunner.resumeOrReuse
|
|
318
|
+
* 已有归属或终态(且 failed 的处置归重派/熔断管辖),均不动。
|
|
319
|
+
* 热重载兼容,双重判据:收敛每张 running 卡前先查宿主 agents 注册表
|
|
320
|
+
* ctx.get('agents').get('kbn-<taskId>')(session id 构造同 AgentRunner.resumeOrReuse)。
|
|
321
321
|
* 两个世界的分野:
|
|
322
322
|
* - 进程重启:agents 注册表随宿主进程消亡,新进程内 kbn-<taskId> 必然查不到(undefined)
|
|
323
323
|
* → 会话已死 → 全部收敛,语义与修复前一致;
|
|
@@ -382,12 +382,12 @@ export function startDispatcher(ctx, configProvider) {
|
|
|
382
382
|
}
|
|
383
383
|
/** 调度器装配主体(startDispatcher 的容错包裹内执行,异常落盘不阻断插件加载)。
|
|
384
384
|
* config = 启动时快照,仅喂静态依赖(EventWaker/Watchdog/maxRetries);
|
|
385
|
-
* wiki 与模型链读点走 configProvider.getEffective()
|
|
385
|
+
* wiki 与模型链读点走 configProvider.getEffective() 热生效。 */
|
|
386
386
|
function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider, agents) {
|
|
387
387
|
const config = configProvider.getEffective();
|
|
388
388
|
const kanban = provider.service;
|
|
389
|
-
//
|
|
390
|
-
// 消费方(AgentRunner/VOrchestrator)字段仍标 WikiVaultClient——local 为 LocalWikiClient(read/write/search
|
|
389
|
+
// 双模式:local 走 LocalWikiClient(互链读写落本地库),remote 用配置的 WikiVaultClient(同款分支)。
|
|
390
|
+
// 消费方(AgentRunner/VOrchestrator)字段仍标 WikiVaultClient——local 为 LocalWikiClient(read/write/search 同面),同款断言。
|
|
391
391
|
const wiki = (configProvider.mode === 'local'
|
|
392
392
|
? new LocalWikiClient(ensureLocalKbRoot())
|
|
393
393
|
: new WikiVaultClient(() => configProvider.getEffective().wikiVault));
|
|
@@ -408,7 +408,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
408
408
|
catch { /* 忽略写失败 */ }
|
|
409
409
|
};
|
|
410
410
|
const vOrch = new VOrchestrator(ctx, kanban, agents, configProvider, orchestrations, wiki, defaultModel);
|
|
411
|
-
//
|
|
411
|
+
// 链完成验收核对——Chain(completed) 时核对主会话是否越权写工作区产物;
|
|
412
412
|
// 发现越权 → chain/audit-warning,阻塞最终汇报直至用户 GUI 确认(chain/audit-confirmed)。
|
|
413
413
|
const auditor = new ChainAuditor({
|
|
414
414
|
kanban,
|
|
@@ -416,7 +416,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
416
416
|
listLiveAgents: () => (ctx.get('agents')?.list?.() ?? []),
|
|
417
417
|
});
|
|
418
418
|
kanban.setOnChainCompleted(async (chainId) => {
|
|
419
|
-
//
|
|
419
|
+
// 传入本链发起工作区(Chain.workspaceDir),审计仅扫描该工作区内的会话,排除其他项目主会话
|
|
420
420
|
const chainState = await kanban.snapshot();
|
|
421
421
|
const workspaceDir = chainState.chains.get(chainId)?.workspaceDir ?? null;
|
|
422
422
|
const evidence = await auditor.check(chainId, workspaceDir);
|
|
@@ -424,7 +424,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
424
424
|
console.warn('[dsh-swarm] chain audit warning: ' + chainId + ' evidence=' + evidence.length);
|
|
425
425
|
await kanban.auditWarning(chainId, evidence, 'system');
|
|
426
426
|
}
|
|
427
|
-
//
|
|
427
|
+
// 合入门控:DT 通过后由 system 合入 TARGET_BRANCH;D 不再提前 merge/push。
|
|
428
428
|
// 解析失败软跳过([merge-skip]),合入失败记录 [merge-failed],均不阻断收尾(坏代码未被合入 = 方向安全)。
|
|
429
429
|
try {
|
|
430
430
|
const r = await mergeDAfterReview(kanban, chainId, storageDir);
|
|
@@ -436,7 +436,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
436
436
|
logToFile(logFile, '[merge-gate] error chain=' + chainId + ' ' + String(err));
|
|
437
437
|
}
|
|
438
438
|
});
|
|
439
|
-
//
|
|
439
|
+
// W2/W3(w:kb) 完成 → 机械互链登记(清单页 ↔ 计划页 ↔ 结果页)。
|
|
440
440
|
// syncKbLinks 内部对 wiki 读写全容错,失败不阻塞完成;钩子本身再包一层 try(防御未来改动抛错)。
|
|
441
441
|
kanban.setOnTaskCompleted(async (taskId) => {
|
|
442
442
|
try {
|
|
@@ -448,9 +448,9 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
448
448
|
}
|
|
449
449
|
});
|
|
450
450
|
const waker = new EventWaker(ctx, config);
|
|
451
|
-
vOrch.onOrchChange = saveOrchs; //
|
|
451
|
+
vOrch.onOrchChange = saveOrchs; // stall re-wake 路径绕过 EventWaker,orch 变化自行落盘
|
|
452
452
|
waker.setWakeImpl(makeWakeImpl(vOrch.wakeV.bind(vOrch), logFile, saveOrchs));
|
|
453
|
-
// 0.1.0 delegation
|
|
453
|
+
// 0.1.0 delegation:全局子代理写护栏——普通插件 ctx 上注册的 guard 全局
|
|
454
454
|
// 生效(dsh-tools:普通上下文守卫全局生效,agent.ctx 守卫仅对该 agent 生效)。
|
|
455
455
|
// guard 内部仅对 kanban-dt 系会话收紧;DT 父会话自身仍由 agent-runner 的 agent.ctx
|
|
456
456
|
// guard 双保险。dispose 时注销。
|
|
@@ -459,8 +459,24 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
459
459
|
if (unguardSubagents) {
|
|
460
460
|
ctx.on('dispose', unguardSubagents);
|
|
461
461
|
}
|
|
462
|
+
// 蜂群模式硬闸:header.agentPreset==='swarm' 时只读+git 反选拦截;
|
|
463
|
+
// 内部对其余会话恒放行。dispose 时注销(与 subagent guard 同款)。
|
|
464
|
+
const unguardSwarm = toolsSvc?.guard?.(buildSwarmSessionGuard());
|
|
465
|
+
if (unguardSwarm) {
|
|
466
|
+
ctx.on('dispose', unguardSwarm);
|
|
467
|
+
}
|
|
468
|
+
// 独立评审硬闸:header.agentPreset==='kanban-dt' 且未经角色组合(session id 非 kbn-
|
|
469
|
+
// 前缀)时收紧(看板写工具拒、bash/run_code 仅只读 git+clone/fetch、wiki_write 仅
|
|
470
|
+
// reviews 命名空间);wiki_write 对非评审会话全局收紧(链上组合会话放行)。dispose
|
|
471
|
+
// 时注销(与 subagent/swarm guard 同款)。
|
|
472
|
+
const unguardStandalone = toolsSvc?.guard?.(buildStandaloneDtGuard());
|
|
473
|
+
if (unguardStandalone) {
|
|
474
|
+
ctx.on('dispose', unguardStandalone);
|
|
475
|
+
}
|
|
476
|
+
// 独立评审工具全局注册:ocr_review + wiki 三原语(wiki_write 会话级收紧由上 guard 完成)。
|
|
477
|
+
registerStandaloneReviewerTools(ctx, configProvider);
|
|
462
478
|
const runner = new AgentRunner(ctx, kanban, configProvider, wiki, defaultModel);
|
|
463
|
-
provider.runner = runner; //
|
|
479
|
+
provider.runner = runner; // HTTP retry 复用同一执行器(failed→claim→spawn/resume)
|
|
464
480
|
const watchdog = new Watchdog(kanban, config.dispatcher);
|
|
465
481
|
const dispatcher = new Dispatcher({
|
|
466
482
|
kanban,
|
|
@@ -468,9 +484,9 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
468
484
|
waker,
|
|
469
485
|
watchdog,
|
|
470
486
|
maxRetries: config.dispatcher.maxRetries,
|
|
471
|
-
stateFile: join(dirname(orchFile), 'dispatcher-state.json'), //
|
|
487
|
+
stateFile: join(dirname(orchFile), 'dispatcher-state.json'), // 与事件日志同目录
|
|
472
488
|
logFile,
|
|
473
|
-
agents, //
|
|
489
|
+
agents, // 启动 reconcile 热重载豁免判据(宿主 agents 注册表)
|
|
474
490
|
stallProbe: vOrch, // 防线①:链级停滞探针(orchestrationOf/isWakeInFlight/wake)
|
|
475
491
|
});
|
|
476
492
|
ctx.on('dispose', () => { dispatcher.stop(); watchdog.stop(); vOrch.dispose(); });
|
|
@@ -478,7 +494,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
478
494
|
watchdog.start(config.dispatcher.heartbeatIntervalSeconds * 1000);
|
|
479
495
|
logToFile(logFile, '[startDispatcher] dispatcher started (tick=2000ms)');
|
|
480
496
|
void dispatcher.tick();
|
|
481
|
-
//
|
|
497
|
+
// 整链硬删除联动:purge 物理重排事件 seq → 游标同步钳回(运行中实例不重启发跳过);
|
|
482
498
|
// V 编排 entry 同步剔除。kanban-http 的 delete 分支调用 provider.onChainDeleted。
|
|
483
499
|
provider.onChainDeleted = async (chainId) => {
|
|
484
500
|
await dispatcher.onPurge();
|
|
@@ -486,7 +502,7 @@ function startDispatcherInner(ctx, configProvider, storageDir, logFile, provider
|
|
|
486
502
|
saveOrchs();
|
|
487
503
|
logToFile(logFile, '[chain-deleted] cursor synced + orch entry removed chain=' + chainId);
|
|
488
504
|
};
|
|
489
|
-
// 启动 reconcile
|
|
505
|
+
// 启动 reconcile:历史残留/外部 purge 留下的死链编排 entry,按事件流存活链剔除
|
|
490
506
|
void (async () => {
|
|
491
507
|
try {
|
|
492
508
|
const snap = await kanban.snapshot();
|
|
@@ -6,7 +6,7 @@ export declare class EventWaker {
|
|
|
6
6
|
private inFlight;
|
|
7
7
|
private wakeImpl;
|
|
8
8
|
constructor(_ctx: unknown, _config: KanbanConfig);
|
|
9
|
-
/** 注入真实唤醒实现(
|
|
9
|
+
/** 注入真实唤醒实现(VOrchestrator.wakeV——创建/恢复 V 编排 agent 会话,按阶段序列建卡)。 */
|
|
10
10
|
setWakeImpl(impl: WakeImpl): void;
|
|
11
11
|
onEvent(ev: KanbanEvent): Promise<void>;
|
|
12
12
|
}
|
|
@@ -3,10 +3,10 @@ export class EventWaker {
|
|
|
3
3
|
inFlight = new Set();
|
|
4
4
|
wakeImpl = async () => { };
|
|
5
5
|
constructor(_ctx, _config) { }
|
|
6
|
-
/** 注入真实唤醒实现(
|
|
6
|
+
/** 注入真实唤醒实现(VOrchestrator.wakeV——创建/恢复 V 编排 agent 会话,按阶段序列建卡)。 */
|
|
7
7
|
setWakeImpl(impl) { this.wakeImpl = impl; }
|
|
8
8
|
async onEvent(ev) {
|
|
9
|
-
//
|
|
9
|
+
// V 仅在规格卡批准后(spec-card/approved)或任务完成/阻塞(驱动阶段推进)时唤醒;
|
|
10
10
|
// chain/created 不再唤醒 V(V 不再处理旧 w1 预取阶段,等 /openspec: 建卡 + 批准后再行动)。
|
|
11
11
|
const wakeable = (ev.kind === 'task/completed' || ev.kind === 'task/blocked' || ev.kind === 'spec-card/approved');
|
|
12
12
|
if (!wakeable)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* D(execute) git 凭据注入:使 D 会话的 git ls-remote/push 可用。
|
|
3
3
|
*
|
|
4
4
|
* 机制:D 会话的 bash 每次调用都是新 shell(无环境持久),故 env 变量不可跨调用。
|
|
5
5
|
* 采用 repo-local http extraheader(AUTHORIZATION: basic),写入 <repo>/.git/config——
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 鉴权按托管方区分:
|
|
9
9
|
* - GitLab PAT(glpat- 前缀)→ basic base64("oauth2:<PAT>")(GitLab 规范,实测 ls-remote 通过);
|
|
10
10
|
* - 其余(GitHub 等)→ basic base64("x-access-token:<PAT>")。
|
|
11
|
-
* D 会话为 danger-full-access
|
|
11
|
+
* D 会话为 danger-full-access(用户决策),HOME 可写,故 git 的 credential.helper
|
|
12
12
|
* (store/osxkeychain 等)不再触发 "credential storage lock",无需中和。
|
|
13
13
|
*
|
|
14
14
|
* PAT 来源(按优先级):DSH 凭据服务 ctx.credentials($DSH_HOME/.credentials.yaml 的
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/dispatcher/git-credentials.ts
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* D(execute) git 凭据注入:使 D 会话的 git ls-remote/push 可用。
|
|
5
5
|
*
|
|
6
6
|
* 机制:D 会话的 bash 每次调用都是新 shell(无环境持久),故 env 变量不可跨调用。
|
|
7
7
|
* 采用 repo-local http extraheader(AUTHORIZATION: basic),写入 <repo>/.git/config——
|
|
@@ -10,7 +10,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
10
10
|
* 鉴权按托管方区分:
|
|
11
11
|
* - GitLab PAT(glpat- 前缀)→ basic base64("oauth2:<PAT>")(GitLab 规范,实测 ls-remote 通过);
|
|
12
12
|
* - 其余(GitHub 等)→ basic base64("x-access-token:<PAT>")。
|
|
13
|
-
* D 会话为 danger-full-access
|
|
13
|
+
* D 会话为 danger-full-access(用户决策),HOME 可写,故 git 的 credential.helper
|
|
14
14
|
* (store/osxkeychain 等)不再触发 "credential storage lock",无需中和。
|
|
15
15
|
*
|
|
16
16
|
* PAT 来源(按优先级):DSH 凭据服务 ctx.credentials($DSH_HOME/.credentials.yaml 的
|
|
@@ -2,7 +2,7 @@ import type { KanbanConfig } from '../config.js';
|
|
|
2
2
|
import type { Role } from '../domain/types.js';
|
|
3
3
|
import type { AgentModelOptions } from './dispatcher.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* 模型候选链:角色创建时主模型 → fallbacks 依次静默切换。
|
|
6
6
|
* primary = config.roles.models[role];未配置回退 defaultModel。
|
|
7
7
|
* reasoningEffort 未指定一律 'high'。
|
|
8
8
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/dispatcher/session-events.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 会话事件条目统一读取助手:
|
|
4
4
|
* 0.1.2(DSH-0.1.2-A4-03)起 Session.events getter 已移除——宿主新读取面是
|
|
5
5
|
* `session.seq`(日志长度)+ `session.snapshotEvents(fromSeq?, toSeqExclusive?)`。
|
|
6
6
|
* 事件条目形态(落盘与快照一致)为 { type, seq, time, data: { ... } }(append 时
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BoardState, Task } from '../domain/types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* D(execute) 目标仓库路径解析:
|
|
4
4
|
* 1. 任务 body 的 TARGET_REPO=<path> 标记(V 生成 D 任务体时写入,见 v-orchestrator D 阶段指令);
|
|
5
5
|
* 2. 规格卡 file-prefetch 附件 ref(需求澄清清单 manifest 的仓库路径);
|
|
6
6
|
* 3. 回退默认目录(kanban 存储 / 会话工作区)。
|
|
@@ -10,6 +10,6 @@ import type { BoardState, Task } from '../domain/types.js';
|
|
|
10
10
|
* 存在性校验同时兜底沙箱根:workspace-write 以会话 cwd 为写边界,指向不存在的
|
|
11
11
|
* 目录会导致所有写被拒,故不存在时回退而非保留。
|
|
12
12
|
*/
|
|
13
|
-
/** 判断 child 是否位于 parent 目录内(含等于)。用于 D
|
|
13
|
+
/** 判断 child 是否位于 parent 目录内(含等于)。用于 D 目标仓库是否在会话工作空间内的判定(前置授权)。 */
|
|
14
14
|
export declare function isPathInside(child: string, parent: string): boolean;
|
|
15
15
|
export declare function resolveTargetRepoDir(task: Task, state: BoardState, fallback: string, allowFallback?: boolean): string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* D(execute) 目标仓库路径解析:
|
|
6
6
|
* 1. 任务 body 的 TARGET_REPO=<path> 标记(V 生成 D 任务体时写入,见 v-orchestrator D 阶段指令);
|
|
7
7
|
* 2. 规格卡 file-prefetch 附件 ref(需求澄清清单 manifest 的仓库路径);
|
|
8
8
|
* 3. 回退默认目录(kanban 存储 / 会话工作区)。
|
|
@@ -12,7 +12,7 @@ import { resolve } from 'node:path';
|
|
|
12
12
|
* 存在性校验同时兜底沙箱根:workspace-write 以会话 cwd 为写边界,指向不存在的
|
|
13
13
|
* 目录会导致所有写被拒,故不存在时回退而非保留。
|
|
14
14
|
*/
|
|
15
|
-
/** 判断 child 是否位于 parent 目录内(含等于)。用于 D
|
|
15
|
+
/** 判断 child 是否位于 parent 目录内(含等于)。用于 D 目标仓库是否在会话工作空间内的判定(前置授权)。 */
|
|
16
16
|
export function isPathInside(child, parent) {
|
|
17
17
|
const c = resolve(child);
|
|
18
18
|
const p = resolve(parent);
|
|
@@ -37,6 +37,6 @@ export function resolveTargetRepoDir(task, state, fallback, allowFallback = true
|
|
|
37
37
|
return abs;
|
|
38
38
|
}
|
|
39
39
|
// allowFallback=false(合入门控用):不降级到回退目录——合入必须落在显式声明的目标仓库,
|
|
40
|
-
// 禁止 merge 到 kanban
|
|
40
|
+
// 禁止 merge 到 kanban 存储/会话工作区等回退目录(方向安全)。
|
|
41
41
|
return allowFallback ? resolve(fallback) : '';
|
|
42
42
|
}
|
|
@@ -18,11 +18,11 @@ export declare const R20_PHASE_EXPECT: Record<VPhase, {
|
|
|
18
18
|
assignee: Role;
|
|
19
19
|
mode: TaskMode;
|
|
20
20
|
} | null>;
|
|
21
|
-
/**
|
|
21
|
+
/** 每阶段建卡的 body 生成指令(角色定位确定性模板,消除 V 自由发挥导致的角色漂移)。
|
|
22
22
|
* P=计划者(绝不执行)、D=唯一执行者(TARGET_REPO 必须取自规格卡 file-prefetch 附件 ref,禁止回退/猜测)、
|
|
23
23
|
* W=KB 同步(绝不执行代码)。V 把对应模板写入 kanban_create 的 body。 */
|
|
24
24
|
export declare const PHASE_INSTRUCTIONS: Partial<Record<VPhase, string>>;
|
|
25
|
-
/**
|
|
25
|
+
/** W 角色知识库双模式:按 kbMode 构建各阶段建卡 body 指令(phase 键控——W2/W3 同为
|
|
26
26
|
* assignee='w'+mode='kb',assignee+mode 签名无法区分两相文案;系统状态本就按 phase 键控)。
|
|
27
27
|
* remote → PHASE_INSTRUCTIONS 原文(护栏测试零改动前提);local → 覆盖 w2/w3/d/dt 四相
|
|
28
28
|
* (skill 工具加载 llm-wiki + 本地库 fs 读写,禁 wiki_write/wiki_read),p/pt/summary 原样回落。
|
|
@@ -67,22 +67,22 @@ export declare class VOrchestrator {
|
|
|
67
67
|
}>;
|
|
68
68
|
}, configProvider: ConfigProvider, orchestrations: Map<string, ChainOrchestration>, wiki: WikiVaultClient, defaultModel?: AgentModelOptions);
|
|
69
69
|
private currentPhase;
|
|
70
|
-
/**
|
|
70
|
+
/** stall 自动再唤醒上限(同一阶段连续零产出 → 自动重试 ≤3 次,间隔递增)后放弃并显形。 */
|
|
71
71
|
private static readonly STALL_REWAKE_LIMIT;
|
|
72
|
-
/**
|
|
72
|
+
/** stall 再唤醒基础延迟(按 stallCount 倍增:5s/10s/15s),给瞬时故障/采样波动恢复窗口。 */
|
|
73
73
|
private static readonly STALL_REWAKE_DELAY_MS;
|
|
74
74
|
private rewakeTimers;
|
|
75
75
|
/** 同链 wakeV 并发防护:在途时后续唤醒合并为 pending,完成后补跑一次(事件不丢)。 */
|
|
76
76
|
private waking;
|
|
77
77
|
private pendingWake;
|
|
78
|
-
/**
|
|
78
|
+
/** orchestration 变更回调(dispatcher 注入 saveOrchs)——stall re-wake 路径绕过
|
|
79
79
|
* EventWaker,其 stallCount/phase 变化需自行落盘,防重启丢重试进度。 */
|
|
80
80
|
onOrchChange?: () => void;
|
|
81
81
|
wakeV(chainId: string): Promise<void>;
|
|
82
|
-
/**
|
|
82
|
+
/** stall 自动再唤醒(≤3 次)。同链 pending 幂等;建卡成功(stallCount=0)后到期的
|
|
83
83
|
* re-wake 自动作废(回调内按 stallCount 判空跳过)。 */
|
|
84
84
|
private scheduleRewake;
|
|
85
|
-
/**
|
|
85
|
+
/** 清理待触发的 re-wake 定时器(插件 dispose 时调用)。 */
|
|
86
86
|
dispose(): void;
|
|
87
87
|
/** 链级看门狗(Dispatcher,防线①)探针:编排 entry;无 entry 返回 null。 */
|
|
88
88
|
orchestrationOf(chainId: string): ChainOrchestration | null;
|
|
@@ -101,7 +101,7 @@ export declare class VOrchestrator {
|
|
|
101
101
|
private handleReviewCompletion;
|
|
102
102
|
private advance;
|
|
103
103
|
private getVAgent;
|
|
104
|
-
/**
|
|
104
|
+
/** 归组:链的 workspaceDir(发起 /plan: 的主 agent 工作空间);缺失返回 null(调用方询问/报错,不落 kanban 存储)。 */
|
|
105
105
|
private chainWorkspace;
|
|
106
106
|
}
|
|
107
107
|
export {};
|