@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.
@@ -1,46 +1,36 @@
1
- // Автоперемещение карточек: вывод колонки из наблюдений и приоритет источников.
2
- //
3
- // Модуль чистый — ни сети, ни хранилища, ни харнесса. Наблюдения приходят
4
- // готовыми, решение возвращается значением. Поэтому вся логика переходов
5
- // проверяется тестами, а места, где она вызывается, остаются тонкими.
1
+ // Card auto-movement: derive column from observations and source priority.
2
+ // Pure module without network, storage, or harness dependencies.
3
+ // Observations arrive as data, verdicts return as values.
6
4
 
7
5
  /**
8
- * Приоритет источников. Без него «PR влит → deploy» через секунду перебивалось
9
- * бы «ход завершился → in-progress», и карточка прыгала бы туда-сюда.
10
- *
11
- * Меньше число — сильнее источник.
6
+ * Source priority.
7
+ * Lower number indicates higher priority source.
12
8
  */
13
9
  const PRIORITY = { gitea: 0, tool: 1, session: 2, manual: 3 }
14
10
 
15
- /** Pull request считается черновиком, пока с него не снят признак WIP. */
11
+ /** Pull request is draft while marked with WIP prefix or draft flag. */
16
12
  export function isDraftPull(pull) {
17
13
  if (pull?.draft === true) return true
18
14
  return /^\s*(wip|draft)\b[:\s-]/i.test(String(pull?.title ?? ''))
19
15
  }
20
16
 
21
17
  /**
22
- * Колонка, которую диктуют наблюдения в Gitea.
23
- *
24
- * Порядок проверок идёт от самого позднего состояния к самому раннему: задача
25
- * не может «вернуться» в in-progress только потому, что ветка ещё цела, если
26
- * issue уже закрыт.
27
- *
28
- * WIP PR намеренно НЕ двигает карточку в review: он открывается рано и означает
29
- * «работа идёт», а не «смотрите».
30
- *
31
- * @param {object} obs наблюдения
32
- * @param {object} [obs.issue] issue из Gitea (`state`)
33
- * @param {Array} [obs.pulls] pull request-ы, связанные с задачей
34
- * @param {boolean} [obs.branchExists] существует ли ветка задачи
35
- * @returns {string|undefined} колонка либо undefined, если наблюдения молчат
18
+ * Target column derived from forge observations.
19
+ * Evaluated from latest lifecycle state to earliest.
20
+ * WIP PR intentionally does NOT move card to review.
21
+ * @param {object} obs observations
22
+ * @param {object} [obs.issue] issue from Gitea (`state`)
23
+ * @param {Array} [obs.pulls] pull requests related to task
24
+ * @param {boolean} [obs.branchExists] whether task branch exists
25
+ * @returns {string|undefined} column or undefined if observations silent
36
26
  */
