@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.
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/README.zh-CN.md +452 -0
- package/client/BoardCard.tsx +41 -0
- package/client/ConnectionBanner.tsx +16 -0
- package/client/KanbanBoard.tsx +168 -0
- package/client/KanbanTab.tsx +26 -0
- package/client/RenameModal.tsx +38 -0
- package/client/TaskDrawer.tsx +212 -0
- package/client/WorkflowRail.tsx +175 -0
- package/client/board-store.ts +196 -0
- package/client/css.d.ts +4 -0
- package/client/index.ts +31 -0
- package/client/kanban.css +653 -0
- package/client/useKanbanBoard.ts +7 -0
- package/client/workflow-model.ts +229 -0
- package/cordis.patch.yml +88 -0
- package/lib/client.js +1205 -0
- package/lib/config.d.ts +46 -0
- package/lib/config.js +43 -0
- package/lib/dispatcher/agent-runner.d.ts +23 -0
- package/lib/dispatcher/agent-runner.js +429 -0
- package/lib/dispatcher/chain-auditor.d.ts +47 -0
- package/lib/dispatcher/chain-auditor.js +194 -0
- package/lib/dispatcher/dispatcher.d.ts +50 -0
- package/lib/dispatcher/dispatcher.js +280 -0
- package/lib/dispatcher/event-waker.d.ts +13 -0
- package/lib/dispatcher/event-waker.js +24 -0
- package/lib/dispatcher/git-credentials.d.ts +33 -0
- package/lib/dispatcher/git-credentials.js +78 -0
- package/lib/dispatcher/merge-gate.d.ts +28 -0
- package/lib/dispatcher/merge-gate.js +74 -0
- package/lib/dispatcher/model-candidates.d.ts +13 -0
- package/lib/dispatcher/model-candidates.js +31 -0
- package/lib/dispatcher/session-events.d.ts +28 -0
- package/lib/dispatcher/session-events.js +33 -0
- package/lib/dispatcher/target-repo.d.ts +15 -0
- package/lib/dispatcher/target-repo.js +42 -0
- package/lib/dispatcher/v-orchestrator.d.ts +74 -0
- package/lib/dispatcher/v-orchestrator.js +452 -0
- package/lib/dispatcher/watchdog.d.ts +15 -0
- package/lib/dispatcher/watchdog.js +30 -0
- package/lib/dispatcher/workspace-attach.d.ts +40 -0
- package/lib/dispatcher/workspace-attach.js +112 -0
- package/lib/domain/delivery-contract.d.ts +18 -0
- package/lib/domain/delivery-contract.js +80 -0
- package/lib/domain/delivery-evidence.d.ts +12 -0
- package/lib/domain/delivery-evidence.js +41 -0
- package/lib/domain/event-store.d.ts +20 -0
- package/lib/domain/event-store.js +53 -0
- package/lib/domain/kanban-service.d.ts +94 -0
- package/lib/domain/kanban-service.js +430 -0
- package/lib/domain/permissions.d.ts +7 -0
- package/lib/domain/permissions.js +54 -0
- package/lib/domain/planning-checklist.d.ts +22 -0
- package/lib/domain/planning-checklist.js +81 -0
- package/lib/domain/prefetch-manifest.d.ts +21 -0
- package/lib/domain/prefetch-manifest.js +73 -0
- package/lib/domain/projection.d.ts +3 -0
- package/lib/domain/projection.js +165 -0
- package/lib/domain/review-evidence.d.ts +13 -0
- package/lib/domain/review-evidence.js +76 -0
- package/lib/domain/state-machine.d.ts +4 -0
- package/lib/domain/state-machine.js +32 -0
- package/lib/domain/task-parents.d.ts +17 -0
- package/lib/domain/task-parents.js +39 -0
- package/lib/domain/tdd-classify.d.ts +5 -0
- package/lib/domain/tdd-classify.js +20 -0
- package/lib/domain/types.d.ts +142 -0
- package/lib/domain/types.js +2 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +56 -0
- package/lib/roles/preset-installer.d.ts +8 -0
- package/lib/roles/preset-installer.js +53 -0
- package/lib/roles/toolsets.d.ts +62 -0
- package/lib/roles/toolsets.js +332 -0
- package/lib/roles/wiki-worker.d.ts +20 -0
- package/lib/roles/wiki-worker.js +44 -0
- package/lib/routes/kanban-http.d.ts +6 -0
- package/lib/routes/kanban-http.js +159 -0
- package/lib/routes/kanban-sse.d.ts +8 -0
- package/lib/routes/kanban-sse.js +58 -0
- package/lib/routes/planning-driver.d.ts +16 -0
- package/lib/routes/planning-driver.js +52 -0
- package/lib/routes/prefix-router.d.ts +29 -0
- package/lib/routes/prefix-router.js +28 -0
- package/lib/services/kanban-provider.d.ts +16 -0
- package/lib/services/kanban-provider.js +14 -0
- package/lib/tools/kanban-tools.d.ts +11 -0
- package/lib/tools/kanban-tools.js +169 -0
- package/lib/tools/main-session-tools.d.ts +20 -0
- package/lib/tools/main-session-tools.js +170 -0
- package/lib/tools/planning-tools.d.ts +32 -0
- package/lib/tools/planning-tools.js +102 -0
- package/lib/tools/prefetch-tools.d.ts +5 -0
- package/lib/tools/prefetch-tools.js +59 -0
- package/lib/tools/spec-card-tools.d.ts +4 -0
- package/lib/tools/spec-card-tools.js +75 -0
- package/lib/tools/wiki-tools.d.ts +4 -0
- package/lib/tools/wiki-tools.js +57 -0
- package/lib/wiki/kb-linkage.d.ts +9 -0
- package/lib/wiki/kb-linkage.js +87 -0
- package/lib/wiki/page-path.d.ts +6 -0
- package/lib/wiki/page-path.js +28 -0
- package/lib/wiki/wiki-vault-client.d.ts +28 -0
- package/lib/wiki/wiki-vault-client.js +48 -0
- package/package.json +83 -0
- package/personas/kanban-d/agent.cordis.yml +157 -0
- package/personas/kanban-d/preset.yml +2 -0
- package/personas/kanban-dt/agent.cordis.yml +71 -0
- package/personas/kanban-dt/preset.yml +2 -0
- package/personas/kanban-p/agent.cordis.yml +66 -0
- package/personas/kanban-p/preset.yml +2 -0
- package/personas/kanban-pt/agent.cordis.yml +47 -0
- package/personas/kanban-pt/preset.yml +2 -0
- package/personas/kanban-v/agent.cordis.yml +47 -0
- package/personas/kanban-v/preset.yml +2 -0
- package/personas/kanban-w/agent.cordis.yml +47 -0
- package/personas/kanban-w/preset.yml +2 -0
- package/personas/persona-d.md +26 -0
- package/personas/persona-dt.md +18 -0
- package/personas/persona-p.md +13 -0
- package/personas/persona-pt.md +13 -0
- package/personas/persona-v.md +18 -0
- package/personas/persona-w.md +12 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { serveKanbanEvents } from './kanban-sse.js';
|
|
2
|
+
function json(res, status, body) {
|
|
3
|
+
res.statusCode = status;
|
|
4
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
5
|
+
res.end(JSON.stringify(body));
|
|
6
|
+
}
|
|
7
|
+
function readBody(req) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
req.on('data', (c) => chunks.push(c));
|
|
11
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
12
|
+
req.on('error', reject);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** 看板 HTTP 桥(Web GUI 浏览器半消费):GET /kanban/board 读快照;POST /kanban/action 执行状态操作。
|
|
16
|
+
* 仅在 webServer 服务存在时挂载(CLI/headless/测试裸 Context 不挂)。 */
|
|
17
|
+
export function registerKanbanHttp(ctx, provider, config) {
|
|
18
|
+
// 可选服务:经 ctx.get 读取(cordis 4 直接属性读取需 inject;get 不需要)
|
|
19
|
+
const webServer = ctx.get('webServer');
|
|
20
|
+
if (!webServer)
|
|
21
|
+
return;
|
|
22
|
+
webServer.register({
|
|
23
|
+
kind: 'prefix',
|
|
24
|
+
path: '/kanban',
|
|
25
|
+
async handler(req, res) {
|
|
26
|
+
try {
|
|
27
|
+
if (req.method === 'GET' && req.url?.startsWith('/kanban/board')) {
|
|
28
|
+
const state = await provider.service.snapshot();
|
|
29
|
+
json(res, 200, {
|
|
30
|
+
chains: [...state.chains.values()],
|
|
31
|
+
tasks: [...state.tasks.values()],
|
|
32
|
+
specCards: [...state.specCards.values()],
|
|
33
|
+
handoffs: [...state.handoffs.entries()].map(([k, v]) => ({ id: k, ...v })),
|
|
34
|
+
auditWarnings: [...state.auditWarnings.entries()].map(([k, v]) => ({ chainId: k, ...v })),
|
|
35
|
+
events: state.events,
|
|
36
|
+
lastSeq: state.events.at(-1)?.seq ?? -1,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (req.method === 'GET' && req.url?.startsWith('/kanban/events')) {
|
|
41
|
+
const heartbeatMs = (config?.ui.sseHeartbeatSeconds ?? 20) * 1000;
|
|
42
|
+
await serveKanbanEvents(req, res, provider.service, { heartbeatMs });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (req.method === 'POST' && req.url?.startsWith('/kanban/action')) {
|
|
46
|
+
const body = JSON.parse((await readBody(req)) || '{}');
|
|
47
|
+
// D23:confirm-audit 是链级 action(无 taskId),提前分流处理
|
|
48
|
+
if (body.type === 'confirm-audit') {
|
|
49
|
+
const chainId = String(body.chainId ?? '').trim();
|
|
50
|
+
if (!chainId) {
|
|
51
|
+
json(res, 400, { error: 'chainId required' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await provider.service.confirmAudit(chainId, 'human');
|
|
55
|
+
json(res, 200, { ok: true });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// 整链硬删除(仅 human;GUI 二次确认):purge 无事件流,客户端需自行 resync
|
|
59
|
+
if (body.type === 'delete') {
|
|
60
|
+
const chainId = String(body.chainId ?? '').trim();
|
|
61
|
+
if (!chainId) {
|
|
62
|
+
json(res, 400, { error: 'chainId required' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await provider.service.deleteChain(chainId, 'human');
|
|
66
|
+
json(res, 200, { ok: true });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// T7:rename 是链级或任务级 action(chainId 或 taskId 二选一),在 taskId 守卫前分流
|
|
70
|
+
if (body.type === 'rename') {
|
|
71
|
+
const title = String(body.title ?? '').trim();
|
|
72
|
+
if (!title) {
|
|
73
|
+
json(res, 400, { error: 'title required' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (body.chainId)
|
|
77
|
+
await provider.service.updateChainTitle(String(body.chainId), title, 'human');
|
|
78
|
+
else if (body.taskId)
|
|
79
|
+
await provider.service.renameTask(String(body.taskId), title, 'human');
|
|
80
|
+
else {
|
|
81
|
+
json(res, 400, { error: 'chainId or taskId required' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
json(res, 200, { ok: true });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const t = body.taskId;
|
|
88
|
+
if (!t) {
|
|
89
|
+
json(res, 400, { error: 'taskId required' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
switch (body.type) {
|
|
93
|
+
case 'block': {
|
|
94
|
+
const reason = String(body.reason ?? '').trim();
|
|
95
|
+
if (!reason) {
|
|
96
|
+
json(res, 400, { error: 'reason required' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await provider.service.blockTask(t, reason, 'human');
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'unblock':
|
|
103
|
+
await provider.service.unblockTask(t, 'human');
|
|
104
|
+
break;
|
|
105
|
+
case 'retry': {
|
|
106
|
+
// T32 fix:retry 走 runner(failed→claim→spawn/resume),而非只 claim 造成 running 悬挂
|
|
107
|
+
const state = await provider.service.snapshot();
|
|
108
|
+
const task = state.tasks.get(t);
|
|
109
|
+
if (!task) {
|
|
110
|
+
json(res, 404, { error: 'unknown task: ' + t });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (task.status !== 'failed') {
|
|
114
|
+
json(res, 409, { error: 'invalid state: task ' + t + ' is ' + task.status + ', only failed tasks can be retried' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!provider.runner) {
|
|
118
|
+
json(res, 503, { error: 'dispatcher not ready' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
void provider.runner.runTask(t).catch((err) => { console.error('[dsh-swarm] retry dispatch failed: ' + String(err)); });
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'complete': {
|
|
125
|
+
const summary = String(body.summary ?? '').trim();
|
|
126
|
+
if (!summary) {
|
|
127
|
+
json(res, 400, { error: 'summary required' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await provider.service.completeTask(t, { summary, metadata: body.metadata ?? {}, completedAt: Date.now() }, 'human');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'archive':
|
|
134
|
+
await provider.service.archiveTask(t, 'human');
|
|
135
|
+
break;
|
|
136
|
+
case 'comment': {
|
|
137
|
+
const bodyText = String(body.body ?? '').trim();
|
|
138
|
+
if (!bodyText) {
|
|
139
|
+
json(res, 400, { error: 'body required' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await provider.service.comment(t, bodyText, 'human');
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
json(res, 400, { error: 'unknown action: ' + String(body.type) });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
json(res, 200, { ok: true });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
json(res, 404, { error: 'not found' });
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
json(res, 500, { error: String(err) });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import type { KanbanEvent } from '../domain/types.js';
|
|
4
|
+
export declare function writeSseEvent(res: ServerResponse, event: KanbanEvent): void;
|
|
5
|
+
/** T23:SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
|
|
6
|
+
export declare function serveKanbanEvents(req: IncomingMessage, res: ServerResponse, service: Pick<KanbanService, 'subscribe' | 'eventsSince'>, options: {
|
|
7
|
+
heartbeatMs: number;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function writeSseEvent(res, event) {
|
|
2
|
+
res.write(`id: ${event.seq}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
3
|
+
}
|
|
4
|
+
function lastSeen(req) {
|
|
5
|
+
const url = new URL(req.url ?? '/kanban/events', 'http://localhost');
|
|
6
|
+
const query = Number(url.searchParams.get('after') ?? -1);
|
|
7
|
+
const headerValue = Array.isArray(req.headers['last-event-id'])
|
|
8
|
+
? req.headers['last-event-id'][0]
|
|
9
|
+
: req.headers['last-event-id'];
|
|
10
|
+
const header = Number(headerValue ?? -1);
|
|
11
|
+
return Math.max(Number.isFinite(query) ? query : -1, Number.isFinite(header) ? header : -1);
|
|
12
|
+
}
|
|
13
|
+
/** T23:SSE 事件桥。先订阅并缓存 live 事件,再补发补偿事件,握手窗口不丢事件。 */
|
|
14
|
+
export async function serveKanbanEvents(req, res, service, options) {
|
|
15
|
+
res.statusCode = 200;
|
|
16
|
+
res.setHeader('content-type', 'text/event-stream; charset=utf-8');
|
|
17
|
+
res.setHeader('cache-control', 'no-cache, no-transform');
|
|
18
|
+
res.setHeader('connection', 'keep-alive');
|
|
19
|
+
res.flushHeaders?.();
|
|
20
|
+
let cursor = lastSeen(req);
|
|
21
|
+
let replaying = true;
|
|
22
|
+
const buffered = [];
|
|
23
|
+
let heartbeat;
|
|
24
|
+
const stop = service.subscribe((event) => {
|
|
25
|
+
if (replaying)
|
|
26
|
+
buffered.push(event);
|
|
27
|
+
else if (event.seq > cursor) {
|
|
28
|
+
writeSseEvent(res, event);
|
|
29
|
+
cursor = event.seq;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
let closed = false;
|
|
33
|
+
const close = () => {
|
|
34
|
+
if (closed)
|
|
35
|
+
return;
|
|
36
|
+
closed = true;
|
|
37
|
+
stop();
|
|
38
|
+
if (heartbeat)
|
|
39
|
+
clearInterval(heartbeat);
|
|
40
|
+
};
|
|
41
|
+
req.once('close', close);
|
|
42
|
+
req.once('aborted', close);
|
|
43
|
+
res.once('close', close);
|
|
44
|
+
heartbeat = setInterval(() => { res.write(': heartbeat\n\n'); }, options.heartbeatMs);
|
|
45
|
+
for (const event of await service.eventsSince(cursor + 1)) {
|
|
46
|
+
if (event.seq > cursor) {
|
|
47
|
+
writeSseEvent(res, event);
|
|
48
|
+
cursor = event.seq;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
replaying = false;
|
|
52
|
+
for (const event of buffered.sort((a, b) => a.seq - b.seq)) {
|
|
53
|
+
if (event.seq > cursor) {
|
|
54
|
+
writeSseEvent(res, event);
|
|
55
|
+
cursor = event.seq;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
2
|
+
import type { SpecCard, SpecCardAttachment } from '../domain/types.js';
|
|
3
|
+
export declare const MATTPOCOCK_PLANNING_GUIDANCE = "\n# \u9636\u6BB5 0 \u89C4\u5212\u5BF9\u8BDD\uFF08v2\uFF1A\u9700\u6C42\u6F84\u6E05\u524D\u7F6E\u5316\uFF09\n1. \u9700\u6C42\u6F84\u6E05\uFF08grill-me\uFF09\uFF1A\u4E00\u6B21\u53EA\u95EE\u4E00\u4E2A\u95EE\u9898\uFF0C\u5148\u6F84\u6E05\u76EE\u7684\u3001\u7EA6\u675F\u3001\u6210\u529F\u6807\u51C6\uFF1B\u9010\u9879\u62F7\u95EE\u5047\u8BBE\u76F4\u81F3\u7528\u6237\"\u6CA1\u6709\u4EFB\u4F55\u7591\u95EE\"\u3002\n2. \u4ED3\u5E93\u4E8B\u5B9E\uFF08planning_prefetch\uFF09\uFF1A\u8C03\u53EA\u8BFB\u5B50\u4EE3\u7406\u91C7\u96C6\u76EE\u6807\u4ED3\u5E93/\u8D44\u6599/\u77E5\u8BC6\u5E93\u4E8B\u5B9E\uFF08\u672C\u5730\u8DEF\u5F84/\u5206\u652F/\u76EE\u6807\u6587\u4EF6\u57FA\u7EBF/\u65E2\u6709\u5B9E\u73B0\uFF09\uFF0C\u4E0D\u51ED\u7A7A\u5047\u8BBE\u3002\n3. \u6536\u655B\uFF08planning_checklist_save\uFF09\uFF1A\u628A\u7ED3\u8BBA\u5199\u6210\u7ED3\u6784\u5316\u9700\u6C42\u6F84\u6E05\u6E05\u5355\uFF08spec \u516D\u6BB5 + manifest repo.files + \u6F84\u6E05\u95EE\u7B54 + \u7591\u95EE\u70B9\uFF09\u5B58\u5165 KB\uFF08KB \u4E0D\u53EF\u8FBE\u81EA\u52A8\u515C\u5E95\u4E34\u65F6\u76EE\u5F55\uFF09\u3002\n4. \u6536\u5C3E\uFF1A\u63D0\u9192\u7528\u6237\u4EE5 /openspec: \u786E\u8BA4\u6267\u884C\u7ED3\u675F\u89C4\u5212\u9636\u6BB5\u2014\u2014/openspec: \u4F1A\u4ECE\u6E05\u5355\u5EFA\u94FE\u5E76\u81EA\u52A8\u4E32\u884C\u6267\u884C\u3002\n\u62A4\u680F\uFF1A\u89C4\u5212\u671F\u53EA\u8BFB\u4ED3\u5E93\uFF0C\u7981\u6B62\u4EFB\u4F55 git/\u6E90\u7801\u5199\u5165\uFF1B\u53EA\u5199 KB \u4E0E\u4E34\u65F6\u76EE\u5F55\u3002\n";
|
|
4
|
+
export declare function validateSpecCardForApproval(card: SpecCard): string[];
|
|
5
|
+
export declare function buildPlanningContext(chainId: string, card: SpecCard, attachments: SpecCardAttachment[]): string;
|
|
6
|
+
export declare function approveIfReady(message: string, service: KanbanService, cfg: {
|
|
7
|
+
plan: string;
|
|
8
|
+
openspec: string;
|
|
9
|
+
}, chainId: string, specCardId: string): Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
card: SpecCard;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
missing: string[];
|
|
15
|
+
guidance: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
2
|
+
import { parsePrefix } from './prefix-router.js';
|
|
3
|
+
export const MATTPOCOCK_PLANNING_GUIDANCE = `
|
|
4
|
+
# 阶段 0 规划对话(v2:需求澄清前置化)
|
|
5
|
+
1. 需求澄清(grill-me):一次只问一个问题,先澄清目的、约束、成功标准;逐项拷问假设直至用户"没有任何疑问"。
|
|
6
|
+
2. 仓库事实(planning_prefetch):调只读子代理采集目标仓库/资料/知识库事实(本地路径/分支/目标文件基线/既有实现),不凭空假设。
|
|
7
|
+
3. 收敛(planning_checklist_save):把结论写成结构化需求澄清清单(spec 六段 + manifest repo.files + 澄清问答 + 疑问点)存入 KB(KB 不可达自动兜底临时目录)。
|
|
8
|
+
4. 收尾:提醒用户以 /openspec: 确认执行结束规划阶段——/openspec: 会从清单建链并自动串行执行。
|
|
9
|
+
护栏:规划期只读仓库,禁止任何 git/源码写入;只写 KB 与临时目录。
|
|
10
|
+
`;
|
|
11
|
+
export function validateSpecCardForApproval(card) {
|
|
12
|
+
const missing = [];
|
|
13
|
+
const s = card.sections;
|
|
14
|
+
if (typeof s.problem !== 'string' || !s.problem.trim())
|
|
15
|
+
missing.push('problem');
|
|
16
|
+
if (typeof s.solution !== 'string' || !s.solution.trim())
|
|
17
|
+
missing.push('solution');
|
|
18
|
+
if (!Array.isArray(s.user_stories) || s.user_stories.length === 0)
|
|
19
|
+
missing.push('user_stories');
|
|
20
|
+
if (typeof s.testing !== 'string' || !s.testing.trim())
|
|
21
|
+
missing.push('testing');
|
|
22
|
+
if (typeof s.out_of_scope !== 'string' || !s.out_of_scope.trim())
|
|
23
|
+
missing.push('out_of_scope');
|
|
24
|
+
if (!card.attachments.some((a) => a.kind === 'file-prefetch'))
|
|
25
|
+
missing.push('attachments:file-prefetch');
|
|
26
|
+
return missing;
|
|
27
|
+
}
|
|
28
|
+
export function buildPlanningContext(chainId, card, attachments) {
|
|
29
|
+
return [
|
|
30
|
+
`# 规划上下文 chain=${chainId} specCard=${card.id}`,
|
|
31
|
+
MATTPOCOCK_PLANNING_GUIDANCE,
|
|
32
|
+
`## 当前规格卡\n${JSON.stringify(card.sections, null, 2)}`,
|
|
33
|
+
`## 仓库事实附件\n${attachments.map((a) => `${a.name}: ${a.ref}`).join('\n') || '(无)'}`,
|
|
34
|
+
].join('\n\n');
|
|
35
|
+
}
|
|
36
|
+
export async function approveIfReady(message, service, cfg, chainId, specCardId) {
|
|
37
|
+
const parsed = parsePrefix(message, cfg);
|
|
38
|
+
if (parsed.kind !== 'openspec')
|
|
39
|
+
return { ok: false, missing: ['prefix'], guidance: MATTPOCOCK_PLANNING_GUIDANCE };
|
|
40
|
+
const state = await service.snapshot();
|
|
41
|
+
const card = state.specCards.get(specCardId);
|
|
42
|
+
if (!card)
|
|
43
|
+
return { ok: false, missing: ['spec-card'], guidance: MATTPOCOCK_PLANNING_GUIDANCE };
|
|
44
|
+
if (card.status === 'approved')
|
|
45
|
+
return { ok: true, card };
|
|
46
|
+
const missing = validateSpecCardForApproval(card);
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
return { ok: false, missing, guidance: MATTPOCOCK_PLANNING_GUIDANCE };
|
|
49
|
+
}
|
|
50
|
+
const approved = await service.approveSpecCard(specCardId, 'human');
|
|
51
|
+
return { ok: true, card: approved };
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
2
|
+
import type { PlanningChecklist } from '../domain/planning-checklist.js';
|
|
3
|
+
export interface PrefixRouteResult {
|
|
4
|
+
kind: 'plan' | 'openspec' | 'none';
|
|
5
|
+
chainId?: string;
|
|
6
|
+
specCardId?: string;
|
|
7
|
+
rest: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parsePrefix(message: string, cfg: {
|
|
10
|
+
plan: string;
|
|
11
|
+
openspec: string;
|
|
12
|
+
}): PrefixRouteResult;
|
|
13
|
+
/** v2:/plan: 零副作用——不建链/规格卡/任务卡,仅返回路由结果(workspaceDir/sessionId 由 main-session-tools 捕获)。 */
|
|
14
|
+
export declare function handlePlanRoute(message: string, _service: KanbanService, cfg: {
|
|
15
|
+
plan: string;
|
|
16
|
+
openspec: string;
|
|
17
|
+
}, _ownerSessionId: string): Promise<PrefixRouteResult>;
|
|
18
|
+
export interface OpenspecPlanningInput {
|
|
19
|
+
workspaceDir: string | null;
|
|
20
|
+
checklist: PlanningChecklist;
|
|
21
|
+
checklistRef: string;
|
|
22
|
+
/** T7:/plan: rest 原始需求描述;null=无 /plan: 捕获(回退 checklist.problem 首句/未命名需求)。 */
|
|
23
|
+
requirementName?: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** v2:/openspec: 建链——从清单机械映射规格卡六段 → 挂 file-prefetch(仓库 localPath)+kb(清单页) → 批准 → executing。 */
|
|
26
|
+
export declare function handleOpenspecRoute(message: string, service: KanbanService, cfg: {
|
|
27
|
+
plan: string;
|
|
28
|
+
openspec: string;
|
|
29
|
+
}, planning: OpenspecPlanningInput, ownerSessionId: string): Promise<PrefixRouteResult>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { KanbanService, buildChainTitle } from '../domain/kanban-service.js';
|
|
2
|
+
export function parsePrefix(message, cfg) {
|
|
3
|
+
const trimmed = message.trim();
|
|
4
|
+
if (trimmed.startsWith(cfg.plan))
|
|
5
|
+
return { kind: 'plan', rest: trimmed.slice(cfg.plan.length).trim() };
|
|
6
|
+
if (trimmed.startsWith(cfg.openspec))
|
|
7
|
+
return { kind: 'openspec', rest: trimmed.slice(cfg.openspec.length).trim() };
|
|
8
|
+
return { kind: 'none', rest: trimmed };
|
|
9
|
+
}
|
|
10
|
+
/** v2:/plan: 零副作用——不建链/规格卡/任务卡,仅返回路由结果(workspaceDir/sessionId 由 main-session-tools 捕获)。 */
|
|
11
|
+
export async function handlePlanRoute(message, _service, cfg, _ownerSessionId) {
|
|
12
|
+
return parsePrefix(message, cfg);
|
|
13
|
+
}
|
|
14
|
+
/** v2:/openspec: 建链——从清单机械映射规格卡六段 → 挂 file-prefetch(仓库 localPath)+kb(清单页) → 批准 → executing。 */
|
|
15
|
+
export async function handleOpenspecRoute(message, service, cfg, planning, ownerSessionId) {
|
|
16
|
+
const parsed = parsePrefix(message, cfg);
|
|
17
|
+
if (parsed.kind !== 'openspec')
|
|
18
|
+
return parsed;
|
|
19
|
+
const chain = await service.createChain({
|
|
20
|
+
title: buildChainTitle(planning.checklist.requirementName ?? planning.requirementName ?? null, parsed.rest, planning.checklist.spec.problem),
|
|
21
|
+
ownerSessionId, workspaceDir: planning.workspaceDir,
|
|
22
|
+
}, 'human');
|
|
23
|
+
const card = await service.createSpecCard(chain.id, planning.checklist.spec, 'human');
|
|
24
|
+
await service.addSpecCardAttachment(card.id, { name: '需求澄清清单(仓库事实)', kind: 'file-prefetch', ref: planning.checklist.manifest.repo.localPath }, 'v');
|
|
25
|
+
await service.addSpecCardAttachment(card.id, { name: '需求澄清清单(完整资料)', kind: 'kb', ref: planning.checklistRef }, 'v');
|
|
26
|
+
await service.approveSpecCard(card.id, 'human');
|
|
27
|
+
return { kind: 'openspec', chainId: chain.id, specCardId: card.id, rest: parsed.rest };
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import type { KanbanConfig } from '../config.js';
|
|
4
|
+
declare module '@deepseek-ai/cordis' {
|
|
5
|
+
interface Context {
|
|
6
|
+
kanban: KanbanProvider;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export declare class KanbanProvider extends Service {
|
|
10
|
+
readonly service: KanbanService;
|
|
11
|
+
/** T32 fix:GUI retry 的任务执行器(由 startDispatcher 装配后注入;webServer 先于 agents 就绪时可为 null)。 */
|
|
12
|
+
runner: {
|
|
13
|
+
runTask(taskId: string): Promise<void>;
|
|
14
|
+
} | null;
|
|
15
|
+
constructor(ctx: Context, config: KanbanConfig);
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
4
|
+
import { FileEventStore } from '../domain/event-store.js';
|
|
5
|
+
export class KanbanProvider extends Service {
|
|
6
|
+
service;
|
|
7
|
+
/** T32 fix:GUI retry 的任务执行器(由 startDispatcher 装配后注入;webServer 先于 agents 就绪时可为 null)。 */
|
|
8
|
+
runner = null;
|
|
9
|
+
constructor(ctx, config) {
|
|
10
|
+
super(ctx, 'kanban');
|
|
11
|
+
const dir = config.storageDir.replace('$DSH_HOME', process.env.DSH_HOME ?? homedir());
|
|
12
|
+
this.service = new KanbanService(new FileEventStore(dir), config.wikiVault?.baseUrl);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
2
|
+
import { type Actor } from '../domain/permissions.js';
|
|
3
|
+
/** 工具执行上下文:由角色 agent scope 注入;主会话=human;调度器=system。
|
|
4
|
+
* boundTaskId:角色 agent 会话绑定的任务(AgentSessionRef.task_id,P1-4 会话绑定)。 */
|
|
5
|
+
export interface ToolCaller {
|
|
6
|
+
actor: Actor;
|
|
7
|
+
boundTaskId?: string;
|
|
8
|
+
}
|
|
9
|
+
/** 工具定义工厂(P1-3):不直接注册;由 T15 toolsets 按角色 agent-scope 装配,或由主会话注册其专属子集。
|
|
10
|
+
* getCaller:装配方提供的闭包(捕获 actor/boundTaskId),execute 内取用——不依赖 execute 第二参数(DSH ToolRunContext 真实形状以官方为准)。 */
|
|
11
|
+
export declare function buildKanbanTools(service: KanbanService, getCaller: () => ToolCaller): import("@deepseek-ai/dsh-tools").ToolDefinition[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { KanbanService } from '../domain/kanban-service.js';
|
|
3
|
+
import { can } from '../domain/permissions.js';
|
|
4
|
+
function guard(action, caller, task = null) {
|
|
5
|
+
if (!can(action, caller.actor, task))
|
|
6
|
+
throw new Error('permission denied: ' + action);
|
|
7
|
+
}
|
|
8
|
+
/** 工具定义工厂(P1-3):不直接注册;由 T15 toolsets 按角色 agent-scope 装配,或由主会话注册其专属子集。
|
|
9
|
+
* getCaller:装配方提供的闭包(捕获 actor/boundTaskId),execute 内取用——不依赖 execute 第二参数(DSH ToolRunContext 真实形状以官方为准)。 */
|
|
10
|
+
export function buildKanbanTools(service, getCaller) {
|
|
11
|
+
return [
|
|
12
|
+
defineTool({
|
|
13
|
+
name: 'kanban_show',
|
|
14
|
+
description: 'Show a kanban task with its handoffs and comments.',
|
|
15
|
+
parameters: {
|
|
16
|
+
taskId: { type: 'string', required: true, description: 'Task id (t_xxx)' },
|
|
17
|
+
},
|
|
18
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
19
|
+
async execute(args) {
|
|
20
|
+
const caller = getCaller();
|
|
21
|
+
guard('comment', caller);
|
|
22
|
+
const state = await service.snapshot();
|
|
23
|
+
const task = state.tasks.get(args.taskId);
|
|
24
|
+
if (!task)
|
|
25
|
+
throw new Error('unknown task: ' + args.taskId);
|
|
26
|
+
const handoff = state.handoffs.get(args.taskId) ?? null;
|
|
27
|
+
const comments = state.events.filter((e) => e.taskId === args.taskId && e.kind === 'task/commented').map((e) => e.payload['body']);
|
|
28
|
+
return { task, handoff, comments };
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
defineTool({
|
|
32
|
+
name: 'kanban_chain',
|
|
33
|
+
description: 'Show a chain\u2019s full status at a glance: chain + spec card + all task cards (with handoffs/comments) + audit warning. Read-only inspection for requirement-chain tracing.',
|
|
34
|
+
parameters: {
|
|
35
|
+
chainId: { type: 'string', required: true, description: 'Chain id (ch_xxx)' },
|
|
36
|
+
},
|
|
37
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
38
|
+
async execute(args) {
|
|
39
|
+
const caller = getCaller();
|
|
40
|
+
guard('comment', caller);
|
|
41
|
+
const state = await service.snapshot();
|
|
42
|
+
const chain = state.chains.get(args.chainId);
|
|
43
|
+
if (!chain)
|
|
44
|
+
throw new Error('unknown chain: ' + args.chainId);
|
|
45
|
+
const specCard = state.specCards.get(chain.specCardId ?? '') ?? null;
|
|
46
|
+
const tasks = [...state.tasks.values()]
|
|
47
|
+
.filter((t) => t.chainId === args.chainId)
|
|
48
|
+
.sort((a, b) => a.priority - b.priority)
|
|
49
|
+
.map((t) => ({
|
|
50
|
+
id: t.id,
|
|
51
|
+
title: t.title,
|
|
52
|
+
assignee: t.assignee,
|
|
53
|
+
mode: t.mode,
|
|
54
|
+
status: t.status,
|
|
55
|
+
parents: t.parents,
|
|
56
|
+
children: t.children,
|
|
57
|
+
attempts: t.attempts,
|
|
58
|
+
blockedReason: state.events
|
|
59
|
+
.filter((e) => e.taskId === t.id && e.kind === 'task/blocked')
|
|
60
|
+
.at(-1)?.payload['reason'] ?? null,
|
|
61
|
+
comments: state.events
|
|
62
|
+
.filter((e) => e.taskId === t.id && e.kind === 'task/commented')
|
|
63
|
+
.map((e) => e.payload['body']),
|
|
64
|
+
handoff: state.handoffs.get(t.id) ?? null,
|
|
65
|
+
}));
|
|
66
|
+
const auditWarning = state.auditWarnings.get(args.chainId) ?? null;
|
|
67
|
+
return { chain, specCard, tasks, auditWarning };
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
defineTool({
|
|
71
|
+
name: 'kanban_create',
|
|
72
|
+
description: 'Create a kanban task (orchestrator V or human only).',
|
|
73
|
+
parameters: {
|
|
74
|
+
chainId: { type: 'string', required: true, description: 'Chain id (ch_xxx)' },
|
|
75
|
+
title: { type: 'string', required: true },
|
|
76
|
+
body: { type: 'string', description: 'Task body; handoffs are injected via parents, not body' },
|
|
77
|
+
assignee: { type: 'string', enum: ['v', 'p', 'w', 'd', 'pt', 'dt'], required: true },
|
|
78
|
+
mode: { type: 'string', enum: ['file', 'external', 'kb', 'openspec', 'mattpocock', 'align', 'execute', 'review-plan', 'review-impl'], required: true },
|
|
79
|
+
parents: { type: 'array', items: { type: 'string' } },
|
|
80
|
+
},
|
|
81
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
82
|
+
async execute(args) {
|
|
83
|
+
const caller = getCaller();
|
|
84
|
+
guard('create-task', caller);
|
|
85
|
+
return (await service.createTask(args, caller.actor));
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
defineTool({
|
|
89
|
+
name: 'kanban_complete',
|
|
90
|
+
description: 'Complete a task with handoff summary and metadata.',
|
|
91
|
+
parameters: {
|
|
92
|
+
taskId: { type: 'string', required: true },
|
|
93
|
+
summary: { type: 'string', required: true, description: 'Human-readable completion summary' },
|
|
94
|
+
metadata: { type: 'json', description: 'Machine-readable handoff: changed_files/verification/kb_url...' },
|
|
95
|
+
},
|
|
96
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
97
|
+
async execute(args) {
|
|
98
|
+
const caller = getCaller();
|
|
99
|
+
const done = await service.completeTask(args.taskId, { summary: args.summary, metadata: args.metadata ?? {}, completedAt: Date.now() }, caller.actor, { boundTaskId: caller.boundTaskId });
|
|
100
|
+
return done;
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
defineTool({
|
|
104
|
+
name: 'kanban_block',
|
|
105
|
+
description: 'Block a task with a reason (needs human input).',
|
|
106
|
+
parameters: {
|
|
107
|
+
taskId: { type: 'string', required: true },
|
|
108
|
+
reason: { type: 'string', required: true },
|
|
109
|
+
},
|
|
110
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
111
|
+
async execute(args) {
|
|
112
|
+
const caller = getCaller();
|
|
113
|
+
const blocked = await service.blockTask(args.taskId, args.reason, caller.actor, { boundTaskId: caller.boundTaskId });
|
|
114
|
+
return blocked;
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
defineTool({
|
|
118
|
+
name: 'kanban_heartbeat',
|
|
119
|
+
description: 'Signal liveness during a long task.',
|
|
120
|
+
parameters: { taskId: { type: 'string', required: true } },
|
|
121
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
122
|
+
async execute(args) {
|
|
123
|
+
const caller = getCaller();
|
|
124
|
+
const beat = await service.heartbeat(args.taskId, caller.actor, { boundTaskId: caller.boundTaskId });
|
|
125
|
+
return beat;
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
defineTool({
|
|
129
|
+
name: 'kanban_comment',
|
|
130
|
+
description: 'Append a persistent comment to a task thread.',
|
|
131
|
+
parameters: {
|
|
132
|
+
taskId: { type: 'string', required: true },
|
|
133
|
+
body: { type: 'string', required: true },
|
|
134
|
+
},
|
|
135
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
136
|
+
async execute(args) {
|
|
137
|
+
const caller = getCaller();
|
|
138
|
+
const ev = await service.comment(args.taskId, args.body, caller.actor);
|
|
139
|
+
return ev;
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
defineTool({
|
|
143
|
+
name: 'kanban_list',
|
|
144
|
+
description: 'List tasks filtered by assignee/status.',
|
|
145
|
+
parameters: {
|
|
146
|
+
assignee: { type: 'string', enum: ['v', 'p', 'w', 'd', 'pt', 'dt'] },
|
|
147
|
+
status: { type: 'string' },
|
|
148
|
+
},
|
|
149
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
150
|
+
async execute(args) {
|
|
151
|
+
const caller = getCaller();
|
|
152
|
+
guard('comment', caller);
|
|
153
|
+
const list = await service.listTasks(args);
|
|
154
|
+
return list;
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
defineTool({
|
|
158
|
+
name: 'kanban_unblock',
|
|
159
|
+
description: 'Unblock a task (human only).',
|
|
160
|
+
parameters: { taskId: { type: 'string', required: true } },
|
|
161
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: JSON.stringify(v) }] },
|
|
162
|
+
async execute(args) {
|
|
163
|
+
const caller = getCaller();
|
|
164
|
+
const task = await service.unblockTask(args.taskId, caller.actor);
|
|
165
|
+
return task;
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
];
|
|
169
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { KanbanConfig } from '../config.js';
|
|
3
|
+
import { type PlanningToolDeps } from './planning-tools.js';
|
|
4
|
+
import type { PlanningChecklist } from '../domain/planning-checklist.js';
|
|
5
|
+
/** v2 规划上下文(/plan: 捕获 → planning_checklist_save 回写 → /openspec: 建链)。模块级内存,随插件进程存活。 */
|
|
6
|
+
export interface PlanningContext {
|
|
7
|
+
workspaceDir: string | null;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
checklist: PlanningChecklist | null;
|
|
10
|
+
checklistRef: string | null;
|
|
11
|
+
checklistSource: 'kb' | 'temp' | null;
|
|
12
|
+
/** T7:/plan: rest 原始需求描述(建链默认标题来源,优先级最高)。 */
|
|
13
|
+
requirementName: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare const planningBySession: Map<string, PlanningContext>;
|
|
16
|
+
export declare function buildSpawnPrefetch(ctx: Context): PlanningToolDeps['spawnPrefetch'] | undefined;
|
|
17
|
+
/** v2 主会话工具面:/plan: 捕获规划上下文(零副作用)→ planning_checklist_save 回写 → /openspec: 用清单建链。
|
|
18
|
+
* 工具面 = kanban_route + 只读 kanban 子集 + spec_card_view + planning 工具;
|
|
19
|
+
* 无 spec_card_edit/approve、无 kanban_create/complete/block(主会话越权写由工具面裁剪 + prefetch 子代理只读护栏双保险)。 */
|
|
20
|
+
export declare function registerMainSessionTools(ctx: Context, config: KanbanConfig): void;
|