@goodandready/dsh-kanban 0.2.1 → 0.2.7
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 +10 -0
- package/{docs/README.ru.md → README.ru.md} +10 -0
- package/{docs/README.zh.md → README.zh.md} +10 -0
- package/lib/board-tool.js +151 -0
- package/lib/client.js +862 -739
- package/lib/gitea.js +15 -0
- package/lib/index.js +112 -93
- package/lib/routes.js +357 -1
- package/lib/store.js +42 -4
- package/lib/sync.js +24 -1
- package/package.json +3 -2
- package/docs/design/DESIGN.md +0 -93
- 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/lib/gitea.js
CHANGED
|
@@ -287,6 +287,21 @@ export function createGiteaClient({ getConfig, resolveToken, fetchImpl = fetch,
|
|
|
287
287
|
})
|
|
288
288
|
},
|
|
289
289
|
|
|
290
|
+
/** Создать Pull Request в репозитории. */
|
|
291
|
+
async createPullRequest({ owner, repo, title, body, head, base = 'main' }) {
|
|
292
|
+
const name = String(title ?? '').trim()
|
|
293
|
+
if (name === '') throw Object.assign(new Error('пустой заголовок'), { code: 'bad-title' })
|
|
294
|
+
const h = String(head ?? '').trim()
|
|
295
|
+
const b = String(base ?? 'main').trim()
|
|
296
|
+
if (!h) throw Object.assign(new Error('ветка head обязательна'), { code: 'bad-head' })
|
|
297
|
+
return request('POST', repoPath(owner, repo, '/pulls'), {
|
|
298
|
+
title: name,
|
|
299
|
+
body: String(body ?? ''),
|
|
300
|
+
head: h,
|
|
301
|
+
base: b,
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
|
|
290
305
|
/**
|
|
291
306
|
* Ветки репозитория: по ним видно, начата ли работа и сделан ли cleanup.
|
|
292
307
|
*
|
package/lib/index.js
CHANGED
|
@@ -9,21 +9,44 @@
|
|
|
9
9
|
// `routes.js`) и проверяется тестами без харнесса. Здесь остаётся обвязка
|
|
10
10
|
// cordis: настройки, хранилище и маршруты.
|
|
11
11
|
|
|
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
12
|
import {
|
|
22
|
-
MAX_BODY_BYTES,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
MAX_BODY_BYTES,
|
|
14
|
+
isTrustedRequest,
|
|
15
|
+
parseTaskPath,
|
|
16
|
+
buildBoard,
|
|
17
|
+
applyMove,
|
|
18
|
+
createTask,
|
|
19
|
+
updateTask,
|
|
26
20
|
confirmPermission,
|
|
21
|
+
appendNote,
|
|
22
|
+
setArchived,
|
|
23
|
+
listArchive,
|
|
24
|
+
assignTask,
|
|
25
|
+
deleteTask,
|
|
26
|
+
boardMetrics,
|
|
27
|
+
taskBody,
|
|
28
|
+
taskLog,
|
|
29
|
+
taskBySession,
|
|
30
|
+
exportBoard,
|
|
31
|
+
importBoard,
|
|
32
|
+
exportTasksCsv,
|
|
33
|
+
handleCleanupWorktree,
|
|
34
|
+
handleTaskDiff,
|
|
35
|
+
handleTaskCommits,
|
|
36
|
+
handleTaskMerge,
|
|
37
|
+
handleTaskWorktreeStatus,
|
|
38
|
+
handleTaskChecklist,
|
|
39
|
+
handleTaskReport,
|
|
40
|
+
handleGetTaskComments,
|
|
41
|
+
handleAddTaskComment,
|
|
42
|
+
handleAcceptTask,
|
|
43
|
+
handleRejectTask,
|
|
44
|
+
handleProgressDump,
|
|
45
|
+
handleTriageTask,
|
|
46
|
+
handleGetSubtasks,
|
|
47
|
+
handleCreateSubtask,
|
|
48
|
+
handleToggleChecklistItem,
|
|
49
|
+
handleCreateTaskPr,
|
|
27
50
|
} from './routes.js'
|
|
28
51
|
import { listImportable, importIssue, refreshTask, searchRepos, createProjectTask } from './import.js'
|
|
29
52
|
import {
|
|
@@ -44,6 +67,8 @@ import {
|
|
|
44
67
|
boardReportDefinition,
|
|
45
68
|
boardCommentsDefinition,
|
|
46
69
|
boardCommentAddDefinition,
|
|
70
|
+
boardDecomposeDefinition,
|
|
71
|
+
boardChecklistItemDefinition,
|
|
47
72
|
} from './board-tool.js'
|
|
48
73
|
import { verifySignature, parseEvent } from './webhook.js'
|
|
49
74
|
import { planOutbound, planAssign, createOutbox } from './outbound.js'
|
|
@@ -51,6 +76,19 @@ import { computeNextRun } from './cron.js'
|
|
|
51
76
|
import { PowerInhibitor } from './power.js'
|
|
52
77
|
import { formatProgressDump, parseProgressDump } from './progress-dump.js'
|
|
53
78
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
79
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
80
|
+
import z from '@deepseek-ai/schemastery'
|
|
81
|
+
import { join } from 'node:path'
|
|
82
|
+
import { homedir } from 'node:os'
|
|
83
|
+
import { openStore } from './store.js'
|
|
84
|
+
import {
|
|
85
|
+
COLUMN_ORDER,
|
|
86
|
+
wipLimitField,
|
|
87
|
+
withDefaults,
|
|
88
|
+
CONFIG_DEFAULTS,
|
|
89
|
+
CONFIG_HINTS,
|
|
90
|
+
rootOf,
|
|
91
|
+
} from './config.js'
|
|
54
92
|
import { eventHub } from './events.js'
|
|
55
93
|
import { TASK_TEMPLATES } from './templates.js'
|
|
56
94
|
|
|
@@ -688,6 +726,8 @@ export function apply(ctx, config) {
|
|
|
688
726
|
ctx.tools.register(defineTool(boardReportDefinition({ store }))),
|
|
689
727
|
ctx.tools.register(defineTool(boardCommentsDefinition({ store }))),
|
|
690
728
|
ctx.tools.register(defineTool(boardCommentAddDefinition({ store }))),
|
|
729
|
+
ctx.tools.register(defineTool(boardDecomposeDefinition({ store, gitea }))),
|
|
730
|
+
ctx.tools.register(defineTool(boardChecklistItemDefinition({ store }))),
|
|
691
731
|
]
|
|
692
732
|
return () => { for (const stop of off) stop?.() }
|
|
693
733
|
}
|
|
@@ -949,112 +989,93 @@ export function apply(ctx, config) {
|
|
|
949
989
|
return
|
|
950
990
|
}
|
|
951
991
|
if (req.method === 'POST' && action === 'cleanup-worktree') {
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
const out = await cleanupTaskWorktree({
|
|
955
|
-
store,
|
|
956
|
-
task,
|
|
992
|
+
const resObj = await handleCleanupWorktree({
|
|
993
|
+
store, task: store.getTask(id),
|
|
957
994
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
958
995
|
force: input?.force === true,
|
|
996
|
+
cleanupTaskWorktree,
|
|
959
997
|
})
|
|
960
|
-
|
|
998
|
+
writeJson(res, resObj.status, resObj.body)
|
|
961
999
|
return
|
|
962
1000
|
}
|
|
963
1001
|
if (req.method === 'GET' && action === 'diff') {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
const diffResult = await getTaskDiff({ worktreePath: task.worktree })
|
|
967
|
-
reply(res, diffResult)
|
|
1002
|
+
const resObj = await handleTaskDiff({ task: store.getTask(id), getTaskDiff })
|
|
1003
|
+
writeJson(res, resObj.status, resObj.body)
|
|
968
1004
|
return
|
|
969
1005
|
}
|
|
970
1006
|
if (req.method === 'GET' && action === 'commits') {
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
const commits = await getTaskCommits({ worktreePath: task.worktree })
|
|
974
|
-
reply(res, { commits })
|
|
1007
|
+
const resObj = await handleTaskCommits({ task: store.getTask(id), getTaskCommits })
|
|
1008
|
+
writeJson(res, resObj.status, resObj.body)
|
|
975
1009
|
return
|
|
976
1010
|
}
|
|
977
1011
|
if (req.method === 'POST' && action === 'merge') {
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
task,
|
|
982
|
-
targetBranch: typeof input?.targetBranch === 'string' ? input.targetBranch.trim() : undefined,
|
|
1012
|
+
const resObj = await handleTaskMerge({
|
|
1013
|
+
store, task: store.getTask(id),
|
|
1014
|
+
targetBranch: input?.targetBranch,
|
|
983
1015
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
1016
|
+
mergeTaskWorktree,
|
|
984
1017
|
})
|
|
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 })
|
|
1018
|
+
writeJson(res, resObj.status, resObj.body)
|
|
998
1019
|
return
|
|
999
1020
|
}
|
|
1000
1021
|
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
|
-
})
|
|
1022
|
+
const resObj = await handleTaskWorktreeStatus({ task: store.getTask(id), checkWorktreeDirty })
|
|
1023
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1011
1024
|
return
|
|
1012
1025
|
}
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1026
|
+
if (req.method === 'GET' && action === 'subtasks') {
|
|
1027
|
+
const resObj = handleGetSubtasks({ store, task: store.getTask(id) })
|
|
1028
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1029
|
+
return
|
|
1030
|
+
}
|
|
1031
|
+
if (req.method === 'POST' && action === 'triage') {
|
|
1032
|
+
const resObj = await handleTriageTask({ store, gitea, task: store.getTask(id), input })
|
|
1033
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1034
|
+
return
|
|
1035
|
+
}
|
|
1036
|
+
if (req.method === 'POST' && (action === 'subtask' || action === 'subtasks')) {
|
|
1037
|
+
const resObj = await handleCreateSubtask({ store, gitea, task: store.getTask(id), input })
|
|
1038
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1039
|
+
return
|
|
1040
|
+
}
|
|
1041
|
+
if (req.method === 'POST' && action === 'checklist-toggle') {
|
|
1042
|
+
const resObj = handleToggleChecklistItem({ store, task: store.getTask(id), input })
|
|
1043
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
if (req.method === 'POST' && action === 'pr') {
|
|
1047
|
+
const resObj = await handleCreateTaskPr({ store, gitea, task: store.getTask(id), input })
|
|
1048
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
1051
|
+
if (req.method === 'POST' && action === 'checklist') {
|
|
1052
|
+
const resObj = handleTaskChecklist({ store, task: store.getTask(id), input })
|
|
1053
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1019
1054
|
return
|
|
1020
1055
|
}
|
|
1021
1056
|
if (req.method === 'POST' && action === 'report') {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
const updated = store.setReport(id, input)
|
|
1025
|
-
reply(res, { task: updated })
|
|
1057
|
+
const resObj = handleTaskReport({ store, task: store.getTask(id), input })
|
|
1058
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1026
1059
|
return
|
|
1027
1060
|
}
|
|
1028
1061
|
if (req.method === 'GET' && action === 'comments') {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
reply(res, { comments: task.comments || [] })
|
|
1062
|
+
const resObj = handleGetTaskComments({ task: store.getTask(id) })
|
|
1063
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1032
1064
|
return
|
|
1033
1065
|
}
|
|
1034
1066
|
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 })
|
|
1067
|
+
const resObj = handleAddTaskComment({ store, task: store.getTask(id), input })
|
|
1068
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1042
1069
|
return
|
|
1043
1070
|
}
|
|
1044
1071
|
if (req.method === 'POST' && action === 'accept') {
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
const updated = store.acceptTask(id)
|
|
1048
|
-
reply(res, { task: updated })
|
|
1072
|
+
const resObj = handleAcceptTask({ store, task: store.getTask(id) })
|
|
1073
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1049
1074
|
return
|
|
1050
1075
|
}
|
|
1051
1076
|
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 })
|
|
1077
|
+
const resObj = handleRejectTask({ store, task: store.getTask(id), input })
|
|
1078
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1058
1079
|
return
|
|
1059
1080
|
}
|
|
1060
1081
|
if (req.method === 'POST' && action === 'resume') {
|
|
@@ -1080,7 +1101,7 @@ export function apply(ctx, config) {
|
|
|
1080
1101
|
text: input?.text,
|
|
1081
1102
|
agentPreset: input?.agentPreset || task.agentPreset,
|
|
1082
1103
|
permission: input?.permission || task.permission,
|
|
1083
|
-
permissions: ctx.
|
|
1104
|
+
permissions: ctx.get('permissionPresets'),
|
|
1084
1105
|
claimAs: req.headers['x-dsh-user'],
|
|
1085
1106
|
gitRunner,
|
|
1086
1107
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
@@ -1095,11 +1116,8 @@ export function apply(ctx, config) {
|
|
|
1095
1116
|
return
|
|
1096
1117
|
}
|
|
1097
1118
|
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 })
|
|
1119
|
+
const resObj = handleProgressDump({ store, task: store.getTask(id), input, formatProgressDump })
|
|
1120
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1103
1121
|
return
|
|
1104
1122
|
}
|
|
1105
1123
|
if (req.method === 'POST' && action === 'stop') {
|
|
@@ -1230,16 +1248,17 @@ export function apply(ctx, config) {
|
|
|
1230
1248
|
path: '/dsh-kanban/models',
|
|
1231
1249
|
handler: async (req, res) => {
|
|
1232
1250
|
const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null
|
|
1251
|
+
const llmService = ctx.get('llm')
|
|
1233
1252
|
let providers = []
|
|
1234
1253
|
try {
|
|
1235
|
-
providers = (
|
|
1254
|
+
providers = ((llmService?.listProviders?.() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id })))
|
|
1236
1255
|
} catch { providers = [] }
|
|
1237
1256
|
|
|
1238
1257
|
const wanted = urlOf(req).searchParams.get('provider') || ''
|
|
1239
1258
|
if (wanted === '') { writeJson(res, 200, { current, providers, models: [] }); return }
|
|
1240
1259
|
|
|
1241
1260
|
try {
|
|
1242
|
-
const rows = await
|
|
1261
|
+
const rows = await llmService?.listModels?.(wanted)
|
|
1243
1262
|
writeJson(res, 200, {
|
|
1244
1263
|
current, providers,
|
|
1245
1264
|
models: (rows ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
|