@goodandready/dsh-kanban 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/board-tool.js +18 -48
- package/lib/client.js +85 -176
- package/lib/commands.js +21 -38
- package/lib/config.js +30 -55
- package/lib/cron.js +13 -13
- package/lib/events.js +17 -5
- package/lib/filters.js +58 -82
- package/lib/git-provider.js +23 -23
- package/lib/gitea.js +59 -110
- package/lib/github.js +39 -37
- package/lib/import.js +46 -67
- package/lib/index.js +131 -140
- package/lib/intake.js +18 -26
- package/lib/launcher.js +107 -170
- package/lib/lifecycle.js +11 -17
- package/lib/markdown.js +16 -23
- package/lib/metrics.js +23 -25
- package/lib/order.js +25 -28
- package/lib/outbound.js +28 -28
- package/lib/plan.js +23 -45
- package/lib/power.js +2 -2
- package/lib/progress-dump.js +17 -17
- package/lib/routes.js +92 -145
- package/lib/store.js +68 -77
- package/lib/sync.js +56 -98
- package/lib/time.js +8 -17
- package/lib/transitions.js +44 -61
- package/lib/webhook.js +16 -23
- package/lib/workflow-bridge.js +72 -68
- package/lib/worktree.js +103 -60
- package/package.json +1 -1
package/lib/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'
|
|
@@ -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', '
|
|
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
|
-
*
|
|
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
|
|
@@ -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
|
|
124
|
-
const
|
|
125
|
-
const
|
|
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(
|
|
129
|
-
`log(
|
|
130
|
+
`phase(${JSON.stringify(rawRole)});`,
|
|
131
|
+
`log(${JSON.stringify(logMsg)});`,
|
|
130
132
|
`const prompt = args.prompt;`,
|
|
131
|
-
`const result = await agent(prompt, { label:
|
|
133
|
+
`const result = await agent(prompt, { label: ${JSON.stringify(rawRole)}, phase: ${JSON.stringify(rawRole)} });`,
|
|
132
134
|
`return {`,
|
|
133
|
-
` role:
|
|
134
|
-
` 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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
*
|
|
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}
|
|
448
|
+
detail: `workflow ${runId} completed successfully (role: ${role})`,
|
|
447
449
|
})
|
|
448
450
|
}
|
|
449
451
|
|
|
450
|
-
//
|
|
452
|
+
// Send comment to linked Gitea / GitHub issue
|
|
451
453
|
if (this.git && task.repo && task.issueNumber) {
|
|
452
454
|
const commentBody = [
|
|
453
|
-
`🚀 **Workflow
|
|
455
|
+
`🚀 **Workflow completed successfully!** (role: \`${role}\`)`,
|
|
454
456
|
summary ? `\n${summary}` : '',
|
|
455
|
-
`\n
|
|
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:
|
|
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 (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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}
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
551
|
-
//
|
|
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',
|