@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/webhook.js CHANGED
@@ -1,23 +1,16 @@
1
- // Приём событий Gitea и GitHub.
2
- //
3
- // Вебхук даёт мгновенную сверку вместо ожидания следующего опроса. Опрос при
4
- // этом остаётся: вебхук может быть не настроен, недоступен снаружи или просто
5
- // потерян по дороге. Мгновенность — ускорение, а не замена надёжности.
6
- //
7
- // Модуль чистый: разбор и проверка подписи без сети и без хранилища.
1
+ // Webhook ingestion for Gitea and GitHub.
2
+ // Webhooks trigger immediate sync alongside periodic polling.
3
+ // Pure module: parsing and signature verification without network or database.
8
4
 
9
5
  import { createHmac, timingSafeEqual } from 'node:crypto'
10
6
 
11
7
  /**
12
- * Проверить подпись доставки (поддерживает Gitea X-Gitea-Signature и GitHub X-Hub-Signature-256).
8
+ * Verify delivery signature (supports Gitea X-Gitea-Signature and GitHub X-Hub-Signature-256).
9
+ * Constant-time comparison to prevent timing attacks.
13
10
  *
14
- * Сравнение постоянного времени: обычное посимвольное сравнение по времени
15
- * ответа выдаёт, сколько первых байтов угадано, и подпись подбирается за
16
- * считанные попытки.
17
- *
18
- * @param {string} secret общий секрет
19
- * @param {string|Buffer} body сырое тело запроса
20
- * @param {string} signature значение заголовка X-Gitea-Signature или X-Hub-Signature-256
11
+ * @param {string} secret shared secret
12
+ * @param {string|Buffer} body raw request body
13
+ * @param {string} signature header value of X-Gitea-Signature or X-Hub-Signature-256
21
14
  */
