@goodandready/dsh-kanban 0.2.18 → 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 +88 -137
- package/lib/intake.js +23 -31
- 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 -78
- package/lib/sync.js +56 -98
- package/lib/templates.js +29 -29
- package/lib/time.js +8 -17
- package/lib/transitions.js +36 -57
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +58 -58
- 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,
|
|
@@ -75,7 +74,7 @@ import {
|
|
|
75
74
|
boardDecomposeDefinition,
|
|
76
75
|
boardChecklistItemDefinition,
|
|
77
76
|
} from './board-tool.js'
|
|
78
|
-
import { verifySignature, parseEvent } from './webhook.js'
|
|
77
|
+
import { verifySignature, parseEvent, detectWebhookProvider } from './webhook.js'
|
|
79
78
|
import { planOutbound, planAssign, createOutbox } from './outbound.js'
|
|
80
79
|
import { computeNextRun } from './cron.js'
|
|
81
80
|
import { PowerInhibitor } from './power.js'
|
|
@@ -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
|
}))
|
|
@@ -659,7 +629,10 @@ export function apply(ctx, config) {
|
|
|
659
629
|
ctx.effect(() => ctx.webServer.register({
|
|
660
630
|
kind: 'exact',
|
|
661
631
|
path: '/dsh-kanban/sessions',
|
|
662
|
-
handler: (req, res) => {
|
|
632
|
+
handler: (req, res) => {
|
|
633
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
634
|
+
reply(res, { sessions: liveSessions({ store, agents: ctx.agents }) })
|
|
635
|
+
},
|
|
663
636
|
}), 'dsh-kanban: live sessions')
|
|
664
637
|
|
|
665
638
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -697,7 +670,10 @@ export function apply(ctx, config) {
|
|
|
697
670
|
ctx.effect(() => ctx.webServer.register({
|
|
698
671
|
kind: 'exact',
|
|
699
672
|
path: '/dsh-kanban/archive',
|
|
700
|
-
handler: (req, res) => {
|
|
673
|
+
handler: (req, res) => {
|
|
674
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
675
|
+
reply(res, listArchive({ store }))
|
|
676
|
+
},
|
|
701
677
|
}), 'dsh-kanban: archive')
|
|
702
678
|
|
|
703
679
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -721,7 +697,7 @@ export function apply(ctx, config) {
|
|
|
721
697
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
722
698
|
try {
|
|
723
699
|
const raw = await readRaw(req)
|
|
724
|
-
const isGithub =
|
|
700
|
+
const isGithub = detectWebhookProvider(req.headers) === 'github'
|
|
725
701
|
const secretRef = isGithub
|
|
726
702
|
? (getConfig().githubWebhookSecretRef || getConfig().webhookSecretRef)
|
|
727
703
|
: getConfig().webhookSecretRef
|
|
@@ -761,9 +737,7 @@ export function apply(ctx, config) {
|
|
|
761
737
|
},
|
|
762
738
|
}), 'dsh-kanban: gitea webhook')
|
|
763
739
|
|
|
764
|
-
//
|
|
765
|
-
// запрещает этому CLI трогать канбан, и включить инструмент раньше правки
|
|
766
|
-
// скилла — значит толкать агента на нарушение.
|
|
740
|
+
// Agent tool definition. Default disabled; workflow skills explicitly enable it.
|
|
767
741
|
ctx.effect(() => {
|
|
768
742
|
let unregister
|
|
769
743
|
const arm = () => {
|
|
@@ -773,8 +747,7 @@ export function apply(ctx, config) {
|
|
|
773
747
|
unregister = registerBoardTool()
|
|
774
748
|
}
|
|
775
749
|
|
|
776
|
-
//
|
|
777
|
-
// множить настройки ради разницы, которой человек не почувствует.
|
|
750
|
+
// Single setting switch controls both board tools.
|
|
778
751
|
const registerBoardTool = () => {
|
|
779
752
|
const off = [
|
|
780
753
|
ctx.tools.register(defineTool(boardMoveDefinition({ store, config: getConfig }))),
|
|
@@ -795,8 +768,7 @@ export function apply(ctx, config) {
|
|
|
795
768
|
}, 'dsh-kanban: board tool')
|
|
796
769
|
|
|
797
770
|
/**
|
|
798
|
-
*
|
|
799
|
-
* Отказ реестра на незнакомый идентификатор ловит сборка доски.
|
|
771
|
+
* Return active agent state or undefined if session is inactive.
|
|
800
772
|
*/
|
|
801
773
|
const liveOf = (sessionId) => ctx.agents.get(sessionId)?.status
|
|
802
774
|
|
|
@@ -804,6 +776,7 @@ export function apply(ctx, config) {
|
|
|
804
776
|
kind: 'exact',
|
|
805
777
|
path: '/dsh-kanban/board',
|
|
806
778
|
handler: (req, res) => {
|
|
779
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
807
780
|
const url = urlOf(req)
|
|
808
781
|
reply(res, buildBoard({
|
|
809
782
|
store,
|
|
@@ -812,27 +785,15 @@ export function apply(ctx, config) {
|
|
|
812
785
|
repo: url.searchParams.get('repo') || undefined,
|
|
813
786
|
liveOf,
|
|
814
787
|
sync: syncState.snapshot(),
|
|
815
|
-
//
|
|
816
|
-
// окне запуска, когда человек не задал корень настройкой, — иначе
|
|
817
|
-
// сессия молча поднимается в рабочей папке харнесса.
|
|
788
|
+
// Resolved project root passed with board payload for session launch modal.
|
|
818
789
|
projectRoot: rootOf(getConfig()),
|
|
819
790
|
}))
|
|
820
791
|
},
|
|
821
792
|
}), 'dsh-kanban: board route')
|
|
822
793
|
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
// Прежде выгрузка звалась `/export`, а загрузка занимала `/import` — тот
|
|
826
|
-
// самый путь, на котором с первого дня живёт импорт issue из Gitea. Два
|
|
827
|
-
// обработчика на одном пути означают, что один из них не отвечает вовсе, и
|
|
828
|
-
// какой именно — решает порядок регистрации, а не мы.
|
|
794
|
+
// Snapshot endpoint handles both export and import.
|
|
829
795
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* Логин нужен и «взять себе», и запуску работы. Спрашивать его у Gitea
|
|
833
|
-
* каждый раз значит добавить в путь запуска ещё одну сеть, которая может не
|
|
834
|
-
* ответить; поэтому спрашиваем один раз и помним. Не ответила — работаем без
|
|
835
|
-
* назначения, а не отказываем в запуске.
|
|
796
|
+
* Cached identity for Gitea client to avoid redundant API lookups on task claim.
|
|
836
797
|
*/
|
|
837
798
|
let knownLogin
|
|
838
799
|
const whoAmI = async () => {
|
|
@@ -849,6 +810,7 @@ export function apply(ctx, config) {
|
|
|
849
810
|
kind: 'exact',
|
|
850
811
|
path: '/dsh-kanban/metrics',
|
|
851
812
|
handler: (req, res) => {
|
|
813
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
852
814
|
const url = urlOf(req)
|
|
853
815
|
const days = Number(url.searchParams.get('staleDays'))
|
|
854
816
|
reply(res, boardMetrics({
|
|
@@ -862,15 +824,18 @@ export function apply(ctx, config) {
|
|
|
862
824
|
ctx.effect(() => ctx.webServer.register({
|
|
863
825
|
kind: 'exact',
|
|
864
826
|
path: '/dsh-kanban/whoami',
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
827
|
+
// Empty login indicates unconfigured or unreachable Gitea; client omits "Mine" filter.
|
|
828
|
+
handler: async (req, res) => {
|
|
829
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
830
|
+
reply(res, { login: await whoAmI() })
|
|
831
|
+
},
|
|
868
832
|
}), 'dsh-kanban: whoami')
|
|
869
833
|
|
|
870
834
|
ctx.effect(() => ctx.webServer.register({
|
|
871
835
|
kind: 'exact',
|
|
872
836
|
path: '/dsh-kanban/export.csv',
|
|
873
837
|
handler: async (req, res) => {
|
|
838
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
874
839
|
const u = urlOf(req)
|
|
875
840
|
const board = u.searchParams.get('board') || 'main'
|
|
876
841
|
const csv = exportTasksCsv({ store, board })
|
|
@@ -891,7 +856,7 @@ export function apply(ctx, config) {
|
|
|
891
856
|
},
|
|
892
857
|
}), 'dsh-kanban: sse events stream')
|
|
893
858
|
|
|
894
|
-
//
|
|
859
|
+
// Clean up SSE client connections and heartbeat timer on plugin disposal (#282)
|
|
895
860
|
if (typeof ctx.on === 'function') {
|
|
896
861
|
ctx.on('dispose', () => {
|
|
897
862
|
eventHub.closeAll()
|
|
@@ -902,6 +867,7 @@ export function apply(ctx, config) {
|
|
|
902
867
|
kind: 'exact',
|
|
903
868
|
path: '/dsh-kanban/templates',
|
|
904
869
|
handler: (req, res) => {
|
|
870
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
905
871
|
const u = new URL(req.url, 'http://localhost')
|
|
906
872
|
const tid = u.searchParams.get('id')
|
|
907
873
|
if (tid) {
|
|
@@ -931,14 +897,13 @@ export function apply(ctx, config) {
|
|
|
931
897
|
}), 'dsh-kanban: board snapshot')
|
|
932
898
|
|
|
933
899
|
ctx.effect(() => ctx.webServer.register({
|
|
934
|
-
//
|
|
935
|
-
// сводится к равенству пути, и всё, что глубже, уходит в статику.
|
|
900
|
+
// Prefix registered without trailing slash for sub-path routing.
|
|
936
901
|
kind: 'prefix',
|
|
937
902
|
path: '/dsh-kanban/task',
|
|
938
903
|
handler: async (req, res) => {
|
|
939
904
|
const pathname = urlOf(req).pathname
|
|
940
905
|
|
|
941
|
-
//
|
|
906
|
+
// Path without ID handles local task creation.
|
|
942
907
|
if (pathname === '/dsh-kanban/task' || pathname === '/dsh-kanban/task/') {
|
|
943
908
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
944
909
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
@@ -957,8 +922,7 @@ export function apply(ctx, config) {
|
|
|
957
922
|
if (req.method === 'GET' && action === 'log') { reply(res, taskLog({ store, id })); return }
|
|
958
923
|
if (req.method === 'GET' && action === 'body') { reply(res, taskBody({ store, id })); return }
|
|
959
924
|
|
|
960
|
-
//
|
|
961
|
-
// иначе шаблон окажется в двух местах и однажды разойдётся.
|
|
925
|
+
// Generate start prompt on server for template consistency.
|
|
962
926
|
if (req.method === 'GET' && action === 'message') {
|
|
963
927
|
const task = store.getTask(id)
|
|
964
928
|
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
@@ -967,8 +931,7 @@ export function apply(ctx, config) {
|
|
|
967
931
|
}
|
|
968
932
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
969
933
|
if (req.method === 'DELETE' && action === undefined) { reply(res, deleteTask({ store, id })); return }
|
|
970
|
-
//
|
|
971
|
-
// локальное свойство доски, в Gitea не пишется (updateTask это знает).
|
|
934
|
+
// Card fields update: title, body, labels, priority. Priority is board-local.
|
|
972
935
|
if (req.method === 'POST' && action === 'update') {
|
|
973
936
|
try {
|
|
974
937
|
const input = await readBody(req)
|
|
@@ -1044,8 +1007,7 @@ export function apply(ctx, config) {
|
|
|
1044
1007
|
config: getConfig(),
|
|
1045
1008
|
provider: picked.provider,
|
|
1046
1009
|
model: picked.model,
|
|
1047
|
-
//
|
|
1048
|
-
// поднимается вовсе, и заранее занятое имя осталось бы висеть.
|
|
1010
|
+
// Mint session ID lazily to avoid dangling IDs on resume.
|
|
1049
1011
|
mintSessionId: () => SessionId(`kanban-${id}-${randomUUID()}`),
|
|
1050
1012
|
createMessage: createUserMessage,
|
|
1051
1013
|
logger: ctx.logger,
|
|
@@ -1060,8 +1022,7 @@ export function apply(ctx, config) {
|
|
|
1060
1022
|
reply(res, started)
|
|
1061
1023
|
} catch (error) {
|
|
1062
1024
|
ctx.logger?.warn?.(`dsh-kanban: task launch failed: ${error?.message}`)
|
|
1063
|
-
//
|
|
1064
|
-
// месте «сессия пошла бы с большими правами» — это молчание.
|
|
1025
|
+
// Explicit permission errors are returned with status and key.
|
|
1065
1026
|
writeJson(res, error?.status ?? 500, { error: error?.key ?? error?.message ?? 'start-failed' })
|
|
1066
1027
|
}
|
|
1067
1028
|
return
|
|
@@ -1070,13 +1031,7 @@ export function apply(ctx, config) {
|
|
|
1070
1031
|
try {
|
|
1071
1032
|
const input = await readBody(req)
|
|
1072
1033
|
if (req.method === 'POST' && action === 'move') {
|
|
1073
|
-
//
|
|
1074
|
-
// Автоматика (сверка, инструмент, жизненный цикл) сюда не приходит:
|
|
1075
|
-
// она двигает карточку мимо этого маршрута.
|
|
1076
|
-
//
|
|
1077
|
-
// Команду отдаём ДО переноса: остановка должна случиться раньше, чем
|
|
1078
|
-
// доска объявит работу прекращённой, а её исход становится
|
|
1079
|
-
// пояснением к единственному переходу в журнале.
|
|
1034
|
+
// Manual move dispatches stop command to agent prior to updating card column.
|
|
1080
1035
|
const was = store.getTask(id)
|
|
1081
1036
|
const acted = was !== undefined && input.column !== was.column
|
|
1082
1037
|
? dispatchMove({
|
|
@@ -1246,8 +1201,7 @@ export function apply(ctx, config) {
|
|
|
1246
1201
|
store.updateTaskRun(task.id, latestRun.id, { endedAt: Date.now(), result: 'cancelled' })
|
|
1247
1202
|
}
|
|
1248
1203
|
syncPower()
|
|
1249
|
-
//
|
|
1250
|
-
// иначе прерванная работа выглядит просто оборвавшейся сама.
|
|
1204
|
+
// Stop event recorded in transition history.
|
|
1251
1205
|
store.addTransition({
|
|
1252
1206
|
taskId: task.id, fromCol: task.column, toCol: task.column,
|
|
1253
1207
|
source: 'manual', detail: STOP_DETAIL.stopped,
|
|
@@ -1259,8 +1213,7 @@ export function apply(ctx, config) {
|
|
|
1259
1213
|
if (req.method === 'POST' && action === 'assign') {
|
|
1260
1214
|
const was = store.getTask(id)
|
|
1261
1215
|
if (was === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
1262
|
-
//
|
|
1263
|
-
// токеном ходит доска: своего понятия пользователя у харнесса нет.
|
|
1216
|
+
// Empty login unassigns; 'me' resolves authenticated token identity.
|
|
1264
1217
|
let login = typeof input.login === 'string' ? input.login.trim() : ''
|
|
1265
1218
|
if (login === 'me') {
|
|
1266
1219
|
login = await whoAmI()
|
|
@@ -1268,8 +1221,7 @@ export function apply(ctx, config) {
|
|
|
1268
1221
|
}
|
|
1269
1222
|
const out = assignTask({ store, id, login })
|
|
1270
1223
|
if (out.task !== undefined && config.pushToGitea !== false) {
|
|
1271
|
-
//
|
|
1272
|
-
// Gitea лежит.
|
|
1224
|
+
// Queue assignment update to outbox for offline resilience.
|
|
1273
1225
|
outbox.push(id, planAssign(out.task, login))
|
|
1274
1226
|
}
|
|
1275
1227
|
reply(res, out)
|
|
@@ -1310,13 +1262,12 @@ export function apply(ctx, config) {
|
|
|
1310
1262
|
},
|
|
1311
1263
|
}), 'dsh-kanban: task routes')
|
|
1312
1264
|
|
|
1313
|
-
//
|
|
1314
|
-
// учётная запись правятся в карточке настроек на ходу, и маршруты обязаны
|
|
1315
|
-
// внятно объяснять причину, а не отвечать 404.
|
|
1265
|
+
// Check Gitea readiness per request to handle dynamically updated credentials.
|
|
1316
1266
|
ctx.effect(() => ctx.webServer.register({
|
|
1317
1267
|
kind: 'exact',
|
|
1318
1268
|
path: '/dsh-kanban/gitea/repos',
|
|
1319
1269
|
handler: async (req, res) => {
|
|
1270
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1320
1271
|
const url = urlOf(req)
|
|
1321
1272
|
reply(res, await searchRepos({ gitea: git, query: url.searchParams.get('q') || '' }))
|
|
1322
1273
|
},
|
|
@@ -1326,6 +1277,7 @@ export function apply(ctx, config) {
|
|
|
1326
1277
|
kind: 'exact',
|
|
1327
1278
|
path: '/dsh-kanban/gitea/issues',
|
|
1328
1279
|
handler: async (req, res) => {
|
|
1280
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1329
1281
|
const url = urlOf(req)
|
|
1330
1282
|
reply(res, await listImportable({
|
|
1331
1283
|
gitea: git,
|
|
@@ -1356,13 +1308,12 @@ export function apply(ctx, config) {
|
|
|
1356
1308
|
},
|
|
1357
1309
|
}), 'dsh-kanban: issue import')
|
|
1358
1310
|
|
|
1359
|
-
//
|
|
1360
|
-
// перечень моделей провайдер отдаёт по сети, и опрашивать все разом ради
|
|
1361
|
-
// одного выпадающего списка незачем.
|
|
1311
|
+
// Providers returned immediately; models queried on-demand for selected provider.
|
|
1362
1312
|
ctx.effect(() => ctx.webServer.register({
|
|
1363
1313
|
kind: 'exact',
|
|
1364
1314
|
path: '/dsh-kanban/models',
|
|
1365
1315
|
handler: async (req, res) => {
|
|
1316
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1366
1317
|
const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null
|
|
1367
1318
|
const llmService = ctx.get('llm')
|
|
1368
1319
|
let providers = []
|
|
@@ -1380,8 +1331,7 @@ export function apply(ctx, config) {
|
|
|
1380
1331
|
models: (rows ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
|
|
1381
1332
|
})
|
|
1382
1333
|
} catch {
|
|
1383
|
-
//
|
|
1384
|
-
// доски, а пустой список с сохранённым выбором провайдера.
|
|
1334
|
+
// Provider errors return empty model list while preserving selected provider.
|
|
1385
1335
|
writeJson(res, 200, { current, providers, models: [] })
|
|
1386
1336
|
}
|
|
1387
1337
|
},
|
|
@@ -1391,6 +1341,7 @@ export function apply(ctx, config) {
|
|
|
1391
1341
|
kind: 'prefix',
|
|
1392
1342
|
path: '/dsh-kanban/session',
|
|
1393
1343
|
handler: (req, res) => {
|
|
1344
|
+
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
1394
1345
|
const m = /^\/dsh-kanban\/session\/([^/]+)\/task\/?$/.exec(urlOf(req).pathname)
|
|
1395
1346
|
if (m === null) { writeJson(res, 404, { error: 'not-found' }); return }
|
|
1396
1347
|
reply(res, taskBySession({ store, sessionId: decodeURIComponent(m[1]), agents: ctx.agents }))
|