@goodandready/dsh-kanban 0.1.14
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 +455 -0
- package/cordis.patch.yml +4 -0
- package/lib/board-tool.js +178 -0
- package/lib/client.js +3197 -0
- package/lib/commands.js +167 -0
- package/lib/config.js +170 -0
- package/lib/filters.js +243 -0
- package/lib/gitea.js +309 -0
- package/lib/import.js +214 -0
- package/lib/index.js +764 -0
- package/lib/intake.js +94 -0
- package/lib/launcher.js +432 -0
- package/lib/lifecycle.js +32 -0
- package/lib/markdown.js +161 -0
- package/lib/order.js +66 -0
- package/lib/outbound.js +88 -0
- package/lib/plan.js +153 -0
- package/lib/routes.js +256 -0
- package/lib/store.js +366 -0
- package/lib/sync.js +369 -0
- package/lib/time.js +45 -0
- package/lib/transitions.js +162 -0
- package/lib/webhook.js +54 -0
- package/package.json +89 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Определение инструмента board_move — отдельно от обвязки cordis, чтобы
|
|
2
|
+
// проверяться без харнесса и без `@deepseek-ai/dsh-tools`.
|
|
3
|
+
//
|
|
4
|
+
// Форма объекта задана контрактом `defineTool` из dsh-tools, и она НЕ является
|
|
5
|
+
// JSON Schema:
|
|
6
|
+
//
|
|
7
|
+
// parameters — карта «имя -> схема значения», без корня `type: 'object'`;
|
|
8
|
+
// необязательность выражается ОТСУТСТВИЕМ `required: true`
|
|
9
|
+
// (`required: false`, `optional` и `nullable` отвергаются);
|
|
10
|
+
// output — обязателен: `defineTool` читает `options.output.render`
|
|
11
|
+
// безусловно, и без него падает не инструмент, а загрузка
|
|
12
|
+
// всего профиля.
|
|
13
|
+
//
|
|
14
|
+
// Схема объекта требует явного `additionalProperties`; здесь результат —
|
|
15
|
+
// строка, поэтому схема строковая.
|
|
16
|
+
|
|
17
|
+
import { COLUMN_ORDER as COLUMNS } from './config.js'
|
|
18
|
+
import { resolveTransition } from './transitions.js'
|
|
19
|
+
import { TOOL_FORBIDDEN_COLUMNS } from './commands.js'
|
|
20
|
+
import { parsePlan, applyPlan, serializePlan, planProgress, MAX_ITEMS } from './plan.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Колонки, доступные инструменту.
|
|
24
|
+
*
|
|
25
|
+
* `done` вырезан из ПЕРЕЧИСЛЕНИЯ, а не только из проверки: подсказывать модели
|
|
26
|
+
* значение, которое она получит отказом, — приглашение его попробовать.
|
|
27
|
+
*/
|
|
28
|
+
const TOOL_COLUMNS = COLUMNS.filter((c) => !TOOL_FORBIDDEN_COLUMNS.includes(c))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Идентификатор сессии из контекста выполнения инструмента.
|
|
32
|
+
*
|
|
33
|
+
* dsh-tools переехал с `ctx.session` на `ctx.agent.session`. Читаем оба:
|
|
34
|
+
* промах здесь не роняет ничего, он молча превращает инструмент в отказ
|
|
35
|
+
* «за этой сессией нет карточки» — а такое ищут долго.
|
|
36
|
+
*/
|
|
37
|
+
export function sessionIdFromExec(exec) {
|
|
38
|
+
return String(exec?.agent?.session?.id ?? exec?.session?.id ?? '')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Задача сессии, с которой работает инструмент.
|
|
43
|
+
*
|
|
44
|
+
* Одна сессия может вести пачку. Пока задача одна — называть нечего. Как
|
|
45
|
+
* только их несколько, инструмент ОБЯЗАН сказать, какую двигает: догадка тут
|
|
46
|
+
* означает подвинутую не ту карточку, и заметят это нескоро.
|
|
47
|
+
*
|
|
48
|
+
* Ссылку принимаем в том виде, в каком агент её видит: `repo#номер` либо
|
|
49
|
+
* идентификатор карточки.
|
|
50
|
+
*
|
|
51
|
+
* @returns {{task: object}|{error: string}}
|
|
52
|
+
*/
|
|
53
|
+
export function pickTask(store, exec, ref) {
|
|
54
|
+
const tasks = store.listTasksBySession(sessionIdFromExec(exec))
|
|
55
|
+
if (tasks.length === 0) return { error: 'This session has no kanban task behind it.' }
|
|
56
|
+
|
|
57
|
+
const wanted = String(ref ?? '').trim()
|
|
58
|
+
if (wanted === '') {
|
|
59
|
+
if (tasks.length === 1) return { task: tasks[0] }
|
|
60
|
+
return {
|
|
61
|
+
error: 'This session carries several tasks. Name the one you mean in `task`: '
|
|
62
|
+
+ tasks.map(nameOf).join(', ') + '.',
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const found = tasks.find((t) => t.id === wanted || nameOf(t) === wanted)
|
|
67
|
+
if (found === undefined) {
|
|
68
|
+
return {
|
|
69
|
+
error: `No task \`${wanted}\` in this session. Available: ` + tasks.map(nameOf).join(', ') + '.',
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { task: found }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Как задача называется для агента: так же, как он видит её в сообщении. */
|
|
76
|
+
function nameOf(task) {
|
|
77
|
+
return task.repo && typeof task.issueNumber === 'number'
|
|
78
|
+
? `${task.repo}#${task.issueNumber}`
|
|
79
|
+
: task.id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Определение board_move, готовое к передаче в `defineTool`. */
|
|
83
|
+
export function boardMoveDefinition({ store }) {
|
|
84
|
+
return {
|
|
85
|
+
name: 'board_move',
|
|
86
|
+
description: 'Move the kanban card of the current task to another column. '
|
|
87
|
+
+ `Columns: ${TOOL_COLUMNS.join(', ')}. Use it when the work reaches a new stage. `
|
|
88
|
+
+ 'Finishing a task is not a move: close the issue and delete the branch, and the '
|
|
89
|
+
+ 'card reaches done by itself once the board sees that.',
|
|
90
|
+
parameters: {
|
|
91
|
+
column: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
enum: [...TOOL_COLUMNS],
|
|
94
|
+
description: 'Target column.',
|
|
95
|
+
required: true,
|
|
96
|
+
},
|
|
97
|
+
detail: { type: 'string', description: 'Short reason, shown in the task log.' },
|
|
98
|
+
task: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Which task to move, as `repo#number`. Required when this session '
|
|
101
|
+
+ 'carries several tasks; omit it when there is only one.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
output: {
|
|
105
|
+
schema: { type: 'string', description: 'Outcome of the move, in one sentence.' },
|
|
106
|
+
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
107
|
+
},
|
|
108
|
+
async execute({ column, detail, task: ref }, exec) {
|
|
109
|
+
const picked = pickTask(store, exec, ref)
|
|
110
|
+
if (picked.error !== undefined) return picked.error
|
|
111
|
+
const task = picked.task
|
|
112
|
+
// Перечисление уже не предлагает `done`, но модель может прислать его
|
|
113
|
+
// мимо схемы, а завершение задачи не должно опираться на заявление.
|
|
114
|
+
if (TOOL_FORBIDDEN_COLUMNS.includes(column)) {
|
|
115
|
+
return 'A task is not finished by moving its card. Close the issue and delete the '
|
|
116
|
+
+ 'branch; the card reaches done once the board sees that.'
|
|
117
|
+
}
|
|
118
|
+
const move = resolveTransition(task.column, [{ column, source: 'tool', detail: detail ?? '' }])
|
|
119
|
+
if (move === undefined) return `The card is already in ${task.column}.`
|
|
120
|
+
store.moveTask(task.id, { column: move.column })
|
|
121
|
+
store.addTransition({
|
|
122
|
+
taskId: task.id, fromCol: task.column, toCol: move.column,
|
|
123
|
+
source: 'tool', detail: move.detail,
|
|
124
|
+
})
|
|
125
|
+
return `Card moved to ${move.column}.`
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Определение board_plan, готовое к передаче в `defineTool`.
|
|
132
|
+
*
|
|
133
|
+
* План получаем прямо от агента, а не разбором его сообщений: разбор — это
|
|
134
|
+
* гадание, а чужой `todo_list` завёл бы зависимость от соседнего плагина.
|
|
135
|
+
*
|
|
136
|
+
* Список пунктов необязателен. Агент публикует план один раз, а дальше двигает
|
|
137
|
+
* только указатель: гонять весь список ради одного числа незачем.
|
|
138
|
+
*/
|
|
139
|
+
export function boardPlanDefinition({ store }) {
|
|
140
|
+
return {
|
|
141
|
+
name: 'board_plan',
|
|
142
|
+
description: 'Publish or update the plan shown on the kanban card of the current task. '
|
|
143
|
+
+ 'Send `items` once to publish the plan, then send only `current` as you move from '
|
|
144
|
+
+ 'step to step. Everything before `current` counts as done. '
|
|
145
|
+
+ `At most ${MAX_ITEMS} steps.`,
|
|
146
|
+
parameters: {
|
|
147
|
+
items: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
items: { type: 'string' },
|
|
150
|
+
description: 'The whole plan, one short entry per step. Omit to keep the current plan.',
|
|
151
|
+
},
|
|
152
|
+
current: {
|
|
153
|
+
type: 'number',
|
|
154
|
+
description: 'Number of the step in work, counting from 1. '
|
|
155
|
+
+ 'One past the last step means the plan is finished.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
output: {
|
|
159
|
+
schema: { type: 'string', description: 'Outcome of the update, in one sentence.' },
|
|
160
|
+
render: (_args, value) => [{ type: 'text', text: String(value) }],
|
|
161
|
+
},
|
|
162
|
+
async execute({ items, current }, exec) {
|
|
163
|
+
// План ОДИН на сессию: это план работы над пачкой, а не над каждой
|
|
164
|
+
// задачей порознь. Поэтому пишем его всем задачам сессии — иначе на
|
|
165
|
+
// карточках второй и третьей плана не будет вовсе.
|
|
166
|
+
const tasks = store.listTasksBySession(sessionIdFromExec(exec))
|
|
167
|
+
if (tasks.length === 0) return 'This session has no kanban task behind it.'
|
|
168
|
+
const task = tasks[0]
|
|
169
|
+
const plan = applyPlan(parsePlan(task.plan), { items, current })
|
|
170
|
+
if (plan.items.length === 0) return 'A plan needs at least one step.'
|
|
171
|
+
for (const one of tasks) store.updateTask(one.id, { plan: serializePlan(plan) })
|
|
172
|
+
const progress = planProgress(plan)
|
|
173
|
+
return progress.text === ''
|
|
174
|
+
? `Plan of ${progress.total} steps recorded; no step is marked as current.`
|
|
175
|
+
: `Step ${progress.current} of ${progress.total}: ${progress.text}.`
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|