@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,112 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ const ASK_TIMEOUT_MS = 120_000;
3
+ function getWs(ctx) {
4
+ return ctx.get?.('workspaceRegistry');
5
+ }
6
+ function getUq(ctx) {
7
+ return ctx.get?.('userQuestions');
8
+ }
9
+ /** 询问用户注册工作区(超时护栏,与 requestRepoPermission 一致)。返回选定路径或 null。 */
10
+ async function askWorkspace(uq, cwd, label) {
11
+ try {
12
+ const options = cwd
13
+ ? [{ label: '创建', description: '注册 ' + cwd + ' 为工作区并归组会话' }]
14
+ : [];
15
+ options.push({ label: '跳过', description: '保持 Ungrouped,不创建工作区' });
16
+ const ans = await Promise.race([
17
+ uq.ask({
18
+ questions: [{
19
+ id: 'workspace-register',
20
+ header: '工作区注册',
21
+ question: cwd
22
+ ? '会话工作目录 ' + cwd + '(' + label + ')未注册为 DSH 工作区。是否注册为工作区以归组角色会话?'
23
+ : label + ' 未绑定工作区。请在 Other 中输入要注册为工作区的目录绝对路径,或选择跳过。',
24
+ detail: '注册后角色/编排会话将与主 agent 同组展示;不注册则保持 Ungrouped。',
25
+ options,
26
+ }],
27
+ }),
28
+ new Promise((_, reject) => setTimeout(() => reject(new Error('workspace-ask-timeout')), ASK_TIMEOUT_MS)),
29
+ ]);
30
+ const item = ans.answers?.[0];
31
+ if (item?.custom?.trim())
32
+ return item.custom.trim();
33
+ if (item?.selected?.includes('创建') && cwd)
34
+ return cwd;
35
+ return null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * 返回可用的会话工作目录路径。
43
+ * - cwd 已知:优先复用已注册工作区;未注册则询问用户(默认创建 @cwd)自动 create;询问失败/跳过仍返回 cwd(目录正确性优先,归组是 bonus)。
44
+ * - cwd 未知:必须经询问拿到路径并 create;无路径/无通道/无服务 → null(调用方 block 或待命)。
45
+ */
46
+ export async function resolveOrCreateWorkspace(ctx, cwd, label) {
47
+ const ws = getWs(ctx);
48
+ if (cwd) {
49
+ if (ws?.resolveByPath) {
50
+ try {
51
+ if (await ws.resolveByPath(cwd))
52
+ return cwd;
53
+ }
54
+ catch { /* 归组失败不阻断 */ }
55
+ }
56
+ if (ws?.create) {
57
+ const uq = getUq(ctx);
58
+ if (uq?.ask) {
59
+ const chosen = await askWorkspace(uq, cwd, label);
60
+ if (chosen) {
61
+ try {
62
+ await ws.create(chosen);
63
+ }
64
+ catch { /* 创建失败不阻断 */ }
65
+ return chosen;
66
+ }
67
+ }
68
+ }
69
+ return cwd;
70
+ }
71
+ if (!ws?.create)
72
+ return null;
73
+ const uq = getUq(ctx);
74
+ if (!uq?.ask)
75
+ return null;
76
+ const chosen = await askWorkspace(uq, null, label);
77
+ if (!chosen)
78
+ return null;
79
+ try {
80
+ await ws.create(chosen);
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ return chosen;
86
+ }
87
+ /** 把已创建/恢复的会话归组到 cwd 对应工作区;未注册时询问用户(仅明确「创建」才 create);全程失败静默(归组是 UX 增强,不阻断任务)。 */
88
+ export async function attachSessionToWorkspace(ctx, sessionId, cwd, label) {
89
+ const ws = getWs(ctx);
90
+ if (!ws?.create || !ws?.resolveByPath)
91
+ return; // 无注册表 → 静默跳过(归组是 bonus)
92
+ try {
93
+ let entity = await ws.resolveByPath(cwd);
94
+ if (!entity) {
95
+ // 未注册:询问用户是否注册;跳过/无通道/超时 → 保持 Ungrouped(不创建)
96
+ const uq = getUq(ctx);
97
+ if (!uq?.ask)
98
+ return;
99
+ const chosen = await askWorkspace(uq, cwd, label);
100
+ if (!chosen)
101
+ return;
102
+ entity = await ws.create(chosen);
103
+ }
104
+ if (!entity?.attachSession)
105
+ return;
106
+ await entity.attachSession(SessionId(sessionId));
107
+ console.error('[dsh-swarm][debug] session attached workspace ' + sessionId + ' cwd=' + cwd + ' label=' + label);
108
+ }
109
+ catch (err) {
110
+ console.error('[dsh-swarm][debug] attach session failed ' + sessionId + ': ' + String(err));
111
+ }
112
+ }
@@ -0,0 +1,18 @@
1
+ import type { BoardState, Handoff, Role, TaskMode } from './types.js';
2
+ export declare function requiredDeliveryKeys(assignee: Role, mode: TaskMode): string[];
3
+ /** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。 */
4
+ export declare function missingPtDecisionKeys(handoff: Handoff | undefined): string[];
5
+ /** 缺失的交付键(存在但为空的字符串/非字符串均视为缺失;pt_decision 走结构校验透传细粒度键)。
6
+ * 可选 kbUrlBase:提供时对 w:kb 的 kb_url 做 host 前缀硬校验(防 LLM 手写错域名)、对 page_path 做
7
+ * 命名空间格式校验(Q3&5:防 LLM 自造路径/拼错层级)——未提供则仅非空校验(兼容旧调用/测试)。 */
8
+ export declare function missingDeliveryKeys(assignee: Role, mode: TaskMode, handoff: Handoff | undefined, kbUrlBase?: string): string[];
9
+ export declare function hasRequiredDelivery(assignee: Role, mode: TaskMode, handoff: Handoff | undefined): boolean;
10
+ /** 交付契约缺失的父卡项(供 V 建下游卡前的前置校验)。 */
11
+ export interface MissingParentDelivery {
12
+ taskId: string;
13
+ assignee: Role;
14
+ mode: TaskMode;
15
+ missing: string[];
16
+ }
17
+ /** 对一组父任务 id 做交付契约校验,返回缺关键交付物的父卡清单(无缺失返回空数组)。 */
18
+ export declare function missingParentDelivery(state: BoardState, parentIds: string[]): MissingParentDelivery[];
@@ -0,0 +1,80 @@
1
+ import { isAllowedWikiPagePath } from '../wiki/page-path.js';
2
+ /**
3
+ * 上游交付契约(R20「上游对下游负责」宗旨):每阶段任务完成交接 metadata 必须产出的键。
4
+ * key = `${assignee}:${mode}`;仅列出「下游实际读取」的硬交付物——
5
+ * - w:kb(W2/W3):kb_url + page_path = KB 同步页(下游 wiki_read 读原文 / 收尾)
6
+ * - p:openspec(P):artifacts_path = openspec 实施计划产物路径(W2 读取同步 KB);
7
+ * pt_decision = P 判定是否需要计划评审的硬键(v2:needed 布尔必填,needed=true 时 reason 必填)
8
+ * d:execute 的 git 产物证据由 hasDeliveryEvidence 单独判定;pt/dt 由 validateReviewEvidence 判定,
9
+ * 二者不重复登记。
10
+ * 未列出的 mode(w:external 可选、align 旧兼容等)无硬交付约束。
11
+ * v2 断代:w:file 交付键随旧 w1 预取阶段已删除。
12
+ */
13
+ const REQUIRED_DELIVERY = {
14
+ 'w:kb': ['kb_url', 'page_path'],
15
+ 'p:openspec': ['artifacts_path', 'pt_decision'],
16
+ };
17
+ export function requiredDeliveryKeys(assignee, mode) {
18
+ return REQUIRED_DELIVERY[`${assignee}:${mode}`] ?? [];
19
+ }
20
+ /** v2:pt_decision 结构校验(needed 布尔必填;needed=true 时 reason 必填)。返回缺失键列表。 */
21
+ export function missingPtDecisionKeys(handoff) {
22
+ const d = (handoff?.metadata ?? {})['pt_decision'];
23
+ if (typeof d !== 'object' || d === null)
24
+ return ['pt_decision'];
25
+ const o = d;
26
+ if (typeof o['needed'] !== 'boolean')
27
+ return ['pt_decision.needed'];
28
+ if (o['needed'] === true && (typeof o['reason'] !== 'string' || o['reason'].trim().length === 0)) {
29
+ return ['pt_decision.reason'];
30
+ }
31
+ return [];
32
+ }
33
+ /** 缺失的交付键(存在但为空的字符串/非字符串均视为缺失;pt_decision 走结构校验透传细粒度键)。
34
+ * 可选 kbUrlBase:提供时对 w:kb 的 kb_url 做 host 前缀硬校验(防 LLM 手写错域名)、对 page_path 做
35
+ * 命名空间格式校验(Q3&5:防 LLM 自造路径/拼错层级)——未提供则仅非空校验(兼容旧调用/测试)。 */
36
+ export function missingDeliveryKeys(assignee, mode, handoff, kbUrlBase) {
37
+ const keys = requiredDeliveryKeys(assignee, mode);
38
+ if (keys.length === 0)
39
+ return [];
40
+ if (!handoff)
41
+ return keys.slice();
42
+ const m = handoff.metadata ?? {};
43
+ const base = kbUrlBase ? kbUrlBase.replace(/\/$/, '') : null;
44
+ const strict = base !== null;
45
+ const missing = [];
46
+ for (const k of keys) {
47
+ if (k === 'pt_decision') {
48
+ missing.push(...missingPtDecisionKeys(handoff));
49
+ }
50
+ else {
51
+ const v = m[k];
52
+ if (typeof v !== 'string' || v.trim().length === 0) {
53
+ missing.push(k);
54
+ }
55
+ else if (strict && k === 'kb_url' && !v.startsWith(base)) {
56
+ missing.push(`${k} (host 前缀必须为 ${base})`);
57
+ }
58
+ else if (strict && k === 'page_path' && !isAllowedWikiPagePath(v)) {
59
+ missing.push(`${k} (必须为 projects/checklists/、projects/ch_*/t_*.md 或 projects/ch_*/review/ 命名空间)`);
60
+ }
61
+ }
62
+ }
63
+ return missing;
64
+ }
65
+ export function hasRequiredDelivery(assignee, mode, handoff) {
66
+ return missingDeliveryKeys(assignee, mode, handoff).length === 0;
67
+ }
68
+ /** 对一组父任务 id 做交付契约校验,返回缺关键交付物的父卡清单(无缺失返回空数组)。 */
69
+ export function missingParentDelivery(state, parentIds) {
70
+ const out = [];
71
+ for (const pid of parentIds) {
72
+ const pt = state.tasks.get(pid);
73
+ if (!pt)
74
+ continue;
75
+ const missing = missingDeliveryKeys(pt.assignee, pt.mode, state.handoffs.get(pid));
76
+ if (missing.length > 0)
77
+ out.push({ taskId: pid, assignee: pt.assignee, mode: pt.mode, missing });
78
+ }
79
+ return out;
80
+ }
@@ -0,0 +1,12 @@
1
+ import type { Handoff } from './types.js';
2
+ /**
3
+ * R20 D(execute) 交付物证据判定(C1/C2 共用):
4
+ * 完成交接 metadata 必须带 changed_files(非空)+ (commit_hash | push) 至少其一,
5
+ * 才算具备"实际执行并产出 git 产物"的证据。无证据 → 不判链完成 / kanban_complete 拒绝。
6
+ *
7
+ * changed_files:string[] 或非空字符串(分支/README/代码变更清单)。
8
+ * commit_hash:提交哈希字符串;push:布尔或描述串(已推送)。
9
+ */
10
+ export declare function hasDeliveryEvidence(handoff: Handoff | undefined): boolean;
11
+ /** D(execute) 的 TDD 声明(2026-08-26):tdd 必须存在且结构合法——test_files 与 skipped 二选一(XOR)。 */
12
+ export declare function hasTddDeclaration(handoff: Handoff | undefined): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * R20 D(execute) 交付物证据判定(C1/C2 共用):
3
+ * 完成交接 metadata 必须带 changed_files(非空)+ (commit_hash | push) 至少其一,
4
+ * 才算具备"实际执行并产出 git 产物"的证据。无证据 → 不判链完成 / kanban_complete 拒绝。
5
+ *
6
+ * changed_files:string[] 或非空字符串(分支/README/代码变更清单)。
7
+ * commit_hash:提交哈希字符串;push:布尔或描述串(已推送)。
8
+ */
9
+ export function hasDeliveryEvidence(handoff) {
10
+ if (!handoff)
11
+ return false;
12
+ const m = handoff.metadata ?? {};
13
+ const changed = m['changed_files'];
14
+ const changedOk = Array.isArray(changed)
15
+ ? changed.length > 0
16
+ : typeof changed === 'string' && changed.trim().length > 0;
17
+ if (!changedOk)
18
+ return false;
19
+ const commit = m['commit_hash'];
20
+ const commitOk = typeof commit === 'string' && commit.trim().length > 0;
21
+ const push = m['push'];
22
+ const pushOk = push === true || (typeof push === 'string' && push.trim().length > 0);
23
+ return commitOk || pushOk;
24
+ }
25
+ /** D(execute) 的 TDD 声明(2026-08-26):tdd 必须存在且结构合法——test_files 与 skipped 二选一(XOR)。 */
26
+ export function hasTddDeclaration(handoff) {
27
+ if (!handoff)
28
+ return false;
29
+ const tdd = (handoff.metadata ?? {})['tdd'];
30
+ if (!tdd || typeof tdd !== 'object')
31
+ return false;
32
+ const hasSkipped = typeof tdd.skipped === 'object' && tdd.skipped !== null;
33
+ const hasFiles = Array.isArray(tdd.test_files) && tdd.test_files.length > 0;
34
+ if (hasSkipped === hasFiles)
35
+ return false; // 二选一(XOR):同时存在或都缺 → 非法
36
+ if (hasSkipped) {
37
+ const skipped = tdd.skipped;
38
+ return typeof skipped['reason'] === 'string' && skipped['reason'].trim().length > 0;
39
+ }
40
+ return true;
41
+ }
@@ -0,0 +1,20 @@
1
+ import type { KanbanEvent } from './types.js';
2
+ export interface EventStore {
3
+ append(ev: Omit<KanbanEvent, 'seq'>): Promise<KanbanEvent>;
4
+ readAll(): Promise<KanbanEvent[]>;
5
+ readAllSync(): KanbanEvent[];
6
+ readSince(seq: number): Promise<KanbanEvent[]>;
7
+ /** 物理移除匹配事件行并重排 seq(整链硬删除用;不可恢复)。 */
8
+ purge?(predicate: (ev: KanbanEvent) => boolean): Promise<number>;
9
+ }
10
+ /** JSONL 事件存储:追加即持久化;重启回放由 readAll 提供。 */
11
+ export declare class FileEventStore implements EventStore {
12
+ private readonly file;
13
+ private seq;
14
+ constructor(dir: string);
15
+ append(ev: Omit<KanbanEvent, 'seq'>): Promise<KanbanEvent>;
16
+ readAll(): Promise<KanbanEvent[]>;
17
+ readAllSync(): KanbanEvent[];
18
+ readSince(seq: number): Promise<KanbanEvent[]>;
19
+ purge(predicate: (ev: KanbanEvent) => boolean): Promise<number>;
20
+ }
@@ -0,0 +1,53 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** JSONL 事件存储:追加即持久化;重启回放由 readAll 提供。 */
4
+ export class FileEventStore {
5
+ file;
6
+ seq = 0;
7
+ constructor(dir) {
8
+ mkdirSync(dir, { recursive: true });
9
+ this.file = join(dir, 'events.jsonl');
10
+ try {
11
+ const lines = readFileSync(this.file, 'utf8').trim().split('\n').filter(Boolean);
12
+ if (lines.length > 0) {
13
+ this.seq = JSON.parse(lines[lines.length - 1]).seq + 1;
14
+ }
15
+ }
16
+ catch {
17
+ this.seq = 0; // 文件不存在或为空
18
+ }
19
+ }
20
+ async append(ev) {
21
+ // P1-8:seq 每次从文件尾行重读分配(跨进程安全,多 dsh 实例并发追加时 seq 唯一);
22
+ // appendFileSync 单行 JSONL 追加在 POSIX 上原子(行 < 4KB,无锁无残留)。
23
+ const all = this.readAllSync();
24
+ const seq = all.length > 0 ? all[all.length - 1].seq + 1 : 0;
25
+ const full = { ...ev, seq };
26
+ const line = JSON.stringify(full) + '\n';
27
+ appendFileSync(this.file, line);
28
+ return full;
29
+ }
30
+ async readAll() {
31
+ return this.readAllSync();
32
+ }
33
+ readAllSync() {
34
+ try {
35
+ const text = readFileSync(this.file, 'utf8');
36
+ return text.split('\n').filter(Boolean).map((l) => JSON.parse(l));
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
42
+ async readSince(seq) {
43
+ const all = await this.readAll();
44
+ return all.filter((e) => e.seq >= seq);
45
+ }
46
+ async purge(predicate) {
47
+ const all = this.readAllSync();
48
+ const kept = all.filter((e) => !predicate(e)).map((e, i) => ({ ...e, seq: i }));
49
+ writeFileSync(this.file, kept.map((e) => JSON.stringify(e)).join('\n') + (kept.length > 0 ? '\n' : ''));
50
+ this.seq = kept.length;
51
+ return all.length - kept.length;
52
+ }
53
+ }
@@ -0,0 +1,94 @@
1
+ import type { EventStore } from './event-store.js';
2
+ import { type Actor } from './permissions.js';
3
+ import type { AuditEvidence, BoardState, Chain, Handoff, KanbanEvent, SpecCard, SpecCardAttachment, SpecCardSections, Task, TaskMode, Role, ReviewEvidence } from './types.js';
4
+ export type KanbanListener = (event: KanbanEvent) => void;
5
+ /** 首句:trim 后按首个换行或「。?! 」截断;超长兜底 40 字(T7 需求标题规范化)。 */
6
+ export declare function firstSentence(text: string): string;
7
+ /** 默认链标题:【需求】<一句话需求描述>。来源优先级 /plan: rest 首句 → checklist.problem 首句 → 未命名需求。 */
8
+ export declare function buildChainTitle(requirementName: string | null, _openspecRest: string, problem: string): string;
9
+ /** 看板领域门面:三界面(工具/CLI/UI)统一路由的唯一入口。 */
10
+ export declare class KanbanService {
11
+ private state;
12
+ private readonly store;
13
+ private readonly kbUrlBase;
14
+ private emitQueue;
15
+ private readonly listeners;
16
+ private onChainCompletedHook;
17
+ private onTaskCompletedHook;
18
+ constructor(store: EventStore, kbUrlBase?: string);
19
+ private emit;
20
+ /** D23:注入链完成核对钩子(由调度层设置;仅一个消费者)。 */
21
+ setOnChainCompleted(hook: (chainId: string) => void | Promise<void>): void;
22
+ /** Q3&5:注入任务完成互链登记钩子(由调度层设置;仅一个消费者)。 */
23
+ setOnTaskCompleted(hook: (taskId: string) => void | Promise<void>): void;
24
+ /** T22:订阅持久化后的看板事件;返回解除订阅函数。listener 异常不影响已落盘状态。 */
25
+ subscribe(listener: KanbanListener): () => void;
26
+ /** T22:返回 seq >= 入参 的事件(与 EventStore.readSince 同为 inclusive 语义)。 */
27
+ eventsSince(seq: number): Promise<KanbanEvent[]>;
28
+ private publish;
29
+ private chainOf;
30
+ private taskOf;
31
+ createChain(input: {
32
+ title: string;
33
+ ownerSessionId: string;
34
+ workspaceDir?: string | null;
35
+ }, actor: Actor): Promise<Chain>;
36
+ createSpecCard(chainId: string, sections: SpecCardSections, actor: Actor): Promise<SpecCard>;
37
+ editSpecCard(cardId: string, sections: SpecCardSections, actor: Actor): Promise<SpecCard>;
38
+ approveSpecCard(cardId: string, actor: Actor): Promise<SpecCard>;
39
+ createTask(input: {
40
+ chainId: string;
41
+ title: string;
42
+ body?: string;
43
+ assignee: Role;
44
+ mode: TaskMode;
45
+ parents?: string[];
46
+ reviewAttempt?: number;
47
+ }, actor: Actor): Promise<Task>;
48
+ claimTask(taskId: string, actor: Actor): Promise<Task>;
49
+ completeTask(taskId: string, handoff: Handoff, actor: Actor, opts?: {
50
+ boundTaskId?: string;
51
+ }): Promise<Task>;
52
+ /** D23:链完成验收核对发警告(仅 system/dispatcher 可发)。Chain 状态保持 completed。 */
53
+ auditWarning(chainId: string, evidence: AuditEvidence[], actor: Actor): Promise<KanbanEvent>;
54
+ /** D23:用户确认产物归属(仅 human,GUI confirm-audit action)。放行最终汇报。 */
55
+ confirmAudit(chainId: string, actor: Actor): Promise<KanbanEvent>;
56
+ /** T7:链标题改名(仅 human,GUI)。发 chain/title-updated 事件(非状态转换)。 */
57
+ updateChainTitle(chainId: string, title: string, actor: Actor): Promise<Chain>;
58
+ /** 整链硬删除(含其下全部角色卡/规格卡事件;仅 human,GUI 二次确认)。物理 purge 事件行,不可恢复。 */
59
+ deleteChain(chainId: string, actor: Actor): Promise<void>;
60
+ renameTask(taskId: string, title: string, actor: Actor): Promise<Task>;
61
+ blockTask(taskId: string, reason: string, actor: Actor, opts?: {
62
+ boundTaskId?: string;
63
+ }): Promise<Task>;
64
+ unblockTask(taskId: string, actor: Actor): Promise<Task>;
65
+ heartbeat(taskId: string, actor: Actor, opts?: {
66
+ boundTaskId?: string;
67
+ }): Promise<Task>;
68
+ /** 标记任务失败(runner 异常/心跳超时回收);投影递增 attempts(infra 瞬时错误不计数)。重试由调度器重派。 */
69
+ failTask(taskId: string, reason: string, actor: Actor, opts?: {
70
+ infra?: boolean;
71
+ }): Promise<Task>;
72
+ comment(taskId: string, body: string, actor: Actor): Promise<KanbanEvent>;
73
+ archiveTask(taskId: string, actor: Actor): Promise<Task>;
74
+ /** T10.5:仅 draft 规格卡可挂附件(V 挂清单附件(/openspec: 建链)/ human GUI 上传)。 */
75
+ addSpecCardAttachment(cardId: string, attachment: SpecCardAttachment, actor: Actor): Promise<SpecCard>;
76
+ /** 评审事件(交付质量链):recordReview 记录评审卡结论并更新被评审任务 reviewStatus。
77
+ * actor 必须 system(V/角色不可伪造评审结论);verdict=pass → review/passed,否则 review/failed。
78
+ * 投影(projection.ts)据事件更新 target.reviewStatus(passed/failed)。 */
79
+ recordReview(reviewTaskId: string, targetTaskId: string, evidence: ReviewEvidence, actor: Actor): Promise<KanbanEvent>;
80
+ /** 评审超限放弃:review/gave-up(含证据链信息)。仅 system。 */
81
+ reviewGaveUp(reviewTaskId: string, targetTaskId: string, reason: string, actor: Actor): Promise<KanbanEvent>;
82
+ /** 评审失败返工卡创建(评审失败闭环):原任务保持 done(不可变),新建返工卡继承 rework 字段。
83
+ * 仅 system(can('create-rework-task')=system);V 建执行卡、system 建返工卡。 */
84
+ createReworkTask(input: {
85
+ sourceTaskId: string;
86
+ reviewTaskId: string;
87
+ reason: string;
88
+ }, actor: Actor): Promise<Task>;
89
+ snapshot(): Promise<BoardState>;
90
+ listTasks(opts?: {
91
+ assignee?: Role;
92
+ status?: Task['status'];
93
+ }): Promise<Task[]>;
94
+ }