@goodandready/dsh-kanban 0.2.23 → 0.2.24
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/client.js +1 -6
- package/lib/config.js +6 -1
- package/lib/import.js +16 -7
- package/lib/index.js +47 -10
- package/lib/launcher.js +19 -9
- package/lib/order.js +15 -0
- package/lib/progress-dump.js +3 -6
- package/lib/routes.js +11 -5
- package/lib/store.js +30 -11
- package/lib/workflow-bridge.js +1 -1
- package/lib/worktree.js +39 -14
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -571,12 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
571
571
|
|
|
572
572
|
async function api(path, options) {
|
|
573
573
|
const timeoutMs = (options && options.timeout) || 20000
|
|
574
|
-
|
|
575
|
-
try {
|
|
576
|
-
if (!options?.signal) {
|
|
577
|
-
timeoutSignal = createTimeoutSignal(timeoutMs)
|
|
578
|
-
}
|
|
579
|
-
} catch (_) {}
|
|
574
|
+
const timeoutSignal = !options?.signal ? createTimeoutSignal(timeoutMs) : undefined
|
|
580
575
|
|
|
581
576
|
const fetchOpts = Object.assign({
|
|
582
577
|
headers: { 'Content-Type': 'application/json' },
|
package/lib/config.js
CHANGED
|
@@ -138,7 +138,12 @@ export function rootOf(config, cwdOf = () => process.cwd()) {
|
|
|
138
138
|
export function withDefaults(raw) {
|
|
139
139
|
const out = {}
|
|
140
140
|
for (const [key, fallback] of Object.entries(CONFIG_DEFAULTS)) {
|
|
141
|
-
|
|
141
|
+
let value = raw?.[key]
|
|
142
|
+
if (value && typeof value === 'object' && typeof value.get === 'function') {
|
|
143
|
+
try {
|
|
144
|
+
value = value.get()
|
|
145
|
+
} catch { /* ignore */ }
|
|
146
|
+
}
|
|
142
147
|
out[key] = typeof value === typeof fallback && value !== null ? value : fallback
|
|
143
148
|
}
|
|
144
149
|
return out
|
package/lib/import.js
CHANGED
|
@@ -66,12 +66,13 @@ export function issueMilestone(issue) {
|
|
|
66
66
|
* Fields `branch`, `worktree`, `sessionId`, `model` are NOT populated here:
|
|
67
67
|
* they are assigned later when work begins. The plugin does not create branches on import.
|
|
68
68
|
*/
|
|
69
|
-
export function issueToTask(issue, { board = 'main', column = 'backlog', owner, repo, provider }) {
|
|
70
|
-
const
|
|
69
|
+
export function issueToTask(issue, { board = 'main', column = 'backlog', owner, repo, provider, forge }) {
|
|
70
|
+
const detectedForge = forge || (typeof issue?.html_url === 'string' && issue.html_url.includes('github.com') ? 'github' : 'gitea')
|
|
71
71
|
return {
|
|
72
72
|
board,
|
|
73
73
|
column,
|
|
74
|
-
|
|
74
|
+
forge: detectedForge,
|
|
75
|
+
...(provider && provider !== 'github' && provider !== 'gitea' ? { provider } : {}),
|
|
75
76
|
title: typeof issue?.title === 'string' ? issue.title : '',
|
|
76
77
|
body: typeof issue?.body === 'string' ? issue.body : '',
|
|
77
78
|
labels: Array.isArray(issue?.labels)
|
|
@@ -167,9 +168,15 @@ export async function importIssue({ gitea, store, owner, repo, issueNumber, boar
|
|
|
167
168
|
if (typeof issueNumber !== 'number') return { error: 'issue-number-required', status: 400 }
|
|
168
169
|
// Simple board is for non-issue tasks. Importing here would bring branches/PRs into columns unable to display them.
|
|
169
170
|
if (store.boardKind(board) !== 'project') return { error: 'board-not-for-issues', status: 400 }
|
|
170
|
-
const
|
|
171
|
+
const effectiveForge = forge || (provider === 'github' || provider === 'gitea' ? provider : undefined)
|
|
172
|
+
const effectiveProvider = (provider !== 'github' && provider !== 'gitea') ? provider : undefined
|
|
173
|
+
if (typeof store.findTaskByIssue === 'function') {
|
|
174
|
+
const existing = store.findTaskByIssue({ owner, repo, issueNumber, forge: effectiveForge })
|
|
175
|
+
if (existing) return { task: existing }
|
|
176
|
+
}
|
|
177
|
+
const issue = await gitea.getIssue({ owner, repo, index: issueNumber, provider: effectiveForge || effectiveProvider })
|
|
171
178
|
if (issue === undefined || issue === null) return { error: 'issue-not-found', status: 404 }
|
|
172
|
-
return { task: store.createTask(issueToTask(issue, { board, column, owner, repo, forge:
|
|
179
|
+
return { task: store.createTask(issueToTask(issue, { board, column, owner, repo, forge: effectiveForge, provider: effectiveProvider })) }
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
/** Re-read issue and update task, preserving local state. */
|
|
@@ -222,13 +229,15 @@ export async function createProjectTask({
|
|
|
222
229
|
targetRepo = wanted
|
|
223
230
|
}
|
|
224
231
|
|
|
232
|
+
const effectiveForge = forge || (provider === 'github' || provider === 'gitea' ? provider : undefined)
|
|
233
|
+
const effectiveProvider = (provider !== 'github' && provider !== 'gitea') ? provider : undefined
|
|
225
234
|
let issue
|
|
226
235
|
try {
|
|
227
|
-
issue = await gitea.createIssue({ owner, repo: targetRepo, title: name, body, provider:
|
|
236
|
+
issue = await gitea.createIssue({ owner, repo: targetRepo, title: name, body, provider: effectiveForge || effectiveProvider })
|
|
228
237
|
} catch (error) {
|
|
229
238
|
// Repository may have already been created; avoid deleting on issue creation failure.
|
|
230
239
|
return { error: 'issue-not-created', status: 502, detail: String(error?.message ?? error) }
|
|
231
240
|
}
|
|
232
241
|
|
|
233
|
-
return { task: store.createTask(issueToTask(issue, { board, column: 'backlog', owner, repo: targetRepo, forge:
|
|
242
|
+
return { task: store.createTask(issueToTask(issue, { board, column: 'backlog', owner, repo: targetRepo, forge: effectiveForge, provider: effectiveProvider })) }
|
|
234
243
|
}
|
package/lib/index.js
CHANGED
|
@@ -234,7 +234,7 @@ export function apply(ctx, config) {
|
|
|
234
234
|
if (typeof settingsSvc?.describe !== 'function') return null
|
|
235
235
|
try {
|
|
236
236
|
const row = settingsSvc.describe().find((r) => r?.ns === SETTINGS_NAMESPACE)
|
|
237
|
-
if (!row || row.status !==
|
|
237
|
+
if (!row || !row.value || (row.status !== undefined && row.status !== 'ready')) return null
|
|
238
238
|
return withDefaults(row.value)
|
|
239
239
|
} catch (err) {
|
|
240
240
|
ctx.logger?.warn?.(`dsh-kanban: settings describe failed: ${err?.message || err}`)
|
|
@@ -268,14 +268,32 @@ export function apply(ctx, config) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
const handleDocumentUpdated = (ns) => {
|
|
272
|
+
if (!ns || ns === SETTINGS_NAMESPACE) {
|
|
273
|
+
const served = readServedConfig()
|
|
274
|
+
if (served) {
|
|
275
|
+
effective = served
|
|
276
|
+
rearm()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const handleVolatileUpdate = (payload) => {
|
|
281
|
+
const served = readServedConfig()
|
|
282
|
+
if (served) {
|
|
283
|
+
effective = served
|
|
284
|
+
rearm()
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
applyPatch(payload)
|
|
288
|
+
}
|
|
271
289
|
if (typeof ctx.effect === 'function') {
|
|
272
|
-
ctx.effect(() => ctx.on('loader/volatile-update',
|
|
290
|
+
ctx.effect(() => ctx.on('loader/volatile-update', handleVolatileUpdate))
|
|
273
291
|
ctx.effect(() => ctx.on('config', applyPatch))
|
|
274
|
-
ctx.effect(() => ctx.on('settings/document-updated',
|
|
292
|
+
ctx.effect(() => ctx.on('settings/document-updated', handleDocumentUpdated))
|
|
275
293
|
} else {
|
|
276
|
-
ctx.on('loader/volatile-update',
|
|
294
|
+
ctx.on('loader/volatile-update', handleVolatileUpdate)
|
|
277
295
|
ctx.on('config', applyPatch)
|
|
278
|
-
ctx.on('settings/document-updated',
|
|
296
|
+
ctx.on('settings/document-updated', handleDocumentUpdated)
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
299
|
|
|
@@ -1142,6 +1160,9 @@ export function apply(ctx, config) {
|
|
|
1142
1160
|
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
1143
1161
|
force: input?.force === true,
|
|
1144
1162
|
cleanupTaskWorktree,
|
|
1163
|
+
config: getConfig(),
|
|
1164
|
+
repoDir: rootOf(getConfig()).path,
|
|
1165
|
+
cwdOf: () => rootOf(getConfig()).path,
|
|
1145
1166
|
})
|
|
1146
1167
|
writeJson(res, resObj.status, resObj.body)
|
|
1147
1168
|
return
|
|
@@ -1209,6 +1230,9 @@ export function apply(ctx, config) {
|
|
|
1209
1230
|
}
|
|
1210
1231
|
if (req.method === 'POST' && action === 'report') {
|
|
1211
1232
|
const resObj = handleTaskReport({ store, task: store.getTask(id), input })
|
|
1233
|
+
if (resObj?.body?.task || resObj?.body?.report) {
|
|
1234
|
+
eventHub.broadcast('task:change', { taskId: id, action: 'updated' })
|
|
1235
|
+
}
|
|
1212
1236
|
writeJson(res, resObj.status, resObj.body)
|
|
1213
1237
|
return
|
|
1214
1238
|
}
|
|
@@ -1310,11 +1334,19 @@ export function apply(ctx, config) {
|
|
|
1310
1334
|
return
|
|
1311
1335
|
}
|
|
1312
1336
|
if (req.method === 'POST' && action === 'archive') {
|
|
1313
|
-
|
|
1337
|
+
const resObj = setArchived({ store, id, archived: true })
|
|
1338
|
+
if (resObj?.task) {
|
|
1339
|
+
eventHub.broadcast('task:change', { taskId: id, action: 'archived' })
|
|
1340
|
+
}
|
|
1341
|
+
reply(res, resObj)
|
|
1314
1342
|
return
|
|
1315
1343
|
}
|
|
1316
1344
|
if (req.method === 'POST' && action === 'restore') {
|
|
1317
|
-
|
|
1345
|
+
const resObj = setArchived({ store, id, archived: false })
|
|
1346
|
+
if (resObj?.task) {
|
|
1347
|
+
eventHub.broadcast('task:change', { taskId: id, action: 'restored' })
|
|
1348
|
+
}
|
|
1349
|
+
reply(res, resObj)
|
|
1318
1350
|
return
|
|
1319
1351
|
}
|
|
1320
1352
|
if (req.method === 'POST' && action === 'queue') {
|
|
@@ -1333,8 +1365,12 @@ export function apply(ctx, config) {
|
|
|
1333
1365
|
reply(res, appendNote({ store, id, input }))
|
|
1334
1366
|
return
|
|
1335
1367
|
}
|
|
1336
|
-
if (req.method === 'PATCH' && action === undefined) {
|
|
1337
|
-
|
|
1368
|
+
if (req.method === 'PATCH' && (action === undefined || action === '')) {
|
|
1369
|
+
const resObj = updateTask({ store, id, input })
|
|
1370
|
+
if (resObj?.task) {
|
|
1371
|
+
eventHub.broadcast('task:change', { taskId: id, action: 'updated' })
|
|
1372
|
+
}
|
|
1373
|
+
reply(res, resObj)
|
|
1338
1374
|
return
|
|
1339
1375
|
}
|
|
1340
1376
|
writeJson(res, 405, { error: 'method-not-allowed' })
|
|
@@ -1380,9 +1416,10 @@ export function apply(ctx, config) {
|
|
|
1380
1416
|
try {
|
|
1381
1417
|
const input = await readBody(req)
|
|
1382
1418
|
reply(res, await importIssue({
|
|
1383
|
-
gitea, store,
|
|
1419
|
+
gitea: git, store,
|
|
1384
1420
|
owner: input.owner, repo: input.repo, issueNumber: input.issueNumber,
|
|
1385
1421
|
board: input.board ?? 'main', column: input.column ?? 'backlog',
|
|
1422
|
+
forge: input.forge || input.provider,
|
|
1386
1423
|
}))
|
|
1387
1424
|
} catch (error) {
|
|
1388
1425
|
writeJson(res, error?.status ?? 500, { error: 'bad-request' })
|
package/lib/launcher.js
CHANGED
|
@@ -163,7 +163,7 @@ export async function obtainAgent({
|
|
|
163
163
|
agentPreset, permission, permissions, gitRunner, workspaceRegistry,
|
|
164
164
|
reasoningEffort,
|
|
165
165
|
}) {
|
|
166
|
-
const chosenPerm = permission || task?.permission || ''
|
|
166
|
+
const chosenPerm = permission || task?.permission || (typeof permissions?.set === 'function' ? (config?.sessionDefaultPermission || '') : '')
|
|
167
167
|
const effort = reasoningEffort || task?.reasoningEffort || undefined
|
|
168
168
|
const agentOptions = { provider, model, ...(effort ? { reasoningEffort: effort } : {}) }
|
|
169
169
|
|
|
@@ -210,7 +210,17 @@ export async function obtainAgent({
|
|
|
210
210
|
let workspaceId = task?.workspaceId || undefined
|
|
211
211
|
let worktreeCreated = false
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
let isGit = false
|
|
214
|
+
try {
|
|
215
|
+
isGit = await isGitRepo(cwd, gitRunner)
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (config?.worktreeIsolation !== false) {
|
|
218
|
+
const error = new Error(`worktree-creation-failed: git probe failed: ${err?.message || err}`)
|
|
219
|
+
error.key = 'worktree-creation-failed'
|
|
220
|
+
error.status = 500
|
|
221
|
+
throw error
|
|
222
|
+
}
|
|
223
|
+
}
|
|
214
224
|
if (config?.worktreeIsolation !== false && isGit) {
|
|
215
225
|
let created
|
|
216
226
|
try {
|
|
@@ -370,9 +380,6 @@ export async function runTask({
|
|
|
370
380
|
agentPreset, permission, permissions, gitRunner, workspaceRegistry,
|
|
371
381
|
reasoningEffort: effort,
|
|
372
382
|
})
|
|
373
|
-
} finally {
|
|
374
|
-
releaseSessionSlot(task.id)
|
|
375
|
-
}
|
|
376
383
|
|
|
377
384
|
if (task.queuedAt > 0 || task.queuedRequest) {
|
|
378
385
|
store.updateTask(task.id, { queuedAt: 0, queuedRequest: '' })
|
|
@@ -455,6 +462,9 @@ export async function runTask({
|
|
|
455
462
|
}
|
|
456
463
|
|
|
457
464
|
return { sessionId: taken.sessionId, mode: revivalKind(taken.mode), task: current }
|
|
465
|
+
} finally {
|
|
466
|
+
releaseSessionSlot(task.id)
|
|
467
|
+
}
|
|
458
468
|
}
|
|
459
469
|
|
|
460
470
|
/** Build prompt text: custom user message if provided, otherwise standard template. */
|
|
@@ -552,9 +562,6 @@ export async function runBatch({
|
|
|
552
562
|
agents, task: list[0], config, provider, model, mintSessionId, cwdOf, logger,
|
|
553
563
|
agentPreset, permission, permissions,
|
|
554
564
|
})
|
|
555
|
-
} finally {
|
|
556
|
-
releaseSessionSlot(list[0].id)
|
|
557
|
-
}
|
|
558
565
|
|
|
559
566
|
await taken.agent.whenIdle()
|
|
560
567
|
const own = typeof text === 'string' ? text.trim() : ''
|
|
@@ -598,6 +605,9 @@ export async function runBatch({
|
|
|
598
605
|
})
|
|
599
606
|
|
|
600
607
|
return { sessionId: taken.sessionId, mode: taken.mode, started: started.length, tasks: started }
|
|
608
|
+
} finally {
|
|
609
|
+
releaseSessionSlot(list[0].id)
|
|
610
|
+
}
|
|
601
611
|
}
|
|
602
612
|
|
|
603
613
|
/**
|
|
@@ -639,7 +649,7 @@ export function unqueueTask({ store, task }) {
|
|
|
639
649
|
if (typeof task.queuedAt !== 'number' || task.queuedAt <= 0) {
|
|
640
650
|
return { error: 'task-not-queued', status: 400 }
|
|
641
651
|
}
|
|
642
|
-
store.updateTask(task.id, { queuedAt: 0, sessionId: null })
|
|
652
|
+
store.updateTask(task.id, { queuedAt: 0, sessionId: null, queuedRequest: '' })
|
|
643
653
|
store.addTransition({
|
|
644
654
|
taskId: task.id, fromCol: task.column, toCol: task.column, source: 'manual',
|
|
645
655
|
detail: 'removed from queue; message already sent to agent',
|
package/lib/order.js
CHANGED
|
@@ -30,4 +30,19 @@ export function keyBetween(a, b) {
|
|
|
30
30
|
return generateKeyBetween(a ?? null, b ?? null)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function isValidOrderKey(key) {
|
|
34
|
+
if (typeof key !== 'string' || !key || /[^0-9a-zA-Z]/.test(key)) return false
|
|
35
|
+
try {
|
|
36
|
+
generateKeyBetween(null, key)
|
|
37
|
+
return true
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
generateKeyBetween(key, null)
|
|
41
|
+
return true
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export { generateKeyBetween, generateNKeysBetween }
|
package/lib/progress-dump.js
CHANGED
|
@@ -62,21 +62,18 @@ export function parseProgressDump(text) {
|
|
|
62
62
|
const line = rawLine.trim()
|
|
63
63
|
const lower = line.toLowerCase()
|
|
64
64
|
|
|
65
|
-
if (lower.startsWith('goal:')
|
|
65
|
+
if (lower.startsWith('goal:')) {
|
|
66
66
|
currentKey = 'goal'
|
|
67
67
|
const val = line.slice(line.indexOf(':') + 1).trim()
|
|
68
68
|
if (val) sections.goal.push(val)
|
|
69
|
-
} else if (lower.startsWith('progress:') || lower.startsWith('done:')
|
|
69
|
+
} else if (lower.startsWith('progress:') || lower.startsWith('done:')) {
|
|
70
70
|
currentKey = 'progress'
|
|
71
71
|
const val = line.slice(line.indexOf(':') + 1).trim()
|
|
72
72
|
if (val) sections.progress.push(val)
|
|
73
73
|
} else if (
|
|
74
74
|
lower.startsWith('next:') ||
|
|
75
75
|
lower.startsWith('next steps:') ||
|
|
76
|
-
lower.startsWith('next step:')
|
|
77
|
-
lower.startsWith('следующие шаги:') ||
|
|
78
|
-
lower.startsWith('шаги:') ||
|
|
79
|
-
lower.startsWith('дальше:')
|
|
76
|
+
lower.startsWith('next step:')
|
|
80
77
|
) {
|
|
81
78
|
currentKey = 'next'
|
|
82
79
|
const val = line.slice(line.indexOf(':') + 1).trim()
|
package/lib/routes.js
CHANGED
|
@@ -238,9 +238,7 @@ export function updateTask({ store, id, input }) {
|
|
|
238
238
|
patch.permissionConfirmedTier = ''
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
|
|
242
|
-
patch.permissionConfirmedTier = input.permissionConfirmedTier
|
|
243
|
-
}
|
|
241
|
+
|
|
244
242
|
if (typeof input?.reasoningEffort === 'string') {
|
|
245
243
|
const val = input.reasoningEffort.trim().toLowerCase()
|
|
246
244
|
if (['', 'low', 'medium', 'high', 'default', 'none', 'minimal', 'thorough'].includes(val)) {
|
|
@@ -524,15 +522,23 @@ export function exportTasksCsv({ store, board = 'main' }) {
|
|
|
524
522
|
/**
|
|
525
523
|
* Clean up task worktree.
|
|
526
524
|
*/
|
|
527
|
-
export async function handleCleanupWorktree({
|
|
525
|
+
export async function handleCleanupWorktree({
|
|
526
|
+
store, task, workspaceRegistry, force, cleanupTaskWorktree,
|
|
527
|
+
config, repoDir, cwdOf, env,
|
|
528
|
+
}) {
|
|
528
529
|
if (!task) return { status: 404, body: { error: 'task-not-found' } }
|
|
529
530
|
const out = await cleanupTaskWorktree({
|
|
530
531
|
store,
|
|
531
532
|
task,
|
|
532
533
|
workspaceRegistry,
|
|
533
534
|
force: force === true,
|
|
535
|
+
config,
|
|
536
|
+
repoDir,
|
|
537
|
+
cwdOf,
|
|
538
|
+
env,
|
|
534
539
|
})
|
|
535
|
-
|
|
540
|
+
const status = out.status || (out.error ? (out.error === 'unfenced-worktree-path' ? 403 : 500) : 200)
|
|
541
|
+
return { status, body: out }
|
|
536
542
|
}
|
|
537
543
|
|
|
538
544
|
/**
|
package/lib/store.js
CHANGED
|
@@ -13,7 +13,7 @@ import { DatabaseSync } from 'node:sqlite'
|
|
|
13
13
|
import { randomUUID } from 'node:crypto'
|
|
14
14
|
import { mkdirSync, copyFileSync, readdirSync, unlinkSync, statSync, chmodSync, existsSync } from 'node:fs'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
|
-
import { keyBetween, generateNKeysBetween } from './order.js'
|
|
16
|
+
import { keyBetween, generateNKeysBetween, isValidOrderKey } from './order.js'
|
|
17
17
|
import { DEFAULT_BOARDS, normalizeKind } from './config.js'
|
|
18
18
|
|
|
19
19
|
const SCHEMA = `
|
|
@@ -73,6 +73,9 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
73
73
|
isEpic INTEGER NOT NULL DEFAULT 0,
|
|
74
74
|
workflowRunId TEXT NOT NULL DEFAULT '',
|
|
75
75
|
workflowStatus TEXT NOT NULL DEFAULT '',
|
|
76
|
+
forge TEXT NOT NULL DEFAULT '',
|
|
77
|
+
permissionConfirmedTier TEXT NOT NULL DEFAULT '',
|
|
78
|
+
queuedRequest TEXT NOT NULL DEFAULT '',
|
|
76
79
|
updatedAt INTEGER NOT NULL
|
|
77
80
|
);
|
|
78
81
|
CREATE INDEX IF NOT EXISTS tasks_board_col ON tasks(board, col, position);
|
|
@@ -103,7 +106,7 @@ const WRITABLE = [
|
|
|
103
106
|
'labelColors', 'queuedAt', 'createdAt', 'author', 'assignee', 'milestone', 'priority', 'dueAt',
|
|
104
107
|
'cron', 'nextRunAt', 'lastRunAt', 'progressDump', 'permission', 'reasoningEffort', 'permissionConfirmedAt',
|
|
105
108
|
'runs', 'checklist', 'report', 'comments', 'rejectReason', 'rejectedAt', 'parentId', 'isEpic',
|
|
106
|
-
'workflowRunId', 'workflowStatus', 'permissionConfirmedTier', 'forge',
|
|
109
|
+
'workflowRunId', 'workflowStatus', 'permissionConfirmedTier', 'forge', 'queuedRequest',
|
|
107
110
|
]
|
|
108
111
|
|
|
109
112
|
/** Valid transition sources; unexpected values are rejected to preserve log integrity. */
|
|
@@ -244,6 +247,7 @@ export function openStore({ dir }) {
|
|
|
244
247
|
['workflowStatus', "TEXT NOT NULL DEFAULT ''"],
|
|
245
248
|
['forge', "TEXT NOT NULL DEFAULT ''"],
|
|
246
249
|
['permissionConfirmedTier', "TEXT NOT NULL DEFAULT ''"],
|
|
250
|
+
['queuedRequest', "TEXT NOT NULL DEFAULT ''"],
|
|
247
251
|
]) {
|
|
248
252
|
try { db.exec(`ALTER TABLE tasks ADD COLUMN ${column} ${definition}`) } catch { /* column already exists */ }
|
|
249
253
|
}
|
|
@@ -283,6 +287,14 @@ export function openStore({ dir }) {
|
|
|
283
287
|
}
|
|
284
288
|
}
|
|
285
289
|
}
|
|
290
|
+
const boardRows = db.prepare('SELECT id, position FROM boards ORDER BY position ASC, id ASC').all()
|
|
291
|
+
if (boardRows.length > 0) {
|
|
292
|
+
const updateBoardPos = db.prepare('UPDATE boards SET position = ? WHERE id = ?')
|
|
293
|
+
const newBoardKeys = generateNKeysBetween(null, null, boardRows.length)
|
|
294
|
+
for (let i = 0; i < boardRows.length; i++) {
|
|
295
|
+
updateBoardPos.run(newBoardKeys[i], boardRows[i].id)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
286
298
|
db.prepare("INSERT OR REPLACE INTO meta (key, val) VALUES ('fractional_indexing_migrated', '1')").run()
|
|
287
299
|
db.exec('COMMIT')
|
|
288
300
|
} catch (e) {
|
|
@@ -303,16 +315,16 @@ export function openStore({ dir }) {
|
|
|
303
315
|
labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
|
|
304
316
|
cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, runs,
|
|
305
317
|
checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic,
|
|
306
|
-
workflowRunId, workflowStatus)
|
|
307
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
318
|
+
workflowRunId, workflowStatus, forge, queuedRequest)
|
|
319
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
308
320
|
restoreTask: db.prepare(`INSERT INTO tasks
|
|
309
321
|
(id, board, col, position, title, body, owner, repo, issueNumber, issueUrl, externalRef,
|
|
310
322
|
labels, branch, worktree, workspaceId, sessionId, model, provider, waiting, syncedAt,
|
|
311
323
|
labelColors, author, assignee, milestone, priority, dueAt, columnAt, createdAt, updatedAt,
|
|
312
324
|
cron, nextRunAt, lastRunAt, progressDump, permission, permissionConfirmedAt, permissionConfirmedTier, runs,
|
|
313
325
|
checklist, report, comments, rejectReason, rejectedAt, reasoningEffort, parentId, isEpic,
|
|
314
|
-
workflowRunId, workflowStatus, archivedAt, queuedAt, plan, forge)
|
|
315
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
326
|
+
workflowRunId, workflowStatus, archivedAt, queuedAt, plan, forge, queuedRequest)
|
|
327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
316
328
|
ON CONFLICT(id) DO UPDATE SET
|
|
317
329
|
board=excluded.board, col=excluded.col, position=excluded.position, title=excluded.title,
|
|
318
330
|
body=excluded.body, owner=excluded.owner, repo=excluded.repo, issueNumber=excluded.issueNumber,
|
|
@@ -330,7 +342,8 @@ export function openStore({ dir }) {
|
|
|
330
342
|
rejectReason=excluded.rejectReason, rejectedAt=excluded.rejectedAt,
|
|
331
343
|
reasoningEffort=excluded.reasoningEffort, parentId=excluded.parentId, isEpic=excluded.isEpic,
|
|
332
344
|
workflowRunId=excluded.workflowRunId, workflowStatus=excluded.workflowStatus,
|
|
333
|
-
archivedAt=excluded.archivedAt, queuedAt=excluded.queuedAt, plan=excluded.plan, forge=excluded.forge
|
|
345
|
+
archivedAt=excluded.archivedAt, queuedAt=excluded.queuedAt, plan=excluded.plan, forge=excluded.forge,
|
|
346
|
+
queuedRequest=excluded.queuedRequest`),
|
|
334
347
|
restoreTransition: db.prepare(`INSERT INTO transitions
|
|
335
348
|
(taskId, fromCol, toCol, source, detail, at)
|
|
336
349
|
VALUES (?, ?, ?, ?, ?, ?)`),
|
|
@@ -529,6 +542,8 @@ export function openStore({ dir }) {
|
|
|
529
542
|
input.isEpic ? 1 : 0,
|
|
530
543
|
input.workflowRunId ?? '',
|
|
531
544
|
input.workflowStatus ?? '',
|
|
545
|
+
input.forge ?? '',
|
|
546
|
+
input.queuedRequest ?? '',
|
|
532
547
|
)
|
|
533
548
|
return rowToTask(q.taskById.get(id))
|
|
534
549
|
},
|
|
@@ -632,11 +647,14 @@ export function openStore({ dir }) {
|
|
|
632
647
|
const report = JSON.stringify(task.report && typeof task.report === 'object' ? task.report : {})
|
|
633
648
|
const comments = JSON.stringify(Array.isArray(task.comments) ? task.comments : [])
|
|
634
649
|
const forge = task.forge || (typeof task.issueUrl === 'string' && task.issueUrl.includes('github.com') ? 'github' : 'gitea')
|
|
650
|
+
const targetBoard = task.board || 'main'
|
|
651
|
+
const targetCol = task.column || task.col || 'backlog'
|
|
652
|
+
const position = isValidOrderKey(task.position) ? task.position : positionFor(targetBoard, targetCol, undefined, undefined)
|
|
635
653
|
q.restoreTask.run(
|
|
636
654
|
task.id,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
655
|
+
targetBoard,
|
|
656
|
+
targetCol,
|
|
657
|
+
position,
|
|
640
658
|
task.title || '',
|
|
641
659
|
task.body || '',
|
|
642
660
|
task.owner ?? null,
|
|
@@ -650,7 +668,7 @@ export function openStore({ dir }) {
|
|
|
650
668
|
task.workspaceId || '',
|
|
651
669
|
task.sessionId ?? null,
|
|
652
670
|
task.model ?? null,
|
|
653
|
-
task.provider
|
|
671
|
+
task.provider !== undefined ? task.provider : null,
|
|
654
672
|
task.waiting ? 1 : 0,
|
|
655
673
|
task.syncedAt || 0,
|
|
656
674
|
colors,
|
|
@@ -684,6 +702,7 @@ export function openStore({ dir }) {
|
|
|
684
702
|
task.queuedAt || 0,
|
|
685
703
|
task.plan || '',
|
|
686
704
|
forge,
|
|
705
|
+
task.queuedRequest || '',
|
|
687
706
|
)
|
|
688
707
|
return rowToTask(q.taskById.get(task.id))
|
|
689
708
|
},
|
package/lib/workflow-bridge.js
CHANGED
|
@@ -489,7 +489,7 @@ export class WorkflowBridge {
|
|
|
489
489
|
repo: task.repo,
|
|
490
490
|
index: task.issueNumber,
|
|
491
491
|
body: commentBody,
|
|
492
|
-
provider: task.
|
|
492
|
+
provider: task.forge || (typeof task.issueUrl === 'string' && task.issueUrl.includes('github.com') ? 'github' : 'gitea'),
|
|
493
493
|
})
|
|
494
494
|
} catch (err) {
|
|
495
495
|
this.logger?.warn?.(`dsh-kanban: failed to comment on issue ${task.repo}#${task.issueNumber}: ${err.message}`)
|
package/lib/worktree.js
CHANGED
|
@@ -136,8 +136,18 @@ export async function isGitRepo(repoDir, gitRunner = defaultGitRunner) {
|
|
|
136
136
|
try {
|
|
137
137
|
const res = await gitRunner(['rev-parse', '--is-inside-work-tree'], repoDir)
|
|
138
138
|
return res.stdout?.trim() === 'true'
|
|
139
|
-
} catch {
|
|
140
|
-
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const msg = String(err?.stderr || err?.message || '')
|
|
141
|
+
if (
|
|
142
|
+
err?.exitCode === 128 ||
|
|
143
|
+
err?.code === 128 ||
|
|
144
|
+
msg.includes('not a git repo') ||
|
|
145
|
+
msg.includes('not a git repository') ||
|
|
146
|
+
err?.code === 'ENOENT'
|
|
147
|
+
) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
throw err
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
|
|
@@ -371,13 +381,15 @@ export async function cleanupTaskWorktree({
|
|
|
371
381
|
if (!current.worktree) return { noop: true, removed: false }
|
|
372
382
|
|
|
373
383
|
const root = resolveWorktreeRoot(config, env, cwdOf)
|
|
384
|
+
const customRoot = typeof config?.worktreeRoot === 'string' && config.worktreeRoot.trim() !== ''
|
|
385
|
+
? root
|
|
386
|
+
: undefined
|
|
374
387
|
const candidateRoots = [
|
|
375
388
|
root,
|
|
376
389
|
repoDir,
|
|
377
|
-
typeof cwdOf === 'function' ? cwdOf() : process.cwd(),
|
|
378
|
-
process.cwd(),
|
|
390
|
+
typeof cwdOf === 'function' ? cwdOf() : (!customRoot && !repoDir ? process.cwd() : undefined),
|
|
379
391
|
].filter(Boolean)
|
|
380
|
-
const isFenced = candidateRoots.some((c) => isPathFenced(current.worktree, c))
|
|
392
|
+
const isFenced = candidateRoots.length === 0 || candidateRoots.some((c) => isPathFenced(current.worktree, c))
|
|
381
393
|
if (!isFenced) {
|
|
382
394
|
return { error: 'unfenced-worktree-path', status: 403 }
|
|
383
395
|
}
|
|
@@ -392,15 +404,22 @@ export async function cleanupTaskWorktree({
|
|
|
392
404
|
gitRunner,
|
|
393
405
|
})
|
|
394
406
|
|
|
395
|
-
if (result.error
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
if (result.error) {
|
|
408
|
+
if (result.error === 'worktree-dirty') {
|
|
409
|
+
store?.addTransition?.({
|
|
410
|
+
taskId: current.id,
|
|
411
|
+
fromCol: current.column,
|
|
412
|
+
toCol: current.column,
|
|
413
|
+
source: 'session',
|
|
414
|
+
detail: `cleanup deferred: uncommitted files detected: ${result.files.slice(0, 3).join(', ')}`,
|
|
415
|
+
})
|
|
416
|
+
return { error: 'worktree-dirty', files: result.files }
|
|
417
|
+
}
|
|
418
|
+
return { success: false, removed: false, error: result.error, detail: result.detail }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!result.removed) {
|
|
422
|
+
return { success: false, removed: false, reason: result.reason }
|
|
404
423
|
}
|
|
405
424
|
|
|
406
425
|
if (workspaceRegistry && current.workspaceId) {
|
|
@@ -548,6 +567,12 @@ export async function mergeTaskWorktree({
|
|
|
548
567
|
return { success: false, merged: false, error: 'main-repo-not-found' }
|
|
549
568
|
}
|
|
550
569
|
|
|
570
|
+
const registered = await listWorktrees(mainRepoDir, gitRunner)
|
|
571
|
+
const normTaskWorktree = resolve(task.worktree)
|
|
572
|
+
if (!registered.some((p) => resolve(p) === normTaskWorktree)) {
|
|
573
|
+
return { success: false, merged: false, error: 'worktree-not-registered' }
|
|
574
|
+
}
|
|
575
|
+
|
|
551
576
|
// 3. Resolve target merge branch
|
|
552
577
|
let target = targetBranch
|
|
553
578
|
if (target && !isSafeBranchName(target)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea and GitHub-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|