@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 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
- let timeoutSignal
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
- const value = raw?.[key]
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 p = provider || (typeof issue?.html_url === 'string' && issue.html_url.includes('github.com') ? 'github' : 'gitea')
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
- provider: p,
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 issue = await gitea.getIssue({ owner, repo, index: issueNumber, provider: forge || provider })
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: forge || provider })) }
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: forge || 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: forge || provider })) }
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 !== 'ready' || !row.value) return null
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', applyPatch))
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', applyPatch))
292
+ ctx.effect(() => ctx.on('settings/document-updated', handleDocumentUpdated))
275
293
  } else {
276
- ctx.on('loader/volatile-update', applyPatch)
294
+ ctx.on('loader/volatile-update', handleVolatileUpdate)
277
295
  ctx.on('config', applyPatch)
278
- ctx.on('settings/document-updated', applyPatch)
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
- reply(res, setArchived({ store, id, archived: true }))
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
- reply(res, setArchived({ store, id, archived: false }))
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
- reply(res, updateTask({ store, id, input }))
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
- const isGit = await isGitRepo(cwd, gitRunner)
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 }
@@ -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:') || lower.startsWith('цель:')) {
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:') || lower.startsWith('прогресс:')) {
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
- if (typeof input?.permissionConfirmedTier === 'string') {
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({ store, task, workspaceRegistry, force, cleanupTaskWorktree }) {
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
- return { status: 200, body: out }
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
- task.board || 'main',
638
- task.column || task.col || 'backlog',
639
- task.position || keyBetween(undefined, undefined),
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 ?? 'gitea',
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
  },
@@ -489,7 +489,7 @@ export class WorkflowBridge {
489
489
  repo: task.repo,
490
490
  index: task.issueNumber,
491
491
  body: commentBody,
492
- provider: task.provider,
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
- return false
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 === 'worktree-dirty') {
396
- store?.addTransition?.({
397
- taskId: current.id,
398
- fromCol: current.column,
399
- toCol: current.column,
400
- source: 'session',
401
- detail: `cleanup deferred: uncommitted files detected: ${result.files.slice(0, 3).join(', ')}`,
402
- })
403
- return { error: 'worktree-dirty', files: result.files }
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.23",
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",