@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/worktree.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Managed worktrees for task agent sessions (Issue #193).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Isolating agent sessions in git worktrees protects the main repository
|
|
4
|
+
// from concurrent modifications, prevents branch overwrite conflicts,
|
|
5
|
+
// and scopes working directory per task.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// 1.
|
|
9
|
-
// 2.
|
|
10
|
-
// 3.
|
|
11
|
-
// 4.
|
|
12
|
-
// 5.
|
|
7
|
+
// Lifecycle:
|
|
8
|
+
// 1. On transition to In Progress: create worktree task/<id>-<slug>
|
|
9
|
+
// 2. Register worktree in DSH workspaceRegistry as active workspace
|
|
10
|
+
// 3. Launch agent session with cwd = worktreePath
|
|
11
|
+
// 4. In Cleanup column: verify uncommitted files (dirtyFiles)
|
|
12
|
+
// 5. Upon confirmation: remove worktree and deregister workspace.
|
|
13
13
|
|
|
14
14
|
import { execFile } from 'node:child_process'
|
|
15
15
|
import { promisify } from 'node:util'
|
|
@@ -20,8 +20,8 @@ import { isAbsolute, join, resolve, sep } from 'node:path'
|
|
|
20
20
|
const execFileAsync = promisify(execFile)
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Standard git command runner.
|
|
24
|
+
* Accepts argument array to eliminate shell injection vulnerabilities.
|
|
25
25
|
*/
|
|
26
26
|
export async function defaultGitRunner(args, cwd) {
|
|
27
27
|
try {
|
|
@@ -41,7 +41,7 @@ export async function defaultGitRunner(args, cwd) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Convert string into safe branch/directory slug.
|
|
45
45
|
*/
|
|
46
46
|
export function slugify(text) {
|
|
47
47
|
if (typeof text !== 'string') return 'task'
|
|
@@ -49,7 +49,7 @@ export function slugify(text) {
|
|
|
49
49
|
.toLowerCase()
|
|
50
50
|
.normalize('NFD')
|
|
51
51
|
.replace(/[\u0300-\u036f]/g, '')
|
|
52
|
-
.replace(/[
|
|
52
|
+
.replace(/[^\p{Letter}0-9_-]+/gu, '-')
|
|
53
53
|
.replace(/-+/g, '-')
|
|
54
54
|
.replace(/^-|-$/g, '')
|
|
55
55
|
.slice(0, 40)
|
|
@@ -57,7 +57,7 @@ export function slugify(text) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* Safe repository key for worktree root directory.
|
|
61
61
|
*/
|
|
62
62
|
export function safeRepoKey(task) {
|
|
63
63
|
const owner = typeof task?.owner === 'string' ? task.owner.trim() : ''
|
|
@@ -68,7 +68,7 @@ export function safeRepoKey(task) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* Resolve root directory for all managed worktrees.
|
|
72
72
|
*/
|
|
73
73
|
export function resolveWorktreeRoot(config, env = process.env, cwdOf = () => process.cwd()) {
|
|
74
74
|
const custom = typeof config?.worktreeRoot === 'string' ? config.worktreeRoot.trim() : ''
|
|
@@ -85,7 +85,7 @@ export function resolveWorktreeRoot(config, env = process.env, cwdOf = () => pro
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
88
|
+
* Resolve canonical worktree path for task with path traversal protection.
|
|
89
89
|
*/
|
|
90
90
|
export function resolveTaskWorktreePath({ task, config, env, cwdOf }) {
|
|
91
91
|
const root = resolveWorktreeRoot(config, env, cwdOf)
|
|
@@ -95,13 +95,13 @@ export function resolveTaskWorktreePath({ task, config, env, cwdOf }) {
|
|
|
95
95
|
|
|
96
96
|
const fenceSep = root.endsWith(sep) || root.endsWith('/') ? root : root + sep
|
|
97
97
|
if (target !== root && !target.startsWith(fenceSep) && !target.startsWith(root + '/')) {
|
|
98
|
-
throw new Error('
|
|
98
|
+
throw new Error('worktree path escapes worktree root')
|
|
99
99
|
}
|
|
100
100
|
return target
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Generate task branch name.
|
|
105
105
|
*/
|
|
106
106
|
export function resolveTaskBranch(task) {
|
|
107
107
|
const prefix = typeof task?.issueNumber === 'number'
|
|
@@ -112,7 +112,7 @@ export function resolveTaskBranch(task) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
115
|
+
* Check if directory is a valid git repository.
|
|
116
116
|
*/
|
|
117
117
|
export async function isGitRepo(repoDir, gitRunner = defaultGitRunner) {
|
|
118
118
|
if (!repoDir) return false
|
|
@@ -125,7 +125,7 @@ export async function isGitRepo(repoDir, gitRunner = defaultGitRunner) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
128
|
+
* List existing worktree paths for repository.
|
|
129
129
|
*/
|
|
130
130
|
export async function listWorktrees(repoDir, gitRunner = defaultGitRunner) {
|
|
131
131
|
try {
|
|
@@ -143,7 +143,7 @@ export async function listWorktrees(repoDir, gitRunner = defaultGitRunner) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
146
|
+
* Resolve base ref for worktree creation.
|
|
147
147
|
*/
|
|
148
148
|
export async function determineBaseRef(repoDir, gitRunner = defaultGitRunner) {
|
|
149
149
|
for (const candidate of ['origin/main', 'origin/master', 'main', 'master', 'HEAD']) {
|
|
@@ -158,7 +158,7 @@ export async function determineBaseRef(repoDir, gitRunner = defaultGitRunner) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
*
|
|
161
|
+
* Create or reuse worktree for task.
|
|
162
162
|
*/
|
|
163
163
|
export async function createTaskWorktree({
|
|
164
164
|
task,
|
|
@@ -180,13 +180,13 @@ export async function createTaskWorktree({
|
|
|
180
180
|
const worktreePath = resolveTaskWorktreePath({ task, config, env, cwdOf })
|
|
181
181
|
const branchName = resolveTaskBranch(task)
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Check if worktree is already registered
|
|
184
184
|
const existing = await listWorktrees(repoDir, gitRunner)
|
|
185
185
|
if (existing.includes(resolve(worktreePath))) {
|
|
186
186
|
return { worktreePath, branchName, reused: true }
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// Check if local branch already exists
|
|
190
190
|
let branchExists = false
|
|
191
191
|
try {
|
|
192
192
|
await gitRunner(['rev-parse', '--verify', '--quiet', `refs/heads/${branchName}`], repoDir)
|
|
@@ -206,7 +206,7 @@ export async function createTaskWorktree({
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
|
-
*
|
|
209
|
+
* Check for uncommitted changes in worktree.
|
|
210
210
|
*/
|
|
211
211
|
function isSubmoduleDrift(worktreePath, line) {
|
|
212
212
|
try {
|
|
@@ -226,14 +226,14 @@ function isSubmoduleDrift(worktreePath, line) {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
} catch {
|
|
229
|
-
//
|
|
229
|
+
// Ignore file access errors during check
|
|
230
230
|
}
|
|
231
231
|
return false
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
235
|
+
* Check for uncommitted changes in worktree.
|
|
236
|
+
* Excludes submodule noise and gitlink drift (#213).
|
|
237
237
|
*/
|
|
238
238
|
export async function checkWorktreeDirty({ worktreePath, gitRunner = defaultGitRunner }) {
|
|
239
239
|
if (!worktreePath || !existsSync(worktreePath)) {
|
|
@@ -253,7 +253,7 @@ export async function checkWorktreeDirty({ worktreePath, gitRunner = defaultGitR
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
|
-
*
|
|
256
|
+
* Safely remove task worktree.
|
|
257
257
|
*/
|
|
258
258
|
export async function removeTaskWorktree({
|
|
259
259
|
repoDir,
|
|
@@ -281,7 +281,7 @@ export async function removeTaskWorktree({
|
|
|
281
281
|
: ['worktree', 'remove', worktreePath]
|
|
282
282
|
|
|
283
283
|
await gitRunner(removeArgs, repoDir)
|
|
284
|
-
try { await gitRunner(['worktree', 'prune'], repoDir) } catch { /* prune
|
|
284
|
+
try { await gitRunner(['worktree', 'prune'], repoDir) } catch { /* prune ignores errors */ }
|
|
285
285
|
|
|
286
286
|
let branchDeleted = false
|
|
287
287
|
if (deleteBranch && branchName && repoDir) {
|
|
@@ -289,7 +289,7 @@ export async function removeTaskWorktree({
|
|
|
289
289
|
await gitRunner(['branch', '-d', branchName], repoDir)
|
|
290
290
|
branchDeleted = true
|
|
291
291
|
} catch {
|
|
292
|
-
//
|
|
292
|
+
// Ignored: branch not deleted if not yet merged
|
|
293
293
|
branchDeleted = false
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -298,7 +298,7 @@ export async function removeTaskWorktree({
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
*
|
|
301
|
+
* Register worktree path in DSH workspaceRegistry.
|
|
302
302
|
*/
|
|
303
303
|
export async function registerDshWorkspace({ workspaceRegistry, worktreePath, title }) {
|
|
304
304
|
if (!workspaceRegistry || typeof workspaceRegistry.create !== 'function') return undefined
|
|
@@ -311,7 +311,7 @@ export async function registerDshWorkspace({ workspaceRegistry, worktreePath, ti
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
|
-
*
|
|
314
|
+
* Deregister workspace from DSH workspaceRegistry.
|
|
315
315
|
*/
|
|
316
316
|
export async function deregisterDshWorkspace({ workspaceRegistry, workspaceId }) {
|
|
317
317
|
if (!workspaceRegistry || typeof workspaceRegistry.delete !== 'function' || !workspaceId) return false
|
|
@@ -323,7 +323,7 @@ export async function deregisterDshWorkspace({ workspaceRegistry, workspaceId })
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
|
-
*
|
|
326
|
+
* Clean up task worktree with dirty files guard.
|
|
327
327
|
*/
|
|
328
328
|
export async function cleanupTaskWorktree({
|
|
329
329
|
store,
|
|
@@ -354,7 +354,7 @@ export async function cleanupTaskWorktree({
|
|
|
354
354
|
fromCol: current.column,
|
|
355
355
|
toCol: current.column,
|
|
356
356
|
source: 'session',
|
|
357
|
-
detail:
|
|
357
|
+
detail: `cleanup deferred: uncommitted files detected: ${result.files.slice(0, 3).join(', ')}`,
|
|
358
358
|
})
|
|
359
359
|
return { error: 'worktree-dirty', files: result.files }
|
|
360
360
|
}
|
|
@@ -369,14 +369,14 @@ export async function cleanupTaskWorktree({
|
|
|
369
369
|
fromCol: current.column,
|
|
370
370
|
toCol: current.column,
|
|
371
371
|
source: 'session',
|
|
372
|
-
detail: '
|
|
372
|
+
detail: 'cleanup: worktree removed' + (result.branchDeleted ? ', branch deleted' : ''),
|
|
373
373
|
})
|
|
374
374
|
|
|
375
375
|
return { success: true, removed: true, branchDeleted: result.branchDeleted }
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
/**
|
|
379
|
-
*
|
|
379
|
+
* Generate unified diff of changes in task worktree (#211).
|
|
380
380
|
*
|
|
381
381
|
* @param {{worktreePath: string, baseRef?: string, gitRunner?: Function}} options
|
|
382
382
|
* @returns {Promise<{diff: string, truncated: boolean, filesChanged: number}>}
|
|
@@ -391,7 +391,7 @@ export async function getTaskDiff({ worktreePath, baseRef, gitRunner = defaultGi
|
|
|
391
391
|
base = await determineBaseRef(worktreePath, gitRunner)
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
//
|
|
394
|
+
// Try git diff base...HEAD first if base ref is available
|
|
395
395
|
let diffOut = ''
|
|
396
396
|
if (base) {
|
|
397
397
|
const res = await gitRunner(['diff', `${base}...HEAD`], worktreePath)
|
|
@@ -400,23 +400,23 @@ export async function getTaskDiff({ worktreePath, baseRef, gitRunner = defaultGi
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
//
|
|
403
|
+
// Include uncommitted changes (unstaged + staged)
|
|
404
404
|
const unstaged = await gitRunner(['diff', 'HEAD'], worktreePath)
|
|
405
405
|
if (unstaged.exitCode === 0 && unstaged.stdout.trim() !== '') {
|
|
406
406
|
diffOut = diffOut ? `${diffOut}\n\n# Uncommitted Changes:\n${unstaged.stdout}` : unstaged.stdout
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
//
|
|
409
|
+
// Fallback to plain git diff if base...HEAD produced no output
|
|
410
410
|
if (!diffOut.trim()) {
|
|
411
411
|
const fallback = await gitRunner(['diff'], worktreePath)
|
|
412
412
|
if (fallback.exitCode === 0) diffOut = fallback.stdout
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
//
|
|
415
|
+
// Count affected files
|
|
416
416
|
const fileLines = diffOut.split('\n').filter((l) => l.startsWith('diff --git'))
|
|
417
417
|
const filesChanged = fileLines.length
|
|
418
418
|
|
|
419
|
-
//
|
|
419
|
+
// Memory guard: cap diff output at 250 KiB
|
|
420
420
|
const MAX_DIFF_BYTES = 250 * 1024
|
|
421
421
|
let truncated = false
|
|
422
422
|
if (Buffer.byteLength(diffOut, 'utf8') > MAX_DIFF_BYTES) {
|
|
@@ -428,7 +428,7 @@ export async function getTaskDiff({ worktreePath, baseRef, gitRunner = defaultGi
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
/**
|
|
431
|
-
*
|
|
431
|
+
* List task branch commits relative to base branch (#211).
|
|
432
432
|
*
|
|
433
433
|
* @param {{worktreePath: string, baseRef?: string, gitRunner?: Function}} options
|
|
434
434
|
* @returns {Promise<Array<{hash: string, subject: string, author: string, date: string}>>}
|
|
@@ -446,7 +446,7 @@ export async function getTaskCommits({ worktreePath, baseRef, gitRunner = defaul
|
|
|
446
446
|
const range = base ? `${base}..HEAD` : '-n 20'
|
|
447
447
|
const logRes = await gitRunner(['log', range, '--pretty=format:%h%x09%an%x09%ad%x09%s', '--date=short'], worktreePath)
|
|
448
448
|
if (logRes.exitCode !== 0 || !logRes.stdout.trim()) {
|
|
449
|
-
//
|
|
449
|
+
// Fallback to last 10 commits
|
|
450
450
|
const fallback = await gitRunner(['log', '-n', '10', '--pretty=format:%h%x09%an%x09%ad%x09%s', '--date=short'], worktreePath)
|
|
451
451
|
if (fallback.exitCode !== 0 || !fallback.stdout.trim()) return []
|
|
452
452
|
return fallback.stdout.trim().split('\n').map(parseCommitLine).filter(Boolean)
|
|
@@ -467,7 +467,7 @@ function parseCommitLine(line) {
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
/**
|
|
470
|
-
*
|
|
470
|
+
* Merge worktree branch into repository main branch (--no-ff) upon acceptance (#212).
|
|
471
471
|
*
|
|
472
472
|
* @param {{task: object, targetBranch?: string, gitRunner?: Function, workspaceRegistry?: object}} options
|
|
473
473
|
* @returns {Promise<{success: boolean, merged: boolean, error?: string}>}
|
|
@@ -479,13 +479,13 @@ export async function mergeTaskWorktree({
|
|
|
479
479
|
return { success: false, merged: false, error: 'no-worktree-or-branch' }
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
// 1.
|
|
482
|
+
// 1. Verify worktree cleanliness
|
|
483
483
|
const dirtyCheck = await checkWorktreeDirty({ worktreePath: task.worktree, gitRunner })
|
|
484
484
|
if (dirtyCheck.dirty) {
|
|
485
485
|
return { success: false, merged: false, error: 'worktree-dirty', files: dirtyCheck.files }
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
// 2.
|
|
488
|
+
// 2. Locate main repository root
|
|
489
489
|
let mainRepoDir = ''
|
|
490
490
|
const revParse = await gitRunner(['rev-parse', '--git-common-dir'], task.worktree)
|
|
491
491
|
if (revParse.exitCode === 0 && revParse.stdout.trim()) {
|
|
@@ -497,14 +497,14 @@ export async function mergeTaskWorktree({
|
|
|
497
497
|
return { success: false, merged: false, error: 'main-repo-not-found' }
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
// 3.
|
|
500
|
+
// 3. Resolve target merge branch
|
|
501
501
|
let target = targetBranch
|
|
502
502
|
if (!target) {
|
|
503
503
|
target = await determineBaseRef(mainRepoDir, gitRunner)
|
|
504
504
|
}
|
|
505
505
|
if (!target) target = 'main'
|
|
506
506
|
|
|
507
|
-
// 4.
|
|
507
|
+
// 4. Switch or verify target branch in main repository
|
|
508
508
|
const currentBranch = await gitRunner(['branch', '--show-current'], mainRepoDir)
|
|
509
509
|
if (currentBranch.stdout.trim() !== target) {
|
|
510
510
|
const checkoutRes = await gitRunner(['checkout', target], mainRepoDir)
|
|
@@ -513,16 +513,16 @@ export async function mergeTaskWorktree({
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
// 5.
|
|
516
|
+
// 5. Execute git merge --no-ff
|
|
517
517
|
const mergeMsg = `Merge branch '${task.branch}' for task ${task.id}`
|
|
518
518
|
const mergeRes = await gitRunner(['merge', '--no-ff', task.branch, '-m', mergeMsg], mainRepoDir)
|
|
519
519
|
if (mergeRes.exitCode !== 0) {
|
|
520
|
-
//
|
|
520
|
+
// Roll back merge on conflict
|
|
521
521
|
await gitRunner(['merge', '--abort'], mainRepoDir)
|
|
522
522
|
return { success: false, merged: false, error: 'merge-conflict', detail: mergeRes.stderr || mergeRes.stdout }
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
// 6.
|
|
525
|
+
// 6. Clean up task worktree
|
|
526
526
|
await cleanupTaskWorktree({
|
|
527
527
|
task,
|
|
528
528
|
workspaceRegistry,
|
|
@@ -535,9 +535,9 @@ export async function mergeTaskWorktree({
|
|
|
535
535
|
|
|
536
536
|
|
|
537
537
|
/**
|
|
538
|
-
*
|
|
538
|
+
* Recursive discovery of git repositories within workspace directory (#210).
|
|
539
539
|
*
|
|
540
|
-
* @param {string} rootDir
|
|
540
|
+
* @param {string} rootDir Root directory to scan
|
|
541
541
|
* @param {{maxDepth?: number, maxRepos?: number, gitRunner?: Function}} options
|
|
542
542
|
* @returns {Promise<Array<{relPath: string, fullPath: string, name: string}>>}
|
|
543
543
|
*/
|
|
@@ -561,7 +561,7 @@ export async function findWorkspaceGitRepos(rootDir, options = {}) {
|
|
|
561
561
|
fullPath: resolve(dir),
|
|
562
562
|
name: basename(resolve(dir)),
|
|
563
563
|
})
|
|
564
|
-
//
|
|
564
|
+
// Do not recurse into nested non-root repositories
|
|
565
565
|
if (depth > 0) return
|
|
566
566
|
}
|
|
567
567
|
|
|
@@ -572,7 +572,7 @@ export async function findWorkspaceGitRepos(rootDir, options = {}) {
|
|
|
572
572
|
await scan(join(dir, entry.name), depth + 1)
|
|
573
573
|
}
|
|
574
574
|
} catch {
|
|
575
|
-
//
|
|
575
|
+
// Ignore inaccessible directories
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
578
|
|
|
@@ -581,7 +581,7 @@ export async function findWorkspaceGitRepos(rootDir, options = {}) {
|
|
|
581
581
|
}
|
|
582
582
|
|
|
583
583
|
/**
|
|
584
|
-
*
|
|
584
|
+
* Multi-repository mirror setup for workspaces with multiple git repositories (#210).
|
|
585
585
|
*
|
|
586
586
|
* @param {{task: object, workspacePath: string, gitRunner?: Function, config?: object}} options
|
|
587
587
|
* @returns {Promise<{mirrors: Array<{repo: string, worktreePath: string, branch: string}>, errors?: Array<string>}>}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
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",
|