@goodandready/dsh-kanban 0.1.32 → 0.1.34
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/docs/design/DESIGN.md +5 -0
- package/docs/plans/2026-09-08-diff-viewer-merge.md +7 -0
- package/lib/client.js +87 -0
- package/lib/index.js +38 -1
- package/lib/worktree.js +159 -1
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
- Фильтрация сессий субагентов: сессии с маркером subagent (origin === 'subagent' или parentId !== undefined) исключаются из liveSessions и не перезаписывают состояние корневых задач (#215).
|
|
68
68
|
## Locked Decisions
|
|
69
69
|
|
|
70
|
+
### 2026-09-08: Релиз Vector 3 (Просмотр Git Diff / Коммитов и Однокликовый Merge) (#211, #212)
|
|
71
|
+
- **Git Diff & Commits Viewer**: В `TaskDrawer` интегрирован блок просмотра истории коммитов сессии и unified diff измененных файлов воркдерева (`GET /tasks/:id/diff` и `GET /tasks/:id/commits`). Diff безопасно ограничен 250 КБ.
|
|
72
|
+
- **One-Click Merge (--no-ff)**: В секции Review Acceptance Gate добавлена кнопка «✓ Принять и смержить» (`POST /tasks/:id/merge`). Выполняется проверка на отсутствие незакоммиченных изменений (`checkWorktreeDirty`), слияние `git merge --no-ff` в целевую ветку (`main`), автоматическая дерегистрация и удаление воркдерева с переводом задачи в `done`.
|
|
73
|
+
|
|
74
|
+
|
|
70
75
|
### 2026-09-08: Релиз Vector 2 (Управление параллелизмом и безопасность) (#214, #216, #224)
|
|
71
76
|
- **maxConcurrentSessions**: Лимит параллельных сессий агентов (по умолчанию 3) предотвращает исчерпание памяти/CPU на хосте. Задачи сверх лимита переводятся в очередь (`queuedAt > 0`) и автоматически запускаются диспетчером при завершении активных сессий.
|
|
72
77
|
- **workspaceClaimBoundaries**: Инструмент `board_move` проверяет принадлежность задачи к воркспейсу текущей сессии агента, предотвращая случайный межпроектный захват карточек.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Implementation Plan: Vector 3 — Git Diff / Commit Viewer & One-Click Merge (#211, #212)
|
|
2
|
+
|
|
3
|
+
Implementation of in-board git diff viewer and one-click worktree merge in `@goodandready/dsh-kanban` (v0.1.33).
|
|
4
|
+
|
|
5
|
+
## Issues Scope
|
|
6
|
+
1. **#211**: In-drawer Git Diff and commits viewer for human acceptance.
|
|
7
|
+
2. **#212**: One-click worktree branch merge upon acceptance (`--no-ff`) with automated cleanup.
|
package/lib/client.js
CHANGED
|
@@ -1134,6 +1134,15 @@ window.__ModuleLoader__.load({
|
|
|
1134
1134
|
'report.checksRun': 'Verification checks',
|
|
1135
1135
|
'report.artifacts': 'Artifacts',
|
|
1136
1136
|
'report.risks': 'Risks & notes',
|
|
1137
|
+
'diff.title': 'Git Changes & Diff',
|
|
1138
|
+
'diff.empty': 'No changes detected in worktree.',
|
|
1139
|
+
'diff.commits': 'Commits ({n})',
|
|
1140
|
+
'diff.filesChanged': '{n} files changed',
|
|
1141
|
+
'diff.truncated': 'Diff truncated (exceeded limit)',
|
|
1142
|
+
'diff.show': 'Show diff',
|
|
1143
|
+
'diff.hide': 'Hide diff',
|
|
1144
|
+
'acceptance.acceptAndMerge': 'Accept & Merge (--no-ff)',
|
|
1145
|
+
'acceptance.merging': 'Merging...',
|
|
1137
1146
|
'acceptance.title': 'Review Acceptance Gate',
|
|
1138
1147
|
'acceptance.accept': 'Accept (Done)',
|
|
1139
1148
|
'acceptance.reject': 'Send back for rework',
|
|
@@ -1458,6 +1467,15 @@ window.__ModuleLoader__.load({
|
|
|
1458
1467
|
'report.checksRun': '验证检查',
|
|
1459
1468
|
'report.artifacts': '生成产物',
|
|
1460
1469
|
'report.risks': '风险与说明',
|
|
1470
|
+
'diff.title': 'Git 变更与 Diff',
|
|
1471
|
+
'diff.empty': '工作区未检测到变更。',
|
|
1472
|
+
'diff.commits': '提交历史 ({n})',
|
|
1473
|
+
'diff.filesChanged': '{n} 个文件变更',
|
|
1474
|
+
'diff.truncated': 'Diff 超限已截断',
|
|
1475
|
+
'diff.show': '查看 diff',
|
|
1476
|
+
'diff.hide': '收起 diff',
|
|
1477
|
+
'acceptance.acceptAndMerge': '验收并合并 (--no-ff)',
|
|
1478
|
+
'acceptance.merging': '合并中...',
|
|
1461
1479
|
'acceptance.title': '评审验收网关',
|
|
1462
1480
|
'acceptance.accept': '验收通过 (完成)',
|
|
1463
1481
|
'acceptance.reject': '退回重做',
|
|
@@ -2851,6 +2869,59 @@ window.__ModuleLoader__.load({
|
|
|
2851
2869
|
}, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
|
|
2852
2870
|
)
|
|
2853
2871
|
: null,
|
|
2872
|
+
openTask.worktree
|
|
2873
|
+
? React.createElement('div', {
|
|
2874
|
+
style: {
|
|
2875
|
+
margin: '8px 0', padding: 12,
|
|
2876
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
2877
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
2878
|
+
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
|
|
2879
|
+
},
|
|
2880
|
+
},
|
|
2881
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2882
|
+
React.createElement('div', { className: 'dkb-group', style: { margin: 0 } },
|
|
2883
|
+
t('diff.title'),
|
|
2884
|
+
commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
|
|
2885
|
+
diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
|
|
2886
|
+
),
|
|
2887
|
+
React.createElement('button', {
|
|
2888
|
+
type: 'button', className: 'dkb-discard',
|
|
2889
|
+
style: { padding: '2px 8px', fontSize: 11 },
|
|
2890
|
+
onClick: () => {
|
|
2891
|
+
if (!diffData && !diffExpanded) loadDiffAndCommits()
|
|
2892
|
+
setDiffExpanded(!diffExpanded)
|
|
2893
|
+
},
|
|
2894
|
+
}, diffExpanded ? t('diff.hide') : t('diff.show')),
|
|
2895
|
+
),
|
|
2896
|
+
diffExpanded ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2897
|
+
commitsData.length > 0
|
|
2898
|
+
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
|
|
2899
|
+
commitsData.map((cm) => React.createElement('div', {
|
|
2900
|
+
key: cm.hash,
|
|
2901
|
+
style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
|
|
2902
|
+
},
|
|
2903
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
|
|
2904
|
+
React.createElement('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
|
|
2905
|
+
React.createElement('span', { style: { opacity: 0.6 } }, cm.date),
|
|
2906
|
+
))
|
|
2907
|
+
)
|
|
2908
|
+
: null,
|
|
2909
|
+
diffLoading
|
|
2910
|
+
? React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
|
|
2911
|
+
: diffData?.diff
|
|
2912
|
+
? React.createElement('pre', {
|
|
2913
|
+
style: {
|
|
2914
|
+
margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
|
|
2915
|
+
background: 'var(--dsw-alias-bg-layer-2, #1e1e1e)',
|
|
2916
|
+
color: 'var(--dsw-alias-label-primary, #ddd)',
|
|
2917
|
+
fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
|
|
2918
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
2919
|
+
},
|
|
2920
|
+
}, diffData.diff)
|
|
2921
|
+
: React.createElement('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
|
|
2922
|
+
) : null
|
|
2923
|
+
)
|
|
2924
|
+
: null,
|
|
2854
2925
|
openTask.column === 'review'
|
|
2855
2926
|
? React.createElement('div', {
|
|
2856
2927
|
style: {
|
|
@@ -2862,6 +2933,22 @@ window.__ModuleLoader__.load({
|
|
|
2862
2933
|
},
|
|
2863
2934
|
React.createElement('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
|
|
2864
2935
|
React.createElement('div', { style: { display: 'flex', gap: 8 } },
|
|
2936
|
+
openTask.worktree
|
|
2937
|
+
? React.createElement('button', {
|
|
2938
|
+
type: 'button', className: 'dkb-save',
|
|
2939
|
+
disabled: merging,
|
|
2940
|
+
style: { background: 'var(--dsw-alias-bg-success, #16a34a)', color: '#fff' },
|
|
2941
|
+
onClick: async () => {
|
|
2942
|
+
try {
|
|
2943
|
+
setMerging(true)
|
|
2944
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/merge', { method: 'POST', body: '{}' })
|
|
2945
|
+
if (out && out.task) setOpenTask(out.task)
|
|
2946
|
+
await load(board)
|
|
2947
|
+
} catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
|
|
2948
|
+
finally { setMerging(false) }
|
|
2949
|
+
},
|
|
2950
|
+
}, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
|
|
2951
|
+
: null,
|
|
2865
2952
|
React.createElement('button', {
|
|
2866
2953
|
type: 'button', className: 'dkb-save',
|
|
2867
2954
|
onClick: async () => {
|
package/lib/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
runBatch, queueTask, unqueueTask, liveSessions, dispatchNextQueuedTask,
|
|
32
32
|
PERMISSION_RANKS,
|
|
33
33
|
} from './launcher.js'
|
|
34
|
-
import { cleanupTaskWorktree, checkWorktreeDirty } from './worktree.js'
|
|
34
|
+
import { cleanupTaskWorktree, checkWorktreeDirty, getTaskDiff, getTaskCommits, mergeTaskWorktree } from './worktree.js'
|
|
35
35
|
import { dispatchMove, MOVE_DETAIL, stopWork, STOP_DETAIL } from './commands.js'
|
|
36
36
|
import { createGiteaClient } from './gitea.js'
|
|
37
37
|
import { syncAll, createSyncState, intakeAll, archiveOverdue } from './sync.js'
|
|
@@ -882,6 +882,43 @@ export function apply(ctx, config) {
|
|
|
882
882
|
reply(res, out)
|
|
883
883
|
return
|
|
884
884
|
}
|
|
885
|
+
if (req.method === 'GET' && action === 'diff') {
|
|
886
|
+
const task = store.getTask(id)
|
|
887
|
+
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
888
|
+
const diffResult = await getTaskDiff({ worktreePath: task.worktree })
|
|
889
|
+
reply(res, diffResult)
|
|
890
|
+
return
|
|
891
|
+
}
|
|
892
|
+
if (req.method === 'GET' && action === 'commits') {
|
|
893
|
+
const task = store.getTask(id)
|
|
894
|
+
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
895
|
+
const commits = await getTaskCommits({ worktreePath: task.worktree })
|
|
896
|
+
reply(res, { commits })
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
if (req.method === 'POST' && action === 'merge') {
|
|
900
|
+
const task = store.getTask(id)
|
|
901
|
+
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
902
|
+
const mergeResult = await mergeTaskWorktree({
|
|
903
|
+
task,
|
|
904
|
+
targetBranch: typeof input?.targetBranch === 'string' ? input.targetBranch.trim() : undefined,
|
|
905
|
+
workspaceRegistry: ctx.get('workspaceRegistry'),
|
|
906
|
+
})
|
|
907
|
+
if (!mergeResult.success) {
|
|
908
|
+
writeJson(res, 400, { error: mergeResult.error, detail: mergeResult.detail, files: mergeResult.files })
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
const updated = store.acceptTask(id)
|
|
912
|
+
store.addTransition({
|
|
913
|
+
taskId: id,
|
|
914
|
+
fromCol: task.column,
|
|
915
|
+
toCol: 'done',
|
|
916
|
+
source: 'manual',
|
|
917
|
+
detail: `принята и смержена ветка ${task.branch} в ${mergeResult.targetBranch}`,
|
|
918
|
+
})
|
|
919
|
+
reply(res, { task: updated, merged: true, targetBranch: mergeResult.targetBranch })
|
|
920
|
+
return
|
|
921
|
+
}
|
|
885
922
|
if (req.method === 'GET' && action === 'worktree-status') {
|
|
886
923
|
const task = store.getTask(id)
|
|
887
924
|
if (task === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
package/lib/worktree.js
CHANGED
|
@@ -117,7 +117,7 @@ export async function isGitRepo(repoDir, gitRunner = defaultGitRunner) {
|
|
|
117
117
|
if (!repoDir) return false
|
|
118
118
|
try {
|
|
119
119
|
const res = await gitRunner(['rev-parse', '--is-inside-work-tree'], repoDir)
|
|
120
|
-
return res.stdout === 'true'
|
|
120
|
+
return res.stdout?.trim() === 'true'
|
|
121
121
|
} catch {
|
|
122
122
|
return false
|
|
123
123
|
}
|
|
@@ -368,3 +368,161 @@ export async function cleanupTaskWorktree({
|
|
|
368
368
|
|
|
369
369
|
return { success: true, removed: true, branchDeleted: result.branchDeleted }
|
|
370
370
|
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Получить unified diff изменений в воркдереве задачи (#211).
|
|
374
|
+
*
|
|
375
|
+
* @param {{worktreePath: string, baseRef?: string, gitRunner?: Function}} options
|
|
376
|
+
* @returns {Promise<{diff: string, truncated: boolean, filesChanged: number}>}
|
|
377
|
+
*/
|
|
378
|
+
export async function getTaskDiff({ worktreePath, baseRef, gitRunner = defaultGitRunner }) {
|
|
379
|
+
if (!worktreePath) return { diff: '', truncated: false, filesChanged: 0 }
|
|
380
|
+
const isRepo = await isGitRepo(worktreePath, gitRunner)
|
|
381
|
+
if (!isRepo) return { diff: '', truncated: false, filesChanged: 0 }
|
|
382
|
+
|
|
383
|
+
let base = baseRef
|
|
384
|
+
if (!base) {
|
|
385
|
+
base = await determineBaseRef(worktreePath, gitRunner)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Сначала пробуем git diff base...HEAD, если base доступен
|
|
389
|
+
let diffOut = ''
|
|
390
|
+
if (base) {
|
|
391
|
+
const res = await gitRunner(['diff', `${base}...HEAD`], worktreePath)
|
|
392
|
+
if (res.exitCode === 0) {
|
|
393
|
+
diffOut = res.stdout
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Дополнительно смотрим незакоммиченные изменения (unstaged + staged)
|
|
398
|
+
const unstaged = await gitRunner(['diff', 'HEAD'], worktreePath)
|
|
399
|
+
if (unstaged.exitCode === 0 && unstaged.stdout.trim() !== '') {
|
|
400
|
+
diffOut = diffOut ? `${diffOut}\n\n# Uncommitted Changes:\n${unstaged.stdout}` : unstaged.stdout
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Если и base...HEAD, и HEAD ничего не дали, берём чистый git diff
|
|
404
|
+
if (!diffOut.trim()) {
|
|
405
|
+
const fallback = await gitRunner(['diff'], worktreePath)
|
|
406
|
+
if (fallback.exitCode === 0) diffOut = fallback.stdout
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Подсчитываем число затронутых файлов
|
|
410
|
+
const fileLines = diffOut.split('\n').filter((l) => l.startsWith('diff --git'))
|
|
411
|
+
const filesChanged = fileLines.length
|
|
412
|
+
|
|
413
|
+
// Защита от переполнения: обрезаем diff до 250 КБ
|
|
414
|
+
const MAX_DIFF_BYTES = 250 * 1024
|
|
415
|
+
let truncated = false
|
|
416
|
+
if (Buffer.byteLength(diffOut, 'utf8') > MAX_DIFF_BYTES) {
|
|
417
|
+
diffOut = diffOut.slice(0, MAX_DIFF_BYTES) + '\n\n... [Diff truncated: exceeded 250 KB limit]'
|
|
418
|
+
truncated = true
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { diff: diffOut, truncated, filesChanged }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Получить список коммитов ветки задачи относительно базовой ветки (#211).
|
|
426
|
+
*
|
|
427
|
+
* @param {{worktreePath: string, baseRef?: string, gitRunner?: Function}} options
|
|
428
|
+
* @returns {Promise<Array<{hash: string, subject: string, author: string, date: string}>>}
|
|
429
|
+
*/
|
|
430
|
+
export async function getTaskCommits({ worktreePath, baseRef, gitRunner = defaultGitRunner }) {
|
|
431
|
+
if (!worktreePath) return []
|
|
432
|
+
const isRepo = await isGitRepo(worktreePath, gitRunner)
|
|
433
|
+
if (!isRepo) return []
|
|
434
|
+
|
|
435
|
+
let base = baseRef
|
|
436
|
+
if (!base) {
|
|
437
|
+
base = await determineBaseRef(worktreePath, gitRunner)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const range = base ? `${base}..HEAD` : '-n 20'
|
|
441
|
+
const logRes = await gitRunner(['log', range, '--pretty=format:%h%x09%an%x09%ad%x09%s', '--date=short'], worktreePath)
|
|
442
|
+
if (logRes.exitCode !== 0 || !logRes.stdout.trim()) {
|
|
443
|
+
// fallback на последние 10 коммитов
|
|
444
|
+
const fallback = await gitRunner(['log', '-n', '10', '--pretty=format:%h%x09%an%x09%ad%x09%s', '--date=short'], worktreePath)
|
|
445
|
+
if (fallback.exitCode !== 0 || !fallback.stdout.trim()) return []
|
|
446
|
+
return fallback.stdout.trim().split('\n').map(parseCommitLine).filter(Boolean)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return logRes.stdout.trim().split('\n').map(parseCommitLine).filter(Boolean)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseCommitLine(line) {
|
|
453
|
+
const parts = line.split('\t')
|
|
454
|
+
if (parts.length < 4) return null
|
|
455
|
+
return {
|
|
456
|
+
hash: parts[0],
|
|
457
|
+
author: parts[1],
|
|
458
|
+
date: parts[2],
|
|
459
|
+
subject: parts[3],
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Слияние ветки воркдерева в основную ветку репозитория (--no-ff) при приёмке (#212).
|
|
465
|
+
*
|
|
466
|
+
* @param {{task: object, targetBranch?: string, gitRunner?: Function, workspaceRegistry?: object}} options
|
|
467
|
+
* @returns {Promise<{success: boolean, merged: boolean, error?: string}>}
|
|
468
|
+
*/
|
|
469
|
+
export async function mergeTaskWorktree({
|
|
470
|
+
task, targetBranch, gitRunner = defaultGitRunner, workspaceRegistry,
|
|
471
|
+
}) {
|
|
472
|
+
if (!task.worktree || !task.branch) {
|
|
473
|
+
return { success: false, merged: false, error: 'no-worktree-or-branch' }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 1. Проверяем чистоту воркдерева
|
|
477
|
+
const dirtyCheck = await checkWorktreeDirty({ worktreePath: task.worktree, gitRunner })
|
|
478
|
+
if (dirtyCheck.dirty) {
|
|
479
|
+
return { success: false, merged: false, error: 'worktree-dirty', files: dirtyCheck.files }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 2. Находим корень основного репозитория (из .git файла воркдерева или config)
|
|
483
|
+
let mainRepoDir = ''
|
|
484
|
+
const revParse = await gitRunner(['rev-parse', '--git-common-dir'], task.worktree)
|
|
485
|
+
if (revParse.exitCode === 0 && revParse.stdout.trim()) {
|
|
486
|
+
const commonDir = revParse.stdout.trim()
|
|
487
|
+
mainRepoDir = commonDir.endsWith('/.git') ? commonDir.slice(0, -5) : commonDir
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!mainRepoDir) {
|
|
491
|
+
return { success: false, merged: false, error: 'main-repo-not-found' }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 3. Определяем целевую ветку для слияния
|
|
495
|
+
let target = targetBranch
|
|
496
|
+
if (!target) {
|
|
497
|
+
target = await determineBaseRef(mainRepoDir, gitRunner)
|
|
498
|
+
}
|
|
499
|
+
if (!target) target = 'main'
|
|
500
|
+
|
|
501
|
+
// 4. Переключаемся или проверяем, что в целевой ветке в основном репо
|
|
502
|
+
const currentBranch = await gitRunner(['branch', '--show-current'], mainRepoDir)
|
|
503
|
+
if (currentBranch.stdout.trim() !== target) {
|
|
504
|
+
const checkoutRes = await gitRunner(['checkout', target], mainRepoDir)
|
|
505
|
+
if (checkoutRes.exitCode !== 0) {
|
|
506
|
+
return { success: false, merged: false, error: 'checkout-target-failed', detail: checkoutRes.stderr }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 5. Выполняем git merge --no-ff
|
|
511
|
+
const mergeMsg = `Merge branch '${task.branch}' for task ${task.id}`
|
|
512
|
+
const mergeRes = await gitRunner(['merge', '--no-ff', task.branch, '-m', mergeMsg], mainRepoDir)
|
|
513
|
+
if (mergeRes.exitCode !== 0) {
|
|
514
|
+
// В случае конфликта откатываем мерж
|
|
515
|
+
await gitRunner(['merge', '--abort'], mainRepoDir)
|
|
516
|
+
return { success: false, merged: false, error: 'merge-conflict', detail: mergeRes.stderr || mergeRes.stdout }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 6. Очищаем воркдерево задачи
|
|
520
|
+
await cleanupTaskWorktree({
|
|
521
|
+
task,
|
|
522
|
+
workspaceRegistry,
|
|
523
|
+
gitRunner,
|
|
524
|
+
force: true,
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
return { success: true, merged: true, targetBranch: target }
|
|
528
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Visual Kanban board for DeepSeek Harness: Gitea-backed tasks, workflow columns, and a dedicated agent session per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|