@goodandready/dsh-kanban 0.2.19 → 0.2.20
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/board-tool.js +18 -48
- package/lib/client.js +85 -176
- package/lib/commands.js +21 -38
- package/lib/config.js +29 -54
- package/lib/cron.js +13 -13
- package/lib/events.js +1 -1
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +51 -109
- package/lib/github.js +27 -31
- package/lib/import.js +46 -67
- package/lib/index.js +66 -132
- package/lib/intake.js +18 -26
- package/lib/launcher.js +100 -167
- package/lib/lifecycle.js +11 -17
- package/lib/markdown.js +16 -23
- package/lib/metrics.js +23 -25
- package/lib/order.js +25 -28
- package/lib/outbound.js +25 -26
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +86 -145
- package/lib/store.js +59 -77
- package/lib/sync.js +56 -98
- package/lib/time.js +8 -17
- package/lib/transitions.js +36 -57
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +57 -57
- package/lib/worktree.js +58 -58
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { registerPluginUpdater } from './updater.js'
|
|
2
|
-
// dsh-kanban —
|
|
2
|
+
// dsh-kanban — task board for DeepSeek Harness.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// Расхождение НЕ даёт ошибки в журнале — интерфейс просто не появляется.
|
|
4
|
+
// Plugin consists of two halves: this module is the backend server, and `lib/client.js` is the
|
|
5
|
+
// browser bundle. The package name `@goodandready/dsh-kanban` must match in three places:
|
|
6
|
+
// `package.json`, `cordis.patch.yml`, and `load({ id })` in client.js.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Pure domain logic is kept in standalone modules (`config.js`, `order.js`, `routes.js`)
|
|
9
|
+
// and tested without harness runtime. This module handles cordis integration:
|
|
10
|
+
// configuration, database storage, and HTTP endpoints.
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
13
|
MAX_BODY_BYTES,
|
|
@@ -102,35 +101,27 @@ import { TASK_TEMPLATES, getTemplateById } from './templates.js'
|
|
|
102
101
|
|
|
103
102
|
export { COLUMN_ORDER, wipLimitField, withDefaults } from './config.js'
|
|
104
103
|
|
|
105
|
-
/**
|
|
104
|
+
/** Stable cordis plugin name (patch identifier). */
|
|
106
105
|
export const name = '@goodandready/dsh-kanban'
|
|
107
106
|
|
|
108
107
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* отсутствующая служба не даёт плагину загрузиться вовсе.
|
|
108
|
+
* Services are injected where needed. Declaring an unavailable service prevents
|
|
109
|
+
* the plugin from loading entirely.
|
|
112
110
|
*/
|
|
113
111
|
export const inject = ['settings', 'webServer', 'agents', 'agentDefaultModel', 'credentials', 'llm', 'tools']
|
|
114
112
|
|
|
115
|
-
/**
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// В 0.1.1 ядро отдавало помощника `settingsNamespace`, который проверял имя по
|
|
119
|
-
// образцу и возвращал ту же строку с типом-меткой. В 0.1.2-alpha.2 помощника
|
|
120
|
-
// убрали, и импорт его роняет ЗАГРУЗКУ ВСЕГО ПРОФИЛЯ, а не один плагин.
|
|
121
|
-
// Проверять здесь нечего: имя — постоянная этого файла, а не пользовательский
|
|
122
|
-
// ввод. На обеих версиях ядра значение одно и то же.
|
|
113
|
+
/** Settings namespace string matching client bundle definition. */
|
|
114
|
+
// Constant string defined in this file; identical across core versions.
|
|
123
115
|
export const SETTINGS_NAMESPACE = 'dsh-kanban'
|
|
124
116
|
|
|
125
117
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
118
|
+
* Schema constructed from default values so fields reside in one place.
|
|
119
|
+
* Scalar values only: client config API only writes scalar fields;
|
|
120
|
+
* dictionaries or arrays here would make the settings card uneditable.
|
|
129
121
|
*/
|
|
130
122
|
export const Config = z.object(Object.fromEntries(
|
|
131
123
|
Object.entries(CONFIG_DEFAULTS).map(([field, fallback]) => {
|
|
132
|
-
//
|
|
133
|
-
// объявить его строкой — и плагин не загрузится вовсе с «invalid config».
|
|
124
|
+
// Node type derived from default value type.
|
|
134
125
|
let node = typeof fallback === 'number' ? z.number()
|
|
135
126
|
: typeof fallback === 'boolean' ? z.boolean()
|
|
136
127
|
: z.string()
|
|
@@ -141,7 +132,7 @@ export const Config = z.object(Object.fromEntries(
|
|
|
141
132
|
}),
|
|
142
133
|
))
|
|
143
134
|
|
|
144
|
-
/**
|
|
135
|
+
/** Storage directory path resolved from environment variable or user home directory. */
|
|
145
136
|
export function storeDir() {
|
|
146
137
|
return process.env.DSH_KANBAN_DIR || join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'kanban')
|
|
147
138
|
}
|
|
@@ -153,7 +144,7 @@ function writeJson(res, code, body) {
|
|
|
153
144
|
} catch { /* socket closed */ }
|
|
154
145
|
}
|
|
155
146
|
|
|
156
|
-
/**
|
|
147
|
+
/** Handler response helper: either an error object or success payload. */
|
|
157
148
|
function reply(res, out) {
|
|
158
149
|
if (out && typeof out === 'object' && typeof out.error === 'string') {
|
|
159
150
|
writeJson(res, out.status ?? 400, { error: out.error })
|
|
@@ -185,11 +176,9 @@ function readBody(req) {
|
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* обратно — значит получить другую последовательность байт и не сойтись в
|
|
192
|
-
* подписи на пустом месте.
|
|
179
|
+
* Read raw request body bytes.
|
|
180
|
+
* Webhook signatures are computed over delivery byte buffers; parsing and re-serializing
|
|
181
|
+
* could alter byte sequences and fail signature verification.
|
|
193
182
|
*/
|
|
194
183
|
function readRaw(req) {
|
|
195
184
|
return new Promise((resolve, reject) => {
|
|
@@ -217,9 +206,8 @@ export function apply(ctx, config) {
|
|
|
217
206
|
let effective = withDefaults(config)
|
|
218
207
|
const getConfig = () => effective
|
|
219
208
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// переключатель в карточке настроек безответным до перезапуска харнесса.
|
|
209
|
+
// Settings control live subscriptions: sync intervals and agent tools.
|
|
210
|
+
// Dynamic watcher re-evaluates configurations on change without requiring restart.
|
|
223
211
|
const rearmers = new Set()
|
|
224
212
|
const rearm = () => {
|
|
225
213
|
for (const again of rearmers) {
|
|
@@ -234,9 +222,8 @@ export function apply(ctx, config) {
|
|
|
234
222
|
ctx.effect(() => scope.watch(() => { effective = read(); rearm() }), 'dsh-kanban: settings watcher')
|
|
235
223
|
})
|
|
236
224
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// наружу не отдаётся — в настройках только ИМЯ учётной записи.
|
|
225
|
+
// Dedicated Gitea client resolves token via core credentials service.
|
|
226
|
+
// Secret token values are never exposed externally; only the credential reference name is stored.
|
|
240
227
|
const resolveSecret = async (name) => {
|
|
241
228
|
if (typeof name !== 'string' || name.trim() === '') return ''
|
|
242
229
|
try {
|
|
@@ -263,8 +250,7 @@ export function apply(ctx, config) {
|
|
|
263
250
|
getConfig,
|
|
264
251
|
})
|
|
265
252
|
|
|
266
|
-
//
|
|
267
|
-
// выгрузка плагина обязана отпустить файл базы.
|
|
253
|
+
// Database store opens once and is closed on effect disposal.
|
|
268
254
|
let store
|
|
269
255
|
const taskService = createKanbanTaskService({ getStore: () => store })
|
|
270
256
|
if (typeof ctx.provide === 'function') ctx.provide('dshKanban', taskService)
|
|
@@ -273,7 +259,7 @@ export function apply(ctx, config) {
|
|
|
273
259
|
return () => { try { store.close() } catch { /* already closed */ } }
|
|
274
260
|
}, 'dsh-kanban: task store')
|
|
275
261
|
|
|
276
|
-
//
|
|
262
|
+
// Cross-platform system sleep inhibitor while tasks are actively running
|
|
277
263
|
const powerInhibitor = new PowerInhibitor({
|
|
278
264
|
platform: process.platform,
|
|
279
265
|
pid: process.pid,
|
|
@@ -312,7 +298,7 @@ export function apply(ctx, config) {
|
|
|
312
298
|
return () => { powerInhibitor.destroy() }
|
|
313
299
|
}, 'dsh-kanban: power inhibitor')
|
|
314
300
|
|
|
315
|
-
//
|
|
301
|
+
// Inject board instructions into agent system prompt
|
|
316
302
|
let disposePromptSection
|
|
317
303
|
const syncPromptSection = () => {
|
|
318
304
|
if (disposePromptSection) {
|
|
@@ -339,10 +325,10 @@ export function apply(ctx, config) {
|
|
|
339
325
|
return () => { if (disposePromptSection) { try { disposePromptSection() } catch { /* cleanup best effort */ } } }
|
|
340
326
|
}, 'dsh-kanban: system prompt section')
|
|
341
327
|
|
|
342
|
-
//
|
|
328
|
+
// Track waiting flags, session outcomes, and progress dump from session events.
|
|
343
329
|
ctx.effect(() => ctx.on('session/event', (session, event) => {
|
|
344
330
|
try {
|
|
345
|
-
//
|
|
331
|
+
// Auto-sync DSH chat sessions into kanban cards (#225)
|
|
346
332
|
const sessionIdStr = String(session?.id ?? '')
|
|
347
333
|
if (
|
|
348
334
|
getConfig().autoSyncSessions === true &&
|
|
@@ -395,7 +381,7 @@ export function apply(ctx, config) {
|
|
|
395
381
|
error: event?.error,
|
|
396
382
|
})
|
|
397
383
|
syncPower()
|
|
398
|
-
//
|
|
384
|
+
// Dispatch next queued task when a session completes or fails (#214)
|
|
399
385
|
if (event?.type === 'turn/end' || event?.type === 'turn/completed' || event?.type === 'session/error' || event?.type === 'turn/error') {
|
|
400
386
|
dispatchNextQueuedTask({
|
|
401
387
|
agents: ctx.agents,
|
|
@@ -414,7 +400,7 @@ export function apply(ctx, config) {
|
|
|
414
400
|
}
|
|
415
401
|
}), 'dsh-kanban: session events')
|
|
416
402
|
|
|
417
|
-
//
|
|
403
|
+
// Background cron scheduler for scheduled tasks
|
|
418
404
|
ctx.effect(() => {
|
|
419
405
|
let cronTimer
|
|
420
406
|
const armCron = () => {
|
|
@@ -482,12 +468,8 @@ export function apply(ctx, config) {
|
|
|
482
468
|
return () => { rearmers.delete(armCron); clearInterval(cronTimer) }
|
|
483
469
|
}, 'dsh-kanban: cron scheduler')
|
|
484
470
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
// очередь сверок вместо одной идущей.
|
|
488
|
-
// Очередь отправки в Gitea. Перенос карточки не ждёт сети: доска обязана
|
|
489
|
-
// работать и при недоступном Gitea, а неудачная отправка повторяется и, если
|
|
490
|
-
// так и не удалась, попадает в журнал задачи.
|
|
471
|
+
// Periodic Gitea reconciliation. 0 disables it; overlapping executions are prevented.
|
|
472
|
+
// Outbound push queue to Gitea. Card moves do not block on network; failures retry in background.
|
|
491
473
|
const outbox = createOutbox({ gitea: git, store, logger: ctx.logger })
|
|
492
474
|
|
|
493
475
|
const queueOutbound = (taskId, column) => {
|
|
@@ -499,11 +481,8 @@ export function apply(ctx, config) {
|
|
|
499
481
|
}
|
|
500
482
|
|
|
501
483
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* Берётся из задач, уже лежащих на доске: другого источника у плагина нет,
|
|
505
|
-
* а спрашивать владельца отдельной настройкой значит просить дважды одно и
|
|
506
|
-
* то же.
|
|
484
|
+
* Default repository owner for short repository names.
|
|
485
|
+
* Inferred from existing board tasks when not explicitly configured.
|
|
507
486
|
*/
|
|
508
487
|
let discoveredOwner
|
|
509
488
|
const defaultOwner = async () => {
|
|
@@ -513,15 +492,13 @@ export function apply(ctx, config) {
|
|
|
513
492
|
if (typeof task.owner === 'string' && task.owner !== '') return task.owner
|
|
514
493
|
}
|
|
515
494
|
if (discoveredOwner !== undefined) return discoveredOwner
|
|
516
|
-
//
|
|
517
|
-
// нескольких гадание молча смотрело бы не туда, а это хуже отказа.
|
|
495
|
+
// Fall back to Gitea organization if exactly one is accessible to the token.
|
|
518
496
|
const orgs = await gitea.listOrgs()
|
|
519
497
|
if (orgs.length === 1) { discoveredOwner = orgs[0]; return discoveredOwner }
|
|
520
498
|
return undefined
|
|
521
499
|
}
|
|
522
500
|
|
|
523
|
-
//
|
|
524
|
-
// обновляться, не имеет права молчать об этом.
|
|
501
|
+
// Sync telemetry state visible in board header.
|
|
525
502
|
const syncState = createSyncState()
|
|
526
503
|
|
|
527
504
|
let syncing = false
|
|
@@ -530,9 +507,8 @@ export function apply(ctx, config) {
|
|
|
530
507
|
syncing = true
|
|
531
508
|
syncState.started()
|
|
532
509
|
try {
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// последней — она смотрит на итог, а не на исходное.
|
|
510
|
+
// Order of operations: intake new issues first, reconcile active tasks, archive overdue.
|
|
511
|
+
// Inspect final outcome rather than initial state.
|
|
536
512
|
const mode = String(getConfig()?.gitProvider || 'auto').toLowerCase()
|
|
537
513
|
const providersToSync = []
|
|
538
514
|
if (mode === 'github') {
|
|
@@ -562,18 +538,15 @@ export function apply(ctx, config) {
|
|
|
562
538
|
}
|
|
563
539
|
const out = await syncAll({ gitea: git, store, logger: ctx.logger })
|
|
564
540
|
const tidied = archiveOverdue({ store, config: getConfig() })
|
|
565
|
-
//
|
|
566
|
-
// означал бы второй источник обращений к Gitea без всякой пользы.
|
|
541
|
+
// Outbox flushes run in the same cycle to minimize redundant Gitea API calls.
|
|
567
542
|
const pushed = await outbox.flush()
|
|
568
|
-
//
|
|
569
|
-
// приезжают, а человеку об этом никто не говорит.
|
|
543
|
+
// Intake errors are reported in sync telemetry to keep failures visible.
|
|
570
544
|
syncState.finished(out.problem === undefined && taken.problem !== undefined
|
|
571
545
|
? { ...out, problem: taken.problem }
|
|
572
546
|
: out)
|
|
573
547
|
return { ...out, pushed, taken, tidied }
|
|
574
548
|
} catch (error) {
|
|
575
|
-
//
|
|
576
|
-
// неразрешённый токен или отсутствующий клиент Gitea.
|
|
549
|
+
// Sync failure to start represents missing client or unconfigured credentials.
|
|
577
550
|
syncState.failedToStart('sync', error?.message)
|
|
578
551
|
throw error
|
|
579
552
|
} finally {
|
|
@@ -620,17 +593,14 @@ export function apply(ctx, config) {
|
|
|
620
593
|
ctx.effect(() => ctx.webServer.register({
|
|
621
594
|
kind: 'exact',
|
|
622
595
|
path: '/dsh-kanban/presets',
|
|
623
|
-
//
|
|
624
|
-
// ни уровни доступа. Пустой список — не поломка, а «выбирать не из чего»,
|
|
625
|
-
// и браузер просто не рисует поле.
|
|
596
|
+
// Both services are optional in minimal environments. Empty lists render gracefully.
|
|
626
597
|
handler: async (req, res) => {
|
|
627
598
|
const presets = ctx.get('agentPresets')
|
|
628
599
|
const permissions = ctx.get('permissionPresets')
|
|
629
600
|
let agentPresets = []
|
|
630
601
|
try {
|
|
631
602
|
const rows = presets === undefined ? [] : await presets.list()
|
|
632
|
-
//
|
|
633
|
-
// сессия на нём не соберётся, а отказ прилетит уже после запуска.
|
|
603
|
+
// Filter out invalid presets that would fail session creation.
|
|
634
604
|
agentPresets = rows.filter((row) => row?.broken === undefined).map((row) => ({
|
|
635
605
|
id: row.id, name: row.name || row.id, description: row.description,
|
|
636
606
|
}))
|
|
@@ -767,9 +737,7 @@ export function apply(ctx, config) {
|
|
|
767
737
|
},
|
|
768
738
|
}), 'dsh-kanban: gitea webhook')
|
|
769
739
|
|
|
770
|
-
//
|
|
771
|
-
// запрещает этому CLI трогать канбан, и включить инструмент раньше правки
|
|
772
|
-
// скилла — значит толкать агента на нарушение.
|
|
740
|
+
// Agent tool definition. Default disabled; workflow skills explicitly enable it.
|
|
773
741
|
ctx.effect(() => {
|
|
774
742
|
let unregister
|
|
775
743
|
const arm = () => {
|
|
@@ -779,8 +747,7 @@ export function apply(ctx, config) {
|
|
|
779
747
|
unregister = registerBoardTool()
|
|
780
748
|
}
|
|
781
749
|
|
|
782
|
-
//
|
|
783
|
-
// множить настройки ради разницы, которой человек не почувствует.
|
|
750
|
+
// Single setting switch controls both board tools.
|
|
784
751
|
const registerBoardTool = () => {
|
|
785
752
|
const off = [
|
|
786
753
|
ctx.tools.register(defineTool(boardMoveDefinition({ store, config: getConfig }))),
|
|
@@ -801,8 +768,7 @@ export function apply(ctx, config) {
|
|
|
801
768
|
}, 'dsh-kanban: board tool')
|
|
802
769
|
|
|
803
770
|
/**
|
|
804
|
-
*
|
|
805
|
-
* Отказ реестра на незнакомый идентификатор ловит сборка доски.
|
|
771
|
+
* Return active agent state or undefined if session is inactive.
|
|
806
772
|
*/
|
|
807
773
|
const liveOf = (sessionId) => ctx.agents.get(sessionId)?.status
|
|
808
774
|
|
|
@@ -819,27 +785,15 @@ export function apply(ctx, config) {
|
|
|
819
785
|
repo: url.searchParams.get('repo') || undefined,
|
|
820
786
|
liveOf,
|
|
821
787
|
sync: syncState.snapshot(),
|
|
822
|
-
//
|
|
823
|
-
// окне запуска, когда человек не задал корень настройкой, — иначе
|
|
824
|
-
// сессия молча поднимается в рабочей папке харнесса.
|
|
788
|
+
// Resolved project root passed with board payload for session launch modal.
|
|
825
789
|
projectRoot: rootOf(getConfig()),
|
|
826
790
|
}))
|
|
827
791
|
},
|
|
828
792
|
}), 'dsh-kanban: board route')
|
|
829
793
|
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
// Прежде выгрузка звалась `/export`, а загрузка занимала `/import` — тот
|
|
833
|
-
// самый путь, на котором с первого дня живёт импорт issue из Gitea. Два
|
|
834
|
-
// обработчика на одном пути означают, что один из них не отвечает вовсе, и
|
|
835
|
-
// какой именно — решает порядок регистрации, а не мы.
|
|
794
|
+
// Snapshot endpoint handles both export and import.
|
|
836
795
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
* Логин нужен и «взять себе», и запуску работы. Спрашивать его у Gitea
|
|
840
|
-
* каждый раз значит добавить в путь запуска ещё одну сеть, которая может не
|
|
841
|
-
* ответить; поэтому спрашиваем один раз и помним. Не ответила — работаем без
|
|
842
|
-
* назначения, а не отказываем в запуске.
|
|
796
|
+
* Cached identity for Gitea client to avoid redundant API lookups on task claim.
|
|
843
797
|
*/
|
|
844
798
|
let knownLogin
|
|
845
799
|
const whoAmI = async () => {
|
|
@@ -870,8 +824,7 @@ export function apply(ctx, config) {
|
|
|
870
824
|
ctx.effect(() => ctx.webServer.register({
|
|
871
825
|
kind: 'exact',
|
|
872
826
|
path: '/dsh-kanban/whoami',
|
|
873
|
-
//
|
|
874
|
-
// отвечать. Браузер по нему просто не рисует «Мои», а не показывает ноль.
|
|
827
|
+
// Empty login indicates unconfigured or unreachable Gitea; client omits "Mine" filter.
|
|
875
828
|
handler: async (req, res) => {
|
|
876
829
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
877
830
|
reply(res, { login: await whoAmI() })
|
|
@@ -903,7 +856,7 @@ export function apply(ctx, config) {
|
|
|
903
856
|
},
|
|
904
857
|
}), 'dsh-kanban: sse events stream')
|
|
905
858
|
|
|
906
|
-
//
|
|
859
|
+
// Clean up SSE client connections and heartbeat timer on plugin disposal (#282)
|
|
907
860
|
if (typeof ctx.on === 'function') {
|
|
908
861
|
ctx.on('dispose', () => {
|
|
909
862
|
eventHub.closeAll()
|
|
@@ -944,14 +897,13 @@ export function apply(ctx, config) {
|
|
|
944
897
|
}), 'dsh-kanban: board snapshot')
|
|
945
898
|
|
|
946
899
|
ctx.effect(() => ctx.webServer.register({
|
|
947
|
-
//
|
|
948
|
-
// сводится к равенству пути, и всё, что глубже, уходит в статику.
|
|
900
|
+
// Prefix registered without trailing slash for sub-path routing.
|
|
949
901
|
kind: 'prefix',
|
|
950
902
|
path: '/dsh-kanban/task',
|
|
951
903
|
handler: async (req, res) => {
|
|
952
904
|
const pathname = urlOf(req).pathname
|
|
953
905
|
|
|
954
|
-
//
|
|
906
|
+
// Path without ID handles local task creation.
|
|
955
907
|
if (pathname === '/dsh-kanban/task' || pathname === '/dsh-kanban/task/') {
|
|
956
908
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
957
909
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
@@ -970,8 +922,7 @@ export function apply(ctx, config) {
|
|
|
970
922
|
if (req.method === 'GET' && action === 'log') { reply(res, taskLog({ store, id })); return }
|
|
971
923
|
if (req.method === 'GET' && action === 'body') { reply(res, taskBody({ store, id })); return }
|
|
972
924
|
|
|
973
|
-
//
|
|
974
|
-
// иначе шаблон окажется в двух местах и однажды разойдётся.
|
|
925
|
+
// Generate start prompt on server for template consistency.
|
|
975
926
|
if (req.method === 'GET' && action === 'message') {
|
|
976
927
|
const task = store.getTask(id)
|
|
977
928
|
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
@@ -980,8 +931,7 @@ export function apply(ctx, config) {
|
|
|
980
931
|
}
|
|
981
932
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
982
933
|
if (req.method === 'DELETE' && action === undefined) { reply(res, deleteTask({ store, id })); return }
|
|
983
|
-
//
|
|
984
|
-
// локальное свойство доски, в Gitea не пишется (updateTask это знает).
|
|
934
|
+
// Card fields update: title, body, labels, priority. Priority is board-local.
|
|
985
935
|
if (req.method === 'POST' && action === 'update') {
|
|
986
936
|
try {
|
|
987
937
|
const input = await readBody(req)
|
|
@@ -1057,8 +1007,7 @@ export function apply(ctx, config) {
|
|
|
1057
1007
|
config: getConfig(),
|
|
1058
1008
|
provider: picked.provider,
|
|
1059
1009
|
model: picked.model,
|
|
1060
|
-
//
|
|
1061
|
-
// поднимается вовсе, и заранее занятое имя осталось бы висеть.
|
|
1010
|
+
// Mint session ID lazily to avoid dangling IDs on resume.
|
|
1062
1011
|
mintSessionId: () => SessionId(`kanban-${id}-${randomUUID()}`),
|
|
1063
1012
|
createMessage: createUserMessage,
|
|
1064
1013
|
logger: ctx.logger,
|
|
@@ -1073,8 +1022,7 @@ export function apply(ctx, config) {
|
|
|
1073
1022
|
reply(res, started)
|
|
1074
1023
|
} catch (error) {
|
|
1075
1024
|
ctx.logger?.warn?.(`dsh-kanban: task launch failed: ${error?.message}`)
|
|
1076
|
-
//
|
|
1077
|
-
// месте «сессия пошла бы с большими правами» — это молчание.
|
|
1025
|
+
// Explicit permission errors are returned with status and key.
|
|
1078
1026
|
writeJson(res, error?.status ?? 500, { error: error?.key ?? error?.message ?? 'start-failed' })
|
|
1079
1027
|
}
|
|
1080
1028
|
return
|
|
@@ -1083,13 +1031,7 @@ export function apply(ctx, config) {
|
|
|
1083
1031
|
try {
|
|
1084
1032
|
const input = await readBody(req)
|
|
1085
1033
|
if (req.method === 'POST' && action === 'move') {
|
|
1086
|
-
//
|
|
1087
|
-
// Автоматика (сверка, инструмент, жизненный цикл) сюда не приходит:
|
|
1088
|
-
// она двигает карточку мимо этого маршрута.
|
|
1089
|
-
//
|
|
1090
|
-
// Команду отдаём ДО переноса: остановка должна случиться раньше, чем
|
|
1091
|
-
// доска объявит работу прекращённой, а её исход становится
|
|
1092
|
-
// пояснением к единственному переходу в журнале.
|
|
1034
|
+
// Manual move dispatches stop command to agent prior to updating card column.
|
|
1093
1035
|
const was = store.getTask(id)
|
|
1094
1036
|
const acted = was !== undefined && input.column !== was.column
|
|
1095
1037
|
? dispatchMove({
|
|
@@ -1259,8 +1201,7 @@ export function apply(ctx, config) {
|
|
|
1259
1201
|
store.updateTaskRun(task.id, latestRun.id, { endedAt: Date.now(), result: 'cancelled' })
|
|
1260
1202
|
}
|
|
1261
1203
|
syncPower()
|
|
1262
|
-
//
|
|
1263
|
-
// иначе прерванная работа выглядит просто оборвавшейся сама.
|
|
1204
|
+
// Stop event recorded in transition history.
|
|
1264
1205
|
store.addTransition({
|
|
1265
1206
|
taskId: task.id, fromCol: task.column, toCol: task.column,
|
|
1266
1207
|
source: 'manual', detail: STOP_DETAIL.stopped,
|
|
@@ -1272,8 +1213,7 @@ export function apply(ctx, config) {
|
|
|
1272
1213
|
if (req.method === 'POST' && action === 'assign') {
|
|
1273
1214
|
const was = store.getTask(id)
|
|
1274
1215
|
if (was === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
1275
|
-
//
|
|
1276
|
-
// токеном ходит доска: своего понятия пользователя у харнесса нет.
|
|
1216
|
+
// Empty login unassigns; 'me' resolves authenticated token identity.
|
|
1277
1217
|
let login = typeof input.login === 'string' ? input.login.trim() : ''
|
|
1278
1218
|
if (login === 'me') {
|
|
1279
1219
|
login = await whoAmI()
|
|
@@ -1281,8 +1221,7 @@ export function apply(ctx, config) {
|
|
|
1281
1221
|
}
|
|
1282
1222
|
const out = assignTask({ store, id, login })
|
|
1283
1223
|
if (out.task !== undefined && config.pushToGitea !== false) {
|
|
1284
|
-
//
|
|
1285
|
-
// Gitea лежит.
|
|
1224
|
+
// Queue assignment update to outbox for offline resilience.
|
|
1286
1225
|
outbox.push(id, planAssign(out.task, login))
|
|
1287
1226
|
}
|
|
1288
1227
|
reply(res, out)
|
|
@@ -1323,9 +1262,7 @@ export function apply(ctx, config) {
|
|
|
1323
1262
|
},
|
|
1324
1263
|
}), 'dsh-kanban: task routes')
|
|
1325
1264
|
|
|
1326
|
-
//
|
|
1327
|
-
// учётная запись правятся в карточке настроек на ходу, и маршруты обязаны
|
|
1328
|
-
// внятно объяснять причину, а не отвечать 404.
|
|
1265
|
+
// Check Gitea readiness per request to handle dynamically updated credentials.
|
|
1329
1266
|
ctx.effect(() => ctx.webServer.register({
|
|
1330
1267
|
kind: 'exact',
|
|
1331
1268
|
path: '/dsh-kanban/gitea/repos',
|
|
@@ -1371,9 +1308,7 @@ export function apply(ctx, config) {
|
|
|
1371
1308
|
},
|
|
1372
1309
|
}), 'dsh-kanban: issue import')
|
|
1373
1310
|
|
|
1374
|
-
//
|
|
1375
|
-
// перечень моделей провайдер отдаёт по сети, и опрашивать все разом ради
|
|
1376
|
-
// одного выпадающего списка незачем.
|
|
1311
|
+
// Providers returned immediately; models queried on-demand for selected provider.
|
|
1377
1312
|
ctx.effect(() => ctx.webServer.register({
|
|
1378
1313
|
kind: 'exact',
|
|
1379
1314
|
path: '/dsh-kanban/models',
|
|
@@ -1396,8 +1331,7 @@ export function apply(ctx, config) {
|
|
|
1396
1331
|
models: (rows ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
|
|
1397
1332
|
})
|
|
1398
1333
|
} catch {
|
|
1399
|
-
//
|
|
1400
|
-
// доски, а пустой список с сохранённым выбором провайдера.
|
|
1334
|
+
// Provider errors return empty model list while preserving selected provider.
|
|
1401
1335
|
writeJson(res, 200, { current, providers, models: [] })
|
|
1402
1336
|
}
|
|
1403
1337
|
},
|
package/lib/intake.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Archival of completed tasks and ingestion of new tasks from Gitea.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// рядом дешевле, чем искать по разным углам, почему карточка появилась или
|
|
6
|
-
// пропала.
|
|
3
|
+
// Two complementary concerns in one module: one clears finished tasks from board,
|
|
4
|
+
// the other ingests new ones. Both govern WHAT belongs on the board.
|
|
7
5
|
|
|
8
|
-
/**
|
|
6
|
+
/** Parse repository list from config: comma-separated, ignoring whitespace. */
|
|
9
7
|
export function parseWatchList(value) {
|
|
10
8
|
return String(value ?? '')
|
|
11
9
|
.split(',')
|
|
@@ -14,11 +12,9 @@ export function parseWatchList(value) {
|
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* Repositories watched explicitly when named.
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
* подхвата: там есть Gitea, у которого можно спросить, какие репозитории вообще
|
|
21
|
-
* существуют.
|
|
17
|
+
* Empty list means 'all', handled in intake pass where Gitea query lists all repos.
|
|
22
18
|
*
|
|
23
19
|
* @returns {Array<{owner: string, repo: string}>}
|
|
24
20
|
*/
|
|
@@ -26,8 +22,7 @@ export function watchedRepos({ config, owner }) {
|
|
|
26
22
|
const out = []
|
|
27
23
|
const seen = new Set()
|
|
28
24
|
for (const name of parseWatchList(config?.watchRepos)) {
|
|
29
|
-
//
|
|
30
|
-
// `репо` — тогда владелец берётся общий.
|
|
25
|
+
// Name can be full or short: `owner/repo` or just `repo` using default owner.
|
|
31
26
|
const at = name.indexOf('/')
|
|
32
27
|
const pair = at > 0
|
|
33
28
|
? { owner: name.slice(0, at), repo: name.slice(at + 1) }
|
|
@@ -42,16 +37,15 @@ export function watchedRepos({ config, owner }) {
|
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
/**
|
|
45
|
-
*
|
|
40
|
+
* Whether to create a card for this issue.
|
|
46
41
|
*
|
|
47
|
-
* @returns {{take: boolean, why: string}} `why`
|
|
48
|
-
* иначе «почему этой задачи нет на доске» превращается в гадание.
|
|
42
|
+
* @returns {{take: boolean, why: string}} `why` states reason on rejection.
|
|
49
43
|
*/
|
|
50
44
|
export function shouldTake({ store, owner, repo, issue }) {
|
|
51
45
|
const number = issue?.number
|
|
52
46
|
if (typeof number !== 'number') return { take: false, why: 'missing-number' }
|
|
53
|
-
// Pull request
|
|
54
|
-
//
|
|
47
|
+
// Pull request in Gitea is also an issue, but not a standalone board card:
|
|
48
|
+
// it reflects execution progress, not the task definition.
|
|
55
49
|
if (issue?.pull_request !== undefined && issue?.pull_request !== null) {
|
|
56
50
|
return { take: false, why: 'pull-request-ignored' }
|
|
57
51
|
}
|
|
@@ -66,13 +60,12 @@ export function shouldTake({ store, owner, repo, issue }) {
|
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
/**
|
|
69
|
-
*
|
|
63
|
+
* Whether task is ready for archival.
|
|
70
64
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
65
|
+
* Archived ONLY from `done`. A task stuck in review for a month is an alert,
|
|
66
|
+
* not clutter, and must not be hidden.
|
|
73
67
|
*
|
|
74
|
-
*
|
|
75
|
-
* а не пропущенное значение.
|
|
68
|
+
* Zero in settings disables auto-archival.
|
|
76
69
|
*/
|
|
77
70
|
export function archiveBefore({ now, afterDays }) {
|
|
78
71
|
const days = Number(afterDays)
|
|
@@ -81,11 +74,10 @@ export function archiveBefore({ now, afterDays }) {
|
|
|
81
74
|
}
|
|
82
75
|
|
|
83
76
|
/**
|
|
84
|
-
*
|
|
77
|
+
* Whether given repository is watched.
|
|
85
78
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* что-либо решать.
|
|
79
|
+
* Events outside watched list are ignored: timer does not poll them,
|
|
80
|
+
* so webhooks should not process them either.
|
|
89
81
|
*/
|
|
90
82
|
export function isWatched({ config, owner, defaultOwner, repo }) {
|
|
91
83
|
return watchedRepos({ config, owner: defaultOwner }).some(
|