@goodandready/dsh-kanban 0.2.2 → 0.2.8
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 +24 -0
- package/{docs/README.ru.md → README.ru.md} +10 -0
- package/{docs/README.zh.md → README.zh.md} +21 -0
- package/lib/board-tool.js +151 -0
- package/lib/client.js +1020 -763
- package/lib/commands.js +6 -11
- package/lib/config.js +31 -29
- package/lib/events.js +2 -2
- package/lib/filters.js +2 -2
- package/lib/gitea.js +15 -0
- package/lib/index.js +171 -142
- package/lib/power.js +1 -1
- package/lib/progress-dump.js +3 -3
- package/lib/routes.js +403 -5
- package/lib/store.js +59 -9
- package/lib/sync.js +26 -3
- package/lib/task-service.js +56 -0
- package/lib/updater.js +338 -0
- package/lib/worktree.js +1 -1
- package/package.json +7 -3
- package/docs/design/DESIGN.md +0 -94
- package/docs/plans/2026-08-26-dsh-kanban-0.1.md +0 -169
- package/docs/plans/2026-09-07-quality-acceptance-gateway.md +0 -25
- package/docs/plans/2026-09-08-concurrency-safety.md +0 -8
- package/docs/plans/2026-09-08-diff-viewer-merge.md +0 -7
- package/docs/plans/2026-09-08-session-worktree-stability.md +0 -33
- package/docs/plans/2026-09-10-issue-238-settings-scope.md +0 -13
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerPluginUpdater } from './updater.js'
|
|
1
2
|
// dsh-kanban — доска задач для DeepSeek Harness.
|
|
2
3
|
//
|
|
3
4
|
// Плагин состоит из двух половин: этот модуль — серверная, `lib/client.js` —
|
|
@@ -9,21 +10,44 @@
|
|
|
9
10
|
// `routes.js`) и проверяется тестами без харнесса. Здесь остаётся обвязка
|
|
10
11
|
// cordis: настройки, хранилище и маршруты.
|
|
11
12
|
|
|
12
|
-
import { homedir } from 'node:os'
|
|
13
|
-
import { join } from 'node:path'
|
|
14
|
-
import { randomUUID } from 'node:crypto'
|
|
15
|
-
import z from '@deepseek-ai/schemastery'
|
|
16
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
17
|
-
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
18
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
19
|
-
import { CONFIG_DEFAULTS, CONFIG_HINTS, withDefaults, rootOf } from './config.js'
|
|
20
|
-
import { openStore } from './store.js'
|
|
21
13
|
import {
|
|
22
|
-
MAX_BODY_BYTES,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
MAX_BODY_BYTES,
|
|
15
|
+
isTrustedRequest,
|
|
16
|
+
parseTaskPath,
|
|
17
|
+
buildBoard,
|
|
18
|
+
applyMove,
|
|
19
|
+
createTask,
|
|
20
|
+
updateTask,
|
|
26
21
|
confirmPermission,
|
|
22
|
+
appendNote,
|
|
23
|
+
setArchived,
|
|
24
|
+
listArchive,
|
|
25
|
+
assignTask,
|
|
26
|
+
deleteTask,
|
|
27
|
+
boardMetrics,
|
|
28
|
+
taskBody,
|
|
29
|
+
taskLog,
|
|
30
|
+
taskBySession,
|
|
31
|
+
exportBoard,
|
|
32
|
+
importBoard,
|
|
33
|
+
exportTasksCsv,
|
|
34
|
+
handleCleanupWorktree,
|
|
35
|
+
handleTaskDiff,
|
|
36
|
+
handleTaskCommits,
|
|
37
|
+
handleTaskMerge,
|
|
38
|
+
handleTaskWorktreeStatus,
|
|
39
|
+
handleTaskChecklist,
|
|
40
|
+
handleTaskReport,
|
|
41
|
+
handleGetTaskComments,
|
|
42
|
+
handleAddTaskComment,
|
|
43
|
+
handleAcceptTask,
|
|
44
|
+
handleRejectTask,
|
|
45
|
+
handleProgressDump,
|
|
46
|
+
handleTriageTask,
|
|
47
|
+
handleGetSubtasks,
|
|
48
|
+
handleCreateSubtask,
|
|
49
|
+
handleToggleChecklistItem,
|
|
50
|
+
handleCreateTaskPr,
|
|
27
51
|
} from './routes.js'
|
|
28
52
|
import { listImportable, importIssue, refreshTask, searchRepos, createProjectTask } from './import.js'
|
|
29
53
|
import {
|
|
@@ -34,6 +58,7 @@ import {
|
|
|
34
58
|
import { cleanupTaskWorktree, checkWorktreeDirty, getTaskDiff, getTaskCommits, mergeTaskWorktree } from './worktree.js'
|
|
35
59
|
import { dispatchMove, MOVE_DETAIL, stopWork, STOP_DETAIL } from './commands.js'
|
|
36
60
|
import { createGiteaClient } from './gitea.js'
|
|
61
|
+
import { createKanbanTaskService } from './task-service.js'
|
|
37
62
|
import { syncAll, createSyncState, intakeAll, archiveOverdue } from './sync.js'
|
|
38
63
|
import { isWatched } from './intake.js'
|
|
39
64
|
import { handleSessionEvent } from './lifecycle.js'
|
|
@@ -44,6 +69,8 @@ import {
|
|
|
44
69
|
boardReportDefinition,
|
|
45
70
|
boardCommentsDefinition,
|
|
46
71
|
boardCommentAddDefinition,
|
|
72
|
+
boardDecomposeDefinition,
|
|
73
|
+
boardChecklistItemDefinition,
|
|
47
74
|
} from './board-tool.js'
|
|
48
75
|
import { verifySignature, parseEvent } from './webhook.js'
|
|
49
76
|
import { planOutbound, planAssign, createOutbox } from './outbound.js'
|
|
@@ -51,13 +78,26 @@ import { computeNextRun } from './cron.js'
|
|
|
51
78
|
import { PowerInhibitor } from './power.js'
|
|
52
79
|
import { formatProgressDump, parseProgressDump } from './progress-dump.js'
|
|
53
80
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
81
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
82
|
+
import z from '@deepseek-ai/schemastery'
|
|
83
|
+
import { join } from 'node:path'
|
|
84
|
+
import { homedir } from 'node:os'
|
|
85
|
+
import { openStore } from './store.js'
|
|
86
|
+
import {
|
|
87
|
+
COLUMN_ORDER,
|
|
88
|
+
wipLimitField,
|
|
89
|
+
withDefaults,
|
|
90
|
+
CONFIG_DEFAULTS,
|
|
91
|
+
CONFIG_HINTS,
|
|
92
|
+
rootOf,
|
|
93
|
+
} from './config.js'
|
|
54
94
|
import { eventHub } from './events.js'
|
|
55
95
|
import { TASK_TEMPLATES } from './templates.js'
|
|
56
96
|
|
|
57
97
|
export { COLUMN_ORDER, wipLimitField, withDefaults } from './config.js'
|
|
58
98
|
|
|
59
99
|
/** Стабильное имя плагина cordis (строка в патче сборки). */
|
|
60
|
-
export const name = 'dsh-kanban'
|
|
100
|
+
export const name = '@goodandready/dsh-kanban'
|
|
61
101
|
|
|
62
102
|
/**
|
|
63
103
|
* Службы добавляются там, где впервые нужны: `gitea` — вместе с импортом
|
|
@@ -97,14 +137,14 @@ export const Config = z.object(Object.fromEntries(
|
|
|
97
137
|
|
|
98
138
|
/** Каталог хранилища. Абсолютных путей в коде нет — только переменная и дом. */
|
|
99
139
|
export function storeDir() {
|
|
100
|
-
return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'kanban')
|
|
140
|
+
return process.env.DSH_KANBAN_DIR || join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'kanban')
|
|
101
141
|
}
|
|
102
142
|
|
|
103
143
|
function writeJson(res, code, body) {
|
|
104
144
|
try {
|
|
105
145
|
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
106
146
|
res.end(JSON.stringify(body))
|
|
107
|
-
} catch { /*
|
|
147
|
+
} catch { /* socket closed */ }
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
/** Ответ обработчика: либо объект с `error` и `status`, либо полезная нагрузка. */
|
|
@@ -123,7 +163,7 @@ function readBody(req) {
|
|
|
123
163
|
req.on('data', (c) => {
|
|
124
164
|
size += c.length
|
|
125
165
|
if (size > MAX_BODY_BYTES) {
|
|
126
|
-
reject(Object.assign(new Error('
|
|
166
|
+
reject(Object.assign(new Error('Payload too large'), { status: 413 }))
|
|
127
167
|
req.destroy()
|
|
128
168
|
return
|
|
129
169
|
}
|
|
@@ -132,7 +172,7 @@ function readBody(req) {
|
|
|
132
172
|
req.on('end', () => {
|
|
133
173
|
const raw = Buffer.concat(chunks).toString('utf8')
|
|
134
174
|
if (raw.trim() === '') { resolve({}); return }
|
|
135
|
-
try { resolve(JSON.parse(raw)) } catch { reject(Object.assign(new Error('
|
|
175
|
+
try { resolve(JSON.parse(raw)) } catch { reject(Object.assign(new Error('Malformed JSON payload'), { status: 400 })) }
|
|
136
176
|
})
|
|
137
177
|
req.on('error', reject)
|
|
138
178
|
})
|
|
@@ -152,7 +192,7 @@ function readRaw(req) {
|
|
|
152
192
|
req.on('data', (c) => {
|
|
153
193
|
size += c.length
|
|
154
194
|
if (size > MAX_BODY_BYTES) {
|
|
155
|
-
reject(Object.assign(new Error('
|
|
195
|
+
reject(Object.assign(new Error('Payload too large'), { status: 413 }))
|
|
156
196
|
req.destroy()
|
|
157
197
|
return
|
|
158
198
|
}
|
|
@@ -177,7 +217,7 @@ export function apply(ctx, config) {
|
|
|
177
217
|
const rearmers = new Set()
|
|
178
218
|
const rearm = () => {
|
|
179
219
|
for (const again of rearmers) {
|
|
180
|
-
try { again() } catch (error) { ctx.logger?.warn?.(`dsh-kanban:
|
|
220
|
+
try { again() } catch (error) { ctx.logger?.warn?.(`dsh-kanban: rearm failed: ${error?.message}`) }
|
|
181
221
|
}
|
|
182
222
|
}
|
|
183
223
|
|
|
@@ -185,7 +225,7 @@ export function apply(ctx, config) {
|
|
|
185
225
|
const scope = sctx.settings.register(SETTINGS_NAMESPACE, Config, { base: effective })
|
|
186
226
|
const read = () => withDefaults(scope.get() ?? effective)
|
|
187
227
|
effective = read()
|
|
188
|
-
ctx.effect(() => scope.watch(() => { effective = read(); rearm() }), 'dsh-kanban:
|
|
228
|
+
ctx.effect(() => scope.watch(() => { effective = read(); rearm() }), 'dsh-kanban: settings watcher')
|
|
189
229
|
})
|
|
190
230
|
|
|
191
231
|
// Клиент Gitea свой: токен всё равно живёт в службе учётных данных ядра, а
|
|
@@ -209,10 +249,12 @@ export function apply(ctx, config) {
|
|
|
209
249
|
// Хранилище открывается один раз и закрывается уборщиком того же эффекта:
|
|
210
250
|
// выгрузка плагина обязана отпустить файл базы.
|
|
211
251
|
let store
|
|
252
|
+
const taskService = createKanbanTaskService({ getStore: () => store })
|
|
253
|
+
if (typeof ctx.provide === 'function') ctx.provide('dshKanban', taskService)
|
|
212
254
|
ctx.effect(() => {
|
|
213
|
-
store = openStore({ dir: storeDir() })
|
|
214
|
-
return () => { try { store.close() } catch { /*
|
|
215
|
-
}, 'dsh-kanban:
|
|
255
|
+
store = openStore({ dir: config?.storeDir || storeDir() })
|
|
256
|
+
return () => { try { store.close() } catch { /* already closed */ } }
|
|
257
|
+
}, 'dsh-kanban: task store')
|
|
216
258
|
|
|
217
259
|
// Кроссплатформенный ингибитор сна системы во время работы задач
|
|
218
260
|
const powerInhibitor = new PowerInhibitor({
|
|
@@ -228,19 +270,19 @@ export function apply(ctx, config) {
|
|
|
228
270
|
const activeCount = store.countInColumn('main', 'in-progress') + store.countInColumn('simple', 'in-progress')
|
|
229
271
|
powerInhibitor.setReasons(activeCount)
|
|
230
272
|
}
|
|
231
|
-
} catch { /*
|
|
273
|
+
} catch { /* ignore */ }
|
|
232
274
|
}
|
|
233
275
|
rearmers.add(syncPower)
|
|
234
276
|
ctx.effect(() => {
|
|
235
277
|
syncPower()
|
|
236
278
|
return () => { powerInhibitor.destroy() }
|
|
237
|
-
}, 'dsh-kanban:
|
|
279
|
+
}, 'dsh-kanban: power inhibitor')
|
|
238
280
|
|
|
239
281
|
// Инъекция секции доски в системный промпт агента
|
|
240
282
|
let disposePromptSection
|
|
241
283
|
const syncPromptSection = () => {
|
|
242
284
|
if (disposePromptSection) {
|
|
243
|
-
try { disposePromptSection() } catch {}
|
|
285
|
+
try { disposePromptSection() } catch { /* cleanup best effort */ }
|
|
244
286
|
disposePromptSection = undefined
|
|
245
287
|
}
|
|
246
288
|
const cfg = getConfig()
|
|
@@ -250,18 +292,18 @@ export function apply(ctx, config) {
|
|
|
250
292
|
disposePromptSection = sp.section({
|
|
251
293
|
name: 'plugin:dsh-kanban',
|
|
252
294
|
order: 200,
|
|
253
|
-
text: '
|
|
295
|
+
text: 'The dsh-kanban task board is available in the system. When working on tasks, you can move cards using the board_move tool and manage the plan checklist via board_plan. When context is running low or a milestone is reached, output a <<<PROGRESSDUMP\nGoal: ...\nProgress: ...\nNext steps: ...\n>>>PROGRESSDUMP block to hand over to the next session.',
|
|
254
296
|
})
|
|
255
297
|
} catch (err) {
|
|
256
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
298
|
+
ctx.logger?.warn?.(`dsh-kanban: failed to register system prompt section: ${err?.message}`)
|
|
257
299
|
}
|
|
258
300
|
}
|
|
259
301
|
}
|
|
260
302
|
rearmers.add(syncPromptSection)
|
|
261
303
|
ctx.effect(() => {
|
|
262
304
|
syncPromptSection()
|
|
263
|
-
return () => { if (disposePromptSection) { try { disposePromptSection() } catch {} } }
|
|
264
|
-
}, 'dsh-kanban:
|
|
305
|
+
return () => { if (disposePromptSection) { try { disposePromptSection() } catch { /* cleanup best effort */ } } }
|
|
306
|
+
}, 'dsh-kanban: system prompt section')
|
|
265
307
|
|
|
266
308
|
// Признак ожидания, исходы сессий и PROGRESSDUMP по событиям сессии.
|
|
267
309
|
ctx.effect(() => ctx.on('session/event', (session, event) => {
|
|
@@ -281,10 +323,10 @@ export function apply(ctx, config) {
|
|
|
281
323
|
let existingTask = store?.findTaskBySession?.(sessionIdStr)
|
|
282
324
|
if (!existingTask && (event?.type === 'turn/start' || event?.type === 'session/created' || event?.type === 'input/user' || event?.type === 'message')) {
|
|
283
325
|
const userText = (event?.content?.[0]?.text ?? event?.text ?? '').trim()
|
|
284
|
-
const sessionTitle = session.title || session.name || (userText ? userText.slice(0, 50) :
|
|
326
|
+
const sessionTitle = session.title || session.name || (userText ? userText.slice(0, 50) : `Dialog ${sessionIdStr.slice(0, 8)}`)
|
|
285
327
|
const createdTask = store.createTask({
|
|
286
328
|
title: sessionTitle,
|
|
287
|
-
body: userText ||
|
|
329
|
+
body: userText || `Automatically created from chat session ${sessionIdStr}`,
|
|
288
330
|
column: 'in-progress',
|
|
289
331
|
board: 'main',
|
|
290
332
|
sessionId: sessionIdStr,
|
|
@@ -334,9 +376,9 @@ export function apply(ctx, config) {
|
|
|
334
376
|
}).catch(() => {})
|
|
335
377
|
}
|
|
336
378
|
} catch (error) {
|
|
337
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
379
|
+
ctx.logger?.warn?.(`dsh-kanban: session event handler failed: ${error?.message}`)
|
|
338
380
|
}
|
|
339
|
-
}), 'dsh-kanban:
|
|
381
|
+
}), 'dsh-kanban: session events')
|
|
340
382
|
|
|
341
383
|
// Фоновый шедулер cron для задач
|
|
342
384
|
ctx.effect(() => {
|
|
@@ -367,7 +409,7 @@ export function apply(ctx, config) {
|
|
|
367
409
|
fromCol: task.column,
|
|
368
410
|
toCol: task.column,
|
|
369
411
|
source: 'tool',
|
|
370
|
-
detail: 'cron:
|
|
412
|
+
detail: 'cron: run rejected (elevated permission required)',
|
|
371
413
|
})
|
|
372
414
|
continue
|
|
373
415
|
}
|
|
@@ -396,7 +438,7 @@ export function apply(ctx, config) {
|
|
|
396
438
|
store.updateTask(task.id, { lastRunAt: now, nextRunAt: next || 0 })
|
|
397
439
|
}
|
|
398
440
|
} catch (err) {
|
|
399
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
441
|
+
ctx.logger?.warn?.(`dsh-kanban: cron scheduler error: ${err?.message}`)
|
|
400
442
|
}
|
|
401
443
|
}, 30_000)
|
|
402
444
|
if (typeof cronTimer.unref === 'function') cronTimer.unref()
|
|
@@ -404,7 +446,7 @@ export function apply(ctx, config) {
|
|
|
404
446
|
armCron()
|
|
405
447
|
rearmers.add(armCron)
|
|
406
448
|
return () => { rearmers.delete(armCron); clearInterval(cronTimer) }
|
|
407
|
-
}, 'dsh-kanban:
|
|
449
|
+
}, 'dsh-kanban: cron scheduler')
|
|
408
450
|
|
|
409
451
|
// Периодическая сверка с Gitea. Ноль в настройках отключает её целиком, а
|
|
410
452
|
// перекрытие запусков не допускается: медленный инстанс иначе копил бы
|
|
@@ -474,7 +516,7 @@ export function apply(ctx, config) {
|
|
|
474
516
|
} catch (error) {
|
|
475
517
|
// Сверка не начиналась вовсе — тоже беда, и своя: так выглядит
|
|
476
518
|
// неразрешённый токен или отсутствующий клиент Gitea.
|
|
477
|
-
syncState.failedToStart('
|
|
519
|
+
syncState.failedToStart('sync', error?.message)
|
|
478
520
|
throw error
|
|
479
521
|
} finally {
|
|
480
522
|
syncing = false
|
|
@@ -494,7 +536,7 @@ export function apply(ctx, config) {
|
|
|
494
536
|
arm()
|
|
495
537
|
rearmers.add(arm)
|
|
496
538
|
return () => { rearmers.delete(arm); clearInterval(timer) }
|
|
497
|
-
}, 'dsh-kanban:
|
|
539
|
+
}, 'dsh-kanban: sync timer')
|
|
498
540
|
|
|
499
541
|
ctx.effect(() => ctx.webServer.register({
|
|
500
542
|
kind: 'exact',
|
|
@@ -515,7 +557,7 @@ export function apply(ctx, config) {
|
|
|
515
557
|
writeJson(res, error?.status ?? 500, { error: 'bad-request' })
|
|
516
558
|
}
|
|
517
559
|
},
|
|
518
|
-
}), 'dsh-kanban:
|
|
560
|
+
}), 'dsh-kanban: project task')
|
|
519
561
|
|
|
520
562
|
ctx.effect(() => ctx.webServer.register({
|
|
521
563
|
kind: 'exact',
|
|
@@ -535,7 +577,7 @@ export function apply(ctx, config) {
|
|
|
535
577
|
id: row.id, name: row.name || row.id, description: row.description,
|
|
536
578
|
}))
|
|
537
579
|
} catch (error) {
|
|
538
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
580
|
+
ctx.logger?.warn?.(`dsh-kanban: agent profiles failed to load: ${error?.message}`)
|
|
539
581
|
}
|
|
540
582
|
let access = []
|
|
541
583
|
let accessDefault = ''
|
|
@@ -545,7 +587,7 @@ export function apply(ctx, config) {
|
|
|
545
587
|
accessDefault = permissions.defaultPreset ?? ''
|
|
546
588
|
}
|
|
547
589
|
} catch (error) {
|
|
548
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
590
|
+
ctx.logger?.warn?.(`dsh-kanban: access levels failed to load: ${error?.message}`)
|
|
549
591
|
}
|
|
550
592
|
reply(res, {
|
|
551
593
|
agentPresets,
|
|
@@ -554,13 +596,13 @@ export function apply(ctx, config) {
|
|
|
554
596
|
accessDefault,
|
|
555
597
|
})
|
|
556
598
|
},
|
|
557
|
-
}), 'dsh-kanban:
|
|
599
|
+
}), 'dsh-kanban: agent profiles and auth scopes')
|
|
558
600
|
|
|
559
601
|
ctx.effect(() => ctx.webServer.register({
|
|
560
602
|
kind: 'exact',
|
|
561
603
|
path: '/dsh-kanban/sessions',
|
|
562
604
|
handler: (req, res) => { reply(res, { sessions: liveSessions({ store, agents: ctx.agents }) }) },
|
|
563
|
-
}), 'dsh-kanban:
|
|
605
|
+
}), 'dsh-kanban: live sessions')
|
|
564
606
|
|
|
565
607
|
ctx.effect(() => ctx.webServer.register({
|
|
566
608
|
kind: 'exact',
|
|
@@ -592,13 +634,13 @@ export function apply(ctx, config) {
|
|
|
592
634
|
writeJson(res, error?.status ?? 500, { error: 'batch-failed' })
|
|
593
635
|
}
|
|
594
636
|
},
|
|
595
|
-
}), 'dsh-kanban:
|
|
637
|
+
}), 'dsh-kanban: batch dispatch')
|
|
596
638
|
|
|
597
639
|
ctx.effect(() => ctx.webServer.register({
|
|
598
640
|
kind: 'exact',
|
|
599
641
|
path: '/dsh-kanban/archive',
|
|
600
642
|
handler: (req, res) => { reply(res, listArchive({ store })) },
|
|
601
|
-
}), 'dsh-kanban:
|
|
643
|
+
}), 'dsh-kanban: archive')
|
|
602
644
|
|
|
603
645
|
ctx.effect(() => ctx.webServer.register({
|
|
604
646
|
kind: 'exact',
|
|
@@ -612,7 +654,7 @@ export function apply(ctx, config) {
|
|
|
612
654
|
writeJson(res, 502, { error: 'sync-failed' })
|
|
613
655
|
}
|
|
614
656
|
},
|
|
615
|
-
}), 'dsh-kanban:
|
|
657
|
+
}), 'dsh-kanban: manual sync')
|
|
616
658
|
|
|
617
659
|
ctx.effect(() => ctx.webServer.register({
|
|
618
660
|
kind: 'exact',
|
|
@@ -664,7 +706,7 @@ export function apply(ctx, config) {
|
|
|
664
706
|
writeJson(res, error?.status ?? 500, { error: 'webhook-failed' })
|
|
665
707
|
}
|
|
666
708
|
},
|
|
667
|
-
}), 'dsh-kanban:
|
|
709
|
+
}), 'dsh-kanban: gitea webhook')
|
|
668
710
|
|
|
669
711
|
// Инструмент для агента. По умолчанию ВЫКЛЮЧЕН: скилл воркфлоу пока
|
|
670
712
|
// запрещает этому CLI трогать канбан, и включить инструмент раньше правки
|
|
@@ -688,6 +730,8 @@ export function apply(ctx, config) {
|
|
|
688
730
|
ctx.tools.register(defineTool(boardReportDefinition({ store }))),
|
|
689
731
|
ctx.tools.register(defineTool(boardCommentsDefinition({ store }))),
|
|
690
732
|
ctx.tools.register(defineTool(boardCommentAddDefinition({ store }))),
|
|
733
|
+
ctx.tools.register(defineTool(boardDecomposeDefinition({ store, gitea }))),
|
|
734
|
+
ctx.tools.register(defineTool(boardChecklistItemDefinition({ store }))),
|
|
691
735
|
]
|
|
692
736
|
return () => { for (const stop of off) stop?.() }
|
|
693
737
|
}
|
|
@@ -695,7 +739,7 @@ export function apply(ctx, config) {
|
|
|
695
739
|
arm()
|
|
696
740
|
rearmers.add(arm)
|
|
697
741
|
return () => { rearmers.delete(arm); unregister?.() }
|
|
698
|
-
}, 'dsh-kanban:
|
|
742
|
+
}, 'dsh-kanban: board tool')
|
|
699
743
|
|
|
700
744
|
/**
|
|
701
745
|
* Состояние живого агента сессии либо `undefined`, если агента нет.
|
|
@@ -721,7 +765,7 @@ export function apply(ctx, config) {
|
|
|
721
765
|
projectRoot: rootOf(getConfig()),
|
|
722
766
|
}))
|
|
723
767
|
},
|
|
724
|
-
}), 'dsh-kanban:
|
|
768
|
+
}), 'dsh-kanban: board route')
|
|
725
769
|
|
|
726
770
|
// Снимок доски — ОДИН ресурс с двумя действиями, а не две ручки.
|
|
727
771
|
//
|
|
@@ -760,7 +804,7 @@ export function apply(ctx, config) {
|
|
|
760
804
|
staleDays: Number.isFinite(days) && days > 0 ? days : 14,
|
|
761
805
|
}))
|
|
762
806
|
},
|
|
763
|
-
}), 'dsh-kanban:
|
|
807
|
+
}), 'dsh-kanban: board metrics')
|
|
764
808
|
|
|
765
809
|
ctx.effect(() => ctx.webServer.register({
|
|
766
810
|
kind: 'exact',
|
|
@@ -768,7 +812,7 @@ export function apply(ctx, config) {
|
|
|
768
812
|
// Пустой логин — законный ответ: Gitea может быть не настроен или не
|
|
769
813
|
// отвечать. Браузер по нему просто не рисует «Мои», а не показывает ноль.
|
|
770
814
|
handler: async (req, res) => { reply(res, { login: await whoAmI() }) },
|
|
771
|
-
}), 'dsh-kanban:
|
|
815
|
+
}), 'dsh-kanban: whoami')
|
|
772
816
|
|
|
773
817
|
ctx.effect(() => ctx.webServer.register({
|
|
774
818
|
kind: 'exact',
|
|
@@ -784,7 +828,7 @@ export function apply(ctx, config) {
|
|
|
784
828
|
})
|
|
785
829
|
res.end(csv)
|
|
786
830
|
},
|
|
787
|
-
}), 'dsh-kanban:
|
|
831
|
+
}), 'dsh-kanban: csv export')
|
|
788
832
|
|
|
789
833
|
ctx.effect(() => ctx.webServer.register({
|
|
790
834
|
kind: 'exact',
|
|
@@ -792,7 +836,7 @@ export function apply(ctx, config) {
|
|
|
792
836
|
handler: (req, res) => {
|
|
793
837
|
eventHub.handleSseRequest(req, res)
|
|
794
838
|
},
|
|
795
|
-
}), 'dsh-kanban:
|
|
839
|
+
}), 'dsh-kanban: sse events stream')
|
|
796
840
|
|
|
797
841
|
ctx.effect(() => ctx.webServer.register({
|
|
798
842
|
kind: 'exact',
|
|
@@ -800,7 +844,7 @@ export function apply(ctx, config) {
|
|
|
800
844
|
handler: (req, res) => {
|
|
801
845
|
reply(res, { templates: TASK_TEMPLATES })
|
|
802
846
|
},
|
|
803
|
-
}), 'dsh-kanban:
|
|
847
|
+
}), 'dsh-kanban: task templates')
|
|
804
848
|
|
|
805
849
|
ctx.effect(() => ctx.webServer.register({
|
|
806
850
|
kind: 'exact',
|
|
@@ -816,7 +860,7 @@ export function apply(ctx, config) {
|
|
|
816
860
|
writeJson(res, 400, { error: 'bad-request' })
|
|
817
861
|
}
|
|
818
862
|
},
|
|
819
|
-
}), 'dsh-kanban:
|
|
863
|
+
}), 'dsh-kanban: board snapshot')
|
|
820
864
|
|
|
821
865
|
ctx.effect(() => ctx.webServer.register({
|
|
822
866
|
// Префикс регистрируется БЕЗ завершающего слэша: со слэшем сопоставление
|
|
@@ -902,7 +946,7 @@ export function apply(ctx, config) {
|
|
|
902
946
|
syncPower()
|
|
903
947
|
reply(res, started)
|
|
904
948
|
} catch (error) {
|
|
905
|
-
ctx.logger?.warn?.(`dsh-kanban:
|
|
949
|
+
ctx.logger?.warn?.(`dsh-kanban: task launch failed: ${error?.message}`)
|
|
906
950
|
// Отказ по уровню доступа называется своим именем: «не удалось» на
|
|
907
951
|
// месте «сессия пошла бы с большими правами» — это молчание.
|
|
908
952
|
writeJson(res, error?.status ?? 500, { error: error?.key ?? 'start-failed' })
|
|
@@ -949,112 +993,93 @@ export function apply(ctx, config) {
|
|
|
949
993
|
return
|
|
950
994
|
}
|
|
951
995
|
if (req.method === 'POST' && action === 'cleanup-worktree') {
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
const out = await cleanupTaskWorktree({
|
|
955
|
-
store,
|
|
956
|
-
task,
|
|
996
|
+
const resObj = await handleCleanupWorktree({
|
|
997
|
+
store, task: store.getTask(id),
|
|
957
998
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
958
999
|
force: input?.force === true,
|
|
1000
|
+
cleanupTaskWorktree,
|
|
959
1001
|
})
|
|
960
|
-
|
|
1002
|
+
writeJson(res, resObj.status, resObj.body)
|
|
961
1003
|
return
|
|
962
1004
|
}
|
|
963
1005
|
if (req.method === 'GET' && action === 'diff') {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
const diffResult = await getTaskDiff({ worktreePath: task.worktree })
|
|
967
|
-
reply(res, diffResult)
|
|
1006
|
+
const resObj = await handleTaskDiff({ task: store.getTask(id), getTaskDiff })
|
|
1007
|
+
writeJson(res, resObj.status, resObj.body)
|
|
968
1008
|
return
|
|
969
1009
|
}
|
|
970
1010
|
if (req.method === 'GET' && action === 'commits') {
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
const commits = await getTaskCommits({ worktreePath: task.worktree })
|
|
974
|
-
reply(res, { commits })
|
|
1011
|
+
const resObj = await handleTaskCommits({ task: store.getTask(id), getTaskCommits })
|
|
1012
|
+
writeJson(res, resObj.status, resObj.body)
|
|
975
1013
|
return
|
|
976
1014
|
}
|
|
977
1015
|
if (req.method === 'POST' && action === 'merge') {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
task,
|
|
982
|
-
targetBranch: typeof input?.targetBranch === 'string' ? input.targetBranch.trim() : undefined,
|
|
1016
|
+
const resObj = await handleTaskMerge({
|
|
1017
|
+
store, task: store.getTask(id),
|
|
1018
|
+
targetBranch: input?.targetBranch,
|
|
983
1019
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
1020
|
+
mergeTaskWorktree,
|
|
984
1021
|
})
|
|
985
|
-
|
|
986
|
-
writeJson(res, 400, { error: mergeResult.error, detail: mergeResult.detail, files: mergeResult.files })
|
|
987
|
-
return
|
|
988
|
-
}
|
|
989
|
-
const updated = store.acceptTask(id)
|
|
990
|
-
store.addTransition({
|
|
991
|
-
taskId: id,
|
|
992
|
-
fromCol: task.column,
|
|
993
|
-
toCol: 'done',
|
|
994
|
-
source: 'manual',
|
|
995
|
-
detail: `принята и смержена ветка ${task.branch} в ${mergeResult.targetBranch}`,
|
|
996
|
-
})
|
|
997
|
-
reply(res, { task: updated, merged: true, targetBranch: mergeResult.targetBranch })
|
|
1022
|
+
writeJson(res, resObj.status, resObj.body)
|
|
998
1023
|
return
|
|
999
1024
|
}
|
|
1000
1025
|
if (req.method === 'GET' && action === 'worktree-status') {
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
const dirtyCheck = await checkWorktreeDirty({ worktreePath: task.worktree })
|
|
1004
|
-
reply(res, {
|
|
1005
|
-
worktree: task.worktree || '',
|
|
1006
|
-
branch: task.branch || '',
|
|
1007
|
-
workspaceId: task.workspaceId || '',
|
|
1008
|
-
dirty: dirtyCheck.dirty,
|
|
1009
|
-
files: dirtyCheck.files,
|
|
1010
|
-
})
|
|
1026
|
+
const resObj = await handleTaskWorktreeStatus({ task: store.getTask(id), checkWorktreeDirty })
|
|
1027
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1011
1028
|
return
|
|
1012
1029
|
}
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1030
|
+
if (req.method === 'GET' && action === 'subtasks') {
|
|
1031
|
+
const resObj = handleGetSubtasks({ store, task: store.getTask(id) })
|
|
1032
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
if (req.method === 'POST' && action === 'triage') {
|
|
1036
|
+
const resObj = await handleTriageTask({ store, gitea, task: store.getTask(id), input })
|
|
1037
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
if (req.method === 'POST' && (action === 'subtask' || action === 'subtasks')) {
|
|
1041
|
+
const resObj = await handleCreateSubtask({ store, gitea, task: store.getTask(id), input })
|
|
1042
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
if (req.method === 'POST' && action === 'checklist-toggle') {
|
|
1046
|
+
const resObj = handleToggleChecklistItem({ store, task: store.getTask(id), input })
|
|
1047
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
if (req.method === 'POST' && action === 'pr') {
|
|
1051
|
+
const resObj = await handleCreateTaskPr({ store, gitea, task: store.getTask(id), input })
|
|
1052
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
if (req.method === 'POST' && action === 'checklist') {
|
|
1056
|
+
const resObj = handleTaskChecklist({ store, task: store.getTask(id), input })
|
|
1057
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1019
1058
|
return
|
|
1020
1059
|
}
|
|
1021
1060
|
if (req.method === 'POST' && action === 'report') {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
const updated = store.setReport(id, input)
|
|
1025
|
-
reply(res, { task: updated })
|
|
1061
|
+
const resObj = handleTaskReport({ store, task: store.getTask(id), input })
|
|
1062
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1026
1063
|
return
|
|
1027
1064
|
}
|
|
1028
1065
|
if (req.method === 'GET' && action === 'comments') {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
reply(res, { comments: task.comments || [] })
|
|
1066
|
+
const resObj = handleGetTaskComments({ task: store.getTask(id) })
|
|
1067
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1032
1068
|
return
|
|
1033
1069
|
}
|
|
1034
1070
|
if (req.method === 'POST' && action === 'comments') {
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
const author = input?.author || 'user'
|
|
1038
|
-
const role = input?.role || 'user'
|
|
1039
|
-
const text = input?.text || ''
|
|
1040
|
-
const updated = store.addComment(id, { author, role, text })
|
|
1041
|
-
reply(res, { task: updated })
|
|
1071
|
+
const resObj = handleAddTaskComment({ store, task: store.getTask(id), input })
|
|
1072
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1042
1073
|
return
|
|
1043
1074
|
}
|
|
1044
1075
|
if (req.method === 'POST' && action === 'accept') {
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
const updated = store.acceptTask(id)
|
|
1048
|
-
reply(res, { task: updated })
|
|
1076
|
+
const resObj = handleAcceptTask({ store, task: store.getTask(id) })
|
|
1077
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1049
1078
|
return
|
|
1050
1079
|
}
|
|
1051
1080
|
if (req.method === 'POST' && action === 'reject') {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
const reason = String(input?.reason || '').trim()
|
|
1055
|
-
if (!reason) { writeJson(res, 400, { error: 'reason-required' }); return }
|
|
1056
|
-
const updated = store.rejectTask(id, reason)
|
|
1057
|
-
reply(res, { task: updated })
|
|
1081
|
+
const resObj = handleRejectTask({ store, task: store.getTask(id), input })
|
|
1082
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1058
1083
|
return
|
|
1059
1084
|
}
|
|
1060
1085
|
if (req.method === 'POST' && action === 'resume') {
|
|
@@ -1080,7 +1105,7 @@ export function apply(ctx, config) {
|
|
|
1080
1105
|
text: input?.text,
|
|
1081
1106
|
agentPreset: input?.agentPreset || task.agentPreset,
|
|
1082
1107
|
permission: input?.permission || task.permission,
|
|
1083
|
-
permissions: ctx.
|
|
1108
|
+
permissions: ctx.get('permissionPresets'),
|
|
1084
1109
|
claimAs: req.headers['x-dsh-user'],
|
|
1085
1110
|
gitRunner,
|
|
1086
1111
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
@@ -1095,11 +1120,8 @@ export function apply(ctx, config) {
|
|
|
1095
1120
|
return
|
|
1096
1121
|
}
|
|
1097
1122
|
if (req.method === 'POST' && action === 'progress-dump') {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
const formatted = typeof input === 'string' ? input : formatProgressDump(input)
|
|
1101
|
-
const updated = store.updateTask(id, { progressDump: formatted })
|
|
1102
|
-
reply(res, { task: updated })
|
|
1123
|
+
const resObj = handleProgressDump({ store, task: store.getTask(id), input, formatProgressDump })
|
|
1124
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1103
1125
|
return
|
|
1104
1126
|
}
|
|
1105
1127
|
if (req.method === 'POST' && action === 'stop') {
|
|
@@ -1174,7 +1196,7 @@ export function apply(ctx, config) {
|
|
|
1174
1196
|
writeJson(res, error?.status ?? 500, { error: 'bad-request' })
|
|
1175
1197
|
}
|
|
1176
1198
|
},
|
|
1177
|
-
}), 'dsh-kanban:
|
|
1199
|
+
}), 'dsh-kanban: task routes')
|
|
1178
1200
|
|
|
1179
1201
|
// Готовность Gitea проверяется на каждом запросе, а не при загрузке: адрес и
|
|
1180
1202
|
// учётная запись правятся в карточке настроек на ходу, и маршруты обязаны
|
|
@@ -1186,7 +1208,7 @@ export function apply(ctx, config) {
|
|
|
1186
1208
|
const url = urlOf(req)
|
|
1187
1209
|
reply(res, await searchRepos({ gitea, query: url.searchParams.get('q') || '' }))
|
|
1188
1210
|
},
|
|
1189
|
-
}), 'dsh-kanban:
|
|
1211
|
+
}), 'dsh-kanban: repo search')
|
|
1190
1212
|
|
|
1191
1213
|
ctx.effect(() => ctx.webServer.register({
|
|
1192
1214
|
kind: 'exact',
|
|
@@ -1201,7 +1223,7 @@ export function apply(ctx, config) {
|
|
|
1201
1223
|
board: url.searchParams.get('board') || 'main',
|
|
1202
1224
|
}))
|
|
1203
1225
|
},
|
|
1204
|
-
}), 'dsh-kanban:
|
|
1226
|
+
}), 'dsh-kanban: importable issues')
|
|
1205
1227
|
|
|
1206
1228
|
ctx.effect(() => ctx.webServer.register({
|
|
1207
1229
|
kind: 'exact',
|
|
@@ -1220,7 +1242,7 @@ export function apply(ctx, config) {
|
|
|
1220
1242
|
writeJson(res, error?.status ?? 500, { error: 'bad-request' })
|
|
1221
1243
|
}
|
|
1222
1244
|
},
|
|
1223
|
-
}), 'dsh-kanban:
|
|
1245
|
+
}), 'dsh-kanban: issue import')
|
|
1224
1246
|
|
|
1225
1247
|
// Провайдеры отдаются сразу, модели — только для запрошенного провайдера:
|
|
1226
1248
|
// перечень моделей провайдер отдаёт по сети, и опрашивать все разом ради
|
|
@@ -1251,7 +1273,7 @@ export function apply(ctx, config) {
|
|
|
1251
1273
|
writeJson(res, 200, { current, providers, models: [] })
|
|
1252
1274
|
}
|
|
1253
1275
|
},
|
|
1254
|
-
}), 'dsh-kanban:
|
|
1276
|
+
}), 'dsh-kanban: providers and models')
|
|
1255
1277
|
|
|
1256
1278
|
ctx.effect(() => ctx.webServer.register({
|
|
1257
1279
|
kind: 'prefix',
|
|
@@ -1261,5 +1283,12 @@ export function apply(ctx, config) {
|
|
|
1261
1283
|
if (m === null) { writeJson(res, 404, { error: 'not-found' }); return }
|
|
1262
1284
|
reply(res, taskBySession({ store, sessionId: decodeURIComponent(m[1]) }))
|
|
1263
1285
|
},
|
|
1264
|
-
}), 'dsh-kanban:
|
|
1286
|
+
}), 'dsh-kanban: task route by session')
|
|
1287
|
+
|
|
1288
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
1289
|
+
endpoint: '/api/dsh-kanban/update',
|
|
1290
|
+
packageName: '@goodandready/dsh-kanban',
|
|
1291
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
1292
|
+
}), 'dsh-kanban: plugin updater route')
|
|
1293
|
+
|
|
1265
1294
|
}
|