@goodandready/dsh-kanban 0.2.39 → 0.2.41
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 +2142 -2123
- package/lib/config.js +2 -0
- package/lib/pr-generator.js +16 -11
- package/lib/remote-runner.js +5 -3
- package/lib/routes/board.js +13 -2
- package/lib/routes/task-actions.js +6 -4
- package/lib/store.js +11 -2
- package/lib/worktree.js +6 -1
- package/package.json +3 -2
package/lib/config.js
CHANGED
|
@@ -98,6 +98,7 @@ export const CONFIG_DEFAULTS = {
|
|
|
98
98
|
timeMachineAutoSnapshot: true,
|
|
99
99
|
notificationsEnabled: false,
|
|
100
100
|
audioAlertsEnabled: false,
|
|
101
|
+
showHeaderChip: true,
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/** Field descriptions reused in schema and settings card. */
|
|
@@ -155,6 +156,7 @@ export const CONFIG_HINTS = {
|
|
|
155
156
|
timeMachineAutoSnapshot: 'Automatically create inception snapshot when task transitions to in-progress.',
|
|
156
157
|
notificationsEnabled: 'Enable desktop push notifications for key task events.',
|
|
157
158
|
audioAlertsEnabled: 'Enable Web Audio synthesized audio alerts on task transitions.',
|
|
159
|
+
showHeaderChip: 'Show current task indicator chip in session chat header.',
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
/** Setting key for column WIP limit; not all columns have limits. */
|
package/lib/pr-generator.js
CHANGED
|
@@ -98,6 +98,7 @@ export function parseDiffStat(statText) {
|
|
|
98
98
|
* @returns {string} PR type
|
|
99
99
|
*/
|
|
100
100
|
export function inferPrType(task, commits = []) {
|
|
101
|
+
const commitList = typeof commits === 'string' ? parseGitLog(commits) : (Array.isArray(commits) ? commits : [])
|
|
101
102
|
const title = String(task?.title ?? '').toLowerCase()
|
|
102
103
|
const labels = Array.isArray(task?.labels) ? task.labels.map((l) => String(l).toLowerCase()) : []
|
|
103
104
|
|
|
@@ -116,7 +117,7 @@ export function inferPrType(task, commits = []) {
|
|
|
116
117
|
if (title.startsWith('docs:') || title.includes('document')) return 'docs'
|
|
117
118
|
if (title.startsWith('chore:') || title.includes('cleanup')) return 'chore'
|
|
118
119
|
|
|
119
|
-
for (const c of
|
|
120
|
+
for (const c of commitList) {
|
|
120
121
|
const msg = String(c?.message ?? '').toLowerCase()
|
|
121
122
|
if (msg.startsWith('fix')) return 'fix'
|
|
122
123
|
if (msg.startsWith('refactor')) return 'refactor'
|
|
@@ -134,7 +135,8 @@ export function inferPrType(task, commits = []) {
|
|
|
134
135
|
* @returns {string} PR title
|
|
135
136
|
*/
|
|
136
137
|
export function generatePrTitle(task, commits = []) {
|
|
137
|
-
const
|
|
138
|
+
const commitList = typeof commits === 'string' ? parseGitLog(commits) : (Array.isArray(commits) ? commits : [])
|
|
139
|
+
const type = inferPrType(task, commitList)
|
|
138
140
|
const scope = inferScope(task)
|
|
139
141
|
let cleanTitle = String(task?.title ?? '').trim()
|
|
140
142
|
|
|
@@ -159,6 +161,8 @@ export function generatePrTitle(task, commits = []) {
|
|
|
159
161
|
* @returns {string} Complete Markdown description for PR
|
|
160
162
|
*/
|
|
161
163
|
export function generatePrDescription({ task, commits = [], diffStats = null, verification = null }) {
|
|
164
|
+
const commitList = typeof commits === 'string' ? parseGitLog(commits) : (Array.isArray(commits) ? commits : [])
|
|
165
|
+
const stats = typeof diffStats === 'string' ? parseDiffStat(diffStats) : diffStats
|
|
162
166
|
const title = String(task?.title ?? '').trim()
|
|
163
167
|
const body = String(task?.body ?? '').trim()
|
|
164
168
|
const issueNum = typeof task?.issueNumber === 'number' ? task.issueNumber : null
|
|
@@ -193,17 +197,17 @@ export function generatePrDescription({ task, commits = [], diffStats = null, ve
|
|
|
193
197
|
lines.push('')
|
|
194
198
|
|
|
195
199
|
// 3. Changed components & diff statistics
|
|
196
|
-
if (
|
|
200
|
+
if (stats && (stats.files?.length > 0 || stats.filesChanged > 0)) {
|
|
197
201
|
lines.push('### Changed Components & Files')
|
|
198
|
-
lines.push(`- Total files changed: ${
|
|
202
|
+
lines.push(`- Total files changed: ${stats.filesChanged || stats.files.length}`)
|
|
199
203
|
if (diffStats.additions || diffStats.deletions) {
|
|
200
|
-
lines.push(`- Changes: +${
|
|
204
|
+
lines.push(`- Changes: +${stats.additions} / -${stats.deletions}`)
|
|
201
205
|
}
|
|
202
|
-
if (Array.isArray(
|
|
203
|
-
for (const f of
|
|
206
|
+
if (Array.isArray(stats.files) && stats.files.length > 0) {
|
|
207
|
+
for (const f of stats.files.slice(0, 12)) {
|
|
204
208
|
lines.push(` - \`${f.path}\``)
|
|
205
209
|
}
|
|
206
|
-
if (
|
|
210
|
+
if (stats.files.length > 12) {
|
|
207
211
|
lines.push(` - ... and ${diffStats.files.length - 12} more files`)
|
|
208
212
|
}
|
|
209
213
|
}
|
|
@@ -213,7 +217,7 @@ export function generatePrDescription({ task, commits = [], diffStats = null, ve
|
|
|
213
217
|
// 4. Commits list
|
|
214
218
|
if (Array.isArray(commits) && commits.length > 0) {
|
|
215
219
|
lines.push('### Commits')
|
|
216
|
-
for (const c of
|
|
220
|
+
for (const c of commitList) {
|
|
217
221
|
const shaShort = c.sha ? `\`${c.sha.slice(0, 7)}\` ` : ''
|
|
218
222
|
lines.push(`- ${shaShort}${c.message}`)
|
|
219
223
|
}
|
|
@@ -252,14 +256,15 @@ export function generatePrDescription({ task, commits = [], diffStats = null, ve
|
|
|
252
256
|
* @returns {string} Formatted release notes bullet
|
|
253
257
|
*/
|
|
254
258
|
export function generateReleaseNotes({ task, commits = [] }) {
|
|
259
|
+
const commitList = typeof commits === 'string' ? parseGitLog(commits) : (Array.isArray(commits) ? commits : [])
|
|
255
260
|
const title = String(task?.title ?? '').trim()
|
|
256
261
|
const issueNum = typeof task?.issueNumber === 'number' ? ` (#${task.issueNumber})` : ''
|
|
257
262
|
const cleanTitle = title.replace(/^[A-Z]:\s*(Feature|Bug|Fix|Refactor|Task):\s*/i, '')
|
|
258
263
|
|
|
259
264
|
const lines = [`- **${cleanTitle}${issueNum}**:`]
|
|
260
265
|
|
|
261
|
-
if (Array.isArray(
|
|
262
|
-
for (const c of
|
|
266
|
+
if (Array.isArray(commitList) && commitList.length > 0) {
|
|
267
|
+
for (const c of commitList.slice(0, 5)) {
|
|
263
268
|
lines.push(` - ${c.message}`)
|
|
264
269
|
}
|
|
265
270
|
} else {
|
package/lib/remote-runner.js
CHANGED
|
@@ -112,7 +112,7 @@ export function createRemoteGitRunner(config, localGitRunner) {
|
|
|
112
112
|
*/
|
|
113
113
|
export async function testRemoteConnection(config) {
|
|
114
114
|
const root = String(config?.remoteRoot || '/mnt/external/Project/DEV').trim()
|
|
115
|
-
const probeCmd =
|
|
115
|
+
const probeCmd = 'echo REMOTE_OK'
|
|
116
116
|
const sshArgs = buildSshArgs(config, probeCmd)
|
|
117
117
|
|
|
118
118
|
const startTime = Date.now()
|
|
@@ -122,9 +122,11 @@ export async function testRemoteConnection(config) {
|
|
|
122
122
|
timeout: 12000,
|
|
123
123
|
env: { ...process.env, LC_ALL: 'C' },
|
|
124
124
|
})
|
|
125
|
+
if (!(stdout || '').includes('REMOTE_OK')) {
|
|
126
|
+
return { ok: false, error: 'Remote probe failed' }
|
|
127
|
+
}
|
|
125
128
|
const latencyMs = Date.now() - startTime
|
|
126
|
-
const
|
|
127
|
-
const remoteRootFound = trimmed.includes('REMOTE_ROOT_OK')
|
|
129
|
+
const remoteRootFound = await remotePathExists(root, config)
|
|
128
130
|
|
|
129
131
|
return {
|
|
130
132
|
ok: true,
|
package/lib/routes/board.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkCostLimit, calculateTokenCost, extractSessionUsage } from '../cost-telemetry.js'
|
|
1
2
|
import { COLUMN_ORDER, columnsOf, wipLimitField, columnNamesOf } from '../config.js'
|
|
2
3
|
import { parsePlan, planItems, planProgress, taskState, queuePosition } from '../plan.js'
|
|
3
4
|
import { columnStats, doneWithin, stale } from '../metrics.js'
|
|
@@ -18,9 +19,19 @@ export function withPlan(task, liveOf, config, now, sameSession) {
|
|
|
18
19
|
? (live.step || live.activity || live.stateDetail || '')
|
|
19
20
|
: ''
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
let costMetrics = task.costMetrics || null
|
|
23
|
+
if (!costMetrics && (task.sessionUsage || live?.usage || live?.projectionView)) {
|
|
24
|
+
const usage = extractSessionUsage(task.sessionUsage || live?.usage || live?.projectionView)
|
|
25
|
+
if (usage.totalTokens > 0) {
|
|
26
|
+
costMetrics = {
|
|
27
|
+
...usage,
|
|
28
|
+
...calculateTokenCost({ usage, usdRate: config?.usdRate, currency: config?.currency }),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
const costLimitExceeded = (config?.costMeterEnabled !== false && Number(config?.taskCostLimit) > 0 && costMetrics?.costUsd)
|
|
23
|
-
? costMetrics.costUsd
|
|
34
|
+
? checkCostLimit({ costUsd: costMetrics.costUsd, limit: Number(config.taskCostLimit) }).exceeded
|
|
24
35
|
: Boolean(task.costLimitExceeded)
|
|
25
36
|
|
|
26
37
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isTimeMachineAvailable, listTaskSnapshots, createCheckpointSnapshot, rollbackToSnapshot } from '../time-machine.js'
|
|
2
2
|
import { generateTechnicalSpec, parseSpecMarkdown } from '../plan-generator.js'
|
|
3
|
-
import { generatePrTitle, generatePrDescription, generateReleaseNotes } from '../pr-generator.js'
|
|
3
|
+
import { generatePrTitle, generatePrDescription, generateReleaseNotes, parseGitLog, parseDiffStat } from '../pr-generator.js'
|
|
4
4
|
import { getTaskCommits, getTaskDiffInspector } from '../worktree.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -484,9 +484,11 @@ export async function handleTaskPrDescription({ store, id, method, body, gitRunn
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
const
|
|
487
|
+
const parsedCommits = typeof commits === 'string' ? parseGitLog(commits) : commits
|
|
488
|
+
const parsedDiff = typeof diffStats === 'string' ? parseDiffStat(diffStats) : diffStats
|
|
489
|
+
const generatedTitle = generatePrTitle(task, parsedCommits)
|
|
490
|
+
const generatedDescription = generatePrDescription({ task, commits: parsedCommits, diffStats: parsedDiff })
|
|
491
|
+
const releaseNotes = generateReleaseNotes({ task, commits: parsedCommits })
|
|
490
492
|
|
|
491
493
|
return {
|
|
492
494
|
status: 200,
|
package/lib/store.js
CHANGED
|
@@ -124,7 +124,11 @@ const WRITABLE = [
|
|
|
124
124
|
/** Valid transition sources; unexpected values are rejected to preserve log integrity. */
|
|
125
125
|
const SOURCES = new Set(['manual', 'session', 'gitea', 'github', 'tool', 'workflow', 'watchdog'])
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
let corruptedCostMetricsCount = 0
|
|
128
|
+
export function getCorruptedCostMetricsCount() { return corruptedCostMetricsCount }
|
|
129
|
+
export function resetCorruptedCostMetricsCount() { corruptedCostMetricsCount = 0 }
|
|
130
|
+
|
|
131
|
+
export function rowToTask(row) {
|
|
128
132
|
if (row === undefined) return undefined
|
|
129
133
|
const { col, labels, waiting, labelColors, runs, checklist, report, comments, rejectReason, rejectedAt, isEpic, parentId, stalled, blockedBy, inceptionSnapshotId, spec, prDescription, ...rest } = row
|
|
130
134
|
let parsedChecklist = []
|
|
@@ -177,7 +181,10 @@ function rowToTask(row) {
|
|
|
177
181
|
try {
|
|
178
182
|
const val = JSON.parse(rest.costMetrics);
|
|
179
183
|
if (val && typeof val === 'object') return val;
|
|
180
|
-
} catch {
|
|
184
|
+
} catch (err) {
|
|
185
|
+
corruptedCostMetricsCount++;
|
|
186
|
+
// Corrupted telemetry JSON counted for observability without breaking task hydration
|
|
187
|
+
}
|
|
181
188
|
}
|
|
182
189
|
return rest.costMetrics && typeof rest.costMetrics === 'object' ? rest.costMetrics : null;
|
|
183
190
|
})(),
|
|
@@ -952,6 +959,8 @@ export function openStore({ dir }) {
|
|
|
952
959
|
secureStorePermissions(dir)
|
|
953
960
|
},
|
|
954
961
|
|
|
962
|
+
getCorruptedMetricsCount: () => getCorruptedCostMetricsCount(),
|
|
963
|
+
resetCorruptedMetricsCount: () => resetCorruptedCostMetricsCount(),
|
|
955
964
|
close() {
|
|
956
965
|
try { secureStorePermissions(dir) } catch { /* ignore */ }
|
|
957
966
|
db.close()
|
package/lib/worktree.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remotePathExists } from './remote-runner.js'
|
|
1
2
|
// Managed worktrees for task agent sessions (Issue #193).
|
|
2
3
|
//
|
|
3
4
|
// Isolating agent sessions in git worktrees protects the main repository
|
|
@@ -78,8 +79,12 @@ export function safeRepoKey(task) {
|
|
|
78
79
|
* @param {Function} [gitRunner]
|
|
79
80
|
* @returns {Promise<boolean>}
|
|
80
81
|
*/
|
|
81
|
-
export async function safeWorktreeExists(worktreePath, gitRunner) {
|
|
82
|
+
export async function safeWorktreeExists(worktreePath, gitRunner, config = null) {
|
|
82
83
|
if (!worktreePath) return false
|
|
84
|
+
if (config?.remoteMode) {
|
|
85
|
+
const existsRemotely = await remotePathExists(worktreePath, config)
|
|
86
|
+
if (existsRemotely) return true
|
|
87
|
+
}
|
|
83
88
|
if (existsSync(worktreePath)) return true
|
|
84
89
|
if (typeof gitRunner === 'function') {
|
|
85
90
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
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",
|
|
@@ -93,5 +93,6 @@
|
|
|
93
93
|
"@deepseek-ai/dsh-tools": {
|
|
94
94
|
"optional": true
|
|
95
95
|
}
|
|
96
|
-
}
|
|
96
|
+
},
|
|
97
|
+
"packageManager": "npm@10.9.8"
|
|
97
98
|
}
|