@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/index.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { registerPluginUpdater } from './updater.js'
2
- // dsh-kanban — доска задач для DeepSeek Harness.
2
+ // dsh-kanban — task board for DeepSeek Harness.
3
3
  //
4
- // Плагин состоит из двух половин: этот модуль — серверная, `lib/client.js` —
5
- // браузерная. Имя пакета `@goodandready/dsh-kanban` обязано совпадать в трёх
6
- // местах: `package.json`, `cordis.patch.yml` и `load({ id })` в client.js.
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
- // Чистая логика вынесена в модули без зависимостей (`config.js`, `order.js`,
10
- // `routes.js`) и проверяется тестами без харнесса. Здесь остаётся обвязка
11
- // cordis: настройки, хранилище и маршруты.
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
- /** Стабильное имя плагина cordis (строка в патче сборки). */
104
+ /** Stable cordis plugin name (patch identifier). */
106
105
  export const name = '@goodandready/dsh-kanban'
107
106
 
108
107
  /**
109
- * Службы добавляются там, где впервые нужны: `gitea` — вместе с импортом
110
- * задач, `agents`/`sessions` — вместе с запуском работы. Объявленная, но
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
- * месте. Только скаляры: клиентский API настроек пишет ТОЛЬКО скалярные поля,
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
- /** Ответ обработчика: либо объект с `error` и `status`, либо полезная нагрузка. */
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
- * Подпись вебхука считается по БАЙТАМ доставки. Разобрать JSON и собрать его
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
- // Клиент Gitea свой: токен всё равно живёт в службе учётных данных ядра, а
238
- // шесть вызовов REST дешевле зависимости от чужого выпуска. Значение токена
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
- // Признак ожидания, исходы сессий и PROGRESSDUMP по событиям сессии.
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
- // Автосинхронизация сессий чата DSH в карточки канбана (#225)
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
- // Если сессия завершила шаг или упала — пробуем продвинуть очередь задач (#214)
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
- // Фоновый шедулер cron для задач
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
- // Периодическая сверка с Gitea. Ноль в настройках отключает её целиком, а
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
- // Спрашиваем у Gitea, и только когда организация ровно одна: при
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
- * Состояние живого агента сессии либо `undefined`, если агента нет.
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
- * Кто мы для Gitea, с памятью на время работы.
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
- // Пустой логин — законный ответ: Gitea может быть не настроен или не
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
- // Очистка SSE-клиентов и heartbeat таймера при выгрузке плагина (#282)
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
- // Правка полей карточки: title, body, метки, приоритет. Приоритет —
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
- // Пустой логин снимает назначение; слово «me» просит того, чьим
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
- // В Gitea уходит очередью: доска не ждёт сеть и работает, когда
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
- // Готовность Gitea проверяется на каждом запросе, а не при загрузке: адрес и
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
- // Архив выполненного и подхват новых задач из Gitea.
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
- * Стоит ли заводить карточку под этот issue.
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 в Gitea — тоже issue, но работой на доске он не является:
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
- * Архивируем ТОЛЬКО из `done`. Задача, застрявшая в ревью на месяц, — это
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(