@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
package/lib/config.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import type { Role } from './domain/types.js';
|
|
3
|
-
/**
|
|
3
|
+
/** 斜杠命令前缀路由(单一事实源):plan/openspec/learning/send 已实现,run/changeset/archive 待落地时追加。 */
|
|
4
4
|
export interface PrefixRoutes {
|
|
5
5
|
plan: string;
|
|
6
6
|
openspec: string;
|
|
7
7
|
learning: string;
|
|
8
|
+
send: string;
|
|
8
9
|
}
|
|
9
10
|
export declare const DEFAULT_PREFIX_ROUTES: PrefixRoutes;
|
|
10
11
|
export interface KanbanConfig {
|
|
@@ -57,5 +58,21 @@ export interface KanbanConfig {
|
|
|
57
58
|
/** 命令黑名单子串(命中即拒执行)。纵深防御:派生命令由系统从 tdd 生成,正常不触黑名单。 */
|
|
58
59
|
forbidden: string[];
|
|
59
60
|
};
|
|
61
|
+
/** IM 主动投递(企微,2026-09-07 评审决议):W3 收尾/链阻塞时经 dsh-im 投群。
|
|
62
|
+
* enabled=false(默认)功能关闭;botId/targetId 留空=运行时自动发现(唯一 wecom bot + 唯一已保存群目标)。 */
|
|
63
|
+
imDelivery: {
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
botId: string;
|
|
66
|
+
targetId: string;
|
|
67
|
+
};
|
|
68
|
+
/** 评审引擎双模:delegate=沿用各角色自有模型评审;managed=统一经 dsh「模型链」评审。
|
|
69
|
+
* managed.provider/model = dsh「模型链」llm-catalog 的 provider/model id,wire 时写成 ocr 自定义 provider(dsh-managed);key 不落本配置。 */
|
|
70
|
+
reviewEngine: {
|
|
71
|
+
mode: 'delegate' | 'managed';
|
|
72
|
+
managed: {
|
|
73
|
+
provider: string;
|
|
74
|
+
model: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
60
77
|
}
|
|
61
78
|
export declare const Config: Schema<KanbanConfig>;
|
package/lib/config.js
CHANGED
|
@@ -3,6 +3,7 @@ export const DEFAULT_PREFIX_ROUTES = {
|
|
|
3
3
|
plan: '/plan:',
|
|
4
4
|
openspec: '/openspec:',
|
|
5
5
|
learning: '/learning',
|
|
6
|
+
send: '/sms', // 手动投递斜杠命令
|
|
6
7
|
};
|
|
7
8
|
const modelItemSchema = () => Schema.object({
|
|
8
9
|
provider: Schema.string().required(),
|
|
@@ -39,6 +40,7 @@ export const Config = Schema.object({
|
|
|
39
40
|
plan: Schema.string().default(DEFAULT_PREFIX_ROUTES.plan),
|
|
40
41
|
openspec: Schema.string().default(DEFAULT_PREFIX_ROUTES.openspec),
|
|
41
42
|
learning: Schema.string().default(DEFAULT_PREFIX_ROUTES.learning),
|
|
43
|
+
send: Schema.string().default(DEFAULT_PREFIX_ROUTES.send),
|
|
42
44
|
}),
|
|
43
45
|
memory: Schema.object({
|
|
44
46
|
enabled: Schema.boolean().default(true),
|
|
@@ -55,4 +57,16 @@ export const Config = Schema.object({
|
|
|
55
57
|
timeoutMs: Schema.number().min(1000).default(600000),
|
|
56
58
|
forbidden: Schema.array(Schema.string()).default(['rm -rf /', 'git push']),
|
|
57
59
|
}).default({ enabled: true, timeoutMs: 600000, forbidden: ['rm -rf /', 'git push'] }),
|
|
60
|
+
imDelivery: Schema.object({
|
|
61
|
+
enabled: Schema.boolean().default(false),
|
|
62
|
+
botId: Schema.string().default(''),
|
|
63
|
+
targetId: Schema.string().default(''),
|
|
64
|
+
}).default({ enabled: false, botId: '', targetId: '' }),
|
|
65
|
+
reviewEngine: Schema.object({
|
|
66
|
+
mode: Schema.union(['delegate', 'managed']).default('delegate'),
|
|
67
|
+
managed: Schema.object({
|
|
68
|
+
provider: Schema.string().default(''),
|
|
69
|
+
model: Schema.string().default(''),
|
|
70
|
+
}),
|
|
71
|
+
}).default({ mode: 'delegate', managed: { provider: '', model: '' } }),
|
|
58
72
|
});
|
|
@@ -3,14 +3,19 @@ import type { KanbanService } from '../domain/kanban-service.js';
|
|
|
3
3
|
import type { ConfigProvider } from '../services/config-provider.js';
|
|
4
4
|
import type { Role } from '../domain/types.js';
|
|
5
5
|
import type { WikiVaultClient } from '../wiki/wiki-vault-client.js';
|
|
6
|
+
import { probeOcr } from '../services/ocr-cli.js';
|
|
6
7
|
import type { AgentModelOptions } from './dispatcher.js';
|
|
7
|
-
/**
|
|
8
|
+
/** 角色组合标记(会话10事故根因A):setup 成功组合角色工具面后写入,live 复用前校验。 */
|
|
8
9
|
interface RoleCompositionMarker {
|
|
9
10
|
role: Role;
|
|
10
11
|
taskId: string;
|
|
11
12
|
}
|
|
12
13
|
/** 写入角色组合标记(setup 在 installRoleTools 成功后调用;导出仅供测试直接构造标记场景)。 */
|
|
13
14
|
export declare function markRoleComposition(agent: unknown, marker: RoleCompositionMarker): void;
|
|
15
|
+
/** 只读判定:该 agent incarnation 是否带角色组合标记(全局 guard 消费;不读 marker 内容)。
|
|
16
|
+
* 与 markRoleComposition 同一 WeakMap 单一事实源;未标记 incarnation(GUI 新开/子代理)
|
|
17
|
+
* 返回 false。 */
|
|
18
|
+
export declare function isRoleComposed(agent: unknown): boolean;
|
|
14
19
|
/** 每任务一次性角色 agent:创建/resume、上下文组装、协议违规检测。 */
|
|
15
20
|
export declare class AgentRunner {
|
|
16
21
|
private readonly ctx;
|
|
@@ -18,11 +23,14 @@ export declare class AgentRunner {
|
|
|
18
23
|
private readonly configProvider;
|
|
19
24
|
private readonly wiki;
|
|
20
25
|
private readonly defaultModel;
|
|
21
|
-
|
|
26
|
+
private readonly probeOcrFn;
|
|
27
|
+
constructor(ctx: Context, kanban: KanbanService, configProvider: ConfigProvider, wiki: WikiVaultClient, defaultModel?: AgentModelOptions, deps?: {
|
|
28
|
+
probeOcrFn?: typeof probeOcr;
|
|
29
|
+
});
|
|
22
30
|
private buildContext;
|
|
23
31
|
runTask(taskId: string): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
32
|
+
/** resume 前先查 agents registry 同名会话是否仍 live——live 且组合标记匹配(role+taskId 一致)才复用。
|
|
33
|
+
* 会话10事故根因A:此前对 live agent 盲复用——GUI 打开同名会话产生的默认组合 incarnation
|
|
26
34
|
* 没有角色 preset/kanban_complete 工具面(setup 被跳过),模型只能把交付塞 kanban_comment。
|
|
27
35
|
* 现在标记缺失/不匹配时按宿主能力降级(探明结论):
|
|
28
36
|
* - (i) dispose 不可行:AgentHandle.dispose 仅创建者持有(dsh-agent types/index.d.ts:155-158,
|
|
@@ -38,7 +46,7 @@ export declare class AgentRunner {
|
|
|
38
46
|
* 直至重试预算耗尽——有界,不会形成无限重派循环(若误标 infra 则 attempts 不递增才会无限)。
|
|
39
47
|
* agents.get 未实现 → 回退 resume(原行为不回归)。 */
|
|
40
48
|
private resumeOrReuse;
|
|
41
|
-
/**
|
|
49
|
+
/** D(execute) 目标仓库在会话工作空间外时,跑 D 前询问用户是否允许。
|
|
42
50
|
* 经 ctx.userQuestions(GUI 弹窗)单次询问;无询问通道或拒绝 → 返回 false(由调用方 claim+block 等待人工放行)。 */
|
|
43
51
|
private requestRepoPermission;
|
|
44
52
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { probeOcr } from '../services/ocr-cli.js';
|
|
2
5
|
import { installRoleTools, buildReadOnlyWriteGuard, buildDTWriteGuard, buildPlanWriteGuard, buildKbWriteGuard, registerDtTaskChain, unregisterDtTaskChain } from '../roles/toolsets.js';
|
|
3
6
|
import { ensureLocalKbRoot } from '../wiki/local-kb.js';
|
|
4
7
|
import { installCleanFsTools } from '../roles/clean-fs-tools.js';
|
|
@@ -7,17 +10,27 @@ import { toolName } from './session-events.js';
|
|
|
7
10
|
import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
|
|
8
11
|
import { isPathInside, resolveTargetRepoDir } from './target-repo.js';
|
|
9
12
|
import { injectGitCredentials, resolveGitPatFromCtx } from './git-credentials.js';
|
|
10
|
-
/**
|
|
13
|
+
/** 目标仓库在会话工作空间外、已 claim+block 等待用户授权且尚未建会话的任务集合(key=taskId)。
|
|
11
14
|
* 人工放行后再次调度会重新询问;已授权后从集合移除。仅进程内记忆,重启后从事件日志恢复(block 事件仍在)。 */
|
|
12
15
|
const permissionBlockedTasks = new Set();
|
|
13
|
-
/**
|
|
16
|
+
/** goal 条件启用:spec 卡六段或父交接命中关键词 → 注入目标模式指令。 */
|
|
14
17
|
const GOAL_MODE_KEYWORDS = ['/goal', '目标模式', 'goal mode'];
|
|
15
|
-
/**
|
|
18
|
+
/** 瞬时基础设施错误(会话 live 锁/网络超时)与任务质量失败区分——infra 不计入 attempts 重试预算。 */
|
|
16
19
|
function isInfraError(err) {
|
|
17
20
|
return /cannot prepare session|while it is live|timeout|ETIMEDOUT|ECONNREFUSED|ECONNRESET|socket/i.test(String(err));
|
|
18
21
|
}
|
|
22
|
+
/** 关键事件追加落盘 storageDir/dispatcher.log(与 dispatcher.ts 同一文件、同一行格式)——
|
|
23
|
+
* agent-runner 未注入 logFile,按 dispatcher 同款方式经 configProvider 派生 storageDir;
|
|
24
|
+
* 写失败静默忽略(日志只是观测面,事件日志才是事实源)。 */
|
|
25
|
+
function logToDispatcherLog(configProvider, msg) {
|
|
26
|
+
try {
|
|
27
|
+
const storageDir = configProvider.getEffective().storageDir.replace('$DSH_HOME', process.env.DSH_HOME ?? process.cwd());
|
|
28
|
+
writeFileSync(join(storageDir, 'dispatcher.log'), new Date().toISOString() + ' ' + msg + '\n', { flag: 'a' });
|
|
29
|
+
}
|
|
30
|
+
catch { /* 忽略写失败 */ }
|
|
31
|
+
}
|
|
19
32
|
/** 组合标记存储:按 Agent 实例(incarnation)键控的进程内 WeakMap。
|
|
20
|
-
* 机制取舍(探明宿主 lib
|
|
33
|
+
* 机制取舍(探明宿主 lib 后确定):
|
|
21
34
|
* - 不用 session.append('kanban/role-composition'):自定义事件类型不在宿主 KNOWN_SESSION_EVENT_TYPES
|
|
22
35
|
* (dsh-session/lib/types/known-event-types.js),持久化读路径 assertEventsSupported
|
|
23
36
|
* (dsh-session-persistence/lib/index.js:1117)对未标 ignorable 的未知类型整份拒绝加载
|
|
@@ -33,6 +46,12 @@ export function markRoleComposition(agent, marker) {
|
|
|
33
46
|
if (agent && typeof agent === 'object')
|
|
34
47
|
roleCompositions.set(agent, marker);
|
|
35
48
|
}
|
|
49
|
+
/** 只读判定:该 agent incarnation 是否带角色组合标记(全局 guard 消费;不读 marker 内容)。
|
|
50
|
+
* 与 markRoleComposition 同一 WeakMap 单一事实源;未标记 incarnation(GUI 新开/子代理)
|
|
51
|
+
* 返回 false。 */
|
|
52
|
+
export function isRoleComposed(agent) {
|
|
53
|
+
return !!agent && typeof agent === 'object' && roleCompositions.has(agent);
|
|
54
|
+
}
|
|
36
55
|
/** 每任务一次性角色 agent:创建/resume、上下文组装、协议违规检测。 */
|
|
37
56
|
export class AgentRunner {
|
|
38
57
|
ctx;
|
|
@@ -40,35 +59,43 @@ export class AgentRunner {
|
|
|
40
59
|
configProvider;
|
|
41
60
|
wiki;
|
|
42
61
|
defaultModel;
|
|
43
|
-
|
|
62
|
+
probeOcrFn;
|
|
63
|
+
constructor(ctx, kanban, configProvider, wiki, defaultModel, deps) {
|
|
64
|
+
this.ctx = ctx;
|
|
65
|
+
this.kanban = kanban;
|
|
66
|
+
this.configProvider = configProvider;
|
|
67
|
+
this.wiki = wiki;
|
|
68
|
+
this.defaultModel = defaultModel;
|
|
69
|
+
this.probeOcrFn = deps?.probeOcrFn ?? probeOcr;
|
|
70
|
+
}
|
|
44
71
|
buildContext(task, state, resume) {
|
|
45
72
|
const parts = [`# Task ${task.id}: ${task.title}`, `assignee=${task.assignee} mode=${task.mode}`];
|
|
46
|
-
//
|
|
73
|
+
// 权限提示仅对受限角色显示(v 无写、d/execute 全权、p/w 全权但受工具级写护栏约束——写边界不靠 prompt 软约束)
|
|
47
74
|
if (task.assignee !== 'v' && !(task.assignee === 'd' && task.mode === 'execute') && task.assignee !== 'p' && task.assignee !== 'w') {
|
|
48
75
|
parts.push('权限提示:你的权限范围固定在会话工作区(workspace-write),越权操作(如 sandbox_permissions 升级写工作区外)会被自动拒绝且不可重试。遇到拒绝不要重试被拒操作,改用工作区内可行方式记录结果,然后调用 kanban_complete。');
|
|
49
76
|
}
|
|
50
|
-
//
|
|
77
|
+
// D(execute) = 唯一执行者(danger-full-access,无权限提示)——目标仓库内实际写代码 + git 提交推送
|
|
51
78
|
if (task.assignee === 'd' && task.mode === 'execute') {
|
|
52
79
|
parts.push('## 执行者职责\n你是链路唯一执行者(不是只读对齐/校验):在目标仓库(见 Body 的 TARGET_REPO)内 git worktree/branch → 按规格卡 solution/testing 改代码/README → git commit → git push → 自检(跑测试/构建)。本会话为 full-access(可写目标仓库与 git 凭据已注入),不要做只读对齐/校验交差。调用 kanban_complete 时 metadata 必须带 git 产物证据:changed_files(数组)+ commit_hash 与 push 至少其一,否则完成会被拒绝、链路不会收尾。');
|
|
53
80
|
}
|
|
54
81
|
if (task.body)
|
|
55
82
|
parts.push(`## Body
|
|
56
83
|
${task.body}`);
|
|
57
|
-
// W local KB
|
|
58
|
-
//
|
|
84
|
+
// W local KB 模式:注入本地库根 + llm-wiki 用法 + 排序原则 + 交付约定(remote 模式不注入)。
|
|
85
|
+
// 库内复制/移动补句:DIRECT cp/mv/mkdir 工具永远匹配不到 KB 目标(无 path/file_path
|
|
59
86
|
// 实参)会被护栏拒绝——必须显式指引 W 用 write 工具做库内复制/移动。
|
|
60
87
|
if (task.assignee === 'w' && this.configProvider.mode === 'local') {
|
|
61
88
|
const kbRoot = ensureLocalKbRoot();
|
|
62
89
|
parts.push('## 知识库(本地模式)', `- 本地库根:${kbRoot}(llm-wiki 原生结构;只读仓库源码,唯一可写区域为该库根);库内复制/移动一律用 write 工具(cp/mv 会被护栏拒绝)`, '- 经 skill 工具加载 llm-wiki 完成查写(query/ingest/crystallize);库未初始化时先按其 init 工作流初始化(幂等)', '- 检索排序原则:相关性优先 7 : 新鲜度 3', '- 交付:page_path = 库根下 wiki/** 相对路径(如 wiki/sources/ch_<chainId>/t_<taskId>.md);kb_url = 空串', '- 本地模式不 ingest 外部 URL;wiki_search/wiki_read/wiki_write 不可用(未注册)');
|
|
63
90
|
}
|
|
64
|
-
// D/DT local
|
|
91
|
+
// D/DT local 库根注入(审查修订):d:execute local 文案称「库根见运行时上下文」,
|
|
65
92
|
// 此处必须真的注入,否则 D 不知道去哪读计划;DT fs 写评审页同理。
|
|
66
93
|
if ((task.assignee === 'd' || task.assignee === 'dt') && this.configProvider.mode === 'local') {
|
|
67
94
|
parts.push('## 本地知识库(local KB mode)', `- 本地库根:${ensureLocalKbRoot()}`, task.assignee === 'd'
|
|
68
95
|
? '- 实施计划原文 = 父任务交接 page_path(库根下 wiki/** 相对路径)→ 直接 fs 读 <库根>/<page_path>;KB 经验召回经 skill 工具加载 llm-wiki(本地库)'
|
|
69
96
|
: '- 评审页经 fs 写 <库根>/wiki/queries/ch_<chainId>/review/<name>.md(库根外一律只读)');
|
|
70
97
|
}
|
|
71
|
-
//
|
|
98
|
+
// 规格卡六段 + 附件注入(经 Chain.specCardId),角色 agent 的输入契约,原汁原味
|
|
72
99
|
const chain = state.chains.get(task.chainId);
|
|
73
100
|
const specCard = chain?.specCardId ? state.specCards.get(chain.specCardId) : null;
|
|
74
101
|
if (specCard) {
|
|
@@ -89,7 +116,7 @@ ${task.body}`);
|
|
|
89
116
|
parts.push(`- metadata: ${JSON.stringify(h.metadata)}`);
|
|
90
117
|
}
|
|
91
118
|
}
|
|
92
|
-
// 0.1.0 delegation
|
|
119
|
+
// 0.1.0 delegation:D(execute) 目标模式条件注入——spec 卡/父交接命中
|
|
93
120
|
// GOAL_MODE_KEYWORDS 才注入;否则默认执行(行为不变)。
|
|
94
121
|
if (task.assignee === 'd' && task.mode === 'execute') {
|
|
95
122
|
const specText = specCard
|
|
@@ -103,7 +130,7 @@ ${task.body}`);
|
|
|
103
130
|
}
|
|
104
131
|
}
|
|
105
132
|
if (resume) {
|
|
106
|
-
//
|
|
133
|
+
// resume 注入运行历史与上次失败原因(不只次数),返工/重试同会话可参考前因
|
|
107
134
|
const lastFail = state.events.filter((e) => e.taskId === task.id && e.kind === 'task/failed').at(-1);
|
|
108
135
|
const reason = lastFail ? String(lastFail.payload['reason'] ?? '') : '';
|
|
109
136
|
parts.push(`## Prior attempts: ${task.attempts} (resume session)` + (reason ? `\nlast failure: ${reason}` : ''));
|
|
@@ -159,12 +186,12 @@ ${task.body}`);
|
|
|
159
186
|
// todo(V 建卡默认态,无父任务即就绪)/ ready / failed(重试)均可调度;其余状态拒
|
|
160
187
|
if (task.status !== 'ready' && task.status !== 'todo' && task.status !== 'failed')
|
|
161
188
|
throw new Error('task not schedulable: ' + task.status);
|
|
162
|
-
// 0.1.0 delegation
|
|
189
|
+
// 0.1.0 delegation:DT 任务运行期注册 chainId(全局子代理 guard 的
|
|
163
190
|
// wiki review namespace 同步解析源);runTask 结束注销。
|
|
164
191
|
if (task.assignee === 'dt')
|
|
165
192
|
registerDtTaskChain(task.id, task.chainId);
|
|
166
193
|
try {
|
|
167
|
-
//
|
|
194
|
+
// 归组:角色会话 cwd 恒为主 agent 工作空间(Chain.workspaceDir)。
|
|
168
195
|
// 缺失时询问用户注册工作区;仍不可得 → block('workspace-unknown'),绝不静默落 kanban 存储目录。
|
|
169
196
|
const chain = state.chains.get(task.chainId);
|
|
170
197
|
let sessionCwd = chain?.workspaceDir ?? null;
|
|
@@ -177,11 +204,11 @@ ${task.body}`);
|
|
|
177
204
|
await this.kanban.blockTask(taskId, 'workspace-unknown: 链未绑定工作区(Chain.workspaceDir 缺失)', 'system');
|
|
178
205
|
return;
|
|
179
206
|
}
|
|
180
|
-
//
|
|
181
|
-
// 会话 cwd 不再指向仓库(会话必须在主 agent
|
|
207
|
+
// D(execute):目标仓库解析(供前置授权判定 + git 凭据注入目标 + 上下文);
|
|
208
|
+
// 会话 cwd 不再指向仓库(会话必须在主 agent 工作空间)。
|
|
182
209
|
const isDExecute = task.assignee === 'd' && task.mode === 'execute';
|
|
183
210
|
const dRepo = isDExecute ? resolveTargetRepoDir(task, state, sessionCwd) : null;
|
|
184
|
-
//
|
|
211
|
+
// D 目标仓库在会话工作空间外 → 跑 D 前先询问用户是否允许(一次授权,D 以 full-access 执行不再逐次提示)。
|
|
185
212
|
// 不允许/无询问通道 → claim+block 等待人工放行(状态机要求 running 才能 block);
|
|
186
213
|
// 放行后再次调度会重新询问。授权前不创建会话,permissionBlockedTasks 避免误走 resume。
|
|
187
214
|
if (isDExecute && dRepo && !isPathInside(dRepo, sessionCwd)) {
|
|
@@ -195,7 +222,7 @@ ${task.body}`);
|
|
|
195
222
|
}
|
|
196
223
|
permissionBlockedTasks.delete(taskId);
|
|
197
224
|
}
|
|
198
|
-
//
|
|
225
|
+
// resume 判定 = 有运行历史(attempts>0 或存在 claimed 事件)且不是「无会话授权阻塞」的任务;
|
|
199
226
|
// 后者虽有 claimed 事件但从未创建会话,必须走 create 而非 resume(否则 resume 不存在会话抛错)。
|
|
200
227
|
const hasRunHistory = !permissionBlockedTasks.has(taskId) &&
|
|
201
228
|
(task.attempts > 0 || state.events.some((e) => e.taskId === taskId && e.kind === 'task/claimed'));
|
|
@@ -215,14 +242,14 @@ ${task.body}`);
|
|
|
215
242
|
return { ...resolved, reasoningEffort: effort };
|
|
216
243
|
});
|
|
217
244
|
// 角色 agent 等同委派子 agent:固定 approval=never(避免后台会话悬挂等审批)。
|
|
218
|
-
//
|
|
245
|
+
// P/W/D = full access(跨目录读:P 读仓库/外部实证、W 读计划、D 执行);
|
|
219
246
|
// 但 P 挂 plan 写护栏、W 挂只读护栏(写边界由工具级强制,防"改动源码",不靠 prompt 软约束);
|
|
220
247
|
// PT/DT/V → workspace-write(评审/编排最小权限,PT/DT 只读护栏由 ToolGuard 独立保证)。
|
|
221
248
|
const session = agentCtx.agent?.session;
|
|
222
249
|
session?.append?.('approval/policy', { policy: 'never', source: 'delegation' });
|
|
223
250
|
const fullAccess = isDExecute || task.assignee === 'p' || task.assignee === 'w';
|
|
224
251
|
session?.append?.('sandbox/mode', fullAccess ? { mode: 'danger-full-access', source: 'delegation' } : { mode: 'workspace-write', source: 'delegation' });
|
|
225
|
-
// 执行角色(P/W/D
|
|
252
|
+
// 执行角色(P/W/D)先挂载裁剪 preset(kanban-p/w/d),把 shell/approval 等服务注入 agent scope;
|
|
226
253
|
// 不再整包继承官方 code preset:bash/fs/fs-search 由裁剪组合装配,run_code/jobs/skill/goal/
|
|
227
254
|
// plan-mode/compaction/delegation/web/todo 按角色裁剪(组合文件随包分发 + 运行时安装到
|
|
228
255
|
// $DSH_HOME/.agent-presets/,见 preset-installer.ts)。否则官方 apply 会抛
|
|
@@ -242,17 +269,17 @@ ${task.body}`);
|
|
|
242
269
|
}
|
|
243
270
|
}
|
|
244
271
|
await installRoleTools(agentCtx, task.assignee, { kanban: this.kanban, wiki: this.wiki, taskId: task.id, kbMode: this.configProvider.mode });
|
|
245
|
-
//
|
|
272
|
+
// danger-full-access 会话(P、D(execute))write/edit/bash 菜单去毒
|
|
246
273
|
// (agent scope shadow 注册干净版盖住宿主带毒版本 + execute 剥参转发,详见 src/roles/clean-fs-tools.ts)。
|
|
247
274
|
// 挂载点选在 setup 内、preset.mount 之后:shadow 需经 get(name, agent) 取 preset standing
|
|
248
275
|
// 祖先层里的宿主原定义(kanban-p/d 的 fs/bash 由 preset 提供),且必须在会话开始前完成注册。
|
|
249
276
|
// 仅 P 与 D(execute):两者是 danger-full-access 天花板,escalation 参数无更宽可升(必被拒);
|
|
250
277
|
// PT/DT 是 workspace-write(sandbox_permissions 扩权到天花板是合法功能,剥了破坏官方能力);
|
|
251
|
-
// W 虽 fullAccess
|
|
278
|
+
// W 虽 fullAccess 但只读护栏全拒 write,shadow 无意义(最小挂载面,V 无执行工具不涉及)。
|
|
252
279
|
if (task.assignee === 'p' || isDExecute) {
|
|
253
280
|
installCleanFsTools(agentCtx, agentCtx.agent);
|
|
254
281
|
}
|
|
255
|
-
//
|
|
282
|
+
// 组合标记——角色工具面成功组合后,把 { role, taskId } 记到进程内 WeakMap(键=Agent 实例)。
|
|
256
283
|
// 宿主探明:setup 收到的 agentCtx.agent 与发布后 agents.get(id) 返回的是同一 Agent 实例
|
|
257
284
|
// (dsh-agent types/index.d.ts:38 `agent?: Agent` 安装为 Agent.ctx own property),
|
|
258
285
|
// 故此处键入的实例即 resumeOrReuse 里 agents.get 命中的实例。agentCtx.agent 缺失(宿主变体)时
|
|
@@ -266,7 +293,7 @@ ${task.body}`);
|
|
|
266
293
|
const repoRoot = dRepo ?? sessionCwd; // DT 评审目标仓库;PT 以会话工作区为只读边界
|
|
267
294
|
const toolsSvc = agentCtx.tools;
|
|
268
295
|
// DT 额外叠加 wiki review namespace 收窄(projects/<repoSlug>/<chain>/review/);
|
|
269
|
-
// local KB
|
|
296
|
+
// local KB 模式:DT 改用 KB 写护栏(唯一可写区域=本地库根,评审页经 fs 写库根),
|
|
270
297
|
// remote 模式仍用 DT 收窄护栏(库根外全只读 + review namespace)。
|
|
271
298
|
const kbMode = this.configProvider.mode;
|
|
272
299
|
const guardFn = task.assignee === 'dt'
|
|
@@ -274,13 +301,13 @@ ${task.body}`);
|
|
|
274
301
|
: buildReadOnlyWriteGuard(repoRoot);
|
|
275
302
|
toolsSvc?.guard?.((e) => guardFn(e));
|
|
276
303
|
}
|
|
277
|
-
//
|
|
304
|
+
// P/W 挂写护栏(wsRoot = 链 workspaceDir = sessionCwd,同 PT/DT 解析方式)。
|
|
278
305
|
// P = plan 写护栏(openspec/changes/** 内可写,禁改源码);W = 只读护栏(交付=读计划→wiki API,
|
|
279
|
-
// fs
|
|
306
|
+
// fs 零写——全名拦截,repo 外/workspace 内任意路径 fs 写一律拒,不再依赖 repoRoot 边界)。
|
|
280
307
|
// 挂载方式同 PT/DT:tools.guard 是注册方法(dsh-tools 类型),execution 以 { name, arguments } 传入。
|
|
281
308
|
if (task.assignee === 'p' || task.assignee === 'w') {
|
|
282
309
|
const toolsSvc = agentCtx.tools;
|
|
283
|
-
// local KB
|
|
310
|
+
// local KB 模式:W 改用 KB 写护栏(唯一可写区域=本地库根,交付页经 write 写库根),
|
|
284
311
|
// remote 模式 W 仍只读护栏(fs 零写);P 恒为 plan 写护栏(与 KB 模式无关)。
|
|
285
312
|
const kbMode = this.configProvider.mode;
|
|
286
313
|
const guardFn = task.assignee === 'p'
|
|
@@ -290,7 +317,7 @@ ${task.body}`);
|
|
|
290
317
|
: buildReadOnlyWriteGuard(sessionCwd);
|
|
291
318
|
toolsSvc?.guard?.((e) => guardFn(e));
|
|
292
319
|
}
|
|
293
|
-
//
|
|
320
|
+
// D(execute) 注入 git 凭据(repo-local http extraheader,GitLab glpat-* 用 oauth2 basic)。
|
|
294
321
|
// 由插件进程(不受 D 会话沙箱限制)写入 <repo>/.git/config;PAT 经 DSH 凭据服务/env 解析;
|
|
295
322
|
// 注入失败仅告警(用户自带凭据/SSH 的仓库不受影响),未配置 PAT 不注入。
|
|
296
323
|
if (isDExecute && dRepo) {
|
|
@@ -305,12 +332,25 @@ ${task.body}`);
|
|
|
305
332
|
}
|
|
306
333
|
};
|
|
307
334
|
try {
|
|
308
|
-
//
|
|
335
|
+
// claim 后任何异常(buildContext/spawn)→ failTask(attempts+1)→ 调度器重派/看门狗熔断;
|
|
309
336
|
// 不留 "running 无 agent" 悬挂。
|
|
310
337
|
await this.kanban.claimTask(taskId, 'system');
|
|
311
338
|
console.error('[dsh-swarm][debug] runner claimed ' + taskId + ' attempts=' + task.attempts);
|
|
339
|
+
// DT spawn 前预检:评审角色依赖 ocr CLI,缺失时模型会在会话内穷尽探活最后才 block(真实事故,
|
|
340
|
+
// 白烧 30+ 步整轮预算)。这里在 spawn 之前直接 block 终态等人工安装——不走 failTask(attempts+1
|
|
341
|
+
// 会触发重派,而重派前 ocr 依旧缺失,纯烧重试预算)。插在 buildContext/模型候选与 create/resume
|
|
342
|
+
// 分叉之前:resume 路径(有运行历史的复用/resume)同样先过预检。
|
|
343
|
+
// probeOcr 内部已吞掉一切异常(网络/权限等一律降级为 installed=false,永不 throw),此处天然 fail-closed。
|
|
344
|
+
if (task.assignee === 'dt') {
|
|
345
|
+
const probe = await this.probeOcrFn();
|
|
346
|
+
if (!probe.installed) {
|
|
347
|
+
await this.kanban.blockTask(taskId, 'review-tool-unavailable: ocr 未安装——GUI 配置面板「评审引擎(ocr)」卡可一键安装,或终端执行 npm install -g @alibaba-group/open-code-review', 'system');
|
|
348
|
+
logToDispatcherLog(this.configProvider, '[ocr-precheck] dt task=' + taskId + ' blocked: ocr not installed');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
312
352
|
context = this.buildContext(task, state, hasRunHistory);
|
|
313
|
-
//
|
|
353
|
+
// 模型候选链:primary + fallbacks,model/provider 不可用时静默切换下一候选;
|
|
314
354
|
// 全部候选不可用 → block(model-unavailable) 抛给用户;非 model 错误 → failTask(原逻辑)。
|
|
315
355
|
const candidates = buildModelCandidates(this.configProvider.getEffective(), task.assignee, this.defaultModel);
|
|
316
356
|
if (candidates.length === 0) {
|
|
@@ -361,7 +401,7 @@ ${task.body}`);
|
|
|
361
401
|
}
|
|
362
402
|
}
|
|
363
403
|
catch (err) {
|
|
364
|
-
//
|
|
404
|
+
// claim/buildContext/spawn 失败也走 failed(attempts 递增)→ 调度器重派/看门狗熔断;不再让任务永久 claimed
|
|
365
405
|
console.error('[dsh-swarm][debug] runner spawn error ' + taskId + ': ' + String(err));
|
|
366
406
|
try {
|
|
367
407
|
await this.kanban.failTask(taskId, 'runner-error: ' + String(err), 'system', { infra: isInfraError(err) });
|
|
@@ -374,7 +414,7 @@ ${task.body}`);
|
|
|
374
414
|
}
|
|
375
415
|
if (!agent)
|
|
376
416
|
return; // 防御:候选链耗尽已在上方 block(model-unavailable)/throw 处理
|
|
377
|
-
//
|
|
417
|
+
// 本轮增量事件基线——agent 拿到之后、followup 之前记录。
|
|
378
418
|
// 0.1.2(DSH-0.1.2-A4-03):Session.events 已移除 → followup 前记录 seq 基线,
|
|
379
419
|
// snapshotEvents(eventsBase) 即「本轮新增事件」(排除旧 incarnation 持久化事件),
|
|
380
420
|
// 供 protocol_violation 时识别 comment-only 收尾(交付滞留 comments,会话10事故形态)。
|
|
@@ -411,7 +451,7 @@ ${task.body}`);
|
|
|
411
451
|
? 'gave_up: protocol_violation after ' + maxPV + ' review cycles without complete/block'
|
|
412
452
|
: 'protocol_violation: idle without complete/block';
|
|
413
453
|
await this.kanban.blockTask(taskId, reason, 'system');
|
|
414
|
-
//
|
|
454
|
+
// comment-only 收尾显形——放 block 之后(保证 block 一定先发生,
|
|
415
455
|
// 告警只追加在已阻塞卡上,不影响终态判据与 [blocked-final] 证据链快照)。
|
|
416
456
|
// 本轮增量事件命中 kanban_comment(经 toolName 兼容读取落盘/live 两形态)且未成功提交
|
|
417
457
|
// (能走到这里即终态判据已确认非确定态)→ 交付内容很可能滞留 comments,显形告警。
|
|
@@ -443,7 +483,7 @@ ${task.body}`);
|
|
|
443
483
|
}
|
|
444
484
|
catch (err) {
|
|
445
485
|
console.error('[dsh-swarm][debug] runner error ' + taskId + ': ' + String(err));
|
|
446
|
-
//
|
|
486
|
+
// 失败语义(统一):发 failed 事件(attempts 递增),由调度器重派或看门狗熔断;不直接 block。
|
|
447
487
|
// 防御:任务可能已被完成/归档(终态),failTask 会抛非法转换(done --task/failed-->)
|
|
448
488
|
try {
|
|
449
489
|
await this.kanban.failTask(taskId, 'runner-error: ' + String(err), 'system', { infra: isInfraError(err) });
|
|
@@ -458,8 +498,8 @@ ${task.body}`);
|
|
|
458
498
|
unregisterDtTaskChain(task.id);
|
|
459
499
|
}
|
|
460
500
|
}
|
|
461
|
-
/**
|
|
462
|
-
*
|
|
501
|
+
/** resume 前先查 agents registry 同名会话是否仍 live——live 且组合标记匹配(role+taskId 一致)才复用。
|
|
502
|
+
* 会话10事故根因A:此前对 live agent 盲复用——GUI 打开同名会话产生的默认组合 incarnation
|
|
463
503
|
* 没有角色 preset/kanban_complete 工具面(setup 被跳过),模型只能把交付塞 kanban_comment。
|
|
464
504
|
* 现在标记缺失/不匹配时按宿主能力降级(探明结论):
|
|
465
505
|
* - (i) dispose 不可行:AgentHandle.dispose 仅创建者持有(dsh-agent types/index.d.ts:155-158,
|
|
@@ -510,7 +550,7 @@ ${task.body}`);
|
|
|
510
550
|
const h = await agents.resume(resumeOpts);
|
|
511
551
|
return h.agent;
|
|
512
552
|
}
|
|
513
|
-
/**
|
|
553
|
+
/** D(execute) 目标仓库在会话工作空间外时,跑 D 前询问用户是否允许。
|
|
514
554
|
* 经 ctx.userQuestions(GUI 弹窗)单次询问;无询问通道或拒绝 → 返回 false(由调用方 claim+block 等待人工放行)。 */
|
|
515
555
|
async requestRepoPermission(task, repo, sessionCwd) {
|
|
516
556
|
const uq = this.ctx.get?.('userQuestions');
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { KanbanService } from '../domain/kanban-service.js';
|
|
2
2
|
import type { AuditEvidence, Task } from '../domain/types.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 链完成验收核对:Chain(completed) 时核对主会话是否越权写工作区产物。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 数据源(修复「评估项目只读排查 run_code 被误判为越权写产物」):
|
|
7
7
|
* 1. 主会话会话事件扫描(primary,尽力而为):枚举活 agent 注册表(ctx.agents.list),
|
|
8
8
|
* 对 id 非 kbn-*(角色会话确定性 id:kbn-<taskId> / kbn-v-<chainId>)的会话,
|
|
9
9
|
* 扫描其 session.events 中的工具调用。插件无法解析主会话真实 session id(路由只用
|
|
10
|
-
* 逻辑 id 'session_main'),故以"非角色会话写 kanban 工作区"
|
|
10
|
+
* 逻辑 id 'session_main'),故以"非角色会话写 kanban 工作区"为近似。收紧三点:
|
|
11
11
|
* a. 作用域收窄:仅扫描会话工作区(session.header.cwd)位于本链发起工作区
|
|
12
12
|
* (Chain.workspaceDir = /plan: 主 agent 所在工作空间)内的会话,
|
|
13
13
|
* 排除其他项目的主会话(如 评估 项目里调试 dsh-swarm 的会话);
|
|
@@ -40,7 +40,7 @@ export declare class ChainAuditor {
|
|
|
40
40
|
private readonly listLiveAgents;
|
|
41
41
|
constructor(deps: ChainAuditorDeps);
|
|
42
42
|
/** 执行核对,返回越权证据(空=通过,不阻塞汇报)。
|
|
43
|
-
* @param workspaceDir 本链发起工作区(Chain.workspaceDir
|
|
43
|
+
* @param workspaceDir 本链发起工作区(Chain.workspaceDir);提供时仅扫描工作区内的会话。 */
|
|
44
44
|
check(chainId: string, workspaceDir?: string | null): Promise<AuditEvidence[]>;
|
|
45
45
|
private reconcileArtifacts;
|
|
46
46
|
}
|
|
@@ -5,12 +5,14 @@ import { eventType, toolArgs, toolName } from './session-events.js';
|
|
|
5
5
|
import { isPathInside } from './target-repo.js';
|
|
6
6
|
/** 直接写工具(无条件视为写能力;只读工具如 read/glob/grep 不算)。 */
|
|
7
7
|
const DIRECT_WRITE_TOOLS = new Set(['write', 'edit', 'rm', 'mv', 'cp', 'mkdir', 'mkfile']);
|
|
8
|
-
/** run_code
|
|
8
|
+
/** run_code:写能力载体,需结合派发子调用判定。 */
|
|
9
9
|
const CODE_RUN_TOOLS = new Set(['run_code']);
|
|
10
10
|
/** bash 命令中的写操作标记(写证据启发式;ls/cat/grep/git status 等只读不算)。
|
|
11
|
-
* 重定向标记用 \s>>?(要求 > 前有空白),避免把 2>/dev/null、2>&1 等只读 stderr 重定向误判为写。
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
* 重定向标记用 \s>>?(要求 > 前有空白),避免把 2>/dev/null、2>&1 等只读 stderr 重定向误判为写。
|
|
12
|
+
* git 细化(2026-09-08):merge(?!-base) 防 merge-base 查询被误判;tag 只认带非 -l/-n 参数的
|
|
13
|
+
* 变更形态(tag -l/-n/裸 为列表查询),两份正则(toolsets/chain-auditor)改动须同步。 */
|
|
14
|
+
const BASH_WRITE_RE = /(?:\b(?:touch|mkdir|rm|rmdir|mv|cp|tee|truncate|install|ln|dd|chmod|chown|make|cmake)\b|\bgit\s+(?:add|commit|push|mv|rm|checkout\s+-b|switch\s+-c|worktree\s+add|merge(?!-base)|rebase|reset|clean|restore|tag\s+(?!-l\b|-n\b)\S+|remote\s+add)\b|\bpnpm\s+(?:add|install|remove|update|link)\b|\bnpm\s+(?:i|install|add|remove|uninstall|update)\b|\byarn\s+(?:add|remove)\b|\bbun\s+(?:add|install|remove)\b|\bsed\s+-i\b|\bperl\s+-i\b|\s>>?)/i;
|
|
15
|
+
/** run_code/兜底 code 字符串中的 Python 写 API 标记(audit 补 open( 兜底)。
|
|
14
16
|
* open() 写模式精确版(读模式 'r' 不命中)、os. 模块写、pathlib Path 写、shutil 复制/移动/删除。 */
|
|
15
17
|
const PY_CODE_WRITE_RE = /(?:\bopen\(\s*['"][^'"\n]*['"]\s*,\s*(?:(?:mode|encoding|errors|buffering|newline|closefd|opener|text)\s*=\s*)?['"][wa][^'"\n]*['"]|\bos\s*\.\s*(?:remove|unlink|write|rmdir|makedirs|rename)\b|\.(?:write_text|write_bytes|unlink|mkdir|rename)\(|shutil\s*\.\s*(?:copy|move|rmtree))/i;
|
|
16
18
|
/** 从工具调用参数中收集含 workspacesRoot 的字符串值(文件路径线索)。 */
|
|
@@ -43,7 +45,7 @@ function dataField(e, key) {
|
|
|
43
45
|
function hasWriteMarker(text) {
|
|
44
46
|
return BASH_WRITE_RE.test(text) || PY_CODE_WRITE_RE.test(text);
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
48
|
+
/** 判定单个工具调用是否构成写证据(行为判定 + 只读排除)。
|
|
47
49
|
* @param subs 该调用的 run_code 派发子调用(无则 null,走兜底/直接判定)。 */
|
|
48
50
|
function handleCallEvidence(name, args, subs, root, paths) {
|
|
49
51
|
if (DIRECT_WRITE_TOOLS.has(name)) {
|
|
@@ -142,20 +144,20 @@ export class ChainAuditor {
|
|
|
142
144
|
this.listLiveAgents = deps.listLiveAgents ?? (() => []);
|
|
143
145
|
}
|
|
144
146
|
/** 执行核对,返回越权证据(空=通过,不阻塞汇报)。
|
|
145
|
-
* @param workspaceDir 本链发起工作区(Chain.workspaceDir
|
|
147
|
+
* @param workspaceDir 本链发起工作区(Chain.workspaceDir);提供时仅扫描工作区内的会话。 */
|
|
146
148
|
async check(chainId, workspaceDir = null) {
|
|
147
149
|
const evidence = [];
|
|
148
150
|
// 源 1:主会话(非 kbn- 角色会话)写能力工具事件扫描
|
|
149
151
|
for (const agent of this.listLiveAgents()) {
|
|
150
152
|
if (String(agent.id ?? '').startsWith('kbn-'))
|
|
151
153
|
continue; // 角色会话(P/W/D/V)跳过
|
|
152
|
-
// 0.1.0 delegation
|
|
154
|
+
// 0.1.0 delegation 豁免:header.agentPreset 以 kanban- 开头 → 角色会话
|
|
153
155
|
// 的子代理(childSessionMeta 记录所 join 的 preset id),产物归属 git 证据链,
|
|
154
156
|
// 不按"主会话越权"判定;源2(无主产物核对)仍兜底其误写链工作区根。
|
|
155
157
|
const joinedPreset = agent.session?.header?.agentPreset;
|
|
156
158
|
if (typeof joinedPreset === 'string' && joinedPreset.startsWith('kanban-'))
|
|
157
159
|
continue;
|
|
158
|
-
//
|
|
160
|
+
// 作用域收窄——仅扫本链发起工作区内的会话;会话无 cwd(测试伪造)时保守保留扫描
|
|
159
161
|
if (workspaceDir && agent.session?.header?.cwd && !isPathInside(agent.session.header.cwd, workspaceDir)) {
|
|
160
162
|
continue;
|
|
161
163
|
}
|
|
@@ -19,11 +19,11 @@ export interface DispatcherDeps {
|
|
|
19
19
|
waker: EventWaker;
|
|
20
20
|
watchdog: Watchdog;
|
|
21
21
|
maxRetries: number;
|
|
22
|
-
/** lastSeq
|
|
22
|
+
/** lastSeq 持久化文件(与事件日志同目录)。 */
|
|
23
23
|
stateFile: string;
|
|
24
|
-
/**
|
|
24
|
+
/** 调度器运行日志文件(storageDir/dispatcher.log)。 */
|
|
25
25
|
logFile: string;
|
|
26
|
-
/**
|
|
26
|
+
/** 宿主 agents 注册表(ctx.get('agents'))——启动 reconcile 判别 kbn-<taskId>
|
|
27
27
|
* 会话是否仍 live(插件热重载豁免);缺省/无 get 方法时按原行为收敛(保守)。 */
|
|
28
28
|
agents?: unknown;
|
|
29
29
|
/** 防线①:链级停滞探针(生产传 VOrchestrator;测试传桩)。缺省=看门狗关闭(行为同旧)。 */
|
|
@@ -39,15 +39,15 @@ export interface DispatcherDeps {
|
|
|
39
39
|
* 原实现仅 console.error,无控制台运行时零痕迹(排障最大障碍)。吞异常是为防 withTimeout
|
|
40
40
|
* 超时后迟到的 rejection 变 unhandledRejection;onSettled(saveOrchs)无论成败都执行。 */
|
|
41
41
|
export declare function makeWakeImpl(wakeV: (chainId: string) => Promise<void>, logFile: string, onSettled: () => void): (chainId: string) => Promise<void>;
|
|
42
|
-
/** 防线①:链级进度看门狗阈值。tick=2000ms × 45 ticks = 90s
|
|
42
|
+
/** 防线①:链级进度看门狗阈值。tick=2000ms × 45 ticks = 90s 无进展即重唤醒。 */
|
|
43
43
|
export declare const STALL_WATCHDOG_TICKS = 45;
|
|
44
|
-
/**
|
|
44
|
+
/** 防线①:链级重唤醒上限(超限 [create-failed] + chain/blocked)。 */
|
|
45
45
|
export declare const STALL_WATCHDOG_REWAKE_LIMIT = 3;
|
|
46
|
-
/** 调度器:事件唤醒 V
|
|
47
|
-
* -
|
|
46
|
+
/** 调度器:事件唤醒 V(逐阶段建卡)+ 每任务一次性角色 agent + 心跳看门狗。
|
|
47
|
+
* - failed 且 attempts<maxRetries 的任务重派(claim→running,AgentRunner resume 同一会话);
|
|
48
48
|
* attempts≥maxRetries 熔断 blocked(gave_up)。
|
|
49
|
-
* -
|
|
50
|
-
* -
|
|
49
|
+
* - lastSeq 持久化,重启后仅唤醒 lastSeq 之后的事件。
|
|
50
|
+
* - inFlight 互斥,防止慢 tick 与下一轮并发重复派发同一任务。 */
|
|
51
51
|
export declare class Dispatcher {
|
|
52
52
|
private readonly kanban;
|
|
53
53
|
private readonly runner;
|
|
@@ -73,26 +73,26 @@ export declare class Dispatcher {
|
|
|
73
73
|
* 仍停滞 → [create-failed] 评论(有锚点卡时)+ blockChain 终态(人工恢复=删链重跑)。 */
|
|
74
74
|
private chainStallWatchdog;
|
|
75
75
|
start(intervalMs: number): void;
|
|
76
|
-
/**
|
|
77
|
-
* 否则删链后新建链的可唤醒事件(seq <
|
|
76
|
+
/** 整链硬删除联动:purge 物理重排 events.jsonl seq,游标必须同步钳到当前 maxSeq。
|
|
77
|
+
* 否则删链后新建链的可唤醒事件(seq < 旧内存游标)被运行中实例永久跳过——启动时自愈,
|
|
78
78
|
* 覆盖不了运行中删链场景(2026-09-02 残留审计结论)。 */
|
|
79
79
|
onPurge(): Promise<void>;
|
|
80
80
|
stop(): void;
|
|
81
81
|
}
|
|
82
|
-
/** 调度层装配:事件唤醒 V
|
|
82
|
+
/** 调度层装配:事件唤醒 V(逐阶段建卡)+ 每任务一次性角色 agent + 心跳看门狗。
|
|
83
83
|
* 仅在 agents 与 kanban 服务同时可用时由插件入口调用(不依赖可能已错过的 ready 事件)。
|
|
84
|
-
*
|
|
85
|
-
/** 启动 reconcile
|
|
84
|
+
* 收 ConfigProvider——storageDir 取启动时快照;wiki/模型链经 getEffective() 调用时热读取。 */
|
|
85
|
+
/** 启动 reconcile:剔除事件流中已不存在的链的编排 entry(历史残留/外部 purge)。
|
|
86
86
|
* 原地删除并返回被移除的 chainId 列表(调用方负责持久化与日志)。 */
|
|
87
87
|
export declare function reconcileOrchestrations<T>(orch: Map<string, T>, chains: Set<string>): string[];
|
|
88
|
-
/** 启动 reconcile
|
|
88
|
+
/** 启动 reconcile:进程重启会杀死 runner 的 whenIdle 协程,上次遗留的 running 卡无人收尾,
|
|
89
89
|
* 看门狗默认 4h(staleTimeoutSeconds=14400)才回收——重启后立即把 running 孤儿卡收敛为 blocked
|
|
90
90
|
* (system comment + blockTask),中断显形且可重派续跑(重派将 resume 同一会话,进度保留)。
|
|
91
91
|
* 状态机注:TaskStatus 无独立 'claimed' 态——claimTask 发 task/claimed 事件后投影即为 running,
|
|
92
92
|
* 扫描 running 即覆盖「claimed 未收尾」;todo/ready/triage 从未派发,done/blocked/failed/archived
|
|
93
|
-
* 已有归属或终态(且 failed
|
|
94
|
-
*
|
|
95
|
-
* ctx.get('agents').get('kbn-<taskId>')(session id 构造同 AgentRunner.resumeOrReuse
|
|
93
|
+
* 已有归属或终态(且 failed 的处置归重派/熔断管辖),均不动。
|
|
94
|
+
* 热重载兼容,双重判据:收敛每张 running 卡前先查宿主 agents 注册表
|
|
95
|
+
* ctx.get('agents').get('kbn-<taskId>')(session id 构造同 AgentRunner.resumeOrReuse)。
|
|
96
96
|
* 两个世界的分野:
|
|
97
97
|
* - 进程重启:agents 注册表随宿主进程消亡,新进程内 kbn-<taskId> 必然查不到(undefined)
|
|
98
98
|
* → 会话已死 → 全部收敛,语义与修复前一致;
|