@goodandready/dsh-kanban 0.2.19 → 0.2.21
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 +30 -55
- package/lib/cron.js +13 -13
- package/lib/events.js +17 -5
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +59 -110
- package/lib/github.js +39 -37
- package/lib/import.js +46 -67
- package/lib/index.js +131 -140
- package/lib/intake.js +18 -26
- package/lib/launcher.js +107 -170
- 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 +28 -28
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +92 -145
- package/lib/store.js +68 -77
- package/lib/sync.js +56 -98
- package/lib/time.js +8 -17
- package/lib/transitions.js +44 -61
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +72 -68
- package/lib/worktree.js +103 -60
- 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,46 +101,51 @@ 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
|
*/
|
|
122
|
+
// Ensure .volatile() helper exists on Schemastery schema prototype
|
|
123
|
+
try {
|
|
124
|
+
const schemaProto = Object.getPrototypeOf(z.boolean())
|
|
125
|
+
if (schemaProto && typeof schemaProto.volatile !== 'function') {
|
|
126
|
+
schemaProto.volatile = function () {
|
|
127
|
+
return typeof this.extra === 'function' ? this.extra('volatile', true) : this
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// safe fallback
|
|
132
|
+
}
|
|
133
|
+
|
|
130
134
|
export const Config = z.object(Object.fromEntries(
|
|
131
135
|
Object.entries(CONFIG_DEFAULTS).map(([field, fallback]) => {
|
|
132
|
-
//
|
|
133
|
-
// объявить его строкой — и плагин не загрузится вовсе с «invalid config».
|
|
136
|
+
// Node type derived from default value type.
|
|
134
137
|
let node = typeof fallback === 'number' ? z.number()
|
|
135
138
|
: typeof fallback === 'boolean' ? z.boolean()
|
|
136
139
|
: z.string()
|
|
137
140
|
if (field === 'giteaTokenRef' || field === 'webhookSecretRef' || field === 'githubTokenRef' || field === 'githubWebhookSecretRef') {
|
|
138
141
|
node = node.role('credential-ref')
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
const described = node.default(fallback).description(CONFIG_HINTS[field] ?? '')
|
|
144
|
+
return [field, typeof described.volatile === 'function' ? described.volatile() : described]
|
|
141
145
|
}),
|
|
142
146
|
))
|
|
143
147
|
|
|
144
|
-
/**
|
|
148
|
+
/** Storage directory path resolved from environment variable or user home directory. */
|
|
145
149
|
export function storeDir() {
|
|
146
150
|
return process.env.DSH_KANBAN_DIR || join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'kanban')
|
|
147
151
|
}
|
|
@@ -153,7 +157,7 @@ function writeJson(res, code, body) {
|
|
|
153
157
|
} catch { /* socket closed */ }
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
/**
|
|
160
|
+
/** Handler response helper: either an error object or success payload. */
|
|
157
161
|
function reply(res, out) {
|
|
158
162
|
if (out && typeof out === 'object' && typeof out.error === 'string') {
|
|
159
163
|
writeJson(res, out.status ?? 400, { error: out.error })
|
|
@@ -185,11 +189,9 @@ function readBody(req) {
|
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* обратно — значит получить другую последовательность байт и не сойтись в
|
|
192
|
-
* подписи на пустом месте.
|
|
192
|
+
* Read raw request body bytes.
|
|
193
|
+
* Webhook signatures are computed over delivery byte buffers; parsing and re-serializing
|
|
194
|
+
* could alter byte sequences and fail signature verification.
|
|
193
195
|
*/
|
|
194
196
|
function readRaw(req) {
|
|
195
197
|
return new Promise((resolve, reject) => {
|
|
@@ -217,9 +219,8 @@ export function apply(ctx, config) {
|
|
|
217
219
|
let effective = withDefaults(config)
|
|
218
220
|
const getConfig = () => effective
|
|
219
221
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// переключатель в карточке настроек безответным до перезапуска харнесса.
|
|
222
|
+
// Settings control live subscriptions: sync intervals and agent tools.
|
|
223
|
+
// Dynamic watcher re-evaluates configurations on change without requiring restart.
|
|
223
224
|
const rearmers = new Set()
|
|
224
225
|
const rearm = () => {
|
|
225
226
|
for (const again of rearmers) {
|
|
@@ -227,16 +228,59 @@ export function apply(ctx, config) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
let settingsSvc = null
|
|
232
|
+
|
|
233
|
+
const readServedConfig = () => {
|
|
234
|
+
if (typeof settingsSvc?.describe !== 'function') return null
|
|
235
|
+
try {
|
|
236
|
+
const row = settingsSvc.describe().find((r) => r?.ns === SETTINGS_NAMESPACE)
|
|
237
|
+
if (!row || row.status !== 'ready' || !row.value) return null
|
|
238
|
+
return withDefaults(row.value)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
ctx.logger?.warn?.(`dsh-kanban: settings describe failed: ${err?.message || err}`)
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
230
245
|
ctx.inject(['settings'], (sctx) => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
try {
|
|
247
|
+
settingsSvc = sctx?.settings ?? sctx?.get?.('settings') ?? null
|
|
248
|
+
const served = readServedConfig()
|
|
249
|
+
if (served) {
|
|
250
|
+
effective = served
|
|
251
|
+
rearm()
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
ctx.logger?.warn?.(`dsh-kanban: settings service unavailable: ${err?.message || err}`)
|
|
255
|
+
}
|
|
235
256
|
})
|
|
236
257
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
258
|
+
// Listen for config and volatile updates from host
|
|
259
|
+
if (typeof ctx?.on === 'function') {
|
|
260
|
+
const applyPatch = (updated) => {
|
|
261
|
+
if (updated && typeof updated === 'object') {
|
|
262
|
+
const incoming = updated[SETTINGS_NAMESPACE] ?? updated
|
|
263
|
+
if (incoming && typeof incoming === 'object') {
|
|
264
|
+
try {
|
|
265
|
+
effective = withDefaults({ ...effective, ...incoming })
|
|
266
|
+
rearm()
|
|
267
|
+
} catch { /* ignore invalid update */ }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (typeof ctx.effect === 'function') {
|
|
272
|
+
ctx.effect(() => ctx.on('loader/volatile-update', applyPatch))
|
|
273
|
+
ctx.effect(() => ctx.on('config', applyPatch))
|
|
274
|
+
ctx.effect(() => ctx.on('settings/document-updated', applyPatch))
|
|
275
|
+
} else {
|
|
276
|
+
ctx.on('loader/volatile-update', applyPatch)
|
|
277
|
+
ctx.on('config', applyPatch)
|
|
278
|
+
ctx.on('settings/document-updated', applyPatch)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Dedicated Gitea client resolves token via core credentials service.
|
|
283
|
+
// Secret token values are never exposed externally; only the credential reference name is stored.
|
|
240
284
|
const resolveSecret = async (name) => {
|
|
241
285
|
if (typeof name !== 'string' || name.trim() === '') return ''
|
|
242
286
|
try {
|
|
@@ -263,8 +307,7 @@ export function apply(ctx, config) {
|
|
|
263
307
|
getConfig,
|
|
264
308
|
})
|
|
265
309
|
|
|
266
|
-
//
|
|
267
|
-
// выгрузка плагина обязана отпустить файл базы.
|
|
310
|
+
// Database store opens once and is closed on effect disposal.
|
|
268
311
|
let store
|
|
269
312
|
const taskService = createKanbanTaskService({ getStore: () => store })
|
|
270
313
|
if (typeof ctx.provide === 'function') ctx.provide('dshKanban', taskService)
|
|
@@ -273,7 +316,7 @@ export function apply(ctx, config) {
|
|
|
273
316
|
return () => { try { store.close() } catch { /* already closed */ } }
|
|
274
317
|
}, 'dsh-kanban: task store')
|
|
275
318
|
|
|
276
|
-
//
|
|
319
|
+
// Cross-platform system sleep inhibitor while tasks are actively running
|
|
277
320
|
const powerInhibitor = new PowerInhibitor({
|
|
278
321
|
platform: process.platform,
|
|
279
322
|
pid: process.pid,
|
|
@@ -312,7 +355,7 @@ export function apply(ctx, config) {
|
|
|
312
355
|
return () => { powerInhibitor.destroy() }
|
|
313
356
|
}, 'dsh-kanban: power inhibitor')
|
|
314
357
|
|
|
315
|
-
//
|
|
358
|
+
// Inject board instructions into agent system prompt
|
|
316
359
|
let disposePromptSection
|
|
317
360
|
const syncPromptSection = () => {
|
|
318
361
|
if (disposePromptSection) {
|
|
@@ -339,10 +382,10 @@ export function apply(ctx, config) {
|
|
|
339
382
|
return () => { if (disposePromptSection) { try { disposePromptSection() } catch { /* cleanup best effort */ } } }
|
|
340
383
|
}, 'dsh-kanban: system prompt section')
|
|
341
384
|
|
|
342
|
-
//
|
|
385
|
+
// Track waiting flags, session outcomes, and progress dump from session events.
|
|
343
386
|
ctx.effect(() => ctx.on('session/event', (session, event) => {
|
|
344
387
|
try {
|
|
345
|
-
//
|
|
388
|
+
// Auto-sync DSH chat sessions into kanban cards (#225)
|
|
346
389
|
const sessionIdStr = String(session?.id ?? '')
|
|
347
390
|
if (
|
|
348
391
|
getConfig().autoSyncSessions === true &&
|
|
@@ -395,7 +438,7 @@ export function apply(ctx, config) {
|
|
|
395
438
|
error: event?.error,
|
|
396
439
|
})
|
|
397
440
|
syncPower()
|
|
398
|
-
//
|
|
441
|
+
// Dispatch next queued task when a session completes or fails (#214)
|
|
399
442
|
if (event?.type === 'turn/end' || event?.type === 'turn/completed' || event?.type === 'session/error' || event?.type === 'turn/error') {
|
|
400
443
|
dispatchNextQueuedTask({
|
|
401
444
|
agents: ctx.agents,
|
|
@@ -414,7 +457,7 @@ export function apply(ctx, config) {
|
|
|
414
457
|
}
|
|
415
458
|
}), 'dsh-kanban: session events')
|
|
416
459
|
|
|
417
|
-
//
|
|
460
|
+
// Background cron scheduler for scheduled tasks
|
|
418
461
|
ctx.effect(() => {
|
|
419
462
|
let cronTimer
|
|
420
463
|
const armCron = () => {
|
|
@@ -482,13 +525,9 @@ export function apply(ctx, config) {
|
|
|
482
525
|
return () => { rearmers.delete(armCron); clearInterval(cronTimer) }
|
|
483
526
|
}, 'dsh-kanban: cron scheduler')
|
|
484
527
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
// Очередь отправки в Gitea. Перенос карточки не ждёт сети: доска обязана
|
|
489
|
-
// работать и при недоступном Gitea, а неудачная отправка повторяется и, если
|
|
490
|
-
// так и не удалась, попадает в журнал задачи.
|
|
491
|
-
const outbox = createOutbox({ gitea: git, store, logger: ctx.logger })
|
|
528
|
+
// Periodic Gitea reconciliation. 0 disables it; overlapping executions are prevented.
|
|
529
|
+
// Outbound push queue to Gitea. Card moves do not block on network; failures retry in background.
|
|
530
|
+
const outbox = createOutbox({ gitea: git, get store() { return store }, logger: ctx.logger })
|
|
492
531
|
|
|
493
532
|
const queueOutbound = (taskId, column) => {
|
|
494
533
|
if (getConfig().pushToGitea !== true) return
|
|
@@ -499,11 +538,8 @@ export function apply(ctx, config) {
|
|
|
499
538
|
}
|
|
500
539
|
|
|
501
540
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* Берётся из задач, уже лежащих на доске: другого источника у плагина нет,
|
|
505
|
-
* а спрашивать владельца отдельной настройкой значит просить дважды одно и
|
|
506
|
-
* то же.
|
|
541
|
+
* Default repository owner for short repository names.
|
|
542
|
+
* Inferred from existing board tasks when not explicitly configured.
|
|
507
543
|
*/
|
|
508
544
|
let discoveredOwner
|
|
509
545
|
const defaultOwner = async () => {
|
|
@@ -513,15 +549,13 @@ export function apply(ctx, config) {
|
|
|
513
549
|
if (typeof task.owner === 'string' && task.owner !== '') return task.owner
|
|
514
550
|
}
|
|
515
551
|
if (discoveredOwner !== undefined) return discoveredOwner
|
|
516
|
-
//
|
|
517
|
-
// нескольких гадание молча смотрело бы не туда, а это хуже отказа.
|
|
552
|
+
// Fall back to Gitea organization if exactly one is accessible to the token.
|
|
518
553
|
const orgs = await gitea.listOrgs()
|
|
519
554
|
if (orgs.length === 1) { discoveredOwner = orgs[0]; return discoveredOwner }
|
|
520
555
|
return undefined
|
|
521
556
|
}
|
|
522
557
|
|
|
523
|
-
//
|
|
524
|
-
// обновляться, не имеет права молчать об этом.
|
|
558
|
+
// Sync telemetry state visible in board header.
|
|
525
559
|
const syncState = createSyncState()
|
|
526
560
|
|
|
527
561
|
let syncing = false
|
|
@@ -530,9 +564,8 @@ export function apply(ctx, config) {
|
|
|
530
564
|
syncing = true
|
|
531
565
|
syncState.started()
|
|
532
566
|
try {
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// последней — она смотрит на итог, а не на исходное.
|
|
567
|
+
// Order of operations: intake new issues first, reconcile active tasks, archive overdue.
|
|
568
|
+
// Inspect final outcome rather than initial state.
|
|
536
569
|
const mode = String(getConfig()?.gitProvider || 'auto').toLowerCase()
|
|
537
570
|
const providersToSync = []
|
|
538
571
|
if (mode === 'github') {
|
|
@@ -562,18 +595,15 @@ export function apply(ctx, config) {
|
|
|
562
595
|
}
|
|
563
596
|
const out = await syncAll({ gitea: git, store, logger: ctx.logger })
|
|
564
597
|
const tidied = archiveOverdue({ store, config: getConfig() })
|
|
565
|
-
//
|
|
566
|
-
// означал бы второй источник обращений к Gitea без всякой пользы.
|
|
598
|
+
// Outbox flushes run in the same cycle to minimize redundant Gitea API calls.
|
|
567
599
|
const pushed = await outbox.flush()
|
|
568
|
-
//
|
|
569
|
-
// приезжают, а человеку об этом никто не говорит.
|
|
600
|
+
// Intake errors are reported in sync telemetry to keep failures visible.
|
|
570
601
|
syncState.finished(out.problem === undefined && taken.problem !== undefined
|
|
571
602
|
? { ...out, problem: taken.problem }
|
|
572
603
|
: out)
|
|
573
604
|
return { ...out, pushed, taken, tidied }
|
|
574
605
|
} catch (error) {
|
|
575
|
-
//
|
|
576
|
-
// неразрешённый токен или отсутствующий клиент Gitea.
|
|
606
|
+
// Sync failure to start represents missing client or unconfigured credentials.
|
|
577
607
|
syncState.failedToStart('sync', error?.message)
|
|
578
608
|
throw error
|
|
579
609
|
} finally {
|
|
@@ -620,17 +650,14 @@ export function apply(ctx, config) {
|
|
|
620
650
|
ctx.effect(() => ctx.webServer.register({
|
|
621
651
|
kind: 'exact',
|
|
622
652
|
path: '/dsh-kanban/presets',
|
|
623
|
-
//
|
|
624
|
-
// ни уровни доступа. Пустой список — не поломка, а «выбирать не из чего»,
|
|
625
|
-
// и браузер просто не рисует поле.
|
|
653
|
+
// Both services are optional in minimal environments. Empty lists render gracefully.
|
|
626
654
|
handler: async (req, res) => {
|
|
627
655
|
const presets = ctx.get('agentPresets')
|
|
628
656
|
const permissions = ctx.get('permissionPresets')
|
|
629
657
|
let agentPresets = []
|
|
630
658
|
try {
|
|
631
659
|
const rows = presets === undefined ? [] : await presets.list()
|
|
632
|
-
//
|
|
633
|
-
// сессия на нём не соберётся, а отказ прилетит уже после запуска.
|
|
660
|
+
// Filter out invalid presets that would fail session creation.
|
|
634
661
|
agentPresets = rows.filter((row) => row?.broken === undefined).map((row) => ({
|
|
635
662
|
id: row.id, name: row.name || row.id, description: row.description,
|
|
636
663
|
}))
|
|
@@ -767,9 +794,7 @@ export function apply(ctx, config) {
|
|
|
767
794
|
},
|
|
768
795
|
}), 'dsh-kanban: gitea webhook')
|
|
769
796
|
|
|
770
|
-
//
|
|
771
|
-
// запрещает этому CLI трогать канбан, и включить инструмент раньше правки
|
|
772
|
-
// скилла — значит толкать агента на нарушение.
|
|
797
|
+
// Agent tool definition. Default disabled; workflow skills explicitly enable it.
|
|
773
798
|
ctx.effect(() => {
|
|
774
799
|
let unregister
|
|
775
800
|
const arm = () => {
|
|
@@ -779,8 +804,7 @@ export function apply(ctx, config) {
|
|
|
779
804
|
unregister = registerBoardTool()
|
|
780
805
|
}
|
|
781
806
|
|
|
782
|
-
//
|
|
783
|
-
// множить настройки ради разницы, которой человек не почувствует.
|
|
807
|
+
// Single setting switch controls both board tools.
|
|
784
808
|
const registerBoardTool = () => {
|
|
785
809
|
const off = [
|
|
786
810
|
ctx.tools.register(defineTool(boardMoveDefinition({ store, config: getConfig }))),
|
|
@@ -801,8 +825,7 @@ export function apply(ctx, config) {
|
|
|
801
825
|
}, 'dsh-kanban: board tool')
|
|
802
826
|
|
|
803
827
|
/**
|
|
804
|
-
*
|
|
805
|
-
* Отказ реестра на незнакомый идентификатор ловит сборка доски.
|
|
828
|
+
* Return active agent state or undefined if session is inactive.
|
|
806
829
|
*/
|
|
807
830
|
const liveOf = (sessionId) => ctx.agents.get(sessionId)?.status
|
|
808
831
|
|
|
@@ -819,27 +842,15 @@ export function apply(ctx, config) {
|
|
|
819
842
|
repo: url.searchParams.get('repo') || undefined,
|
|
820
843
|
liveOf,
|
|
821
844
|
sync: syncState.snapshot(),
|
|
822
|
-
//
|
|
823
|
-
// окне запуска, когда человек не задал корень настройкой, — иначе
|
|
824
|
-
// сессия молча поднимается в рабочей папке харнесса.
|
|
845
|
+
// Resolved project root passed with board payload for session launch modal.
|
|
825
846
|
projectRoot: rootOf(getConfig()),
|
|
826
847
|
}))
|
|
827
848
|
},
|
|
828
849
|
}), 'dsh-kanban: board route')
|
|
829
850
|
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
// Прежде выгрузка звалась `/export`, а загрузка занимала `/import` — тот
|
|
833
|
-
// самый путь, на котором с первого дня живёт импорт issue из Gitea. Два
|
|
834
|
-
// обработчика на одном пути означают, что один из них не отвечает вовсе, и
|
|
835
|
-
// какой именно — решает порядок регистрации, а не мы.
|
|
851
|
+
// Snapshot endpoint handles both export and import.
|
|
836
852
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
* Логин нужен и «взять себе», и запуску работы. Спрашивать его у Gitea
|
|
840
|
-
* каждый раз значит добавить в путь запуска ещё одну сеть, которая может не
|
|
841
|
-
* ответить; поэтому спрашиваем один раз и помним. Не ответила — работаем без
|
|
842
|
-
* назначения, а не отказываем в запуске.
|
|
853
|
+
* Cached identity for Gitea client to avoid redundant API lookups on task claim.
|
|
843
854
|
*/
|
|
844
855
|
let knownLogin
|
|
845
856
|
const whoAmI = async () => {
|
|
@@ -870,8 +881,7 @@ export function apply(ctx, config) {
|
|
|
870
881
|
ctx.effect(() => ctx.webServer.register({
|
|
871
882
|
kind: 'exact',
|
|
872
883
|
path: '/dsh-kanban/whoami',
|
|
873
|
-
//
|
|
874
|
-
// отвечать. Браузер по нему просто не рисует «Мои», а не показывает ноль.
|
|
884
|
+
// Empty login indicates unconfigured or unreachable Gitea; client omits "Mine" filter.
|
|
875
885
|
handler: async (req, res) => {
|
|
876
886
|
if (req.method !== 'GET') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
877
887
|
reply(res, { login: await whoAmI() })
|
|
@@ -899,11 +909,11 @@ export function apply(ctx, config) {
|
|
|
899
909
|
kind: 'exact',
|
|
900
910
|
path: '/dsh-kanban/events',
|
|
901
911
|
handler: (req, res) => {
|
|
902
|
-
eventHub.handleSseRequest(req, res)
|
|
912
|
+
eventHub.handleSseRequest(req, res, { isTrustedRequest })
|
|
903
913
|
},
|
|
904
914
|
}), 'dsh-kanban: sse events stream')
|
|
905
915
|
|
|
906
|
-
//
|
|
916
|
+
// Clean up SSE client connections and heartbeat timer on plugin disposal (#282)
|
|
907
917
|
if (typeof ctx.on === 'function') {
|
|
908
918
|
ctx.on('dispose', () => {
|
|
909
919
|
eventHub.closeAll()
|
|
@@ -944,14 +954,13 @@ export function apply(ctx, config) {
|
|
|
944
954
|
}), 'dsh-kanban: board snapshot')
|
|
945
955
|
|
|
946
956
|
ctx.effect(() => ctx.webServer.register({
|
|
947
|
-
//
|
|
948
|
-
// сводится к равенству пути, и всё, что глубже, уходит в статику.
|
|
957
|
+
// Prefix registered without trailing slash for sub-path routing.
|
|
949
958
|
kind: 'prefix',
|
|
950
959
|
path: '/dsh-kanban/task',
|
|
951
960
|
handler: async (req, res) => {
|
|
952
961
|
const pathname = urlOf(req).pathname
|
|
953
962
|
|
|
954
|
-
//
|
|
963
|
+
// Path without ID handles local task creation.
|
|
955
964
|
if (pathname === '/dsh-kanban/task' || pathname === '/dsh-kanban/task/') {
|
|
956
965
|
if (req.method !== 'POST') { writeJson(res, 405, { error: 'method-not-allowed' }); return }
|
|
957
966
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
@@ -970,8 +979,7 @@ export function apply(ctx, config) {
|
|
|
970
979
|
if (req.method === 'GET' && action === 'log') { reply(res, taskLog({ store, id })); return }
|
|
971
980
|
if (req.method === 'GET' && action === 'body') { reply(res, taskBody({ store, id })); return }
|
|
972
981
|
|
|
973
|
-
//
|
|
974
|
-
// иначе шаблон окажется в двух местах и однажды разойдётся.
|
|
982
|
+
// Generate start prompt on server for template consistency.
|
|
975
983
|
if (req.method === 'GET' && action === 'message') {
|
|
976
984
|
const task = store.getTask(id)
|
|
977
985
|
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
@@ -980,8 +988,7 @@ export function apply(ctx, config) {
|
|
|
980
988
|
}
|
|
981
989
|
if (!isTrustedRequest(req)) { writeJson(res, 403, { error: 'cross-site' }); return }
|
|
982
990
|
if (req.method === 'DELETE' && action === undefined) { reply(res, deleteTask({ store, id })); return }
|
|
983
|
-
//
|
|
984
|
-
// локальное свойство доски, в Gitea не пишется (updateTask это знает).
|
|
991
|
+
// Card fields update: title, body, labels, priority. Priority is board-local.
|
|
985
992
|
if (req.method === 'POST' && action === 'update') {
|
|
986
993
|
try {
|
|
987
994
|
const input = await readBody(req)
|
|
@@ -1057,8 +1064,7 @@ export function apply(ctx, config) {
|
|
|
1057
1064
|
config: getConfig(),
|
|
1058
1065
|
provider: picked.provider,
|
|
1059
1066
|
model: picked.model,
|
|
1060
|
-
//
|
|
1061
|
-
// поднимается вовсе, и заранее занятое имя осталось бы висеть.
|
|
1067
|
+
// Mint session ID lazily to avoid dangling IDs on resume.
|
|
1062
1068
|
mintSessionId: () => SessionId(`kanban-${id}-${randomUUID()}`),
|
|
1063
1069
|
createMessage: createUserMessage,
|
|
1064
1070
|
logger: ctx.logger,
|
|
@@ -1073,8 +1079,7 @@ export function apply(ctx, config) {
|
|
|
1073
1079
|
reply(res, started)
|
|
1074
1080
|
} catch (error) {
|
|
1075
1081
|
ctx.logger?.warn?.(`dsh-kanban: task launch failed: ${error?.message}`)
|
|
1076
|
-
//
|
|
1077
|
-
// месте «сессия пошла бы с большими правами» — это молчание.
|
|
1082
|
+
// Explicit permission errors are returned with status and key.
|
|
1078
1083
|
writeJson(res, error?.status ?? 500, { error: error?.key ?? error?.message ?? 'start-failed' })
|
|
1079
1084
|
}
|
|
1080
1085
|
return
|
|
@@ -1083,13 +1088,7 @@ export function apply(ctx, config) {
|
|
|
1083
1088
|
try {
|
|
1084
1089
|
const input = await readBody(req)
|
|
1085
1090
|
if (req.method === 'POST' && action === 'move') {
|
|
1086
|
-
//
|
|
1087
|
-
// Автоматика (сверка, инструмент, жизненный цикл) сюда не приходит:
|
|
1088
|
-
// она двигает карточку мимо этого маршрута.
|
|
1089
|
-
//
|
|
1090
|
-
// Команду отдаём ДО переноса: остановка должна случиться раньше, чем
|
|
1091
|
-
// доска объявит работу прекращённой, а её исход становится
|
|
1092
|
-
// пояснением к единственному переходу в журнале.
|
|
1091
|
+
// Manual move dispatches stop command to agent prior to updating card column.
|
|
1093
1092
|
const was = store.getTask(id)
|
|
1094
1093
|
const acted = was !== undefined && input.column !== was.column
|
|
1095
1094
|
? dispatchMove({
|
|
@@ -1232,7 +1231,7 @@ export function apply(ctx, config) {
|
|
|
1232
1231
|
agentPreset: input?.agentPreset || task.agentPreset,
|
|
1233
1232
|
permission: input?.permission || task.permission,
|
|
1234
1233
|
permissions: ctx.get('permissionPresets'),
|
|
1235
|
-
claimAs:
|
|
1234
|
+
claimAs: await whoAmI(),
|
|
1236
1235
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
1237
1236
|
resume: true,
|
|
1238
1237
|
})
|
|
@@ -1259,8 +1258,7 @@ export function apply(ctx, config) {
|
|
|
1259
1258
|
store.updateTaskRun(task.id, latestRun.id, { endedAt: Date.now(), result: 'cancelled' })
|
|
1260
1259
|
}
|
|
1261
1260
|
syncPower()
|
|
1262
|
-
//
|
|
1263
|
-
// иначе прерванная работа выглядит просто оборвавшейся сама.
|
|
1261
|
+
// Stop event recorded in transition history.
|
|
1264
1262
|
store.addTransition({
|
|
1265
1263
|
taskId: task.id, fromCol: task.column, toCol: task.column,
|
|
1266
1264
|
source: 'manual', detail: STOP_DETAIL.stopped,
|
|
@@ -1272,8 +1270,7 @@ export function apply(ctx, config) {
|
|
|
1272
1270
|
if (req.method === 'POST' && action === 'assign') {
|
|
1273
1271
|
const was = store.getTask(id)
|
|
1274
1272
|
if (was === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
1275
|
-
//
|
|
1276
|
-
// токеном ходит доска: своего понятия пользователя у харнесса нет.
|
|
1273
|
+
// Empty login unassigns; 'me' resolves authenticated token identity.
|
|
1277
1274
|
let login = typeof input.login === 'string' ? input.login.trim() : ''
|
|
1278
1275
|
if (login === 'me') {
|
|
1279
1276
|
login = await whoAmI()
|
|
@@ -1281,8 +1278,7 @@ export function apply(ctx, config) {
|
|
|
1281
1278
|
}
|
|
1282
1279
|
const out = assignTask({ store, id, login })
|
|
1283
1280
|
if (out.task !== undefined && config.pushToGitea !== false) {
|
|
1284
|
-
//
|
|
1285
|
-
// Gitea лежит.
|
|
1281
|
+
// Queue assignment update to outbox for offline resilience.
|
|
1286
1282
|
outbox.push(id, planAssign(out.task, login))
|
|
1287
1283
|
}
|
|
1288
1284
|
reply(res, out)
|
|
@@ -1323,9 +1319,7 @@ export function apply(ctx, config) {
|
|
|
1323
1319
|
},
|
|
1324
1320
|
}), 'dsh-kanban: task routes')
|
|
1325
1321
|
|
|
1326
|
-
//
|
|
1327
|
-
// учётная запись правятся в карточке настроек на ходу, и маршруты обязаны
|
|
1328
|
-
// внятно объяснять причину, а не отвечать 404.
|
|
1322
|
+
// Check Gitea readiness per request to handle dynamically updated credentials.
|
|
1329
1323
|
ctx.effect(() => ctx.webServer.register({
|
|
1330
1324
|
kind: 'exact',
|
|
1331
1325
|
path: '/dsh-kanban/gitea/repos',
|
|
@@ -1371,9 +1365,7 @@ export function apply(ctx, config) {
|
|
|
1371
1365
|
},
|
|
1372
1366
|
}), 'dsh-kanban: issue import')
|
|
1373
1367
|
|
|
1374
|
-
//
|
|
1375
|
-
// перечень моделей провайдер отдаёт по сети, и опрашивать все разом ради
|
|
1376
|
-
// одного выпадающего списка незачем.
|
|
1368
|
+
// Providers returned immediately; models queried on-demand for selected provider.
|
|
1377
1369
|
ctx.effect(() => ctx.webServer.register({
|
|
1378
1370
|
kind: 'exact',
|
|
1379
1371
|
path: '/dsh-kanban/models',
|
|
@@ -1396,8 +1388,7 @@ export function apply(ctx, config) {
|
|
|
1396
1388
|
models: (rows ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
|
|
1397
1389
|
})
|
|
1398
1390
|
} catch {
|
|
1399
|
-
//
|
|
1400
|
-
// доски, а пустой список с сохранённым выбором провайдера.
|
|
1391
|
+
// Provider errors return empty model list while preserving selected provider.
|
|
1401
1392
|
writeJson(res, 200, { current, providers, models: [] })
|
|
1402
1393
|
}
|
|
1403
1394
|
},
|