@joekytc/dsh-swarm 0.1.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +504 -0
  3. package/README.zh-CN.md +452 -0
  4. package/client/BoardCard.tsx +41 -0
  5. package/client/ConnectionBanner.tsx +16 -0
  6. package/client/KanbanBoard.tsx +168 -0
  7. package/client/KanbanTab.tsx +26 -0
  8. package/client/RenameModal.tsx +38 -0
  9. package/client/TaskDrawer.tsx +212 -0
  10. package/client/WorkflowRail.tsx +175 -0
  11. package/client/board-store.ts +196 -0
  12. package/client/css.d.ts +4 -0
  13. package/client/index.ts +31 -0
  14. package/client/kanban.css +653 -0
  15. package/client/useKanbanBoard.ts +7 -0
  16. package/client/workflow-model.ts +229 -0
  17. package/cordis.patch.yml +88 -0
  18. package/lib/client.js +1205 -0
  19. package/lib/config.d.ts +46 -0
  20. package/lib/config.js +43 -0
  21. package/lib/dispatcher/agent-runner.d.ts +23 -0
  22. package/lib/dispatcher/agent-runner.js +429 -0
  23. package/lib/dispatcher/chain-auditor.d.ts +47 -0
  24. package/lib/dispatcher/chain-auditor.js +194 -0
  25. package/lib/dispatcher/dispatcher.d.ts +50 -0
  26. package/lib/dispatcher/dispatcher.js +280 -0
  27. package/lib/dispatcher/event-waker.d.ts +13 -0
  28. package/lib/dispatcher/event-waker.js +24 -0
  29. package/lib/dispatcher/git-credentials.d.ts +33 -0
  30. package/lib/dispatcher/git-credentials.js +78 -0
  31. package/lib/dispatcher/merge-gate.d.ts +28 -0
  32. package/lib/dispatcher/merge-gate.js +74 -0
  33. package/lib/dispatcher/model-candidates.d.ts +13 -0
  34. package/lib/dispatcher/model-candidates.js +31 -0
  35. package/lib/dispatcher/session-events.d.ts +28 -0
  36. package/lib/dispatcher/session-events.js +33 -0
  37. package/lib/dispatcher/target-repo.d.ts +15 -0
  38. package/lib/dispatcher/target-repo.js +42 -0
  39. package/lib/dispatcher/v-orchestrator.d.ts +74 -0
  40. package/lib/dispatcher/v-orchestrator.js +452 -0
  41. package/lib/dispatcher/watchdog.d.ts +15 -0
  42. package/lib/dispatcher/watchdog.js +30 -0
  43. package/lib/dispatcher/workspace-attach.d.ts +40 -0
  44. package/lib/dispatcher/workspace-attach.js +112 -0
  45. package/lib/domain/delivery-contract.d.ts +18 -0
  46. package/lib/domain/delivery-contract.js +80 -0
  47. package/lib/domain/delivery-evidence.d.ts +12 -0
  48. package/lib/domain/delivery-evidence.js +41 -0
  49. package/lib/domain/event-store.d.ts +20 -0
  50. package/lib/domain/event-store.js +53 -0
  51. package/lib/domain/kanban-service.d.ts +94 -0
  52. package/lib/domain/kanban-service.js +430 -0
  53. package/lib/domain/permissions.d.ts +7 -0
  54. package/lib/domain/permissions.js +54 -0
  55. package/lib/domain/planning-checklist.d.ts +22 -0
  56. package/lib/domain/planning-checklist.js +81 -0
  57. package/lib/domain/prefetch-manifest.d.ts +21 -0
  58. package/lib/domain/prefetch-manifest.js +73 -0
  59. package/lib/domain/projection.d.ts +3 -0
  60. package/lib/domain/projection.js +165 -0
  61. package/lib/domain/review-evidence.d.ts +13 -0
  62. package/lib/domain/review-evidence.js +76 -0
  63. package/lib/domain/state-machine.d.ts +4 -0
  64. package/lib/domain/state-machine.js +32 -0
  65. package/lib/domain/task-parents.d.ts +17 -0
  66. package/lib/domain/task-parents.js +39 -0
  67. package/lib/domain/tdd-classify.d.ts +5 -0
  68. package/lib/domain/tdd-classify.js +20 -0
  69. package/lib/domain/types.d.ts +142 -0
  70. package/lib/domain/types.js +2 -0
  71. package/lib/index.d.ts +5 -0
  72. package/lib/index.js +56 -0
  73. package/lib/roles/preset-installer.d.ts +8 -0
  74. package/lib/roles/preset-installer.js +53 -0
  75. package/lib/roles/toolsets.d.ts +62 -0
  76. package/lib/roles/toolsets.js +332 -0
  77. package/lib/roles/wiki-worker.d.ts +20 -0
  78. package/lib/roles/wiki-worker.js +44 -0
  79. package/lib/routes/kanban-http.d.ts +6 -0
  80. package/lib/routes/kanban-http.js +159 -0
  81. package/lib/routes/kanban-sse.d.ts +8 -0
  82. package/lib/routes/kanban-sse.js +58 -0
  83. package/lib/routes/planning-driver.d.ts +16 -0
  84. package/lib/routes/planning-driver.js +52 -0
  85. package/lib/routes/prefix-router.d.ts +29 -0
  86. package/lib/routes/prefix-router.js +28 -0
  87. package/lib/services/kanban-provider.d.ts +16 -0
  88. package/lib/services/kanban-provider.js +14 -0
  89. package/lib/tools/kanban-tools.d.ts +11 -0
  90. package/lib/tools/kanban-tools.js +169 -0
  91. package/lib/tools/main-session-tools.d.ts +20 -0
  92. package/lib/tools/main-session-tools.js +170 -0
  93. package/lib/tools/planning-tools.d.ts +32 -0
  94. package/lib/tools/planning-tools.js +102 -0
  95. package/lib/tools/prefetch-tools.d.ts +5 -0
  96. package/lib/tools/prefetch-tools.js +59 -0
  97. package/lib/tools/spec-card-tools.d.ts +4 -0
  98. package/lib/tools/spec-card-tools.js +75 -0
  99. package/lib/tools/wiki-tools.d.ts +4 -0
  100. package/lib/tools/wiki-tools.js +57 -0
  101. package/lib/wiki/kb-linkage.d.ts +9 -0
  102. package/lib/wiki/kb-linkage.js +87 -0
  103. package/lib/wiki/page-path.d.ts +6 -0
  104. package/lib/wiki/page-path.js +28 -0
  105. package/lib/wiki/wiki-vault-client.d.ts +28 -0
  106. package/lib/wiki/wiki-vault-client.js +48 -0
  107. package/package.json +83 -0
  108. package/personas/kanban-d/agent.cordis.yml +157 -0
  109. package/personas/kanban-d/preset.yml +2 -0
  110. package/personas/kanban-dt/agent.cordis.yml +71 -0
  111. package/personas/kanban-dt/preset.yml +2 -0
  112. package/personas/kanban-p/agent.cordis.yml +66 -0
  113. package/personas/kanban-p/preset.yml +2 -0
  114. package/personas/kanban-pt/agent.cordis.yml +47 -0
  115. package/personas/kanban-pt/preset.yml +2 -0
  116. package/personas/kanban-v/agent.cordis.yml +47 -0
  117. package/personas/kanban-v/preset.yml +2 -0
  118. package/personas/kanban-w/agent.cordis.yml +47 -0
  119. package/personas/kanban-w/preset.yml +2 -0
  120. package/personas/persona-d.md +26 -0
  121. package/personas/persona-dt.md +18 -0
  122. package/personas/persona-p.md +13 -0
  123. package/personas/persona-pt.md +13 -0
  124. package/personas/persona-v.md +18 -0
  125. package/personas/persona-w.md +12 -0
