@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,229 @@
1
+ import type { AuditEvidence, BoardState, Chain, Task } from '../src/domain/types.js';
2
+
3
+ export type WorkflowLineState = 'complete' | 'active' | 'pending' | 'blocked';
4
+
5
+ /** 链路状态筛选(多选,取并集):执行中/阻塞/失败/已完成(含归档)。空集合=默认视图(隐藏归档链路)。 */
6
+ export type ChainFilter = 'executing' | 'blocked' | 'failed' | 'completed';
7
+
8
+ export const CHAIN_FILTERS: readonly ChainFilter[] = ['executing', 'blocked', 'failed', 'completed'];
9
+
10
+ export const CHAIN_FILTER_LABEL: Record<ChainFilter, string> = {
11
+ executing: '执行中',
12
+ blocked: '阻塞',
13
+ failed: '失败',
14
+ completed: '已完成',
15
+ };
16
+
17
+ /** 链路派生状态(供状态筛选/排序复用)。 */
18
+ export interface ChainFilterState {
19
+ executing: boolean;
20
+ blocked: boolean;
21
+ failed: boolean;
22
+ completed: boolean;
23
+ archived: boolean;
24
+ }
25
+
26
+ export function chainFilterStateOf(chain: Chain, chainTasks: Task[]): ChainFilterState {
27
+ const archived = chain.status === 'aborted' || (chainTasks.length > 0 && chainTasks.every((t) => t.status === 'archived'));
28
+ // 执行中 不含含 blocked/failed 任务的链(与排序阶梯一致:阻塞→运行中→规划中→已完成),保证筛选互斥直观
29
+ const blocked = chainTasks.some((t) => t.status === 'blocked');
30
+ const failed = chainTasks.some((t) => t.status === 'failed');
31
+ return {
32
+ executing: chain.status === 'executing' && !blocked && !failed,
33
+ blocked,
34
+ failed,
35
+ completed: chain.status === 'completed' || chain.status === 'aborted' || archived,
36
+ archived,
37
+ };
38
+ }
39
+
40
+ export interface TaskCardView {
41
+ task: Task;
42
+ phase: string;
43
+ statusLabel: string;
44
+ activityLabel: string;
45
+ dependencyLabel: string;
46
+ lineState: WorkflowLineState;
47
+ selected: boolean;
48
+ related: boolean;
49
+ }
50
+
51
+ export interface ChainWorkflowView {
52
+ chain: Chain;
53
+ tasks: TaskCardView[];
54
+ blockedSummary: string | null;
55
+ /** D23:链完成验收核对视图(completed 链若存在未确认的 audit-warning,UI 阻塞最终汇报)。 */
56
+ audit: { warned: boolean; confirmed: boolean; evidenceCount: number; evidence: AuditEvidence[] } | null;
57
+ sortRank: number;
58
+ lastActivityAt: number;
59
+ }
60
+
61
+ const STATUS_LABEL: Record<Task['status'], string> = {
62
+ triage: '分诊',
63
+ todo: '待办',
64
+ ready: '就绪',
65
+ running: '执行中',
66
+ blocked: '受阻',
67
+ done: '完成',
68
+ failed: '失败',
69
+ archived: '已归档',
70
+ };
71
+
72
+ export function statusLabelOf(status: Task['status']): string {
73
+ return STATUS_LABEL[status];
74
+ }
75
+
76
+ export function phaseOf(task: Task, ordered: Task[]): string {
77
+ if (task.assignee === 'p') return 'P';
78
+ if (task.assignee === 'd') return 'D';
79
+ if (task.assignee === 'w' && task.mode === 'kb') {
80
+ const dIndex = ordered.findIndex((value) => value.assignee === 'd');
81
+ return dIndex >= 0 && ordered.indexOf(task) > dIndex ? 'W3' : 'W2';
82
+ }
83
+ return task.assignee.toUpperCase();
84
+ }
85
+
86
+ export function lineStateOf(task: Task, selectedTaskId: string | null): WorkflowLineState {
87
+ if (task.status === 'blocked' || task.status === 'failed') return 'blocked';
88
+ if (task.id === selectedTaskId || task.status === 'running') return 'active';
89
+ if (task.status === 'done' || task.status === 'archived') return 'complete';
90
+ return 'pending';
91
+ }
92
+
93
+ function taskOrder(tasks: Task[], state: BoardState): Task[] {
94
+ const seq = new Map<string, number>();
95
+ for (const ev of state.events) {
96
+ if (ev.kind === 'task/created' && ev.taskId) seq.set(ev.taskId, ev.seq);
97
+ }
98
+ return [...tasks].sort((a, b) => {
99
+ const aSeq = seq.get(a.id) ?? Number.MAX_SAFE_INTEGER;
100
+ const bSeq = seq.get(b.id) ?? Number.MAX_SAFE_INTEGER;
101
+ return aSeq - bSeq || a.id.localeCompare(b.id);
102
+ });
103
+ }
104
+
105
+ function activityLabel(task: Task, state: BoardState, now: number): string {
106
+ let lastAt = 0;
107
+ for (const h of task.heartbeats) lastAt = Math.max(lastAt, h);
108
+ for (const ev of state.events) {
109
+ if (ev.taskId === task.id) lastAt = Math.max(lastAt, ev.at);
110
+ }
111
+ if (!lastAt) return '待启动';
112
+ const diff = Math.max(0, now - lastAt);
113
+ if (diff < 60_000) return '刚刚';
114
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;
115
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`;
116
+ return `${Math.floor(diff / 86_400_000)}d`;
117
+ }
118
+
119
+ function blockedSummary(chainId: string, state: BoardState): string | null {
120
+ let latest: string | null = null;
121
+ let latestSeq = -1;
122
+ for (const ev of state.events) {
123
+ if (ev.chainId !== chainId) continue;
124
+ if (ev.kind === 'task/blocked' || ev.kind === 'task/failed') {
125
+ if (ev.seq > latestSeq) {
126
+ latestSeq = ev.seq;
127
+ latest = String(ev.payload['reason'] ?? '');
128
+ }
129
+ }
130
+ }
131
+ return latest;
132
+ }
133
+
134
+ function relatedIds(state: BoardState, chainId: string, selectedTaskId: string): Set<string> {
135
+ const set = new Set<string>();
136
+ const chainTasks = [...state.tasks.values()].filter((t) => t.chainId === chainId);
137
+ const byId = new Map(chainTasks.map((t) => [t.id, t]));
138
+ const childrenBy = new Map<string, string[]>();
139
+ for (const t of chainTasks) {
140
+ for (const p of t.parents) {
141
+ const list = childrenBy.get(p) ?? [];
142
+ list.push(t.id);
143
+ childrenBy.set(p, list);
144
+ }
145
+ }
146
+ if (!byId.has(selectedTaskId)) return set;
147
+ set.add(selectedTaskId);
148
+ let frontier = [selectedTaskId];
149
+ while (frontier.length > 0) {
150
+ const next: string[] = [];
151
+ for (const id of frontier) {
152
+ const t = byId.get(id);
153
+ if (!t) continue;
154
+ for (const p of t.parents) if (byId.has(p) && !set.has(p)) { set.add(p); next.push(p); }
155
+ for (const c of childrenBy.get(id) ?? []) if (byId.has(c) && !set.has(c)) { set.add(c); next.push(c); }
156
+ }
157
+ frontier = next;
158
+ }
159
+ return set;
160
+ }
161
+
162
+ function sortRankOf(chain: Chain, tasks: Task[]): number {
163
+ if (tasks.some((t) => t.status === 'blocked' || t.status === 'failed')) return 0;
164
+ if (chain.status === 'executing') return 1;
165
+ if (chain.status === 'planning') return 2;
166
+ return 3;
167
+ }
168
+
169
+ /** T25/T32:纯投影 view model。UI 只消费该结果,不复制领域状态机。 */
170
+ export function deriveWorkflowBoard(
171
+ state: BoardState,
172
+ opts: { selectedTaskId: string | null; now: number; statusFilter?: ReadonlySet<ChainFilter> },
173
+ ): ChainWorkflowView[] {
174
+ const filter = opts.statusFilter ?? new Set<ChainFilter>();
175
+ const views: ChainWorkflowView[] = [];
176
+ for (const chain of state.chains.values()) {
177
+ const chainTasks = [...state.tasks.values()].filter((t) => t.chainId === chain.id);
178
+ // 状态筛选:多选并集;空集合=默认视图(隐藏归档/中止链路)
179
+ const st = chainFilterStateOf(chain, chainTasks);
180
+ const archived = st.archived;
181
+ const matchesFilter =
182
+ (filter.has('executing') && st.executing) ||
183
+ (filter.has('blocked') && st.blocked) ||
184
+ (filter.has('failed') && st.failed) ||
185
+ (filter.has('completed') && st.completed);
186
+ // D17:选中任务所在链路即使整链归档也保留在活动视图,详情只读直到返回列表后移除
187
+ const selectedInChain = opts.selectedTaskId != null && chainTasks.some((t) => t.id === opts.selectedTaskId);
188
+ if (selectedInChain) { /* 选中链路恒可见 */ }
189
+ else if (filter.size > 0) { if (!matchesFilter) continue; }
190
+ else if (archived) continue;
191
+ const ordered = taskOrder(chainTasks, state);
192
+ const related = opts.selectedTaskId ? relatedIds(state, chain.id, opts.selectedTaskId) : new Set<string>();
193
+ let lastActivityAt = chain.createdAt;
194
+ for (const ev of state.events) {
195
+ if (ev.chainId === chain.id) lastActivityAt = Math.max(lastActivityAt, ev.at);
196
+ }
197
+ const auditRec = state.auditWarnings.get(chain.id);
198
+ views.push({
199
+ chain,
200
+ sortRank: sortRankOf(chain, chainTasks),
201
+ lastActivityAt,
202
+ blockedSummary: blockedSummary(chain.id, state),
203
+ audit: auditRec
204
+ ? {
205
+ warned: true,
206
+ confirmed: auditRec.confirmedAt !== null,
207
+ evidenceCount: auditRec.evidence.length,
208
+ evidence: auditRec.evidence,
209
+ }
210
+ : null,
211
+ tasks: ordered.map((task) => ({
212
+ task,
213
+ phase: phaseOf(task, ordered),
214
+ statusLabel: statusLabelOf(task.status),
215
+ activityLabel: activityLabel(task, state, opts.now),
216
+ // D15/D17:阻塞/失败任务优先展示阻塞原因,其余展示父依赖
217
+ dependencyLabel: task.status === 'blocked' || task.status === 'failed'
218
+ ? blockedSummary(chain.id, state) ?? ''
219
+ : task.parents.map((id) => state.tasks.get(id)?.title ?? id).join(', '),
220
+ lineState: lineStateOf(task, opts.selectedTaskId),
221
+ selected: task.id === opts.selectedTaskId,
222
+ related: opts.selectedTaskId === task.id || related.has(task.id),
223
+ })),
224
+ });
225
+ }
226
+
227
+ views.sort((a, b) => a.sortRank - b.sortRank || b.lastActivityAt - a.lastActivityAt);
228
+ return views;
229
+ }
@@ -0,0 +1,88 @@
1
+ # dsh-swarm bundle patch: kanban service row.
2
+ # Applied by the Loader on top of the deployment's own patch layers.
3
+ #
4
+ # The `kanban` row mounts the node half (src/index.ts apply) with its config.
5
+ # The browser half is auto-discovered: this package declares
6
+ # `dsh.client.platform: web` (package.json), so the client-modules node half
7
+ # composes it into window.__DSH_BOOT__ and serves /plugins/<id>/client.js from
8
+ # the package's ./client export (the built lib/client.js bundle).
9
+ #
10
+ # Personas: the V/P/W/D role texts ship in personas/persona-{v,p,w,d}.md (canonical source)
11
+ # and are wired into the runtime via the preset combos personas/kanban-{v,p,w,d}/agent.cordis.yml
12
+ # (their persona row carries the system-prompt text). The plugin installs the combos to
13
+ # $DSH_HOME/.agent-presets/<id>/ at apply time (src/roles/preset-installer.ts), then
14
+ # agentPresets.mount(agentCtx, 'kanban-<role>') is called in the agent factory setup
15
+ # (agent-runner.ts for P/W/D, v-orchestrator.ts for V).
16
+
17
+ - insert:
18
+ - id: kanban
19
+ name: '@joekytc/dsh-swarm'
20
+ config:
21
+ storageDir: !!js dshHomePath("storages/kanban")
22
+ wikiVault:
23
+ baseUrl: 'http://192.168.122.111:3000'
24
+ pagePrefix: 'projects/'
25
+ dispatcher:
26
+ staleTimeoutSeconds: 14400
27
+ maxRetries: 3
28
+ heartbeatIntervalSeconds: 300
29
+ # 角色模型候选链(Task 12):逐角色 primary + fallbacks,主模型不可用静默切换([model-fallback] 证据),
30
+ # 全候选不可用 → block(model-unavailable) 抛用户;reasoningEffort 缺省 high。部署实际值以本文件为准。
31
+ roles:
32
+ models:
33
+ v:
34
+ provider: 'ark'
35
+ model: 'deepseek-v4-flash'
36
+ reasoningEffort: 'high'
37
+ fallbacks:
38
+ - provider: 'openai'
39
+ model: 'gpt-5.6-sol'
40
+ reasoningEffort: 'high'
41
+ p:
42
+ provider: 'ark'
43
+ model: 'deepseek-v4-flash'
44
+ reasoningEffort: 'high'
45
+ fallbacks:
46
+ - provider: 'openai'
47
+ model: 'gpt-5.6-sol'
48
+ reasoningEffort: 'high'
49
+ w:
50
+ provider: 'ark'
51
+ model: 'deepseek-v4-flash'
52
+ reasoningEffort: 'high'
53
+ fallbacks:
54
+ - provider: 'openai'
55
+ model: 'gpt-5.6-sol'
56
+ reasoningEffort: 'high'
57
+ d:
58
+ provider: 'ark'
59
+ model: 'deepseek-v4-flash'
60
+ reasoningEffort: 'high'
61
+ fallbacks:
62
+ - provider: 'openai'
63
+ model: 'gpt-5.6-sol'
64
+ reasoningEffort: 'high'
65
+ pt:
66
+ provider: 'ark'
67
+ model: 'deepseek-v4-flash'
68
+ reasoningEffort: 'high'
69
+ fallbacks:
70
+ - provider: 'openai'
71
+ model: 'gpt-5.6-sol'
72
+ reasoningEffort: 'high'
73
+ dt:
74
+ provider: 'ark'
75
+ model: 'deepseek-v4-flash'
76
+ reasoningEffort: 'high'
77
+ fallbacks:
78
+ - provider: 'openai'
79
+ model: 'gpt-5.6-sol'
80
+ reasoningEffort: 'high'
81
+ prefixRoutes:
82
+ plan: '/plan:'
83
+ openspec: '/openspec:'
84
+ ui:
85
+ enabled: true
86
+ contentMinWidth: 715
87
+ contentMaxWidth: 780
88
+ sseHeartbeatSeconds: 20