@goodandready/dsh-kanban 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/board-tool.js +18 -48
- package/lib/client.js +85 -176
- package/lib/commands.js +21 -38
- package/lib/config.js +29 -54
- package/lib/cron.js +13 -13
- package/lib/events.js +1 -1
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +51 -109
- package/lib/github.js +27 -31
- package/lib/import.js +46 -67
- package/lib/index.js +88 -137
- package/lib/intake.js +23 -31
- package/lib/launcher.js +100 -167
- package/lib/lifecycle.js +11 -17
- package/lib/markdown.js +16 -23
- package/lib/metrics.js +23 -25
- package/lib/order.js +25 -28
- package/lib/outbound.js +25 -26
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +86 -145
- package/lib/store.js +59 -78
- package/lib/sync.js +56 -98
- package/lib/templates.js +29 -29
- package/lib/time.js +8 -17
- package/lib/transitions.js +36 -57
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +58 -58
- package/lib/worktree.js +58 -58
- package/package.json +1 -1
package/lib/transitions.js
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* issue
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
//
|
|
43
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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]
|
|
132
|
-
* @param {number} state.localUpdatedAt
|
|
133
|
-
* @param {number} state.remoteUpdatedAt
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
63
|
-
//
|
|
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)
|
package/lib/workflow-bridge.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Bridge between Kanban cards and core DSH workflowEngine.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// 1.
|
|
5
|
-
// 2.
|
|
6
|
-
//
|
|
7
|
-
// 3.
|
|
8
|
-
// 4.
|
|
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
|
-
// -
|
|
11
|
-
// -
|
|
12
|
-
// 5.
|
|
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
|
-
|
|
27
|
-
{ role: 'spec', patterns: ['spec', 'specification', 'requirements', 'requirements-analysis', '
|
|
28
|
-
{ role: 'architecture', patterns: ['architecture', '
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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("
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
*
|
|
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}
|
|
446
|
+
detail: `workflow ${runId} completed successfully (role: ${role})`,
|
|
447
447
|
})
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
//
|
|
450
|
+
// Send comment to linked Gitea / GitHub issue
|
|
451
451
|
if (this.git && task.repo && task.issueNumber) {
|
|
452
452
|
const commentBody = [
|
|
453
|
-
`🚀 **Workflow
|
|
453
|
+
`🚀 **Workflow completed successfully!** (role: \`${role}\`)`,
|
|
454
454
|
summary ? `\n${summary}` : '',
|
|
455
|
-
`\n
|
|
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:
|
|
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
|
|
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}
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
551
|
-
//
|
|
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',
|