@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/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, isTrustedRequest, parseTaskPath,
23
- buildBoard, applyMove, createTask, updateTask, deleteTask, appendNote, setArchived, listArchive,
24
- taskBody,
25
- taskLog, taskBySession, exportBoard, importBoard, exportTasksCsv, assignTask, boardMetrics,
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 task = store.getTask(id)
953
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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
- reply(res, out)
998
+ writeJson(res, resObj.status, resObj.body)
961
999
  return
962
1000
  }
963
1001
  if (req.method === 'GET' && action === 'diff') {
964
- const task = store.getTask(id)
965
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
972
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
979
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
980
- const mergeResult = await mergeTaskWorktree({
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
- if (!mergeResult.success) {
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 task = store.getTask(id)
1002
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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
- if (req.method === 'POST' && action === 'checklist') {
1014
- const task = store.getTask(id)
1015
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
1016
- const list = Array.isArray(input?.checklist) ? input.checklist : Array.isArray(input) ? input : []
1017
- const updated = store.updateChecklist(id, list)
1018
- reply(res, { task: updated })
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 task = store.getTask(id)
1023
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
1030
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
1036
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
1046
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 task = store.getTask(id)
1053
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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.permissions,
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 task = store.getTask(id)
1099
- if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
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 = (ctx.llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }))
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 ctx.llm.listModels(wanted)
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 })),