37
27
  export function deriveColumn(obs) {
38
28
  const issueClosed = obs?.issue?.state === 'closed'
39
29
  const pulls = Array.isArray(obs?.pulls) ? obs.pulls : []
40
30
  const branchExists = obs?.branchExists === true
41
31
 
42
- // Задача завершена, только когда cleanup сделан: issue закрыт И ветки нет.
43
- // Пока ветка цела, работа не закончена, сколько бы issue ни был закрыт.
32
+ // Task is finished only when cleanup is done: issue closed AND branch deleted.
33
+ // Task is finished only when cleanup is done: issue closed AND branch deleted.
44
34
  if (issueClosed && !branchExists) return 'done'
45
35
  if (issueClosed) return 'cleanup'
46
36
 
@@ -51,12 +41,10 @@ export function deriveColumn(obs) {
51
41
  }
52
42
 
53
43
  /**
54
- * Выбрать перевод из нескольких предложений разных источников.
44
+ * Select candidate transition from multiple sources.
55
45
  *
56
- * Предложение, совпадающее с текущей колонкой, отбрасывается: перевод в ту же
57
- * колонку — не перевод, а лишняя строка в журнале.
58
46
  *
59
- * @param {string} current текущая колонка
47
+ * @param {string} current current column
60
48
  * @param {Array<{column: string, source: string, detail?: string}>} candidates
61
49
  * @returns {{column: string, source: string, detail: string}|undefined}
62
50
  */
@@ -70,14 +58,10 @@ export function resolveTransition(current, candidates) {
70
58
  }
71
59
 
72
60
  /**
73
- * Нужен ли человек по событию сессии.
61
+ * Whether human intervention is required.
74
62
  *
75
- * Колонки «Блокировано» в согласованном наборе нет, поэтому запрос разрешения и
76
- * вопрос агента не двигают карточку, а поднимают на ней признак ожидания.
77
- * Двигать в несуществующую колонку нельзя, а прятать «агент стоит и ждёт» —
78
- * значит терять главное, ради чего смотрят на доску.
79
63
  *
80
- * @returns {boolean|undefined} true — поднять, false — снять, undefined — событие не про это
64
+ * @returns {boolean|undefined} true - raise, false - clear, undefined - not applicable
81
65
  */
82
66
  export function waitingFromEvent(type) {
83
67
  switch (type) {
@@ -93,20 +77,20 @@ export function waitingFromEvent(type) {
93
77
  }
94
78
  }
95
79
 
96
- /** Имя ветки, по которому задача узнаёт свою: то, что наблюдалось, либо ничего. */
80
+ /** Branch name identified for task: observed branch or undefined. */
97
81
  export function branchOfTask(task, branches) {
98
82
  const known = typeof task?.branch === 'string' && task.branch !== '' ? task.branch : undefined
99
83
  const names = (branches ?? []).map((b) => (typeof b === 'string' ? b : b?.name)).filter(Boolean)
100
84
  if (known !== undefined) return names.includes(known) ? known : undefined
101
85
 
102
- // Ветку задачи заводит агент и называет как хочет, но номер issue в имени —
103
- // общее правило воркфлоу. Ищем по номеру, а не по угаданному шаблону.
86
+ // Agent creates branch matching workflow conventions with issue number.
87
+ // Look up by issue number rather than fixed string template.
104
88
  if (typeof task?.issueNumber !== 'number') return undefined
105
89
  const marker = String(task.issueNumber)
106
90
  return names.find((n) => new RegExp(`(^|[^0-9])${marker}([^0-9]|$)`).test(n))
107
91
  }
108
92
 
109
- /** Pull request-ы, относящиеся к задаче: по ветке либо по упоминанию issue. */
93
+ /** Pull requests related to task: matched by branch or issue reference. */
110
94
  export function pullsOfTask(task, pulls, branch) {
111
95
  const number = typeof task?.issueNumber === 'number' ? task.issueNumber : undefined
112
96
  return (pulls ?? []).filter((p) => {
@@ -118,20 +102,16 @@ export function pullsOfTask(task, pulls, branch) {
118
102
  }
119
103
 
120
104
  /**
121
- * Кто победил, когда обе стороны изменились с последней сверки.
122
- *
123
- * Правило простое: побеждает более позднее изменение. Важно другое —
124
- * проигравшая сторона НЕ исчезает молча, а возвращается в поле `overridden`,
125
- * чтобы вызывающий записал её в журнал задачи. Синхронизация, которая тихо
126
- * отменяет правку человека, хуже синхронизации, которой нет: расхождение
127
- * остаётся, а узнать о нём неоткуда.
105
+ * Conflict resolution when both local and remote changed since last sync.
106
+ * Rule: latest timestamp wins, and overridden side is reported in `overridden`
107
+ * to record the conflict explicitly in task history.
128
108
  *
129
109
  * @param {object} state
130
- * @param {string} state.localColumn колонка на доске
131
- * @param {string} [state.remoteColumn] колонка, выведенная из Gitea
132
- * @param {number} state.localUpdatedAt когда правили на доске
133
- * @param {number} state.remoteUpdatedAt когда правили в Gitea
134
- * @param {number} state.syncedAt время последней удачной сверки
110
+ * @param {string} state.localColumn board column
111
+ * @param {string} [state.remoteColumn] column derived from forge
112
+ * @param {number} state.localUpdatedAt local update timestamp
113
+ * @param {number} state.remoteUpdatedAt remote update timestamp
114
+ * @param {number} state.syncedAt last successful sync timestamp
135
115
  * @returns {{winner: 'local'|'remote'|'none', column?: string, overridden?: string}}
136
116
  */
137
117
  export function resolveConflict({ localColumn, remoteColumn, localUpdatedAt, remoteUpdatedAt, syncedAt }) {
@@ -140,22 +120,21 @@ export function resolveConflict({ localColumn, remoteColumn, localUpdatedAt, rem
140
120
 
141
121
  const since = Number(syncedAt) || 0
142
122
 
143
- // До первой сверки «изменилось с прошлого раза» не имеет смысла: прошлого
144
- // раза не было. Карточку только что завели или импортировали, защищать в ней
145
- // нечего, и первая сверка обязана принять положение дел, а не спорить с ним.
123
+ // Prior to first sync, 'changed since last time' has no prior baseline;
124
+ // accept remote state initially.
146
125
  const firstSync = since === 0
147
126
  const localChanged = !firstSync && (Number(localUpdatedAt) || 0) > since
148
127
  const remoteChanged = firstSync || (Number(remoteUpdatedAt) || 0) > since
149
128
 
150
129
  if (localChanged && remoteChanged) {
151
- // Обе стороны двигались. Побеждает более позднее, проигравшее называется.
130
+ // Both sides modified: latest wins, overridden is recorded.
152
131
  if ((Number(localUpdatedAt) || 0) > (Number(remoteUpdatedAt) || 0)) {
153
132
  return { winner: 'local', overridden: remoteColumn }
154
133
  }
155
134
  return { winner: 'remote', column: remoteColumn, overridden: localColumn }
156
135
  }
157
136
 
158
- // Двигалась только доска — Gitea не имеет права тянуть карточку назад.
137
+ // Only board moved: remote sync does not pull card backward.
159
138
  if (localChanged) return { winner: 'local' }
160
139
 
161
140
  return { winner: 'remote', column: remoteColumn }
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'
@@ -23,26 +23,26 @@ export const DEFAULT_COLUMN_ROLES = {
23
23
  done: 'docs',
24
24
  }
25
25
 
26
- export 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', 'бэкенд'] },
26
+ const LABEL_ROLE_PATTERNS = [
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
@@ -126,7 +126,7 @@ export function buildWorkflowScript({ task, role }) {
126
126
 
127
127
  return [
128
128
  `phase("${safeRole}");`,
129
- `log("Запуск этапа ${safeRole} по карточке ${issueNum}");`,
129
+ `log("Starting phase ${safeRole} for card ${issueNum}");`,
130
130
  `const prompt = args.prompt;`,
131
131
  `const result = await agent(prompt, { label: "${safeRole}", phase: "${safeRole}" });`,
132
132
  `return {`,
@@ -138,7 +138,7 @@ export function buildWorkflowScript({ task, role }) {
138
138
  }
139
139
 
140
140
  /**
141
- * Сформировать метаданные сценария workflow.
141
+ * Construct metadata for workflow script.
142
142
  *
143
143
  * @param {object} params
144
144
  * @param {object} params.task
@@ -160,7 +160,7 @@ export function buildWorkflowMeta({ task, role }) {
160
160
  }
161
161
 
162
162
  /**
163
- * Определить целевую колонку после успешного завершения этапа workflow.
163
+ * Determine target column upon successful completion of workflow phase.
164
164
  *
165
165
  * @param {string} currentCol
166
166
  * @returns {string}
@@ -177,7 +177,7 @@ export function resolveTargetColumn(currentCol) {
177
177
  }
178
178
 
179
179
  /**
180
- * Класс-мост между доской и встроенным сервисом workflow ядра DSH.
180
+ * Bridge class connecting Kanban board to core DSH workflow service.
181
181
  */
182
182
  export class WorkflowBridge {
183
183
  constructor({
@@ -206,7 +206,7 @@ export class WorkflowBridge {
206
206
  }
207
207
 
208
208
  /**
209
- * Список зарегистрированных пресетов ядра.
209
+ * List of core registered presets.
210
210
  */
211
211
  async getAvailablePresets() {
212
212
  if (!this.agentPresets || typeof this.agentPresets.list !== 'function') {
@@ -215,13 +215,13 @@ export class WorkflowBridge {
215
215
  try {
216
216
  return await this.agentPresets.list()
217
217
  } catch (err) {
218
- this.logger?.warn?.(`dsh-kanban: не удалось получить список пресетов: ${err.message}`)
218
+ this.logger?.warn?.(`dsh-kanban: failed to get presets list: ${err.message}`)
219
219
  return []
220
220
  }
221
221
  }
222
222
 
223
223
  /**
224
- * Запуск workflow по карточке.
224
+ * Launch workflow for card.
225
225
  */
226
226
  async startTaskWorkflow({
227
227
  task,
@@ -317,7 +317,7 @@ export class WorkflowBridge {
317
317
  this.activeRuns.set(task.id, entry)
318
318
  this.runToTask.set(runId, task.id)
319
319
 
320
- // Обновляем карточку в базе данных
320
+ // Update task card in database
321
321
  this.store.updateTask(task.id, {
322
322
  workflowRunId: runId,
323
323
  workflowStatus: 'running',
@@ -335,7 +335,7 @@ export class WorkflowBridge {
335
335
  fromCol: task.col || task.column,
336
336
  toCol: task.col || task.column,
337
337
  source: 'workflow',
338
- detail: `запуск workflow ${runId} (роль: ${role}, пресет: ${preset || 'default'})`,
338
+ detail: `starting workflow ${runId} (role: ${role}, preset: ${preset || 'default'})`,
339
339
  })
340
340
 
341
341
  this.eventHub?.broadcast?.('task:change', {
@@ -347,7 +347,7 @@ export class WorkflowBridge {
347
347
  sessionId: taken.sessionId,
348
348
  })
349
349
 
350
- // Асинхронное ожидание завершения run
350
+ // Asynchronously await run completion
351
351
  run.result.then(async (result) => {
352
352
  await this.handleWorkflowResult(task.id, runId, role, result)
353
353
  }).catch(async (err) => {
@@ -370,14 +370,14 @@ export class WorkflowBridge {
370
370
  }
371
371
 
372
372
  /**
373
- * Отмена запущенного workflow.
373
+ * Cancel running workflow.
374
374
  */
375
375
  async cancelTaskWorkflow(taskId, reason = 'cancelled-by-user') {
376
376
  if (!taskId) return { cancelled: false, error: 'task-id-required' }
377
377
 
378
378
  const entry = this.activeRuns.get(taskId)
379
379
  if (!entry) {
380
- // Возможно, статус в БД ещё числится running
380
+ // Status in DB might still indicate running
381
381
  const task = this.store.getTask(taskId)
382
382
  if (task?.workflowStatus === 'running') {
383
383
  this.store.updateTask(taskId, { workflowStatus: 'cancelled' })
@@ -398,7 +398,7 @@ export class WorkflowBridge {
398
398
  fromCol: entry.task?.col || entry.task?.column || 'progress',
399
399
  toCol: entry.task?.col || entry.task?.column || 'progress',
400
400
  source: 'workflow',
401
- detail: `отмена workflow ${entry.runId}: ${reason}`,
401
+ detail: `cancelled workflow ${entry.runId}: ${reason}`,
402
402
  })
403
403
 
404
404
  this.eventHub?.broadcast?.('task:change', {
@@ -412,7 +412,7 @@ export class WorkflowBridge {
412
412
  }
413
413
 
414
414
  /**
415
- * Обработка завершения выполнения workflow.
415
+ * Handle workflow execution completion.
416
416
  */
417
417
  async handleWorkflowResult(taskId, runId, role, result) {
418
418
  const entry = this.activeRuns.get(taskId)
@@ -443,16 +443,16 @@ export class WorkflowBridge {
443
443
  id: taskId,
444
444
  column: targetCol,
445
445
  source: 'workflow',
446
- detail: `workflow ${runId} завершён успешно (роль: ${role})`,
446
+ detail: `workflow ${runId} completed successfully (role: ${role})`,
447
447
  })
448
448
  }
449
449
 
450
- // Отправляем комментарий в связанный issue Gitea / GitHub
450
+ // Send comment to linked Gitea / GitHub issue
451
451
  if (this.git && task.repo && task.issueNumber) {
452
452
  const commentBody = [
453
- `🚀 **Workflow завершён успешно!** (роль: \`${role}\`)`,
453
+ `🚀 **Workflow completed successfully!** (role: \`${role}\`)`,
454
454
  summary ? `\n${summary}` : '',
455
- `\n*Статус карточки переведён в колонку "${targetCol}".*`,
455
+ `\n*Card status moved to column "${targetCol}".*`,
456
456
  ].filter(Boolean).join('\n')
457
457
 
458
458
  try {
@@ -464,15 +464,15 @@ export class WorkflowBridge {
464
464
  provider: task.provider,
465
465
  })
466
466
  } catch (err) {
467
- this.logger?.warn?.(`dsh-kanban: не удалось отправить комментарий в issue ${task.repo}#${task.issueNumber}: ${err.message}`)
467
+ this.logger?.warn?.(`dsh-kanban: failed to comment on issue ${task.repo}#${task.issueNumber}: ${err.message}`)
468
468
  }
469
469
  }
470
470
 
471
- // Добавляем комментарий в карточку
471
+ // Add comment to card
472
472
  if (typeof this.store.addTaskComment === 'function' && summary) {
473
473
  this.store.addTaskComment(taskId, {
474
474
  author: 'workflow',
475
- body: `🚀 **Workflow завершён (${role}):**\n\n${summary}`,
475
+ body: `🚀 **Workflow completed (${role}):**\n\n${summary}`,
476
476
  })
477
477
  }
478
478
  } else {
@@ -481,7 +481,7 @@ export class WorkflowBridge {
481
481
  fromCol: task.col || task.column,
482
482
  toCol: task.col || task.column,
483
483
  source: 'workflow',
484
- detail: `workflow ${runId} завершён со статусом ${stopReason}: ${result?.error || ''}`,
484
+ detail: `workflow ${runId} completed with status ${stopReason}: ${result?.error || ''}`,
485
485
  })
486
486
  }
487
487
 
@@ -489,7 +489,7 @@ export class WorkflowBridge {
489
489
  try {
490
490
  await entry.run.dispose()
491
491
  } catch (err) {
492
- this.logger?.warn?.(`dsh-kanban: ошибка при dispose workflow run ${runId}: ${err.message}`)
492
+ this.logger?.warn?.(`dsh-kanban: error disposing workflow run ${runId}: ${err.message}`)
493
493
  }
494
494
  }
495
495
 
@@ -504,7 +504,7 @@ export class WorkflowBridge {
504
504
  }
505
505
 
506
506
  /**
507
- * Подключить слушатели событий ядра workflow/*.
507
+ * Attach core workflow/* event listeners.
508
508
  *
509
509
  * @param {object} ctx Cordis context
510
510
  */
@@ -547,8 +547,8 @@ export class WorkflowBridge {
547
547
  const taskId = this.runToTask.get(runId)
548
548
  if (!taskId) return
549
549
 
550
- // handleWorkflowResult также вызывается через run.result.then,
551
- // но событие workflow/end гарантирует синхронизацию наблюдателей
550
+ // handleWorkflowResult is also invoked via run.result.then,
551
+ // but workflow/end event guarantees observer synchronization
552
552
  this.eventHub?.broadcast?.('task:change', {
553
553
  taskId,
554
554
  action: 'workflow-core-end',