22
15
  export function verifySignature(secret, body, signature) {
23
16
  if (!secret) return false
@@ -31,9 +24,9 @@ export function verifySignature(secret, body, signature) {
31
24
  }
32
25
 
33
26
  /**
34
- * Определить провайдера вебхука по входящим заголовкам запроса.
27
+ * Detect webhook provider from incoming request headers.
35
28
  *
36
- * @param {object} headers заголовки HTTP-запроса
29
+ * @param {object} headers HTTP request headers
37
30
  * @returns {'github'|'gitea'}
38
31
  */
39
32
  export function detectWebhookProvider(headers) {
@@ -45,11 +38,11 @@ export function detectWebhookProvider(headers) {
45
38
  }
46
39
 
47
40
  /**
48
- * Что за задачу затронуло событие.
41
+ * Identifies which task was affected by the event.
49
42
  *
50
- * Возвращается адрес задачи, а не готовое решение: решать, куда двинуть
51
- * карточку, — дело сверки, у которой перед глазами полная картина. Вебхук
52
- * только говорит «вот здесь что-то произошло, посмотри сейчас, а не потом».
43
+ * Returns task coordinates rather than a transition verdict: deciding
44
+ * where to move the card belongs to sync logic which holds the complete state.
45
+ * Webhook merely notifies: 'an event occurred here, inspect now'.
53
46
  *
54
47
  * @returns {{owner: string, repo: string, issueNumber?: number}|undefined}
55
48
  */
@@ -59,8 +52,8 @@ export function parseEvent(payload) {
59
52
  const repo = repository?.name
60
53
  if (typeof owner !== 'string' || owner === '' || typeof repo !== 'string' || repo === '') return undefined
61
54
 
62
- // Номер issue приходит по-разному в зависимости от события; PR при этом сам
63
- // является issue с тем же номером.
55
+ // Issue number format varies depending on event payload; PR is also an issue
56
+ // with matching number.
64
57
  const number = payload?.issue?.number
65
58
  ?? payload?.pull_request?.number
66
59
  ?? (typeof payload?.number === 'number' ? payload.number : undefined)
@@ -1,15 +1,15 @@
1
- // Мост между карточками Kanban и встроенным в ядро DSH workflowEngine.
1
+ // Bridge between Kanban cards and core DSH workflowEngine.
2
2
  //
3
- // Выполняет перенос логики из dsh-agent-orchestrator/lib/integrations/kanban-bridge.js:
4
- // 1. Сопоставление карточка ↔ роль (по меткам и колонкам).
5
- // 2. Выбор пресета агента с проверкой доступности через ctx.agentPresets.list()
6
- // и фоллбэком на пресет по умолчанию.
7
- // 3. Формирование сценария и запуск через ctx.workflowEngine.start().
8
- // 4. Отслеживание жизненного цикла workflow/* (agent-start, agent-end, end):
3
+ // Integrates card lifecycle with core workflowEngine:
4
+ // 1. Map card <-> role (via labels and columns).
5
+ // 2. Select agent preset with availability check via ctx.agentPresets.list()
6
+ // and fallback to default preset.
7
+ // 3. Construct script and execute via ctx.workflowEngine.start().
8
+ // 4. Track workflow/* lifecycle (agent-start, agent-end, end):
9
9
  // - running -> review/done/failed;
10
- // - фиксация runId и сессии в задаче;
11
- // - отправка summary в связанный issue Gitea / GitHub.
12
- // 5. Отмена активного workflow по запросу из карточки.
10
+ // - record runId and session in task;
11
+ // - post summary to linked Gitea / GitHub issue.
12
+ // 5. Cancel active workflow on card request.
13
13
 
14
14
  import { buildStartMessage, obtainAgent } from './launcher.js'
15
15
  import { applyMove } from './routes.js'
@@ -24,25 +24,25 @@ export const DEFAULT_COLUMN_ROLES = {
24
24
  }
25
25
 
26
26
  const LABEL_ROLE_PATTERNS = [
27
- { role: 'spec', patterns: ['spec', 'specification', 'requirements', 'requirements-analysis', 'анализ', 'тз'] },
28
- { role: 'architecture', patterns: ['architecture', 'архитектура', 'contract'] },
29
- { role: 'ui_design', patterns: ['ui', 'ui_design', 'design', 'ux', 'дизайн'] },
30
- { role: 'frontend', patterns: ['frontend', 'web', 'client', 'ui-impl', 'фронтенд'] },
31
- { role: 'backend', patterns: ['backend', 'api', 'server', 'бэкенд'] },
27
+ { role: 'spec', patterns: ['spec', 'specification', 'requirements', 'requirements-analysis', 'analysis'] },
28
+ { role: 'architecture', patterns: ['architecture', 'contract'] },
29
+ { role: 'ui_design', patterns: ['ui', 'ui_design', 'design', 'ux'] },
30
+ { role: 'frontend', patterns: ['frontend', 'web', 'client', 'ui-impl'] },
31
+ { role: 'backend', patterns: ['backend', 'api', 'server'] },
32
32
  { role: 'fullstack', patterns: ['fullstack', 'full-stack', 'e2e'] },
33
- { role: 'qa_tests', patterns: ['qa', 'qa_tests', 'test', 'tests', 'testing', 'verification', 'тест', 'тесты'] },
34
- { role: 'bugfix', patterns: ['bug', 'bugfix', 'fix', 'баг', 'фикс'] },
35
- { role: 'docs', patterns: ['docs', 'doc', 'documentation', 'документация'] },
36
- { role: 'refactoring', patterns: ['refactor', 'refactoring', 'рефакторинг'] },
37
- { role: 'research', patterns: ['research', 'spike', 'исследование'] },
38
- { role: 'devops', patterns: ['devops', 'infra', 'ci', 'cd', 'deploy', 'деплой'] },
33
+ { role: 'qa_tests', patterns: ['qa', 'qa_tests', 'test', 'tests', 'testing', 'verification'] },
34
+ { role: 'bugfix', patterns: ['bug', 'bugfix', 'fix'] },
35
+ { role: 'docs', patterns: ['docs', 'doc', 'documentation'] },
36
+ { role: 'refactoring', patterns: ['refactor', 'refactoring'] },
37
+ { role: 'research', patterns: ['research', 'spike'] },
38
+ { role: 'devops', patterns: ['devops', 'infra', 'ci', 'cd', 'deploy'] },
39
39
  ]
40
40
 
41
41
  /**
42
- * Определить роль для задачи по меткам, колонке или явному полю role.
42
+ * Resolve role for task by labels, column or explicit role property.
43
43
  *
44
- * @param {object} task Задача доски
45
- * @returns {string} Идентификатор роли
44
+ * @param {object} task Board task
45
+ * @returns {string} Role ID
46
46
  */
47
47
  export function resolveRole(task) {
48
48
  if (!task) return 'backend'
@@ -81,14 +81,14 @@ export function resolveRole(task) {
81
81
  }
82
82
 
83
83
  /**
84
- * Подобрать пресет агента из доступных.
85
- * Если роль найдена в списке доступных пресетов — возвращает её.
86
- * Иначе выполняет безопасную деградацию на defaultPreset или undefined.
84
+ * Resolve agent preset from available list.
85
+ * If role matches available preset, returns it.
86
+ * Otherwise gracefully degrades to defaultPreset or undefined.
87
87
  *
88
88
  * @param {object} params
89
- * @param {string} params.role Имя роли
90
- * @param {Array<object|string>} [params.availablePresets] Список пресетов
91
- * @param {string} [params.defaultPreset] Пресет по умолчанию
89
+ * @param {string} params.role Role name
90
+ * @param {Array<object|string>} [params.availablePresets] Available presets
91
+ * @param {string} [params.defaultPreset] Default preset
92
92
  * @returns {string|undefined}
93
93
  */
94
94
  export function resolvePreset({ role, availablePresets = [], defaultPreset = undefined }) {
@@ -112,7 +112,7 @@ export function resolvePreset({ role, availablePresets = [], defaultPreset = und
112
112
  }
113
113
 
114
114
  /**
115
- * Построить JavaScript сценарий для ctx.workflowEngine.start().
115
+ * Construct JavaScript workflow script for ctx.workflowEngine.start().
116
116
  *
117
117
  * @param {object} params
118
118
  * @param {object} params.task
@@ -120,25 +120,27 @@ export function resolvePreset({ role, availablePresets = [], defaultPreset = und
120
120
  * @returns {string}
121
121
  */
122
122
  export function buildWorkflowScript({ task, role }) {
123
- const taskId = String(task?.id || 'unknown').replace(/["\\]/g, '')
124
- const safeRole = String(role || 'backend').replace(/["\\]/g, '')
125
- const issueNum = task?.issueNumber ? `#${task.issueNumber}` : taskId
123
+ const rawRole = String(role || 'backend')
124
+ const rawTaskId = String(task?.id || 'unknown')
125
+ const validIssue = Number.isSafeInteger(task?.issueNumber) && task.issueNumber > 0 ? task.issueNumber : undefined
126
+ const cardLabel = validIssue !== undefined ? `#${validIssue}` : rawTaskId
127
+ const logMsg = `Starting phase ${rawRole} for card ${cardLabel}`
126
128
 
127
129
  return [
128
- `phase("${safeRole}");`,
129
- `log("Запуск этапа ${safeRole} по карточке ${issueNum}");`,
130
+ `phase(${JSON.stringify(rawRole)});`,
131
+ `log(${JSON.stringify(logMsg)});`,
130
132
  `const prompt = args.prompt;`,
131
- `const result = await agent(prompt, { label: "${safeRole}", phase: "${safeRole}" });`,
133
+ `const result = await agent(prompt, { label: ${JSON.stringify(rawRole)}, phase: ${JSON.stringify(rawRole)} });`,
132
134
  `return {`,
133
- ` role: "${safeRole}",`,
134
- ` taskId: "${taskId}",`,
135
+ ` role: ${JSON.stringify(rawRole)},`,
136
+ ` taskId: ${JSON.stringify(rawTaskId)},`,
135
137
  ` summary: typeof result === 'string' ? result : (result ? JSON.stringify(result) : '')`,
136
138
  `};`,
137
139
  ].join('\n')
138
140
  }
139
141
 
140
142
  /**
141
- * Сформировать метаданные сценария workflow.
143
+ * Construct metadata for workflow script.
142
144
  *
143
145
  * @param {object} params
144
146
  * @param {object} params.task
@@ -160,7 +162,7 @@ export function buildWorkflowMeta({ task, role }) {
160
162
  }
161
163
 
162
164
  /**
163
- * Определить целевую колонку после успешного завершения этапа workflow.
165
+ * Determine target column upon successful completion of workflow phase.
164
166
  *
165
167
  * @param {string} currentCol
166
168
  * @returns {string}
@@ -177,7 +179,7 @@ export function resolveTargetColumn(currentCol) {
177
179
  }
178
180
 
179
181
  /**
180
- * Класс-мост между доской и встроенным сервисом workflow ядра DSH.
182
+ * Bridge class connecting Kanban board to core DSH workflow service.
181
183
  */
182
184
  export class WorkflowBridge {
183
185
  constructor({
@@ -206,7 +208,7 @@ export class WorkflowBridge {
206
208
  }
207
209
 
208
210
  /**
209
- * Список зарегистрированных пресетов ядра.
211
+ * List of core registered presets.
210
212
  */
211
213
  async getAvailablePresets() {
212
214
  if (!this.agentPresets || typeof this.agentPresets.list !== 'function') {
@@ -215,13 +217,13 @@ export class WorkflowBridge {
215
217
  try {
216
218
  return await this.agentPresets.list()
217
219
  } catch (err) {
218
- this.logger?.warn?.(`dsh-kanban: не удалось получить список пресетов: ${err.message}`)
220
+ this.logger?.warn?.(`dsh-kanban: failed to get presets list: ${err.message}`)
219
221
  return []
220
222
  }
221
223
  }
222
224
 
223
225
  /**
224
- * Запуск workflow по карточке.
226
+ * Launch workflow for card.
225
227
  */
226
228
  async startTaskWorkflow({
227
229
  task,
@@ -317,7 +319,7 @@ export class WorkflowBridge {
317
319
  this.activeRuns.set(task.id, entry)
318
320
  this.runToTask.set(runId, task.id)
319
321
 
320
- // Обновляем карточку в базе данных
322
+ // Update task card in database
321
323
  this.store.updateTask(task.id, {
322
324
  workflowRunId: runId,
323
325
  workflowStatus: 'running',
@@ -335,7 +337,7 @@ export class WorkflowBridge {
335
337
  fromCol: task.col || task.column,
336
338
  toCol: task.col || task.column,
337
339
  source: 'workflow',
338
- detail: `запуск workflow ${runId} (роль: ${role}, пресет: ${preset || 'default'})`,
340
+ detail: `starting workflow ${runId} (role: ${role}, preset: ${preset || 'default'})`,
339
341
  })
340
342
 
341
343
  this.eventHub?.broadcast?.('task:change', {
@@ -347,7 +349,7 @@ export class WorkflowBridge {
347
349
  sessionId: taken.sessionId,
348
350
  })
349
351
 
350
- // Асинхронное ожидание завершения run
352
+ // Asynchronously await run completion
351
353
  run.result.then(async (result) => {
352
354
  await this.handleWorkflowResult(task.id, runId, role, result)
353
355
  }).catch(async (err) => {
@@ -370,14 +372,14 @@ export class WorkflowBridge {
370
372
  }
371
373
 
372
374
  /**
373
- * Отмена запущенного workflow.
375
+ * Cancel running workflow.
374
376
  */
375
377
  async cancelTaskWorkflow(taskId, reason = 'cancelled-by-user') {
376
378
  if (!taskId) return { cancelled: false, error: 'task-id-required' }
377
379
 
378
380
  const entry = this.activeRuns.get(taskId)
379
381
  if (!entry) {
380
- // Возможно, статус в БД ещё числится running
382
+ // Status in DB might still indicate running
381
383
  const task = this.store.getTask(taskId)
382
384
  if (task?.workflowStatus === 'running') {
383
385
  this.store.updateTask(taskId, { workflowStatus: 'cancelled' })
@@ -398,7 +400,7 @@ export class WorkflowBridge {
398
400
  fromCol: entry.task?.col || entry.task?.column || 'progress',
399
401
  toCol: entry.task?.col || entry.task?.column || 'progress',
400
402
  source: 'workflow',
401
- detail: `отмена workflow ${entry.runId}: ${reason}`,
403
+ detail: `cancelled workflow ${entry.runId}: ${reason}`,
402
404
  })
403
405
 
404
406
  this.eventHub?.broadcast?.('task:change', {
@@ -412,7 +414,7 @@ export class WorkflowBridge {
412
414
  }
413
415
 
414
416
  /**
415
- * Обработка завершения выполнения workflow.
417
+ * Handle workflow execution completion.
416
418
  */
417
419
  async handleWorkflowResult(taskId, runId, role, result) {
418
420
  const entry = this.activeRuns.get(taskId)
@@ -443,16 +445,16 @@ export class WorkflowBridge {
443
445
  id: taskId,
444
446
  column: targetCol,
445
447
  source: 'workflow',
446
- detail: `workflow ${runId} завершён успешно (роль: ${role})`,
448
+ detail: `workflow ${runId} completed successfully (role: ${role})`,
447
449
  })
448
450
  }
449
451
 
450
- // Отправляем комментарий в связанный issue Gitea / GitHub
452
+ // Send comment to linked Gitea / GitHub issue
451
453
  if (this.git && task.repo && task.issueNumber) {
452
454
  const commentBody = [
453
- `🚀 **Workflow завершён успешно!** (роль: \`${role}\`)`,
455
+ `🚀 **Workflow completed successfully!** (role: \`${role}\`)`,
454
456
  summary ? `\n${summary}` : '',
455
- `\n*Статус карточки переведён в колонку "${targetCol}".*`,
457
+ `\n*Card status moved to column "${targetCol}".*`,
456
458
  ].filter(Boolean).join('\n')
457
459
 
458
460
  try {
@@ -464,16 +466,18 @@ export class WorkflowBridge {
464
466
  provider: task.provider,
465
467
  })
466
468
  } catch (err) {
467
- this.logger?.warn?.(`dsh-kanban: не удалось отправить комментарий в issue ${task.repo}#${task.issueNumber}: ${err.message}`)
469
+ this.logger?.warn?.(`dsh-kanban: failed to comment on issue ${task.repo}#${task.issueNumber}: ${err.message}`)
468
470
  }
469
471
  }
470
472
 
471
- // Добавляем комментарий в карточку
472
- if (typeof this.store.addTaskComment === 'function' && summary) {
473
- this.store.addTaskComment(taskId, {
474
- author: 'workflow',
475
- body: `🚀 **Workflow завершён (${role}):**\n\n${summary}`,
476
- })
473
+ // Add comment to card
474
+ if (summary) {
475
+ const text = `🚀 **Workflow completed (${role}):**\n\n${summary}`
476
+ if (typeof this.store?.addComment === 'function') {
477
+ this.store.addComment(taskId, { author: 'workflow', role: 'workflow', text })
478
+ } else if (typeof this.store?.addTaskComment === 'function') {
479
+ this.store.addTaskComment(taskId, { author: 'workflow', role: 'workflow', text, body: text })
480
+ }
477
481
  }
478
482
  } else {
479
483
  this.store.addTransition({
@@ -481,7 +485,7 @@ export class WorkflowBridge {
481
485
  fromCol: task.col || task.column,
482
486
  toCol: task.col || task.column,
483
487
  source: 'workflow',
484
- detail: `workflow ${runId} завершён со статусом ${stopReason}: ${result?.error || ''}`,
488
+ detail: `workflow ${runId} completed with status ${stopReason}: ${result?.error || ''}`,
485
489
  })
486
490
  }
487
491
 
@@ -489,7 +493,7 @@ export class WorkflowBridge {
489
493
  try {
490
494
  await entry.run.dispose()
491
495
  } catch (err) {
492
- this.logger?.warn?.(`dsh-kanban: ошибка при dispose workflow run ${runId}: ${err.message}`)
496
+ this.logger?.warn?.(`dsh-kanban: error disposing workflow run ${runId}: ${err.message}`)
493
497
  }
494
498
  }
495
499
 
@@ -504,7 +508,7 @@ export class WorkflowBridge {
504
508
  }
505
509
 
506
510
  /**
507
- * Подключить слушатели событий ядра workflow/*.
511
+ * Attach core workflow/* event listeners.
508
512
  *
509
513
  * @param {object} ctx Cordis context
510
514
  */
@@ -547,8 +551,8 @@ export class WorkflowBridge {
547
551
  const taskId = this.runToTask.get(runId)
548
552
  if (!taskId) return
549
553
 
550
- // handleWorkflowResult также вызывается через run.result.then,
551
- // но событие workflow/end гарантирует синхронизацию наблюдателей
554
+ // handleWorkflowResult is also invoked via run.result.then,
555
+ // but workflow/end event guarantees observer synchronization
552
556
  this.eventHub?.broadcast?.('task:change', {
553
557
  taskId,
554
558
  action: 'workflow-core-end',