@joekytc/dsh-swarm 0.1.1 → 0.1.2
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/client/TaskDrawer.tsx +29 -5
- package/client/kanban.css +115 -0
- package/client/timeline-model.ts +201 -0
- package/lib/client.js +192 -12
- package/lib/config.d.ts +11 -3
- package/lib/config.js +12 -2
- package/lib/dispatcher/agent-runner.js +1 -1
- package/lib/dispatcher/v-orchestrator.js +1 -1
- package/lib/domain/delivery-contract.js +1 -1
- package/lib/domain/memory.d.ts +29 -0
- package/lib/domain/memory.js +145 -0
- package/lib/routes/planning-driver.d.ts +5 -6
- package/lib/routes/planning-driver.js +11 -7
- package/lib/routes/prefix-router.d.ts +12 -13
- package/lib/routes/prefix-router.js +30 -0
- package/lib/tools/main-session-tools.js +35 -15
- package/lib/tools/planning-tools.d.ts +5 -0
- package/lib/tools/planning-tools.js +91 -2
- package/lib/wiki/memory-recall.d.ts +16 -0
- package/lib/wiki/memory-recall.js +58 -0
- package/lib/wiki/page-path.d.ts +3 -0
- package/lib/wiki/page-path.js +8 -3
- package/lib/wiki/wiki-vault-client.d.ts +1 -0
- package/package.json +1 -1
- package/personas/kanban-dt/agent.cordis.yml +1 -1
package/client/TaskDrawer.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import type { Chain, Handoff, KanbanEvent, SpecCard, Task } from '../src/domain/types.js';
|
|
3
3
|
import { statusLabelOf } from './workflow-model.js';
|
|
4
|
+
import { foldTimeline } from './timeline-model.js';
|
|
4
5
|
|
|
5
6
|
const ROLE_NAME: Record<Task['assignee'], string> = {
|
|
6
7
|
v: 'orchestrator', p: 'planner', w: 'wiki-bridge', d: 'fullstack-dev', pt: 'plan-review', dt: 'impl-review',
|
|
@@ -138,11 +139,34 @@ export function TaskDrawer(props: {
|
|
|
138
139
|
)}
|
|
139
140
|
{tab === 'timeline' && (
|
|
140
141
|
<section role="tabpanel">
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
{(() => {
|
|
143
|
+
const items = foldTimeline(timeline);
|
|
144
|
+
return (
|
|
145
|
+
<ol className="dsh-kb-timeline">
|
|
146
|
+
{items.map((item, i) => (
|
|
147
|
+
<li
|
|
148
|
+
key={item.seq}
|
|
149
|
+
className={`dsh-kb-timeline__item dsh-kb-timeline__item--${item.status}${i === 0 ? ' dsh-kb-timeline__item--latest' : ''}`}
|
|
150
|
+
data-exception={item.exception || undefined}
|
|
151
|
+
>
|
|
152
|
+
<div className="dsh-kb-timeline__axis">
|
|
153
|
+
<span className="dsh-kb-timeline__dot" aria-hidden="true" />
|
|
154
|
+
</div>
|
|
155
|
+
<div className="dsh-kb-timeline__body">
|
|
156
|
+
<div className="dsh-kb-timeline__row">
|
|
157
|
+
<strong className="dsh-kb-timeline__label">
|
|
158
|
+
{item.count ? `${item.label}(${item.count} 次)` : item.label}
|
|
159
|
+
</strong>
|
|
160
|
+
<time dateTime={new Date(item.at).toISOString()}>{formatTime(item.at)}</time>
|
|
161
|
+
</div>
|
|
162
|
+
{item.summary && <p className="dsh-kb-timeline__summary" title={item.summary}>{item.summary}</p>}
|
|
163
|
+
<span className="dsh-kb-timeline__author">{item.author}</span>
|
|
164
|
+
</div>
|
|
165
|
+
</li>
|
|
166
|
+
))}
|
|
167
|
+
</ol>
|
|
168
|
+
);
|
|
169
|
+
})()}
|
|
146
170
|
</section>
|
|
147
171
|
)}
|
|
148
172
|
{tab === 'handoff' && (
|
package/client/kanban.css
CHANGED
|
@@ -511,6 +511,121 @@
|
|
|
511
511
|
.dsh-kb-detail dd { margin: 0; }
|
|
512
512
|
.dsh-kb-detail time { opacity: 0.6; font-size: 12px; margin-inline-end: 4px; }
|
|
513
513
|
|
|
514
|
+
/* 轨迹 tab 可读化:物流式纵向时间轴(四态配色 + 最新节点高亮;异常整行标红) */
|
|
515
|
+
.dsh-kb-timeline {
|
|
516
|
+
list-style: none;
|
|
517
|
+
margin: 0;
|
|
518
|
+
padding: 0;
|
|
519
|
+
display: flex;
|
|
520
|
+
flex-direction: column;
|
|
521
|
+
}
|
|
522
|
+
.dsh-kb-timeline__item {
|
|
523
|
+
position: relative;
|
|
524
|
+
display: grid;
|
|
525
|
+
grid-template-columns: 16px 1fr;
|
|
526
|
+
gap: 8px;
|
|
527
|
+
padding: 0 0 14px;
|
|
528
|
+
}
|
|
529
|
+
.dsh-kb-timeline__item:last-child {
|
|
530
|
+
padding-bottom: 0;
|
|
531
|
+
}
|
|
532
|
+
/* 纵向时间线:贯穿节点(最新节点上方线不延伸,模拟物流头节点) */
|
|
533
|
+
.dsh-kb-timeline__axis {
|
|
534
|
+
position: relative;
|
|
535
|
+
}
|
|
536
|
+
.dsh-kb-timeline__item:not(:last-child) .dsh-kb-timeline__axis::after {
|
|
537
|
+
content: '';
|
|
538
|
+
position: absolute;
|
|
539
|
+
left: 50%;
|
|
540
|
+
top: 14px;
|
|
541
|
+
bottom: -4px;
|
|
542
|
+
width: 2px;
|
|
543
|
+
transform: translateX(-50%);
|
|
544
|
+
background: var(--dsh-kb-border);
|
|
545
|
+
}
|
|
546
|
+
.dsh-kb-timeline__dot {
|
|
547
|
+
position: relative;
|
|
548
|
+
z-index: 1;
|
|
549
|
+
display: block;
|
|
550
|
+
width: 10px;
|
|
551
|
+
height: 10px;
|
|
552
|
+
/* 水平居中于 16px 轴列(中心=8px),与 left:50% 连接线对齐 */
|
|
553
|
+
margin: 2px auto 0;
|
|
554
|
+
border-radius: 50%;
|
|
555
|
+
border: 2px solid var(--dsh-kb-surface);
|
|
556
|
+
box-sizing: border-box;
|
|
557
|
+
background: var(--dsh-kb-text-tertiary);
|
|
558
|
+
}
|
|
559
|
+
/* 四态配色:neutral 灰 / running 蓝 / success 绿 / exception 红 */
|
|
560
|
+
.dsh-kb-timeline__item--running .dsh-kb-timeline__dot {
|
|
561
|
+
background: var(--dsh-kb-active);
|
|
562
|
+
}
|
|
563
|
+
.dsh-kb-timeline__item--success .dsh-kb-timeline__dot {
|
|
564
|
+
background: var(--dsh-kb-complete);
|
|
565
|
+
}
|
|
566
|
+
.dsh-kb-timeline__item--exception .dsh-kb-timeline__dot {
|
|
567
|
+
background: var(--dsh-kb-blocked);
|
|
568
|
+
}
|
|
569
|
+
/* 最新节点高亮:主色圆点放大 + 柔和辉光 */
|
|
570
|
+
.dsh-kb-timeline__item--latest .dsh-kb-timeline__dot {
|
|
571
|
+
width: 12px;
|
|
572
|
+
height: 12px;
|
|
573
|
+
margin-top: 0;
|
|
574
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsh-kb-active) 22%, transparent);
|
|
575
|
+
}
|
|
576
|
+
.dsh-kb-timeline__item--latest.dsh-kb-timeline__item--success .dsh-kb-timeline__dot {
|
|
577
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsh-kb-complete) 22%, transparent);
|
|
578
|
+
}
|
|
579
|
+
.dsh-kb-timeline__item--latest.dsh-kb-timeline__item--exception .dsh-kb-timeline__dot {
|
|
580
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsh-kb-blocked) 22%, transparent);
|
|
581
|
+
}
|
|
582
|
+
/* 异常行:浅红底 + 左侧红条,摘要红加粗 */
|
|
583
|
+
.dsh-kb-timeline__item--exception .dsh-kb-timeline__body {
|
|
584
|
+
background: color-mix(in srgb, var(--dsh-kb-blocked) 8%, transparent);
|
|
585
|
+
border-left: 3px solid var(--dsh-kb-blocked);
|
|
586
|
+
border-radius: var(--dsh-kb-radius-sm);
|
|
587
|
+
padding: 6px 8px;
|
|
588
|
+
}
|
|
589
|
+
.dsh-kb-timeline__body {
|
|
590
|
+
min-width: 0;
|
|
591
|
+
}
|
|
592
|
+
.dsh-kb-timeline__row {
|
|
593
|
+
display: flex;
|
|
594
|
+
align-items: baseline;
|
|
595
|
+
gap: 8px;
|
|
596
|
+
flex-wrap: wrap;
|
|
597
|
+
}
|
|
598
|
+
.dsh-kb-timeline__label {
|
|
599
|
+
font-size: 13px;
|
|
600
|
+
font-weight: 600;
|
|
601
|
+
color: var(--dsh-kb-text-primary);
|
|
602
|
+
}
|
|
603
|
+
.dsh-kb-timeline__item--success .dsh-kb-timeline__label {
|
|
604
|
+
color: var(--dsh-kb-complete);
|
|
605
|
+
}
|
|
606
|
+
.dsh-kb-timeline__item--exception .dsh-kb-timeline__label {
|
|
607
|
+
color: var(--dsh-kb-blocked);
|
|
608
|
+
}
|
|
609
|
+
.dsh-kb-timeline__summary {
|
|
610
|
+
margin: 2px 0 0;
|
|
611
|
+
font-size: 12px;
|
|
612
|
+
color: var(--dsh-kb-text-secondary);
|
|
613
|
+
overflow: hidden;
|
|
614
|
+
text-overflow: ellipsis;
|
|
615
|
+
white-space: nowrap;
|
|
616
|
+
}
|
|
617
|
+
.dsh-kb-timeline__item--exception .dsh-kb-timeline__summary {
|
|
618
|
+
color: var(--dsh-kb-blocked);
|
|
619
|
+
font-weight: 600;
|
|
620
|
+
}
|
|
621
|
+
.dsh-kb-timeline__author {
|
|
622
|
+
display: block;
|
|
623
|
+
margin-top: 2px;
|
|
624
|
+
font-size: 11px;
|
|
625
|
+
opacity: 0.7;
|
|
626
|
+
color: var(--dsh-kb-text-tertiary);
|
|
627
|
+
}
|
|
628
|
+
|
|
514
629
|
.dsh-kb-spec-attachments {
|
|
515
630
|
list-style: none;
|
|
516
631
|
margin: 4px 0;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// client/timeline-model.ts — 轨迹 tab 纯投影(TDD):事件 → 人类可读时间线条目。
|
|
2
|
+
// 只做展示层派生,不触碰领域事件结构。
|
|
3
|
+
import type { EventKind, KanbanEvent } from '../src/domain/types.js';
|
|
4
|
+
|
|
5
|
+
/** 轨迹单条目:心跳折叠后的事件 + 展示派生字段。 */
|
|
6
|
+
export interface TimelineItem {
|
|
7
|
+
seq: number;
|
|
8
|
+
kind: EventKind;
|
|
9
|
+
label: string;
|
|
10
|
+
author: string;
|
|
11
|
+
at: number;
|
|
12
|
+
/** 心跳折叠:连续心跳数量(非心跳条目为 undefined)。 */
|
|
13
|
+
count?: number;
|
|
14
|
+
/** 心跳折叠:末条心跳时间。 */
|
|
15
|
+
lastAt?: number;
|
|
16
|
+
/** 摘要(payload 关键字段),无可展示则为空串。 */
|
|
17
|
+
summary: string;
|
|
18
|
+
/** 异常事件(阻塞/失败/评审驳回/超限放弃/越权警告)醒目标红。 */
|
|
19
|
+
exception: boolean;
|
|
20
|
+
/** 状态语义(物流式四态配色)。 */
|
|
21
|
+
status: TimelineStatus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 节点状态语义:neutral 常规 / running 进行中 / success 成功 / exception 异常。 */
|
|
25
|
+
export type TimelineStatus = 'neutral' | 'running' | 'success' | 'exception';
|
|
26
|
+
|
|
27
|
+
/** 异常事件全集(红系高亮)。 */
|
|
28
|
+
const EXCEPTION_KINDS: ReadonlySet<EventKind> = new Set<EventKind>([
|
|
29
|
+
'task/blocked', 'task/failed', 'review/failed', 'review/gave-up', 'chain/audit-warning', 'chain/aborted',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** 四态状态映射:异常集优先,其余按成功/进行中/中性归类。 */
|
|
33
|
+
const STATUS_OF: Record<EventKind, TimelineStatus> = {
|
|
34
|
+
'chain/created': 'neutral',
|
|
35
|
+
'chain/executing': 'running',
|
|
36
|
+
'chain/completed': 'success',
|
|
37
|
+
'chain/aborted': 'exception',
|
|
38
|
+
'chain/root-task-set': 'neutral',
|
|
39
|
+
'chain/audit-warning': 'exception',
|
|
40
|
+
'chain/audit-confirmed': 'neutral',
|
|
41
|
+
'chain/title-updated': 'neutral',
|
|
42
|
+
'spec-card/created': 'neutral',
|
|
43
|
+
'spec-card/edited': 'neutral',
|
|
44
|
+
'spec-card/approved': 'success',
|
|
45
|
+
'task/created': 'neutral',
|
|
46
|
+
'task/claimed': 'running',
|
|
47
|
+
'task/heartbeat': 'running',
|
|
48
|
+
'task/commented': 'neutral',
|
|
49
|
+
'task/completed': 'success',
|
|
50
|
+
'task/blocked': 'exception',
|
|
51
|
+
'task/unblocked': 'neutral',
|
|
52
|
+
'task/archived': 'neutral',
|
|
53
|
+
'task/failed': 'exception',
|
|
54
|
+
'task/renamed': 'neutral',
|
|
55
|
+
'review/passed': 'success',
|
|
56
|
+
'review/failed': 'exception',
|
|
57
|
+
'review/gave-up': 'exception',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function timelineStatusOf(kind: EventKind): TimelineStatus {
|
|
61
|
+
return STATUS_OF[kind] ?? 'neutral';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** kind → 中文动作标签(缺省回退裸 kind)。 */
|
|
65
|
+
const KIND_LABEL: Record<EventKind, string> = {
|
|
66
|
+
'chain/created': '链路创建',
|
|
67
|
+
'chain/executing': '链路开始执行',
|
|
68
|
+
'chain/completed': '链路完成',
|
|
69
|
+
'chain/aborted': '链路中止',
|
|
70
|
+
'chain/root-task-set': '设为根任务',
|
|
71
|
+
'chain/audit-warning': '越权警告',
|
|
72
|
+
'chain/audit-confirmed': '越权已确认',
|
|
73
|
+
'chain/title-updated': '链路改名',
|
|
74
|
+
'spec-card/created': '规格卡创建',
|
|
75
|
+
'spec-card/edited': '规格卡编辑',
|
|
76
|
+
'spec-card/approved': '规格卡批准',
|
|
77
|
+
'task/created': '任务创建',
|
|
78
|
+
'task/claimed': '任务认领',
|
|
79
|
+
'task/heartbeat': '任务心跳',
|
|
80
|
+
'task/commented': '评论',
|
|
81
|
+
'task/completed': '任务完成',
|
|
82
|
+
'task/blocked': '任务阻塞',
|
|
83
|
+
'task/unblocked': '解除阻塞',
|
|
84
|
+
'task/archived': '任务归档',
|
|
85
|
+
'task/failed': '任务失败',
|
|
86
|
+
'task/renamed': '任务改名',
|
|
87
|
+
'review/passed': '评审通过',
|
|
88
|
+
'review/failed': '评审驳回',
|
|
89
|
+
'review/gave-up': '评审超限放弃',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** author id → 友好名(复用 ROLE_NAME 语义,system/human 另映射)。 */
|
|
93
|
+
const AUTHOR_NAME: Record<string, string> = {
|
|
94
|
+
v: 'orchestrator', p: 'planner', w: 'wiki-bridge', d: 'fullstack-dev',
|
|
95
|
+
pt: 'plan-review', dt: 'impl-review', system: '系统', human: '你',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function eventLabelOf(kind: EventKind): string {
|
|
99
|
+
return KIND_LABEL[kind] ?? kind;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isExceptionEvent(e: KanbanEvent): boolean {
|
|
103
|
+
return EXCEPTION_KINDS.has(e.kind);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function authorNameOf(author: string): string {
|
|
107
|
+
return AUTHOR_NAME[author] ?? author;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
111
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 取字符串字段,缺失/非字符串返回 ''(优雅降级)。 */
|
|
115
|
+
function strField(rec: Record<string, unknown>, key: string): string {
|
|
116
|
+
const v = rec[key];
|
|
117
|
+
return typeof v === 'string' ? v : '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 截断长文本(>120 字),不截断短文本。 */
|
|
121
|
+
function truncate(text: string, max = 120): string {
|
|
122
|
+
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 提取事件 payload 的展示摘要(按 kind 分支,缺失字段降级为空)。 */
|
|
126
|
+
export function eventSummary(e: KanbanEvent): string {
|
|
127
|
+
const payload = asRecord(e.payload);
|
|
128
|
+
switch (e.kind) {
|
|
129
|
+
case 'task/completed': {
|
|
130
|
+
const summary = truncate(strField(payload, 'summary'));
|
|
131
|
+
return summary;
|
|
132
|
+
}
|
|
133
|
+
case 'task/blocked':
|
|
134
|
+
case 'task/failed':
|
|
135
|
+
return truncate(strField(payload, 'reason'));
|
|
136
|
+
case 'task/commented':
|
|
137
|
+
return truncate(strField(payload, 'body'));
|
|
138
|
+
case 'task/renamed':
|
|
139
|
+
case 'chain/title-updated': {
|
|
140
|
+
const from = strField(payload, 'from');
|
|
141
|
+
const to = strField(payload, 'to');
|
|
142
|
+
return from || to ? `${from} → ${to}` : '';
|
|
143
|
+
}
|
|
144
|
+
case 'review/passed':
|
|
145
|
+
case 'review/failed':
|
|
146
|
+
case 'review/gave-up': {
|
|
147
|
+
const evidence = asRecord(payload['evidence']);
|
|
148
|
+
const verdict = strField(evidence, 'verdict') || strField(payload, 'verdict');
|
|
149
|
+
const issues = Array.isArray(evidence['issues']) ? (evidence['issues'] as unknown[]).length : undefined;
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
if (verdict) parts.push(`verdict: ${verdict}`);
|
|
152
|
+
if (issues !== undefined) parts.push(`issues: ${issues}`);
|
|
153
|
+
const reason = strField(payload, 'reason');
|
|
154
|
+
if (reason) parts.push(reason);
|
|
155
|
+
return truncate(parts.join(' · '));
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 相邻连续心跳折叠为一条(count + lastAt),其余事件原样透传;最新在上(seq 降序)。 */
|
|
163
|
+
export function foldTimeline(events: KanbanEvent[]): TimelineItem[] {
|
|
164
|
+
const sorted = [...events].sort((a, b) => b.seq - a.seq);
|
|
165
|
+
const items: TimelineItem[] = [];
|
|
166
|
+
for (const e of sorted) {
|
|
167
|
+
if (e.kind !== 'task/heartbeat') {
|
|
168
|
+
items.push({
|
|
169
|
+
seq: e.seq,
|
|
170
|
+
kind: e.kind,
|
|
171
|
+
label: eventLabelOf(e.kind),
|
|
172
|
+
author: authorNameOf(e.author),
|
|
173
|
+
at: e.at,
|
|
174
|
+
summary: eventSummary(e),
|
|
175
|
+
exception: isExceptionEvent(e),
|
|
176
|
+
status: timelineStatusOf(e.kind),
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// 与上一条(seq 更大、更晚)同为心跳 → 折叠计数
|
|
181
|
+
const prev = items[items.length - 1];
|
|
182
|
+
if (prev && prev.kind === 'task/heartbeat') {
|
|
183
|
+
prev.count = (prev.count ?? 1) + 1;
|
|
184
|
+
prev.lastAt = e.at;
|
|
185
|
+
} else {
|
|
186
|
+
items.push({
|
|
187
|
+
seq: e.seq,
|
|
188
|
+
kind: e.kind,
|
|
189
|
+
label: eventLabelOf(e.kind),
|
|
190
|
+
author: authorNameOf(e.author),
|
|
191
|
+
at: e.at,
|
|
192
|
+
lastAt: e.at,
|
|
193
|
+
count: 1,
|
|
194
|
+
summary: '',
|
|
195
|
+
exception: false,
|
|
196
|
+
status: timelineStatusOf(e.kind),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return items;
|
|
201
|
+
}
|