@joekytc/dsh-swarm 0.1.0 → 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/README.md +22 -21
- package/README.zh-CN.md +20 -19
- 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 +13 -3
- 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/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**A governed swarm of six specialist DSH agents that turns one requirement into a strict, evidence-verified pipeline.**
|
|
8
8
|
|
|
9
|
-
An orchestrator (V) decomposes an approved spec into a strictly ordered phase chain (`p → (pt?) → w2 → d → dt → w3 → summary`); six single-purpose roles (V / P / W / D / PT / DT) run each phase with isolated, permission-gated tool faces; every handoff is machine-verified against an evidence contract; failures recover through idempotent retry and human-gated reviews; and a live Workflow kanban tab streams all state to the browser via SSE. Design inspired by the Hermes Agent kanban.
|
|
9
|
+
An orchestrator (V) decomposes an approved spec into a strictly ordered phase chain (`p → (pt?) → w2 → d → dt → w3 → summary`); six single-purpose roles (V / P / W / D / PT / DT) run each phase with isolated, permission-gated tool faces; every handoff is machine-verified against an evidence contract; failures recover through idempotent retry and human-gated reviews; and a live Workflow kanban tab streams all state to the browser via SSE. Design inspired by the [Hermes Agent kanban](https://github.com/NousResearch/hermes-agent).
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|

|
|
@@ -36,10 +36,10 @@ Six roles are dispatched by the scheduler as one-shot agent sessions (determinis
|
|
|
36
36
|
| **P** | Planner | Reads spec + repo facts (incl. read-only self-checks), writes an OpenSpec implementation plan, opts into PT via `pt_decision.needed`. Never executes. | Task tools + spec view, read-only (writes only `openspec/changes/`) |
|
|
37
37
|
| **PT** | Plan reviewer | Read-only review of P's plan (requirements alignment, completeness, logic). Outputs verdict + issues. | Task tools + spec view, **read-only ToolGuard** |
|
|
38
38
|
| **W** | Wiki bridge | W2/W3 KB sync (`w:kb`). Never touches code/git. | Task tools + `wiki_search/read/write` + read-only spec view |
|
|
39
|
-
| **D** | Executor | The *only* role that writes code: worktree → implement → verify → `[AI-GEN]` commit → push feature branch (merging into
|
|
39
|
+
| **D** | Executor | The *only* role that writes code: worktree → implement → verify → `[AI-GEN]` commit → push feature branch (merging into the spec-declared target branch is done by the system only after DT passes). | Task tools + wiki read + bash/fs/run_code (full dev) + subagent (spawn/fork/list-agents) + goal |
|
|
40
40
|
| **DT** | Implementation reviewer | Empirically verifies D's work (test/build/typecheck/diff/git + open-code-review), writes review page to KB. Read-only against the repo. | Task tools + wiki read/write (review namespace) + bash/fs/run_code, **read-only ToolGuard** |
|
|
41
41
|
|
|
42
|
-
The pipeline (
|
|
42
|
+
The pipeline (strictly serial within a chain, parallel across chains):
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
|
|
@@ -119,7 +119,7 @@ dsh plugin --profile <name> add ./dsh-swarm
|
|
|
119
119
|
Handoff / Spec / Comments.
|
|
120
120
|
|
|
121
121
|
4. When a chain completes, the system audits the workspace for out-of-chain writes
|
|
122
|
-
and (for D chains) merges D's feature branch into
|
|
122
|
+
and (for D chains) merges D's feature branch into the spec-declared target branch. If an audit
|
|
123
123
|
warning is raised, confirm ownership in the GUI before the final summary is shown.
|
|
124
124
|
|
|
125
125
|
---
|
|
@@ -131,7 +131,7 @@ All keys are optional; defaults shown. Schema lives in `src/config.ts`.
|
|
|
131
131
|
| Key | Default | Description |
|
|
132
132
|
|---|---|---|
|
|
133
133
|
| `storageDir` | `$DSH_HOME/storages/kanban` | Event log (`events.jsonl`), orchestration state, per-task workspaces, `dispatcher.log` |
|
|
134
|
-
| `wikiVault.baseUrl` | `
|
|
134
|
+
| `wikiVault.baseUrl` | `''` (empty) | wiki-vault HTTP service for KB reads/writes — required for KB features; set to your own server |
|
|
135
135
|
| `wikiVault.pagePrefix` | `projects/` | Whitelist prefix for W page writes |
|
|
136
136
|
| `roles.models.<role>` | `{}` | Per-role model: `{ provider, model, reasoningEffort?, fallbacks?[] }` |
|
|
137
137
|
| `roles.models.<role>.reasoningEffort` | `high` | Default reasoning effort for all roles |
|
|
@@ -231,7 +231,7 @@ blocks the save, and `/openspec:` mounts the checklist as the `file-prefetch` +
|
|
|
231
231
|
- **PT/DT** are read-only: a ToolGuard mechanically denies writes to the repo
|
|
232
232
|
sources, git mutations, and (for DT) wiki writes outside the review namespace.
|
|
233
233
|
- **DT** review engine: `open-code-review` (ocr, delegation mode, diff
|
|
234
|
-
`--from
|
|
234
|
+
`--from <target branch> --to <feature branch>`) → fallback `superpowers
|
|
235
235
|
code-review` → block `review-tool-unavailable` only if both are unavailable.
|
|
236
236
|
- `review_evidence` must pass `validateReviewEvidence` or the review card cannot
|
|
237
237
|
complete: PT needs verdict + issues + plan ref; DT additionally needs
|
|
@@ -273,19 +273,20 @@ Two orthogonal failure paths, both human-recoverable:
|
|
|
273
273
|
When the mechanical chain-complete rule fires, two gates run in the
|
|
274
274
|
`chain/completed` hook:
|
|
275
275
|
|
|
276
|
-
1. **Completion audit gate
|
|
276
|
+
1. **Completion audit gate**: the `ChainAuditor` cross-checks the chain
|
|
277
277
|
workspace for artifacts written outside the known task outputs. Orphaned writes
|
|
278
278
|
emit `chain/audit-warning`; the UI shows a warning banner and blocks the final
|
|
279
279
|
summary until the human confirms ownership (`chain/audit-confirmed`, human-only).
|
|
280
|
-
2. **Merge gate (post-DT system merge)**: D never merges to
|
|
280
|
+
2. **Merge gate (post-DT system merge)**: D never merges to the target branch and
|
|
281
281
|
never pushes it — it only commits to (and optionally pushes) its feature branch,
|
|
282
|
-
carrying `branch` in its handoff.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
282
|
+
carrying `branch` in its handoff. The target branch is the one declared in the
|
|
283
|
+
spec (written by V into the D task body). After DT approves and the chain
|
|
284
|
+
completes, `merge-gate.ts` performs, as `system`: `git checkout <target-branch>
|
|
285
|
+
→ git merge --no-ff <feature-branch> → git push`. Outcomes are recorded as
|
|
286
|
+
idempotent comments: `[merge-done]` (with hash), `[merge-skip]` (merge input
|
|
287
|
+
unresolvable), or `[merge-failed]` (checkout/merge/push failed, e.g. a conflict).
|
|
288
|
+
Failures never throw — a bad merge is never performed, which is the safe
|
|
289
|
+
direction; humans can repair afterwards.
|
|
289
290
|
|
|
290
291
|
---
|
|
291
292
|
|
|
@@ -380,10 +381,10 @@ flowchart TB
|
|
|
380
381
|
|
|
381
382
|
subgraph Dispatcher ["dispatcher/"]
|
|
382
383
|
WAKER["event-waker (events → wake V)"]
|
|
383
|
-
VORCH["v-orchestrator (
|
|
384
|
+
VORCH["v-orchestrator (phase machine)"]
|
|
384
385
|
RUNNER["agent-runner (one-shot role sessions, presets, ToolGuards)"]
|
|
385
386
|
WD["watchdog (heartbeat / stale reclaim / circuit)"]
|
|
386
|
-
AUDIT["chain-auditor (
|
|
387
|
+
AUDIT["chain-auditor (completion audit)"]
|
|
387
388
|
MG["merge-gate (post-DT system merge)"]
|
|
388
389
|
end
|
|
389
390
|
|
|
@@ -423,7 +424,7 @@ flowchart TB
|
|
|
423
424
|
- **Integration** (`src/tools/`, `src/routes/`) — cordis tools and routes:
|
|
424
425
|
the role tool faces, main-session tools (`kanban_route` + read-only subset), and
|
|
425
426
|
the `/kanban/*` HTTP/SSE bridge.
|
|
426
|
-
- **Dispatcher** (`src/dispatcher/`) — event wake,
|
|
427
|
+
- **Dispatcher** (`src/dispatcher/`) — event wake, phase orchestration, one-shot
|
|
427
428
|
agent runner (persona preset mounting, model candidate chain, ToolGuard
|
|
428
429
|
installation), watchdog, chain auditor, and merge gate.
|
|
429
430
|
- **Roles** (`src/roles/`, `personas/`) — trimmed agent presets installed into
|
|
@@ -459,14 +460,14 @@ python tests/e2e/gui-check.py --url http://127.0.0.1:3080/
|
|
|
459
460
|
### Implemented (v0.1.0)
|
|
460
461
|
|
|
461
462
|
- [x] Event-sourced domain + deterministic state machines (red-team replay)
|
|
462
|
-
- [x] 6-role
|
|
463
|
+
- [x] 6-role phase pipeline with trimmed presets and session-bound permissions
|
|
463
464
|
- [x] Delivery contract + review evidence gates + rework lifecycle
|
|
464
465
|
- [x] TDD hard gate (D `tdd` handoff + DT `test_first` / `runner=vitest` verification)
|
|
465
466
|
- [x] Protocol-violation recovery, heartbeat watchdog, failure circuit
|
|
466
|
-
- [x] Chain completion audit gate
|
|
467
|
+
- [x] Chain completion audit gate + human confirm
|
|
467
468
|
- [x] Post-DT merge gate (D pushes feature branch only)
|
|
468
469
|
- [x] Phase-0 planning checklist + `file-prefetch` attachment
|
|
469
|
-
- [x] GUI chain/task rename + chain delete (
|
|
470
|
+
- [x] GUI chain/task rename + chain delete (human-only)
|
|
470
471
|
- [x] Model candidate chain with silent fallback + high reasoning effort
|
|
471
472
|
- [x] Live SSE kanban tab (Conversation → Trajectory → Kanban)
|
|
472
473
|
|
package/README.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**受管六角色 DSH agent 蜂群:把单个需求变成严格、证据可核验的流水线。**
|
|
8
8
|
|
|
9
|
-
编排者(V)把已批准的规格拆成严格有序的相位链(`p → (pt?) → w2 → d → dt → w3 → summary`);六个单一职责的角色(V / P / W / D / PT / DT)以隔离、受权限约束的工具面执行每个相位;每份交接都经过针对证据契约的机器校验;故障通过幂等重试与人工把关的评审恢复;实时 Workflow 看板标签页通过 SSE 把全部状态流式同步到浏览器。设计灵感源自 Hermes Agent kanban。
|
|
9
|
+
编排者(V)把已批准的规格拆成严格有序的相位链(`p → (pt?) → w2 → d → dt → w3 → summary`);六个单一职责的角色(V / P / W / D / PT / DT)以隔离、受权限约束的工具面执行每个相位;每份交接都经过针对证据契约的机器校验;故障通过幂等重试与人工把关的评审恢复;实时 Workflow 看板标签页通过 SSE 把全部状态流式同步到浏览器。设计灵感源自 [Hermes Agent kanban](https://github.com/NousResearch/hermes-agent)。
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|

|
|
@@ -36,10 +36,10 @@ dsh-swarm 针对以上三种问题编码了*契约*:每个角色只有一项
|
|
|
36
36
|
| **P** | 规划者 | 读取规格 + 仓库事实(含只读自查),编写 OpenSpec 实施计划,用 `pt_decision.needed` 决定是否需要 PT。绝不执行。 | 任务工具 + 规格查看,只读(仅写 `openspec/changes/`) |
|
|
37
37
|
| **PT** | 计划评审者 | 对 P 的计划做只读评审(需求对齐、完整性、逻辑)。输出裁决 + 问题清单。 | 任务工具 + 规格查看,**只读 ToolGuard** |
|
|
38
38
|
| **W** | 知识库桥 | W2/W3 知识库同步(`w:kb`)。绝不碰代码/git。 | 任务工具 + `wiki_search/read/write` + 只读规格查看 |
|
|
39
|
-
| **D** | 执行者 | *唯一*写代码的角色:worktree → 实现 → 验证 → `[AI-GEN]` 提交 →
|
|
39
|
+
| **D** | 执行者 | *唯一*写代码的角色:worktree → 实现 → 验证 → `[AI-GEN]` 提交 → 推送特性分支(合入规格声明的目标分支由 system 在 DT 通过后执行)。 | 任务工具 + wiki 只读 + bash/fs/run_code(完整开发面)+ subagent(spawn/fork/list-agents)+ goal |
|
|
40
40
|
| **DT** | 实现评审者 | 实证验证 D 的工作(test/build/typecheck/diff/git + open-code-review),把评审页写入知识库。对仓库只读。 | 任务工具 + wiki 读写(评审命名空间)+ bash/fs/run_code,**只读 ToolGuard** |
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
管线(链路内严格串行,链路间并行):
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
p ──> (pt?) ──> w2 ──> d ──> dt ──> w3 ──> summary
|
|
@@ -111,7 +111,7 @@ dsh plugin --profile <name> add ./dsh-swarm
|
|
|
111
111
|
概览 / 轨迹 / 交接 / 规格 / 评论。
|
|
112
112
|
|
|
113
113
|
4. 链路完成时,系统审计工作区中是否有链路之外的写入,并(对 D 链路)把 D 的特性分支
|
|
114
|
-
|
|
114
|
+
合并到规格声明的目标分支。若触发审计警告,需先在 GUI 中确认归属,才会展示最终汇报。
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
@@ -122,7 +122,7 @@ dsh plugin --profile <name> add ./dsh-swarm
|
|
|
122
122
|
| 键 | 默认值 | 说明 |
|
|
123
123
|
|---|---|---|
|
|
124
124
|
| `storageDir` | `$DSH_HOME/storages/kanban` | 事件日志(`events.jsonl`)、编排状态、每任务工作区、`dispatcher.log` |
|
|
125
|
-
| `wikiVault.baseUrl` | `
|
|
125
|
+
| `wikiVault.baseUrl` | `''`(空) | 知识库读写用的 wiki-vault HTTP 服务——知识库功能必需,填你自己的服务地址 |
|
|
126
126
|
| `wikiVault.pagePrefix` | `projects/` | W 页面写入的白名单前缀 |
|
|
127
127
|
| `roles.models.<role>` | `{}` | 每角色模型:`{ provider, model, reasoningEffort?, fallbacks?[] }` |
|
|
128
128
|
| `roles.models.<role>.reasoningEffort` | `high` | 所有角色默认推理强度 |
|
|
@@ -215,7 +215,7 @@ D 只有带 `tdd` 才能完成——`test_files`(含 `test_first`)或 `skipp
|
|
|
215
215
|
- **D** 完成后**总是**创建 **DT** 卡。
|
|
216
216
|
- **PT/DT 只读**:ToolGuard 机械性拒绝写仓库源码、git 变更,以及(对 DT)评审命名空间
|
|
217
217
|
之外的 wiki 写入。
|
|
218
|
-
- **DT 评审引擎**:`open-code-review`(ocr,委派模式,diff `--from
|
|
218
|
+
- **DT 评审引擎**:`open-code-review`(ocr,委派模式,diff `--from <目标分支> --to <特性分支>`)
|
|
219
219
|
→ 回退 `superpowers code-review` → 两者都不可用才 block `review-tool-unavailable`。
|
|
220
220
|
- `review_evidence` 必须通过 `validateReviewEvidence`,否则评审卡无法完成:PT 需要
|
|
221
221
|
verdict + issues + 计划引用;DT 额外需要 test(通过时退出码 0)、build/typecheck、
|
|
@@ -250,15 +250,16 @@ D 只有带 `tdd` 才能完成——`test_files`(含 `test_first`)或 `skipp
|
|
|
250
250
|
|
|
251
251
|
机械性链路完成规则触发时,两个闸门在 `chain/completed` 钩子中运行:
|
|
252
252
|
|
|
253
|
-
1.
|
|
253
|
+
1. **完成审计闸门**:`ChainAuditor` 交叉核对链路工作区中是否存在已知任务输出
|
|
254
254
|
之外的产物。发现孤儿写入即发出 `chain/audit-warning`;UI 显示警告横幅并阻塞最终
|
|
255
255
|
汇报,直到人类确认归属(`chain/audit-confirmed`,仅限人类)。
|
|
256
|
-
2. **合并闸门(DT 通过后的系统合并)**:D
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
`[merge-skip]`(合并输入无法解析)、`[merge-failed]
|
|
261
|
-
|
|
256
|
+
2. **合并闸门(DT 通过后的系统合并)**:D 从不合并到目标分支,也不推送它——D 只提交到
|
|
257
|
+
(可选推送)自己的特性分支,并在交接中携带 `branch`。目标分支是规格中声明的分支
|
|
258
|
+
(V 写入 D 任务体)。DT 批准且链路完成后,`merge-gate.ts` 以 `system` 身份执行:
|
|
259
|
+
`git checkout <目标分支> → git merge --no-ff <特性分支> → git push`。结果以幂等评论记录:
|
|
260
|
+
`[merge-done]`(带 hash)、`[merge-skip]`(合并输入无法解析)、`[merge-failed]`
|
|
261
|
+
(checkout/merge/push 失败,例如冲突)。失败绝不抛错——坏合并*不执行*,这是安全方向;
|
|
262
|
+
人类事后可修复。
|
|
262
263
|
|
|
263
264
|
---
|
|
264
265
|
|
|
@@ -340,10 +341,10 @@ flowchart TB
|
|
|
340
341
|
|
|
341
342
|
subgraph Dispatcher ["dispatcher/"]
|
|
342
343
|
WAKER["event-waker (events → wake V)"]
|
|
343
|
-
VORCH["v-orchestrator (
|
|
344
|
+
VORCH["v-orchestrator (phase machine)"]
|
|
344
345
|
RUNNER["agent-runner (one-shot role sessions, presets, ToolGuards)"]
|
|
345
346
|
WD["watchdog (heartbeat / stale reclaim / circuit)"]
|
|
346
|
-
AUDIT["chain-auditor (
|
|
347
|
+
AUDIT["chain-auditor (completion audit)"]
|
|
347
348
|
MG["merge-gate (post-DT system merge)"]
|
|
348
349
|
end
|
|
349
350
|
|
|
@@ -381,7 +382,7 @@ flowchart TB
|
|
|
381
382
|
单一权威的 `KanbanService` 门面。单测充分覆盖。
|
|
382
383
|
- **集成层**(`src/tools/`、`src/routes/`)—— cordis 工具与路由:角色工具面、主会话
|
|
383
384
|
工具(`kanban_route` + 只读子集)、`/kanban/*` HTTP/SSE 桥。
|
|
384
|
-
- **调度层**(`src/dispatcher/`)——
|
|
385
|
+
- **调度层**(`src/dispatcher/`)—— 事件唤醒、相位编排、一次性 agent 运行器(persona
|
|
385
386
|
preset 挂载、模型候选链、ToolGuard 安装)、看门狗、链路审计器、合并闸门。
|
|
386
387
|
- **角色层**(`src/roles/`、`personas/`)—— 安装到 `$DSH_HOME/.agent-presets/` 的裁剪
|
|
387
388
|
preset、每角色工具装配、写保护逻辑。
|
|
@@ -414,14 +415,14 @@ python tests/e2e/gui-check.py --url http://127.0.0.1:3080/
|
|
|
414
415
|
### 已实现(v0.1.0)
|
|
415
416
|
|
|
416
417
|
- [x] 事件溯源领域 + 确定性状态机(红队回放)
|
|
417
|
-
- [x] 6
|
|
418
|
+
- [x] 6 角色相位管线 + 裁剪 preset + 会话绑定权限
|
|
418
419
|
- [x] 交付契约 + 评审证据闸门 + 返工生命周期
|
|
419
420
|
- [x] TDD 硬闸(D `tdd` 交接 + DT `test_first` / `runner=vitest` 核验)
|
|
420
421
|
- [x] 协议违规恢复、心跳看门狗、失败熔断
|
|
421
|
-
- [x]
|
|
422
|
+
- [x] 链路完成审计闸门 + 人工确认
|
|
422
423
|
- [x] DT 通过后合并闸门(D 只推送特性分支)
|
|
423
424
|
- [x] 阶段 0 规划清单 + `file-prefetch` 附件
|
|
424
|
-
- [x] GUI 链/任务改名 +
|
|
425
|
+
- [x] GUI 链/任务改名 + 整链删除(仅 human)
|
|
425
426
|
- [x] 模型候选链:静默回退 + High 推理强度
|
|
426
427
|
- [x] 实时 SSE 看板标签页(对话 → 轨迹 → 看板)
|
|
427
428
|
|
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
|
+
}
|