@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/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. */
@@ -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 commits) {
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 type = inferPrType(task, commits)
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 (diffStats && (diffStats.files?.length > 0 || diffStats.filesChanged > 0)) {
200
+ if (stats && (stats.files?.length > 0 || stats.filesChanged > 0)) {
197
201
  lines.push('### Changed Components & Files')
198
- lines.push(`- Total files changed: ${diffStats.filesChanged || diffStats.files.length}`)
202
+ lines.push(`- Total files changed: ${stats.filesChanged || stats.files.length}`)
199
203
  if (diffStats.additions || diffStats.deletions) {
200
- lines.push(`- Changes: +${diffStats.additions} / -${diffStats.deletions}`)
204
+ lines.push(`- Changes: +${stats.additions} / -${stats.deletions}`)
201
205
  }
202
- if (Array.isArray(diffStats.files) && diffStats.files.length > 0) {
203
- for (const f of diffStats.files.slice(0, 12)) {
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 (diffStats.files.length > 12) {
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 commits) {
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(commits) && commits.length > 0) {
262
- for (const c of commits.slice(0, 5)) {
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 {
@@ -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 = `test -d ${quoteArg(root)} && echo REMOTE_ROOT_OK || echo REMOTE_ROOT_MISSING`
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 trimmed = (stdout || '').trim()
127
- const remoteRootFound = trimmed.includes('REMOTE_ROOT_OK')
129
+ const remoteRootFound = await remotePathExists(root, config)
128
130
 
129
131
  return {
130
132
  ok: true,
@@ -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
- const costMetrics = task.costMetrics || null
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 >= Number(config.taskCostLimit)
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 generatedTitle = generatePrTitle(task, commits)
488
- const generatedDescription = generatePrDescription({ task, commits, diffStats })
489
- const releaseNotes = generateReleaseNotes({ task, commits })
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
- function rowToTask(row) {
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.39",
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
  }