@goodandready/dsh-kanban 0.2.31 → 0.2.33
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 +15 -0
- package/lib/client.js +378 -12
- package/lib/config.js +10 -0
- package/lib/cost-telemetry.js +146 -0
- package/lib/index.js +68 -3
- package/lib/routes.js +104 -4
- package/lib/store.js +25 -0
- package/lib/verification.js +242 -0
- package/lib/worktree.js +162 -1
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token and cost telemetry integration with @goodandready/dsh-cost-meter (#374).
|
|
3
|
+
* All functions fail-soft without throwing when cost-meter is absent.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check whether @goodandready/dsh-cost-meter plugin is active in current runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} ctx - Cordis context
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isCostMeterAvailable(ctx) {
|
|
13
|
+
if (!ctx || typeof ctx !== 'object') return false
|
|
14
|
+
try {
|
|
15
|
+
if (ctx.costMeter || (typeof ctx.get === 'function' && ctx.get('costMeter'))) return true
|
|
16
|
+
if (ctx.settings && typeof ctx.settings.describe === 'function') {
|
|
17
|
+
const rows = ctx.settings.describe({ redactSecrets: true })
|
|
18
|
+
if (Array.isArray(rows) && rows.some((r) => r?.ns === 'dsh-cost-meter')) return true
|
|
19
|
+
}
|
|
20
|
+
if (ctx.registry && typeof ctx.registry[Symbol.iterator] === 'function') {
|
|
21
|
+
for (const [plugin] of ctx.registry) {
|
|
22
|
+
if (plugin === '@goodandready/dsh-cost-meter' || plugin?.name === '@goodandready/dsh-cost-meter') {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract aggregated token usage from a session's costByModel projection view.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} projectionView - View object from usageByModelProjection
|
|
37
|
+
* @returns {{
|
|
38
|
+
* totalTokens: number,
|
|
39
|
+
* uncachedInputTokens: number,
|
|
40
|
+
* outputTokens: number,
|
|
41
|
+
* cacheReadTokens: number,
|
|
42
|
+
* cacheWriteTokens: number,
|
|
43
|
+
* turnsCount: number,
|
|
44
|
+
* models: Array<string>
|
|
45
|
+
* }}
|
|
46
|
+
*/
|
|
47
|
+
export function extractSessionUsage(projectionView) {
|
|
48
|
+
const result = {
|
|
49
|
+
totalTokens: 0,
|
|
50
|
+
uncachedInputTokens: 0,
|
|
51
|
+
outputTokens: 0,
|
|
52
|
+
cacheReadTokens: 0,
|
|
53
|
+
cacheWriteTokens: 0,
|
|
54
|
+
turnsCount: 0,
|
|
55
|
+
models: [],
|
|
56
|
+
}
|
|
57
|
+
if (!projectionView || typeof projectionView !== 'object') return result
|
|
58
|
+
|
|
59
|
+
const modelsList = Array.isArray(projectionView.models) ? projectionView.models : []
|
|
60
|
+
for (const m of modelsList) {
|
|
61
|
+
const key = (m.provider ? m.provider + '/' : '') + (m.model || '')
|
|
62
|
+
if (key && !result.models.includes(key)) result.models.push(key)
|
|
63
|
+
const slots = m.slots && typeof m.slots === 'object' ? m.slots : {}
|
|
64
|
+
for (const slotKey of Object.keys(slots)) {
|
|
65
|
+
const b = slots[slotKey] || {}
|
|
66
|
+
result.uncachedInputTokens += Number(b.uncachedInputTokens) || 0
|
|
67
|
+
result.outputTokens += Number(b.outputTokens) || 0
|
|
68
|
+
result.cacheReadTokens += Number(b.cacheReadTokens) || 0
|
|
69
|
+
result.cacheWriteTokens += Number(b.cacheWriteTokens) || 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(projectionView.turns)) {
|
|
74
|
+
result.turnsCount = projectionView.turns.length
|
|
75
|
+
if (result.totalTokens === 0 && projectionView.turns.length > 0) {
|
|
76
|
+
for (const t of projectionView.turns) {
|
|
77
|
+
result.uncachedInputTokens += Number(t.uncachedInputTokens) || 0
|
|
78
|
+
result.outputTokens += Number(t.outputTokens) || 0
|
|
79
|
+
result.cacheReadTokens += Number(t.cacheReadTokens) || 0
|
|
80
|
+
result.cacheWriteTokens += Number(t.cacheWriteTokens) || 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result.totalTokens = result.uncachedInputTokens + result.outputTokens + result.cacheReadTokens + result.cacheWriteTokens
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Calculate cost for token usage given rate schedule or fallback tariff.
|
|
91
|
+
* Default tariff: standard DeepSeek rates ($0.14/M input, $0.28/M output, $0.014/M cache hit).
|
|
92
|
+
*
|
|
93
|
+
* @param {{
|
|
94
|
+
* usage: object,
|
|
95
|
+
* schedule?: object,
|
|
96
|
+
* usdRate?: number,
|
|
97
|
+
* currency?: string
|
|
98
|
+
* }} options
|
|
99
|
+
* @returns {{
|
|
100
|
+
* costUsd: number,
|
|
101
|
+
* costFormatted: string,
|
|
102
|
+
* currency: string
|
|
103
|
+
* }}
|
|
104
|
+
*/
|
|
105
|
+
export function calculateTokenCost({ usage, schedule, usdRate = 1, currency = '$' }) {
|
|
106
|
+
if (!usage || typeof usage !== 'object') {
|
|
107
|
+
return { costUsd: 0, costFormatted: currency + '0.00', currency }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const uncachedInput = Number(usage.uncachedInputTokens) || 0
|
|
111
|
+
const output = Number(usage.outputTokens) || 0
|
|
112
|
+
const cacheRead = Number(usage.cacheReadTokens) || 0
|
|
113
|
+
const cacheWrite = Number(usage.cacheWriteTokens) || 0
|
|
114
|
+
|
|
115
|
+
const rateInput = Number(schedule?.inputPerMillion ?? schedule?.input) || 0.14
|
|
116
|
+
const rateOutput = Number(schedule?.outputPerMillion ?? schedule?.output) || 0.28
|
|
117
|
+
const rateCacheRead = Number(schedule?.cacheReadPerMillion ?? schedule?.cacheHit) || 0.014
|
|
118
|
+
const rateCacheWrite = Number(schedule?.cacheWritePerMillion) || rateInput
|
|
119
|
+
|
|
120
|
+
const costUsd = (uncachedInput * rateInput + output * rateOutput + cacheRead * rateCacheRead + cacheWrite * rateCacheWrite) / 1_000_000
|
|
121
|
+
const converted = costUsd * (Number(usdRate) > 0 ? Number(usdRate) : 1)
|
|
122
|
+
|
|
123
|
+
const costFormatted = currency + (converted < 0.01 && converted > 0 ? '<0.01' : converted.toFixed(2))
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
costUsd: Math.round(costUsd * 10000) / 10000,
|
|
127
|
+
costFormatted,
|
|
128
|
+
currency,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check whether task cost exceeds configured budget limit.
|
|
134
|
+
*
|
|
135
|
+
* @param {{ costUsd: number, limit: number }} options
|
|
136
|
+
* @returns {{ exceeded: boolean, limit: number, costUsd: number }}
|
|
137
|
+
*/
|
|
138
|
+
export function checkCostLimit({ costUsd = 0, limit = 0 }) {
|
|
139
|
+
const numLimit = Number(limit) || 0
|
|
140
|
+
if (numLimit <= 0) return { exceeded: false, limit: 0, costUsd }
|
|
141
|
+
return {
|
|
142
|
+
exceeded: costUsd >= numLimit,
|
|
143
|
+
limit: numLimit,
|
|
144
|
+
costUsd,
|
|
145
|
+
}
|
|
146
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { verifyAndRecordGate } from './verification.js'
|
|
1
2
|
import { registerPluginUpdater } from './updater.js'
|
|
2
3
|
// dsh-kanban — task board for DeepSeek Harness.
|
|
3
4
|
//
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
exportTasksCsv,
|
|
35
36
|
handleCleanupWorktree,
|
|
36
37
|
handleTaskDiff,
|
|
38
|
+
handleTaskDiscard,
|
|
37
39
|
handleTaskCommits,
|
|
38
40
|
handleTaskMerge,
|
|
39
41
|
handleTaskWorktreeStatus,
|
|
@@ -49,7 +51,9 @@ import {
|
|
|
49
51
|
handleCreateSubtask,
|
|
50
52
|
handleToggleChecklistItem,
|
|
51
53
|
handleCreateTaskPr,
|
|
54
|
+
handleTaskNudge,
|
|
52
55
|
} from './routes.js'
|
|
56
|
+
import { isCostMeterAvailable } from './cost-telemetry.js'
|
|
53
57
|
import { listImportable, importIssue, refreshTask, searchRepos, createProjectTask } from './import.js'
|
|
54
58
|
import {
|
|
55
59
|
runTask, resolveModel, buildStartMessage,
|
|
@@ -865,7 +869,20 @@ export function apply(ctx, config) {
|
|
|
865
869
|
/**
|
|
866
870
|
* Return active agent state or undefined if session is inactive.
|
|
867
871
|
*/
|
|
868
|
-
const liveOf = (sessionId) =>
|
|
872
|
+
const liveOf = (sessionId) => {
|
|
873
|
+
try {
|
|
874
|
+
const a = ctx.agents?.get?.(sessionId);
|
|
875
|
+
if (!a) return undefined;
|
|
876
|
+
return {
|
|
877
|
+
status: a.status || (typeof a.isIdle === 'function' && a.isIdle() ? 'idle' : 'running'),
|
|
878
|
+
state: a.state || a.lifecycleState,
|
|
879
|
+
step: a.step || a.currentStep || a.stateDetail || a.activity || '',
|
|
880
|
+
activity: a.activity || a.statusDetail || '',
|
|
881
|
+
};
|
|
882
|
+
} catch {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
869
886
|
|
|
870
887
|
ctx.effect(() => ctx.webServer.register({
|
|
871
888
|
kind: 'exact',
|
|
@@ -883,6 +900,7 @@ export function apply(ctx, config) {
|
|
|
883
900
|
sync: syncState.snapshot(),
|
|
884
901
|
// Resolved project root passed with board payload for session launch modal.
|
|
885
902
|
projectRoot: rootOf(getConfig()),
|
|
903
|
+
costMeterAvailable: isCostMeterAvailable(ctx),
|
|
886
904
|
}))
|
|
887
905
|
},
|
|
888
906
|
}), 'dsh-kanban: board route')
|
|
@@ -1151,8 +1169,31 @@ export function apply(ctx, config) {
|
|
|
1151
1169
|
try {
|
|
1152
1170
|
const input = await readBody(req)
|
|
1153
1171
|
if (req.method === 'POST' && action === 'move') {
|
|
1154
|
-
// Manual move dispatches stop command to agent prior to updating card column.
|
|
1155
1172
|
const was = store.getTask(id)
|
|
1173
|
+
if (was === undefined) { writeJson(res, 404, { error: 'task-not-found' }); return }
|
|
1174
|
+
|
|
1175
|
+
// Automated Verification Gate (#372): intercept transition to review
|
|
1176
|
+
if (input.column === 'review' && was.column !== 'review') {
|
|
1177
|
+
const gateResult = await verifyAndRecordGate({
|
|
1178
|
+
task: was,
|
|
1179
|
+
config: getConfig(),
|
|
1180
|
+
store,
|
|
1181
|
+
agents: ctx.agents,
|
|
1182
|
+
createMessage: createUserMessage,
|
|
1183
|
+
})
|
|
1184
|
+
if (!gateResult.allowed) {
|
|
1185
|
+
writeJson(res, 422, {
|
|
1186
|
+
error: 'verification-failed',
|
|
1187
|
+
command: gateResult.verification.command,
|
|
1188
|
+
exitCode: gateResult.verification.exitCode,
|
|
1189
|
+
log: gateResult.verification.logSnippet,
|
|
1190
|
+
task: store.getTask(id),
|
|
1191
|
+
})
|
|
1192
|
+
return
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Manual move dispatches stop command to agent prior to updating card column.
|
|
1156
1197
|
const acted = was !== undefined && input.column !== was.column
|
|
1157
1198
|
? dispatchMove({
|
|
1158
1199
|
agents: ctx.agents, task: was, column: input.column,
|
|
@@ -1193,8 +1234,32 @@ export function apply(ctx, config) {
|
|
|
1193
1234
|
writeJson(res, resObj.status, resObj.body)
|
|
1194
1235
|
return
|
|
1195
1236
|
}
|
|
1237
|
+
if (req.method === 'POST' && action === 'discard') {
|
|
1238
|
+
const resObj = await handleTaskDiscard({
|
|
1239
|
+
task: store.getTask(id),
|
|
1240
|
+
file: input?.file,
|
|
1241
|
+
discardTaskFile,
|
|
1242
|
+
})
|
|
1243
|
+
if (resObj?.body?.success) {
|
|
1244
|
+
eventHub.broadcast('task:change', { taskId: id, action: 'discard', file: input?.file })
|
|
1245
|
+
}
|
|
1246
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1247
|
+
return
|
|
1248
|
+
}
|
|
1249
|
+
if (req.method === 'POST' && (action === 'nudge' || (input && input.action === 'nudge'))) {
|
|
1250
|
+
const resObj = await handleTaskNudge({
|
|
1251
|
+
store,
|
|
1252
|
+
id,
|
|
1253
|
+
prompt: input?.prompt || input?.text,
|
|
1254
|
+
agents: ctx.agents,
|
|
1255
|
+
createMessage: createUserMessage,
|
|
1256
|
+
eventHub,
|
|
1257
|
+
})
|
|
1258
|
+
writeJson(res, resObj.status, resObj.body)
|
|
1259
|
+
return
|
|
1260
|
+
}
|
|
1196
1261
|
if (req.method === 'GET' && action === 'diff') {
|
|
1197
|
-
const resObj = await handleTaskDiff({ task: store.getTask(id), getTaskDiff })
|
|
1262
|
+
const resObj = await handleTaskDiff({ task: store.getTask(id), getTaskDiff, getTaskDiffInspector })
|
|
1198
1263
|
writeJson(res, resObj.status, resObj.body)
|
|
1199
1264
|
return
|
|
1200
1265
|
}
|
package/lib/routes.js
CHANGED
|
@@ -308,10 +308,23 @@ function withPlan(task, liveOf, config, now, sameSession) {
|
|
|
308
308
|
try {
|
|
309
309
|
if (typeof liveOf === 'function' && task.sessionId) live = liveOf(task.sessionId)
|
|
310
310
|
} catch { live = undefined }
|
|
311
|
+
const liveStatus = typeof live === 'object' && live !== null ? live.status : live
|
|
312
|
+
const liveStep = typeof live === 'object' && live !== null
|
|
313
|
+
? (live.step || live.activity || live.stateDetail || '')
|
|
314
|
+
: ''
|
|
315
|
+
|
|
316
|
+
const costMetrics = task.costMetrics || null
|
|
317
|
+
const costLimitExceeded = (config?.costMeterEnabled !== false && Number(config?.taskCostLimit) > 0 && costMetrics?.costUsd)
|
|
318
|
+
? costMetrics.costUsd >= Number(config.taskCostLimit)
|
|
319
|
+
: Boolean(task.costLimitExceeded)
|
|
320
|
+
|
|
311
321
|
return {
|
|
312
322
|
...task,
|
|
313
323
|
plan: { items: planItems(plan), progress: planProgress(plan) },
|
|
314
|
-
state: taskState({ task, live }),
|
|
324
|
+
state: taskState({ task, live: liveStatus }),
|
|
325
|
+
liveStep: liveStep || task.liveStep || '',
|
|
326
|
+
costMetrics,
|
|
327
|
+
costLimitExceeded,
|
|
315
328
|
// Queue position calculated dynamically across active tasks in same session.
|
|
316
329
|
queuePos: queuePosition(task, sameSession),
|
|
317
330
|
// Label facets extracted on server for consistent client filtering.
|
|
@@ -344,7 +357,7 @@ function orderForBoard(tasks, columns) {
|
|
|
344
357
|
/**
|
|
345
358
|
* Build board response payload: columns in canonical order, tasks, and limits.
|
|
346
359
|
*/
|
|
347
|
-
export function buildBoard({ store, config, board = 'main', repo, liveOf, sync, projectRoot }) {
|
|
360
|
+
export function buildBoard({ store, config, board = 'main', repo, liveOf, sync, projectRoot, costMeterAvailable = false }) {
|
|
348
361
|
// Uniform timestamp across snapshot calculation.
|
|
349
362
|
const now = Date.now()
|
|
350
363
|
// Tasks are sorted and grouped on server: repo grouping with active items first.
|
|
@@ -384,6 +397,7 @@ export function buildBoard({ store, config, board = 'main', repo, liveOf, sync,
|
|
|
384
397
|
facets: facetsOf(tasks),
|
|
385
398
|
// Sync telemetry state attached directly to board snapshot.
|
|
386
399
|
sync: sync ?? { state: 'never' },
|
|
400
|
+
costMeterAvailable: Boolean(costMeterAvailable),
|
|
387
401
|
}
|
|
388
402
|
}
|
|
389
403
|
|
|
@@ -773,12 +787,27 @@ export async function handleCleanupWorktree({
|
|
|
773
787
|
/**
|
|
774
788
|
* Get working copy diff for task.
|
|
775
789
|
*/
|
|
776
|
-
export async function handleTaskDiff({ task, getTaskDiff }) {
|
|
790
|
+
export async function handleTaskDiff({ task, getTaskDiff, getTaskDiffInspector }) {
|
|
777
791
|
if (!task) return { status: 404, body: { error: 'task-not-found' } }
|
|
792
|
+
if (typeof getTaskDiffInspector === 'function') {
|
|
793
|
+
const inspectorResult = await getTaskDiffInspector({ worktreePath: task.worktree })
|
|
794
|
+
return { status: 200, body: inspectorResult }
|
|
795
|
+
}
|
|
778
796
|
const diffResult = await getTaskDiff({ worktreePath: task.worktree })
|
|
779
797
|
return { status: 200, body: diffResult }
|
|
780
798
|
}
|
|
781
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Discard file changes in task worktree (#371).
|
|
802
|
+
*/
|
|
803
|
+
export async function handleTaskDiscard({ task, file, discardTaskFile }) {
|
|
804
|
+
if (!task) return { status: 404, body: { error: 'task-not-found' } }
|
|
805
|
+
if (!file || typeof file !== 'string') return { status: 400, body: { error: 'file-required' } }
|
|
806
|
+
if (!task.worktree) return { status: 400, body: { error: 'no-worktree' } }
|
|
807
|
+
const result = await discardTaskFile({ worktreePath: task.worktree, file })
|
|
808
|
+
return { status: result.success ? 200 : 400, body: result }
|
|
809
|
+
}
|
|
810
|
+
|
|
782
811
|
/**
|
|
783
812
|
* Get commit list for task.
|
|
784
813
|
*/
|
|
@@ -1104,4 +1133,75 @@ export async function handleCreateTaskPr({ store, gitea, task, input }) {
|
|
|
1104
1133
|
} catch (err) {
|
|
1105
1134
|
return { status: 502, body: { error: 'gitea-pr-failed', detail: err?.message } }
|
|
1106
1135
|
}
|
|
1107
|
-
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Handle interactive live steering (nudge) for an active task agent (#373).
|
|
1139
|
+
*
|
|
1140
|
+
* @param {{
|
|
1141
|
+
* store: object,
|
|
1142
|
+
* id: string,
|
|
1143
|
+
* prompt: string,
|
|
1144
|
+
* agents?: object,
|
|
1145
|
+
* createMessage?: Function,
|
|
1146
|
+
* eventHub?: object,
|
|
1147
|
+
* now?: number
|
|
1148
|
+
* }} options
|
|
1149
|
+
* @returns {Promise<{ status: number, body: object }>}
|
|
1150
|
+
*/
|
|
1151
|
+
export async function handleTaskNudge({ store, id, prompt, agents, createMessage, eventHub, now = Date.now() }) {
|
|
1152
|
+
if (!store || !id) return { status: 400, body: { error: 'invalid-parameters' } }
|
|
1153
|
+
const task = store.getTask(id)
|
|
1154
|
+
if (!task) return { status: 404, body: { error: 'task-not-found' } }
|
|
1155
|
+
|
|
1156
|
+
const text = typeof prompt === 'string' ? prompt.trim() : ''
|
|
1157
|
+
if (!text) return { status: 400, body: { error: 'prompt-required' } }
|
|
1158
|
+
|
|
1159
|
+
const sessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
1160
|
+
if (!sessionId) return { status: 409, body: { error: 'no-session' } }
|
|
1161
|
+
|
|
1162
|
+
let agent
|
|
1163
|
+
try { agent = agents?.get?.(sessionId) } catch { agent = undefined }
|
|
1164
|
+
if (!agent || typeof agent.followup !== 'function') {
|
|
1165
|
+
return { status: 409, body: { error: 'session-not-live' } }
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const msg = typeof createMessage === 'function'
|
|
1169
|
+
? createMessage({
|
|
1170
|
+
content: [{ type: 'text', text }],
|
|
1171
|
+
source: { kind: 'dsh-kanban', plugin: 'dsh-kanban', form: 'task-nudge' },
|
|
1172
|
+
})
|
|
1173
|
+
: { content: [{ type: 'text', text }] }
|
|
1174
|
+
|
|
1175
|
+
agent.followup(msg)
|
|
1176
|
+
|
|
1177
|
+
// Append user steering comment to task
|
|
1178
|
+
if (typeof store.addComment === 'function') {
|
|
1179
|
+
store.addComment(id, {
|
|
1180
|
+
author: 'user',
|
|
1181
|
+
role: 'user',
|
|
1182
|
+
text: `[Steering Nudge] ${text}`,
|
|
1183
|
+
})
|
|
1184
|
+
} else if (typeof store.addTaskComment === 'function') {
|
|
1185
|
+
store.addTaskComment(id, {
|
|
1186
|
+
author: 'user',
|
|
1187
|
+
role: 'user',
|
|
1188
|
+
text: `[Steering Nudge] ${text}`,
|
|
1189
|
+
})
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Broadcast change via eventHub
|
|
1193
|
+
if (eventHub && typeof eventHub.broadcast === 'function') {
|
|
1194
|
+
eventHub.broadcast('task:change', { id, col: task.col || task.column })
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
status: 200,
|
|
1199
|
+
body: {
|
|
1200
|
+
success: true,
|
|
1201
|
+
id,
|
|
1202
|
+
sessionId,
|
|
1203
|
+
prompt: text,
|
|
1204
|
+
nudgedAt: now,
|
|
1205
|
+
},
|
|
1206
|
+
}
|
|
1207
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -108,6 +108,8 @@ const WRITABLE = [
|
|
|
108
108
|
'cron', 'nextRunAt', 'lastRunAt', 'progressDump', 'permission', 'reasoningEffort', 'permissionConfirmedAt',
|
|
109
109
|
'runs', 'checklist', 'report', 'comments', 'rejectReason', 'rejectedAt', 'parentId', 'isEpic',
|
|
110
110
|
'workflowRunId', 'workflowStatus', 'permissionConfirmedTier', 'forge', 'queuedRequest',
|
|
111
|
+
'verificationCommand', 'verificationStatus',
|
|
112
|
+
'costMetrics', 'costLimitExceeded',
|
|
111
113
|
]
|
|
112
114
|
|
|
113
115
|
/** Valid transition sources; unexpected values are rejected to preserve log integrity. */
|
|
@@ -161,6 +163,16 @@ function rowToTask(row) {
|
|
|
161
163
|
reasoningEffort: rest.reasoningEffort ?? '',
|
|
162
164
|
isEpic: isEpic === 1,
|
|
163
165
|
parentId: parentId ?? '',
|
|
166
|
+
costMetrics: (() => {
|
|
167
|
+
if (typeof rest.costMetrics === 'string' && rest.costMetrics.trim()) {
|
|
168
|
+
try {
|
|
169
|
+
const val = JSON.parse(rest.costMetrics);
|
|
170
|
+
if (val && typeof val === 'object') return val;
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
return rest.costMetrics && typeof rest.costMetrics === 'object' ? rest.costMetrics : null;
|
|
174
|
+
})(),
|
|
175
|
+
costLimitExceeded: rest.costLimitExceeded === 1 || rest.costLimitExceeded === true,
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
|
|
@@ -249,6 +261,10 @@ export function openStore({ dir }) {
|
|
|
249
261
|
['forge', "TEXT NOT NULL DEFAULT ''"],
|
|
250
262
|
['permissionConfirmedTier', "TEXT NOT NULL DEFAULT ''"],
|
|
251
263
|
['queuedRequest', "TEXT NOT NULL DEFAULT ''"],
|
|
264
|
+
['verificationCommand', "TEXT NOT NULL DEFAULT ''"],
|
|
265
|
+
['verificationStatus', "TEXT NOT NULL DEFAULT ''"],
|
|
266
|
+
['costMetrics', "TEXT NOT NULL DEFAULT ''"],
|
|
267
|
+
['costLimitExceeded', "INTEGER NOT NULL DEFAULT 0"],
|
|
252
268
|
]) {
|
|
253
269
|
try { db.exec(`ALTER TABLE tasks ADD COLUMN ${column} ${definition}`) } catch { /* column already exists */ }
|
|
254
270
|
}
|
|
@@ -650,6 +666,15 @@ export function openStore({ dir }) {
|
|
|
650
666
|
})
|
|
651
667
|
},
|
|
652
668
|
|
|
669
|
+
appendComment(id, comment) {
|
|
670
|
+
if (!comment || typeof comment !== 'object') return undefined
|
|
671
|
+
return this.addComment(id, {
|
|
672
|
+
author: comment.author || 'user',
|
|
673
|
+
role: comment.role || 'user',
|
|
674
|
+
text: comment.text || comment.body || '',
|
|
675
|
+
})
|
|
676
|
+
},
|
|
677
|
+
|
|
653
678
|
updateChecklist(id, items) {
|
|
654
679
|
const task = rowToTask(q.taskById.get(id))
|
|
655
680
|
if (!task) return undefined
|