@@ -0,0 +1,78 @@
1
+ // src/dispatcher/git-credentials.ts
2
+ import { execFileSync } from 'node:child_process';
3
+ /**
4
+ * R20 D(execute) git 凭据注入(M4):使 D 会话的 git ls-remote/push 可用。
5
+ *
6
+ * 机制:D 会话的 bash 每次调用都是新 shell(无环境持久),故 env 变量不可跨调用。
7
+ * 采用 repo-local http extraheader(AUTHORIZATION: basic),写入 <repo>/.git/config——
8
+ * 持久跨 bash 调用、由插件进程(web app 侧,不受 D 会话沙箱限制)注入。
9
+ *
10
+ * 鉴权按托管方区分:
11
+ * - GitLab PAT(glpat- 前缀)→ basic base64("oauth2:<PAT>")(GitLab 规范,实测 ls-remote 通过);
12
+ * - 其余(GitHub 等)→ basic base64("x-access-token:<PAT>")。
13
+ * D 会话为 danger-full-access(用户决策 Q4),HOME 可写,故 git 的 credential.helper
14
+ * (store/osxkeychain 等)不再触发 "credential storage lock",无需中和。
15
+ *
16
+ * PAT 来源(按优先级):DSH 凭据服务 ctx.credentials($DSH_HOME/.credentials.yaml 的
17
+ * KANBAN_GIT_PAT/GIT_PAT)→ 进程 env(KANBAN_GIT_PAT > GIT_PAT > GH_TOKEN > GITHUB_TOKEN)。
18
+ * 未配置 PAT 时不注入(回退用户已有 git 凭据/SSH)。
19
+ */
20
+ /** D 会话可用 git PAT 的 env 变量名(按优先级)。 */
21
+ export const GIT_PAT_ENV_NAMES = ['KANBAN_GIT_PAT', 'GIT_PAT', 'GH_TOKEN', 'GITHUB_TOKEN'];
22
+ /** 从进程 env 解析非空 PAT;无则返回 null。 */
23
+ export function resolveGitPat(env) {
24
+ for (const name of GIT_PAT_ENV_NAMES) {
25
+ const v = env[name];
26
+ if (v && v.trim())
27
+ return v.trim();
28
+ }
29
+ return null;
30
+ }
31
+ /** 经 DSH 凭据服务解析 PAT($DSH_HOME/.credentials.yaml / env / .env,见 dsh-credentials-local 分层);无则回退进程 env。 */
32
+ export async function resolveGitPatFromCtx(ctx, env) {
33
+ const creds = ctx.get?.('credentials');
34
+ if (creds?.resolve) {
35
+ for (const name of GIT_PAT_ENV_NAMES) {
36
+ try {
37
+ const r = await creds.resolve(name);
38
+ if (r?.value && r.value.trim())
39
+ return r.value.trim();
40
+ }
41
+ catch { /* 单个引用解析失败继续下一个 */ }
42
+ }
43
+ }
44
+ return resolveGitPat(env);
45
+ }
46
+ /** PAT → http extraheader 值:GitLab(glpat-*) 用 oauth2 前缀,其余 x-access-token。 */
47
+ export function gitAuthHeader(pat) {
48
+ const prefix = pat.startsWith('glpat-') ? 'oauth2:' : 'x-access-token:';
49
+ return 'AUTHORIZATION: basic ' + Buffer.from(prefix + pat).toString('base64');
50
+ }
51
+ /**
52
+ * 注入 repo-local http extraheader。返回 { ok, detail }(失败不抛——注入失败仅告警,
53
+ * D 仍可执行;用户自带凭据/SSH 的仓库不受影响)。
54
+ */
55
+ export function injectGitCredentials(repoDir, pat) {
56
+ const targets = [];
57
+ try {
58
+ const url = execFileSync('git', ['-C', repoDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8' }).trim();
59
+ if (url.startsWith('http://') || url.startsWith('https://'))
60
+ targets.push(url);
61
+ }
62
+ catch {
63
+ // 非 git 仓库或无 origin:注入目标为空
64
+ }
65
+ if (targets.length === 0)
66
+ return { ok: false, detail: 'no http remote origin' };
67
+ try {
68
+ const header = gitAuthHeader(pat);
69
+ for (const target of targets) {
70
+ // git config <section>.<subsection>.<variable>:http.<url>.extraheader
71
+ execFileSync('git', ['-C', repoDir, 'config', '--local', `http.${target}.extraheader`, header]);
72
+ }
73
+ return { ok: true, detail: targets.join(',') };
74
+ }
75
+ catch (err) {
76
+ return { ok: false, detail: String(err) };
77
+ }
78
+ }
@@ -0,0 +1,28 @@
1
+ import type { KanbanService } from '../domain/kanban-service.js';
2
+ import type { BoardState, Task } from '../domain/types.js';
3
+ /** 合入输入:repoDir=目标仓库绝对路径;targetBranch=规格卡声明分支;featureBranch=D 交接 metadata.branch。 */
4
+ export interface MergeInput {
5
+ repoDir: string;
6
+ targetBranch: string;
7
+ featureBranch: string;
8
+ }
9
+ export declare const MERGE_DONE_PREFIX = "[merge-done]";
10
+ export declare const MERGE_SKIP_PREFIX = "[merge-skip]";
11
+ export declare const MERGE_FAILED_PREFIX = "[merge-failed]";
12
+ export interface GitRun {
13
+ (args: string[], cwd: string): string;
14
+ }
15
+ /** 真实 git 执行器:execFileSync 同步执行,非零退出抛错(与 git-credentials.ts 同模式)。 */
16
+ export declare function realGitRun(args: string[], cwd: string): string;
17
+ /** 从 D(execute) 终态卡解析合入输入。任一要素缺失(repo 不存在 / TARGET_BRANCH 标记 / branch metadata)→ null(软跳过)。 */
18
+ export declare function resolveMergeInput(dTask: Task, state: BoardState, fallbackRepo: string): MergeInput | null;
19
+ /** 幂等判定:已存在 [merge-done] 评论,或 feature 分支已是 target 祖先(git merge-base --is-ancestor exit 0)。 */
20
+ export declare function isAlreadyMerged(state: BoardState, dTaskId: string, repoDir: string, targetBranch: string, featureBranch: string, git: GitRun): boolean;
21
+ /**
22
+ * 执行合入(DT 通过后由 system 调用):checkout target → merge --no-ff feature → push。
23
+ * 返回 'merged' | 'failed' | 'skipped'。失败不抛:记录 [merge-failed] 评论,链仍可收尾
24
+ * (坏代码未被合入 = 方向安全;人工可事后修复)。
25
+ */
26
+ export declare function runMergeGate(kanban: KanbanService, state: BoardState, dTask: Task, input: MergeInput, git?: GitRun): Promise<'merged' | 'failed' | 'skipped'>;
27
+ /** 链完成钩子入口(dispatcher setOnChainCompleted 调用):DT 通过后合入。解析失败软跳过。 */
28
+ export declare function mergeDAfterReview(kanban: KanbanService, chainId: string, fallbackRepo: string): Promise<'merged' | 'failed' | 'skipped'>;
@@ -0,0 +1,74 @@
1
+ // src/dispatcher/merge-gate.ts
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { resolveTargetRepoDir } from './target-repo.js';
6
+ export const MERGE_DONE_PREFIX = '[merge-done]';
7
+ export const MERGE_SKIP_PREFIX = '[merge-skip]';
8
+ export const MERGE_FAILED_PREFIX = '[merge-failed]';
9
+ /** 真实 git 执行器:execFileSync 同步执行,非零退出抛错(与 git-credentials.ts 同模式)。 */
10
+ export function realGitRun(args, cwd) {
11
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 }).trim();
12
+ }
13
+ /** 从 D(execute) 终态卡解析合入输入。任一要素缺失(repo 不存在 / TARGET_BRANCH 标记 / branch metadata)→ null(软跳过)。 */
14
+ export function resolveMergeInput(dTask, state, fallbackRepo) {
15
+ // allowFallback=false:合入仓库必须是显式声明的 TARGET_REPO/规格卡 ref,绝不回退到 kanban 存储等目录。
16
+ const repoDir = resolveTargetRepoDir(dTask, state, fallbackRepo, false);
17
+ if (!repoDir || !existsSync(resolve(repoDir)))
18
+ return null;
19
+ const targetBranch = dTask.body?.match(/TARGET_BRANCH\s*=\s*(\S+)/)?.[1];
20
+ const featureBranch = String(state.handoffs.get(dTask.id)?.metadata?.['branch'] ?? '').trim();
21
+ if (!targetBranch || !featureBranch)
22
+ return null;
23
+ return { repoDir, targetBranch, featureBranch };
24
+ }
25
+ /** 幂等判定:已存在 [merge-done] 评论,或 feature 分支已是 target 祖先(git merge-base --is-ancestor exit 0)。 */
26
+ export function isAlreadyMerged(state, dTaskId, repoDir, targetBranch, featureBranch, git) {
27
+ // 仅认 system 写入的 [merge-done] 标记:角色可伪造评论体,author 过滤防幂等门被冒名绕过。
28
+ const doneComment = state.events.some((e) => e.taskId === dTaskId && e.kind === 'task/commented' && e.author === 'system' &&
29
+ String(e.payload['body'] ?? '').startsWith(MERGE_DONE_PREFIX));
30
+ if (doneComment)
31
+ return true;
32
+ try {
33
+ git(['merge-base', '--is-ancestor', featureBranch, targetBranch], repoDir);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * 执行合入(DT 通过后由 system 调用):checkout target → merge --no-ff feature → push。
42
+ * 返回 'merged' | 'failed' | 'skipped'。失败不抛:记录 [merge-failed] 评论,链仍可收尾
43
+ * (坏代码未被合入 = 方向安全;人工可事后修复)。
44
+ */
45
+ export async function runMergeGate(kanban, state, dTask, input, git = realGitRun) {
46
+ const { repoDir, targetBranch, featureBranch } = input;
47
+ if (isAlreadyMerged(state, dTask.id, repoDir, targetBranch, featureBranch, git))
48
+ return 'skipped';
49
+ try {
50
+ git(['checkout', targetBranch], repoDir);
51
+ git(['merge', '--no-ff', featureBranch, '-m', `[AI-GEN] merge ${featureBranch} into ${targetBranch} after DT pass`], repoDir);
52
+ git(['push'], repoDir);
53
+ const hash = git(['rev-parse', 'HEAD'], repoDir);
54
+ await kanban.comment(dTask.id, `${MERGE_DONE_PREFIX} merged ${featureBranch} → ${targetBranch} hash=${hash}`, 'system');
55
+ return 'merged';
56
+ }
57
+ catch (err) {
58
+ await kanban.comment(dTask.id, `${MERGE_FAILED_PREFIX} merge ${featureBranch} → ${targetBranch} failed: ${String(err)}`, 'system');
59
+ return 'failed';
60
+ }
61
+ }
62
+ /** 链完成钩子入口(dispatcher setOnChainCompleted 调用):DT 通过后合入。解析失败软跳过。 */
63
+ export async function mergeDAfterReview(kanban, chainId, fallbackRepo) {
64
+ const state = await kanban.snapshot();
65
+ const dTask = [...state.tasks.values()].find((t) => t.chainId === chainId && t.assignee === 'd' && t.mode === 'execute' && t.status === 'done');
66
+ if (!dTask)
67
+ return 'skipped';
68
+ const input = resolveMergeInput(dTask, state, fallbackRepo);
69
+ if (!input) {
70
+ await kanban.comment(dTask.id, `${MERGE_SKIP_PREFIX} 无法解析合入输入(缺 branch metadata / TARGET_BRANCH 标记 / repo 不存在),跳过自动合入`, 'system');
71
+ return 'skipped';
72
+ }
73
+ return runMergeGate(kanban, state, dTask, input);
74
+ }
@@ -0,0 +1,13 @@
1
+ import type { KanbanConfig } from '../config.js';
2
+ import type { Role } from '../domain/types.js';
3
+ import type { AgentModelOptions } from './dispatcher.js';
4
+ /**
5
+ * 模型候选链(交付质量链 Task 12):角色创建时主模型 → fallbacks 依次静默切换。
6
+ * primary = config.roles.models[role];未配置回退 defaultModel。
7
+ * reasoningEffort 未指定一律 'high'。
8
+ *
9
+ * @returns 有序候选链(不含不可用者;可能为空 = 无任何配置)
10
+ */
11
+ export declare function buildModelCandidates(config: KanbanConfig, role: Role, defaultModel?: AgentModelOptions): AgentModelOptions[];
12
+ /** 判定 create/resume 错误是否属于 model/provider 不可用(可静默切换下一候选)。 */
13
+ export declare function isModelUnavailableError(err: unknown): boolean;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 模型候选链(交付质量链 Task 12):角色创建时主模型 → fallbacks 依次静默切换。
3
+ * primary = config.roles.models[role];未配置回退 defaultModel。
4
+ * reasoningEffort 未指定一律 'high'。
5
+ *
6
+ * @returns 有序候选链(不含不可用者;可能为空 = 无任何配置)
7
+ */
8
+ export function buildModelCandidates(config, role, defaultModel) {
9
+ const m = config.roles?.models?.[role];
10
+ const chain = [];
11
+ if (m?.provider && m?.model) {
12
+ chain.push({ provider: m.provider, model: m.model, reasoningEffort: m.reasoningEffort ?? 'high' });
13
+ for (const f of m.fallbacks ?? []) {
14
+ if (f?.provider && f?.model)
15
+ chain.push({ provider: f.provider, model: f.model, reasoningEffort: f.reasoningEffort ?? 'high' });
16
+ }
17
+ }
18
+ else if (defaultModel?.provider && defaultModel?.model) {
19
+ chain.push({ provider: defaultModel.provider, model: defaultModel.model, reasoningEffort: defaultModel.reasoningEffort ?? 'high' });
20
+ }
21
+ return chain;
22
+ }
23
+ /** 判定 create/resume 错误是否属于 model/provider 不可用(可静默切换下一候选)。 */
24
+ export function isModelUnavailableError(err) {
25
+ const msg = String(err instanceof Error ? err.message : err).toLowerCase();
26
+ if (msg.includes('adapter'))
27
+ return true; // no adapter registered for provider …
28
+ if (msg.includes('provider') && (msg.includes('unavailable') || msg.includes('not') || msg.includes('fail')))
29
+ return true;
30
+ return msg.includes('model') && (msg.includes('unavailable') || msg.includes('not found') || msg.includes('no adapter'));
31
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 会话事件条目统一读取助手(修复轮 6,举一反三):
3
+ * dsh-session 的 session.events 条目落盘形态为 { type, seq, time, data: { ... } }(append 时
4
+ * data 嵌套在 data 键下),name/arguments 等字段位于 e.data 下而非顶层。
5
+ * 本助手兼容两种形态(顶层展开 / data 嵌套),供 v-orchestrator / agent-runner / chain-auditor
6
+ * 等所有读取 agent.session.events 的消费点使用,避免逐个踩 `e.name 取不到` 的坑。
7
+ */
8
+ /** 取事件类型(如 'tool/call')。 */
9
+ export declare function eventType(e: {
10
+ type?: unknown;
11
+ data?: {
12
+ type?: unknown;
13
+ };
14
+ }): string | undefined;
15
+ /** 取工具调用名(如 'kanban_complete' / 'bash' / 'kanban_create')。 */
16
+ export declare function toolName(e: {
17
+ name?: unknown;
18
+ data?: {
19
+ name?: unknown;
20
+ };
21
+ }): string | undefined;
22
+ /** 取工具调用参数对象(兼容 JSON 字符串 / 对象两种落盘形态)。 */
23
+ export declare function toolArgs(e: {
24
+ arguments?: unknown;
25
+ data?: {
26
+ arguments?: unknown;
27
+ };
28
+ }): Record<string, unknown>;
@@ -0,0 +1,33 @@
1
+ // src/dispatcher/session-events.ts
2
+ /**
3
+ * 会话事件条目统一读取助手(修复轮 6,举一反三):
4
+ * dsh-session 的 session.events 条目落盘形态为 { type, seq, time, data: { ... } }(append 时
5
+ * data 嵌套在 data 键下),name/arguments 等字段位于 e.data 下而非顶层。
6
+ * 本助手兼容两种形态(顶层展开 / data 嵌套),供 v-orchestrator / agent-runner / chain-auditor
7
+ * 等所有读取 agent.session.events 的消费点使用,避免逐个踩 `e.name 取不到` 的坑。
8
+ */
9
+ /** 取事件类型(如 'tool/call')。 */
10
+ export function eventType(e) {
11
+ const t = e?.type ?? e?.data?.type;
12
+ return typeof t === 'string' ? t : undefined;
13
+ }
14
+ /** 取工具调用名(如 'kanban_complete' / 'bash' / 'kanban_create')。 */
15
+ export function toolName(e) {
16
+ const n = e?.name ?? e?.data?.name;
17
+ return typeof n === 'string' ? n : undefined;
18
+ }
19
+ /** 取工具调用参数对象(兼容 JSON 字符串 / 对象两种落盘形态)。 */
20
+ export function toolArgs(e) {
21
+ const a = e?.arguments ?? e?.data?.arguments;
22
+ if (a && typeof a === 'object')
23
+ return a;
24
+ if (typeof a === 'string') {
25
+ try {
26
+ return JSON.parse(a);
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ return {};
33
+ }
@@ -0,0 +1,15 @@
1
+ import type { BoardState, Task } from '../domain/types.js';
2
+ /**
3
+ * R20 D(execute) 目标仓库路径解析(B2/B3 共用):
4
+ * 1. 任务 body 的 TARGET_REPO=<path> 标记(V 生成 D 任务体时写入,见 v-orchestrator D 阶段指令);
5
+ * 2. 规格卡 file-prefetch 附件 ref(需求澄清清单 manifest 的仓库路径);
6
+ * 3. 回退默认目录(kanban 存储 / 会话工作区)。
7
+ *
8
+ * 解析为绝对路径并校验存在性;候选不存在时继续降级,全部失败回退默认目录——
9
+ * 保证给 agents.create 的 meta.cwd 恒为绝对路径(会话校验要求 isAbsolute)。
10
+ * 存在性校验同时兜底沙箱根:workspace-write 以会话 cwd 为写边界,指向不存在的
11
+ * 目录会导致所有写被拒,故不存在时回退而非保留。
12
+ */
13
+ /** 判断 child 是否位于 parent 目录内(含等于)。用于 D 目标仓库是否在会话工作空间内的判定(B 前置授权)。 */
14
+ export declare function isPathInside(child: string, parent: string): boolean;
15
+ export declare function resolveTargetRepoDir(task: Task, state: BoardState, fallback: string, allowFallback?: boolean): string;
@@ -0,0 +1,42 @@
1
+ // src/dispatcher/target-repo.ts
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ /**
5
+ * R20 D(execute) 目标仓库路径解析(B2/B3 共用):
6
+ * 1. 任务 body 的 TARGET_REPO=<path> 标记(V 生成 D 任务体时写入,见 v-orchestrator D 阶段指令);
7
+ * 2. 规格卡 file-prefetch 附件 ref(需求澄清清单 manifest 的仓库路径);
8
+ * 3. 回退默认目录(kanban 存储 / 会话工作区)。
9
+ *
10
+ * 解析为绝对路径并校验存在性;候选不存在时继续降级,全部失败回退默认目录——
11
+ * 保证给 agents.create 的 meta.cwd 恒为绝对路径(会话校验要求 isAbsolute)。
12
+ * 存在性校验同时兜底沙箱根:workspace-write 以会话 cwd 为写边界,指向不存在的
13
+ * 目录会导致所有写被拒,故不存在时回退而非保留。
14
+ */
15
+ /** 判断 child 是否位于 parent 目录内(含等于)。用于 D 目标仓库是否在会话工作空间内的判定(B 前置授权)。 */
16
+ export function isPathInside(child, parent) {
17
+ const c = resolve(child);
18
+ const p = resolve(parent);
19
+ if (c === p)
20
+ return true;
21
+ return c.startsWith(p.endsWith('/') ? p : p + '/');
22
+ }
23
+ export function resolveTargetRepoDir(task, state, fallback, allowFallback = true) {
24
+ const candidates = [];
25
+ const body = task.body ?? '';
26
+ const m = body.match(/TARGET_REPO\s*=\s*(\S+)/);
27
+ if (m)
28
+ candidates.push(m[1]);
29
+ const chain = state.chains.get(task.chainId);
30
+ const card = chain?.specCardId ? state.specCards.get(chain.specCardId) : undefined;
31
+ const prefetch = card?.attachments.find((a) => a.kind === 'file-prefetch');
32
+ if (prefetch?.ref)
33
+ candidates.push(prefetch.ref);
34
+ for (const c of candidates) {
35
+ const abs = resolve(c);
36
+ if (existsSync(abs))
37
+ return abs;
38
+ }
39
+ // allowFallback=false(合入门控用):不降级到回退目录——合入必须落在显式声明的目标仓库,
40
+ // 禁止 merge 到 kanban 存储/会话工作区等回退目录(M1,方向安全)。
41
+ return allowFallback ? resolve(fallback) : '';
42
+ }
@@ -0,0 +1,74 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { KanbanService } from '../domain/kanban-service.js';
3
+ import type { KanbanConfig } from '../config.js';
4
+ import type { Role, TaskMode } from '../domain/types.js';
5
+ import type { WikiVaultClient } from '../wiki/wiki-vault-client.js';
6
+ import type { AgentModelOptions } from './dispatcher.js';
7
+ export type VPhase = 'p' | 'pt' | 'w2' | 'd' | 'dt' | 'w3' | 'summary';
8
+ export interface ChainOrchestration {
9
+ chainId: string;
10
+ phase: VPhase;
11
+ sessionId: string | null;
12
+ waitingOn: string | null;
13
+ stallCount?: number;
14
+ }
15
+ export declare const R20_PHASE_ORDER: VPhase[];
16
+ /** 每 phase 的期望建卡(pt 由 P 交付 pt_decision.needed=true 触发;dt 固定)。 */
17
+ export declare const R20_PHASE_EXPECT: Record<VPhase, {
18
+ assignee: Role;
19
+ mode: TaskMode;
20
+ } | null>;
21
+ /** M5:每阶段建卡的 body 生成指令(角色定位确定性模板,消除 V 自由发挥导致的角色漂移)。
22
+ * P=计划者(绝不执行)、D=唯一执行者(TARGET_REPO 必须取自规格卡 file-prefetch 附件 ref,禁止回退/猜测)、
23
+ * W=KB 同步(绝不执行代码)。V 把对应模板写入 kanban_create 的 body。 */
24
+ export declare const PHASE_INSTRUCTIONS: Partial<Record<VPhase, string>>;
25
+ interface AgentLike {
26
+ followup(msg: {
27
+ content: {
28
+ type: string;
29
+ text: string;
30
+ }[];
31
+ source: {
32
+ kind: string;
33
+ };
34
+ }): void;
35
+ whenIdle(): Promise<void>;
36
+ session: {
37
+ events: Array<{
38
+ name?: string;
39
+ arguments?: unknown;
40
+ }>;
41
+ };
42
+ }
43
+ export declare class VOrchestrator {
44
+ private readonly ctx;
45
+ private readonly kanban;
46
+ private readonly agents;
47
+ private readonly config;
48
+ private readonly orchestrations;
49
+ private readonly wiki;
50
+ private readonly defaultModel;
51
+ constructor(ctx: Context, kanban: KanbanService, agents: {
52
+ create(o: unknown): Promise<{
53
+ agent: AgentLike;
54
+ }>;
55
+ resume(o: unknown): Promise<{
56
+ agent: AgentLike;
57
+ }>;
58
+ }, config: KanbanConfig, orchestrations: Map<string, ChainOrchestration>, wiki: WikiVaultClient, defaultModel?: AgentModelOptions);
59
+ private currentPhase;
60
+ wakeV(chainId: string): Promise<void>;
61
+ /** 阻塞复核幂等判定:任务最近一次 task/blocked 之后已存在 [blocked-review] 开头的评论。
62
+ * 注:at 为 Date.now() 毫秒精度,block 与评论可能同毫秒(测试/快路径实测碰撞)→ 用 seq 比较(确定性)。 */
63
+ private hasBlockReview;
64
+ /** 评审卡 completed 处理(交付质量链):读 handoff 的 review_evidence verdict 分流。
65
+ * pass → recordReview(passed) + 推进;fail → recordReview(failed) + createReworkTask + 新建复审卡;
66
+ * reviewAttempt ≥ maxReworksPerRole → review/gave-up + [review-final] 证据链(链保持)。
67
+ * 严禁对已完成的上游 P/D 调 blockTask(done 不可变;返工走新 rework 卡)。 */
68
+ private handleReviewCompletion;
69
+ private advance;
70
+ private getVAgent;
71
+ /** M2(Q5)+归组:链的 workspaceDir(发起 /plan: 的主 agent 工作空间);缺失返回 null(调用方询问/报错,不落 kanban 存储)。 */
72
+ private chainWorkspace;
73
+ }
74
+ export {};