dsh-taskboard 0.6.5 → 0.6.7
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/README.md +301 -282
- package/lib/client.js +937 -920
- package/lib/host/archive-sessions.js +30 -0
- package/lib/host/archive-sessions.js.map +1 -0
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/locale.js +17 -0
- package/lib/host/locale.js.map +1 -0
- package/lib/host/routes.js +45 -6
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +2 -0
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/session-sync.js +4 -1
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/templates.js +11 -75
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +19 -6
- package/lib/host/tools.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/builtin-templates.js +155 -0
- package/lib/shared/builtin-templates.js.map +1 -0
- package/lib/shared/protocol.js +39 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +90 -90
- package/src/client/api.ts +5 -1
- package/src/client/board/TaskBoard.tsx +16 -11
- package/src/client/board/TaskDetail.tsx +116 -10
- package/src/client/board/TemplateManager.tsx +22 -10
- package/src/client/board-mount.tsx +4 -0
- package/src/client/controller.ts +23 -3
- package/src/client/i18n/en.ts +18 -0
- package/src/client/i18n/templates.ts +25 -0
- package/src/client/i18n/zh.ts +18 -0
- package/src/client/styles.ts +14 -3
- package/src/client/window-inset.ts +38 -0
- package/src/host/archive-sessions.ts +18 -0
- package/src/host/execution.ts +4 -1
- package/src/host/locale.ts +44 -0
- package/src/host/routes.ts +40 -7
- package/src/host/scheduler.ts +2 -0
- package/src/host/session-sync.ts +4 -1
- package/src/host/templates.ts +8 -59
- package/src/host/tools.ts +26 -9
- package/src/shared/api.ts +6 -3
- package/src/shared/builtin-templates.ts +153 -0
- package/src/shared/protocol.ts +64 -0
- package/src/shared/version.ts +9 -9
package/src/host/routes.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { archiveTaskSessions } from './archive-sessions.ts'
|
|
1
2
|
/**
|
|
2
3
|
* /dsh-taskboard routes on the shared DSH webserver: a JSON API for the
|
|
3
4
|
* GUI's human operations (create/update/move/comment/delete — actor `user`,
|
|
@@ -40,10 +41,12 @@ import {
|
|
|
40
41
|
type TaskLedger,
|
|
41
42
|
type TaskModel,
|
|
42
43
|
type TaskRecord,
|
|
44
|
+
type SystemCommentRow,
|
|
43
45
|
} from '../shared/protocol.ts'
|
|
44
46
|
import { WORKTREE_DIR, worktreePathOf, type GitFace } from './git.ts'
|
|
45
47
|
import { removeMirror, repoMainPath } from './isolation.ts'
|
|
46
48
|
import { createRepoScanner, type RepoScanner } from './repos.ts'
|
|
49
|
+
import { activeHostLocale } from './locale.ts'
|
|
47
50
|
import type { CatalogModelItem, CatalogPresetItem, MergeRepoResult, TaskTemplate } from '../shared/api.ts'
|
|
48
51
|
import type { TemplateStore } from './templates.ts'
|
|
49
52
|
import { ROUTE_PREFIX, SSE_PATH, type ApiFail, type ApiResult } from '../shared/api.ts'
|
|
@@ -316,10 +319,15 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
316
319
|
} catch { /* fail-soft → false */ }
|
|
317
320
|
// gitignore 建议 (plan §3.2): suggest (never write) ignoring our
|
|
318
321
|
// worktree directory, once per workspace per host run. Root repos only.
|
|
322
|
+
// The line is localized from the DSH locale preference (see host/locale.ts).
|
|
319
323
|
if (rootRepo && !gitHinted.has(path)) {
|
|
320
324
|
gitHinted.add(path)
|
|
321
325
|
if (await gitignoreMissing(path)) {
|
|
322
|
-
|
|
326
|
+
const file = `${path}/.gitignore`
|
|
327
|
+
const hint = activeHostLocale(ctx) === 'zh'
|
|
328
|
+
? `建议在 ${file} 加入一行 ${WORKTREE_DIR}/ 以隐藏任务 worktree 目录(不会自动修改)`
|
|
329
|
+
: `suggests adding one line to ${file}: ${WORKTREE_DIR}/ to hide the task worktree directory (no automatic edits)`
|
|
330
|
+
console.info(`[dsh-taskboard] ${hint}`)
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
333
|
// The nested scan always runs: repoCount needs it even when the root
|
|
@@ -372,7 +380,7 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
372
380
|
if (req.method === 'GET') {
|
|
373
381
|
if (pathname === `${ROUTE_PREFIX}/state`) {
|
|
374
382
|
await store.load()
|
|
375
|
-
json(res, { ok: true, value: store.snapshot() })
|
|
383
|
+
json(res, { ok: true, value: { ...store.snapshot(), capabilities: { archiveSessions: typeof workspaces.archiveSession === 'function' } } })
|
|
376
384
|
return
|
|
377
385
|
}
|
|
378
386
|
if (pathname === `${ROUTE_PREFIX}/workspaces`) {
|
|
@@ -615,6 +623,12 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
615
623
|
try {
|
|
616
624
|
const task = store.get(id)
|
|
617
625
|
if (task === undefined) throw new Error('Error: not_found: no such task')
|
|
626
|
+
if (action === 'archive-sessions') {
|
|
627
|
+
if (task.trashedAt !== undefined || task.status !== 'archived') throw new Error('Error: invalid_transition: only archived live tasks can retry session archiving')
|
|
628
|
+
const result = await archiveTaskSessions(task, workspaces.archiveSession)
|
|
629
|
+
json(res, { ok: true, value: result })
|
|
630
|
+
return
|
|
631
|
+
}
|
|
618
632
|
if (action === 'update') {
|
|
619
633
|
const ifVersion = num(body, 'ifVersion')
|
|
620
634
|
if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')
|
|
@@ -678,13 +692,16 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
678
692
|
if (action === 'move') {
|
|
679
693
|
const ifVersion = num(body, 'ifVersion')
|
|
680
694
|
const status = str(body, 'status') ?? ''
|
|
695
|
+
const archiveSessions = body.archiveSessions === true
|
|
681
696
|
if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')
|
|
682
697
|
const to = asStatus(status)
|
|
683
698
|
let next: TaskRecord | undefined
|
|
699
|
+
let beforeTask: TaskRecord | undefined
|
|
684
700
|
await store.mutate('task-moved', ledger => {
|
|
685
701
|
const { index, task } = liveTaskAt(ledger, id)
|
|
686
702
|
if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)
|
|
687
703
|
if (!canTransition(task.status, to)) throw new Error(`Error: invalid_transition: illegal transition ${task.status} → ${to}`)
|
|
704
|
+
beforeTask = task
|
|
688
705
|
next = structuredClone(task)
|
|
689
706
|
next.status = to
|
|
690
707
|
next.version = task.version + 1
|
|
@@ -696,7 +713,10 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
696
713
|
ledger.tasks[index] = next
|
|
697
714
|
return [next]
|
|
698
715
|
})
|
|
699
|
-
|
|
716
|
+
const sessionArchive = to === 'archived' && archiveSessions
|
|
717
|
+
? await archiveTaskSessions(beforeTask ?? next!, workspaces.archiveSession)
|
|
718
|
+
: undefined
|
|
719
|
+
json(res, { ok: true, value: { ...summarize(next!), ...(sessionArchive !== undefined ? { sessionArchive } : {}) } })
|
|
700
720
|
return
|
|
701
721
|
}
|
|
702
722
|
if (action === 'reject') {
|
|
@@ -909,11 +929,21 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
909
929
|
}
|
|
910
930
|
// R1: the git merges above are slow — re-find the FRESH task inside
|
|
911
931
|
// the mutation so a concurrent comment is never overwritten.
|
|
912
|
-
const pushComment = (body: string): Promise<void> =>
|
|
932
|
+
const pushComment = (body: string, system?: { key: string; params?: Record<string, string>; rows?: SystemCommentRow[] }): Promise<void> =>
|
|
913
933
|
store.mutate('comment-added', ledger => {
|
|
914
934
|
const { index, task: fresh } = liveTaskAt(ledger, id)
|
|
915
935
|
const next = structuredClone(fresh)
|
|
916
|
-
next.comments.push({
|
|
936
|
+
next.comments.push({
|
|
937
|
+
id: newCommentId(),
|
|
938
|
+
body: normalizeBody(body),
|
|
939
|
+
...(system !== undefined ? {
|
|
940
|
+
systemKey: system.key,
|
|
941
|
+
...(system.params !== undefined ? { systemParams: system.params } : {}),
|
|
942
|
+
...(system.rows !== undefined ? { systemRows: system.rows } : {}),
|
|
943
|
+
} : {}),
|
|
944
|
+
version: 1,
|
|
945
|
+
createdAt: options.now(),
|
|
946
|
+
})
|
|
917
947
|
next.version = fresh.version + 1
|
|
918
948
|
next.updatedAt = options.now()
|
|
919
949
|
ledger.tasks[index] = next
|
|
@@ -930,7 +960,7 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
930
960
|
if (root.outcome === 'failed') {
|
|
931
961
|
throw new Error(`Error: invalid_input: ${root.error ?? '合并失败'}`)
|
|
932
962
|
}
|
|
933
|
-
await pushComment(`[系统] 分支 ${root.branch} 已合并到主工作区(--no-ff
|
|
963
|
+
await pushComment(`[系统] 分支 ${root.branch} 已合并到主工作区(--no-ff)。`, { key: 'sys.mergeSingle', params: { branch: root.branch } })
|
|
934
964
|
json(res, { ok: true, value: { merged: true, branch: root.branch } })
|
|
935
965
|
return
|
|
936
966
|
}
|
|
@@ -942,7 +972,10 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
942
972
|
? `${labelOf(r.repo)} ✓ 已合并`
|
|
943
973
|
: r.outcome === 'noop' ? `${labelOf(r.repo)} ⟲ 无新提交` : `${labelOf(r.repo)} ✗ ${(r.error ?? '合并失败').slice(0, 150)}`)
|
|
944
974
|
.join(';')
|
|
945
|
-
await pushComment(`[系统] 分支已按仓库合并(--no-ff):${summary}
|
|
975
|
+
await pushComment(`[系统] 分支已按仓库合并(--no-ff):${summary}`, {
|
|
976
|
+
key: 'sys.mergeMulti',
|
|
977
|
+
rows: results.map(r => ({ repo: r.repo, outcome: r.outcome, ...(r.error !== undefined ? { error: r.error.slice(0, 150) } : {}) })),
|
|
978
|
+
})
|
|
946
979
|
json(res, {
|
|
947
980
|
ok: true,
|
|
948
981
|
value: {
|
package/src/host/scheduler.ts
CHANGED
|
@@ -136,6 +136,8 @@ export class SchedulerService {
|
|
|
136
136
|
task.comments.push({
|
|
137
137
|
id: newCommentId(),
|
|
138
138
|
body: normalizeBody(`[系统] 定时表达式 ${deadCron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。`),
|
|
139
|
+
systemKey: 'sys.cronDead',
|
|
140
|
+
systemParams: { cron: deadCron },
|
|
139
141
|
version: 1,
|
|
140
142
|
createdAt: now,
|
|
141
143
|
})
|
package/src/host/session-sync.ts
CHANGED
|
@@ -213,7 +213,7 @@ export class ExternalSessionSyncService {
|
|
|
213
213
|
|
|
214
214
|
constructor(private readonly deps: SessionSyncDeps) {
|
|
215
215
|
this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {
|
|
216
|
-
|
|
216
|
+
return this.handleSessionEvent(sessionId, event, sessionMeta)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
219
|
const interval = deps.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS
|
|
@@ -628,6 +628,8 @@ export class ExternalSessionSyncService {
|
|
|
628
628
|
task.comments.push({
|
|
629
629
|
id: newCommentId(),
|
|
630
630
|
body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),
|
|
631
|
+
systemKey: 'sys.sessionError',
|
|
632
|
+
systemParams: { error: errorMessage.slice(0, 300) },
|
|
631
633
|
version: 1,
|
|
632
634
|
createdAt: now,
|
|
633
635
|
})
|
|
@@ -639,6 +641,7 @@ export class ExternalSessionSyncService {
|
|
|
639
641
|
task.comments.push({
|
|
640
642
|
id: newCommentId(),
|
|
641
643
|
body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),
|
|
644
|
+
systemKey: 'sys.sessionDone',
|
|
642
645
|
version: 1,
|
|
643
646
|
createdAt: now,
|
|
644
647
|
})
|
package/src/host/templates.ts
CHANGED
|
@@ -10,66 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFile } from 'node:fs/promises'
|
|
12
12
|
import type { TaskTemplate } from '../shared/api.ts'
|
|
13
|
+
import { BUILTIN_TEMPLATE_CONTENT, BUILTIN_TEMPLATE_IDS, type BuiltinTemplateId } from '../shared/builtin-templates.ts'
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
prompt: [
|
|
22
|
-
'实现以上新功能并按序交接:',
|
|
23
|
-
'1. 明确需求边界与验收标准,列出实现要点',
|
|
24
|
-
'2. 实现功能(含类型定义与错误处理)',
|
|
25
|
-
'3. 补充测试(单测/回归)',
|
|
26
|
-
'4. 运行相关测试套件确认通过',
|
|
27
|
-
].join('\n'),
|
|
28
|
-
urgency: 'normal',
|
|
29
|
-
checklist: ['实现要点已明确(需求边界与验收标准)', '功能已实现并补充测试', '相关测试套件通过'],
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'tpl-bugfix',
|
|
34
|
-
name: 'Bug 修复',
|
|
35
|
-
task: {
|
|
36
|
-
title: '修复:',
|
|
37
|
-
prompt: [
|
|
38
|
-
'修复以上问题并按序交接:',
|
|
39
|
-
'1. 复现问题(写最小复现步骤或测试)',
|
|
40
|
-
'2. 定位根因,说明为什么会发生',
|
|
41
|
-
'3. 修复并补回归测试',
|
|
42
|
-
'4. 运行相关测试套件确认无回归',
|
|
43
|
-
].join('\n'),
|
|
44
|
-
urgency: 'urgent',
|
|
45
|
-
checklist: ['已复现并定位根因', '修复已提交到任务分支', '回归测试通过'],
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: 'tpl-release',
|
|
50
|
-
name: '发布检查',
|
|
51
|
-
task: {
|
|
52
|
-
title: '发布:',
|
|
53
|
-
prompt: '执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。',
|
|
54
|
-
urgency: 'normal',
|
|
55
|
-
checklist: ['版本号已更新(package.json 与版本常量同步)', '构建通过', '全部测试通过', '变更记录已写'],
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'tpl-patrol',
|
|
60
|
-
name: '例行巡检',
|
|
61
|
-
task: {
|
|
62
|
-
title: '巡检:',
|
|
63
|
-
prompt: [
|
|
64
|
-
'例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。',
|
|
65
|
-
'发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。',
|
|
66
|
-
'输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。',
|
|
67
|
-
].join('\n'),
|
|
68
|
-
urgency: 'relaxed',
|
|
69
|
-
execution: { mode: 'scheduled', cron: '0 9 * * 1' },
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
]
|
|
15
|
+
/**
|
|
16
|
+
* The built-in templates seeded when the side file does not exist yet.
|
|
17
|
+
* Seeded from the shared zh content (the side file is plain data; the client
|
|
18
|
+
* resolves the active locale at render time — see shared/builtin-templates.ts).
|
|
19
|
+
*/
|
|
20
|
+
export const BUILTIN_TEMPLATES: ReadonlyArray<{ id: BuiltinTemplateId; name: string; task: TaskTemplate['task'] }> =
|
|
21
|
+
BUILTIN_TEMPLATE_IDS.map(id => ({ id, name: BUILTIN_TEMPLATE_CONTENT.zh[id].name, task: BUILTIN_TEMPLATE_CONTENT.zh[id].task }))
|
|
73
22
|
|
|
74
23
|
/** Mint a template id. */
|
|
75
24
|
function newTemplateId(): string {
|
package/src/host/tools.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SessionArchiveResult } from '../shared/api.ts'
|
|
2
|
+
import { archiveTaskSessions } from './archive-sessions.ts'
|
|
1
3
|
/**
|
|
2
4
|
* The ten `taskboard_*` agent tools. All writes require a calling agent
|
|
3
5
|
* session (ownership audit), carry optimistic-version checks, and enforce
|
|
@@ -98,11 +100,15 @@ function taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {
|
|
|
98
100
|
if (t.checklist !== undefined && t.checklist.length > 0) {
|
|
99
101
|
const done = t.checklist.filter(i => i.checked).length
|
|
100
102
|
lines.push(`验收清单 (${done}/${t.checklist.length}):`)
|
|
101
|
-
for (const item of t.checklist) {
|
|
103
|
+
for (const [index, item] of t.checklist.entries()) {
|
|
102
104
|
const mark = item.checked ? '☑' : '☐'
|
|
103
105
|
const who = item.checkedBy === undefined ? '' : item.checkedBy === 'user' ? ' ·用户勾选' : ` ·agent ${String(item.checkedBy).slice(0, 24)}勾选`
|
|
104
106
|
const note = item.note !== undefined ? ` ·证据: ${item.note}` : ''
|
|
105
|
-
|
|
107
|
+
// Carry the checklist item id so `taskboard_checklist check/uncheck` can
|
|
108
|
+
// address it without guessing — a terse render starves the agent (render
|
|
109
|
+
// is fed to the model as result.content). Mirrors the index+id carried
|
|
110
|
+
// by the taskboard_checklist tool output.
|
|
111
|
+
lines.push(` ${mark} [${index + 1}] ${item.text}${who}${note} id=${item.id}`)
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
if (t.comments.length > 0) {
|
|
@@ -158,6 +164,8 @@ export interface WorkspaceFace {
|
|
|
158
164
|
get(id: string): { id: string; path: string; title: string } | undefined
|
|
159
165
|
/** List all workspaces. */
|
|
160
166
|
list(): Array<{ id: string; path: string; title: string }>
|
|
167
|
+
/** Archive one session durably (when supported by runtime workspaceRegistry). */
|
|
168
|
+
archiveSession?(sessionId: string): Promise<void>
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
/** Adapt the real registry to the narrow face. */
|
|
@@ -174,6 +182,9 @@ export function workspaceFace(registry: WorkspaceRegistry): WorkspaceFace {
|
|
|
174
182
|
return ws === undefined ? undefined : { id: ws.id, path: ws.path, title: ws.title }
|
|
175
183
|
},
|
|
176
184
|
list: () => registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title })),
|
|
185
|
+
...(typeof registry.archiveSession === 'function'
|
|
186
|
+
? { archiveSession: (sessionId: string) => registry.archiveSession(sessionId as Parameters<WorkspaceRegistry['archiveSession']>[0]) }
|
|
187
|
+
: {}),
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
|
|
@@ -407,7 +418,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
407
418
|
output: {
|
|
408
419
|
schema: JSON_OUT,
|
|
409
420
|
render: (_args, value) => {
|
|
410
|
-
const v = value as { task?: { id?: string; status?: string; version?: number } }
|
|
421
|
+
const v = value as { task?: { id?: string; status?: string; version?: number }; sessionArchive?: SessionArchiveResult }
|
|
411
422
|
const t = v.task
|
|
412
423
|
return [{ type: 'text', text: t === undefined ? '创建失败。' : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。` }]
|
|
413
424
|
},
|
|
@@ -497,7 +508,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
497
508
|
output: {
|
|
498
509
|
schema: JSON_OUT,
|
|
499
510
|
render: (_args, value) => {
|
|
500
|
-
const v = value as { task?: { id?: string; status?: string; version?: number } }
|
|
511
|
+
const v = value as { task?: { id?: string; status?: string; version?: number }; sessionArchive?: SessionArchiveResult }
|
|
501
512
|
const t = v.task
|
|
502
513
|
return [{ type: 'text', text: t === undefined ? '更新失败。' : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。` }]
|
|
503
514
|
},
|
|
@@ -550,16 +561,17 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
550
561
|
id: { type: 'string', required: true, description: 'Task id.' },
|
|
551
562
|
status: { type: 'string', required: true, description: 'Target status.' },
|
|
552
563
|
ifVersion: { type: 'number', required: true, description: 'Task version you read; fails on mismatch.' },
|
|
564
|
+
archiveSessions: { type: 'boolean', description: 'When moving to archived: whether to archive associated execution sessions as well. Defaults to false.' },
|
|
553
565
|
},
|
|
554
566
|
output: {
|
|
555
567
|
schema: JSON_OUT,
|
|
556
568
|
render: (_args, value) => {
|
|
557
|
-
const v = value as { task?: { id?: string; status?: string; version?: number } }
|
|
569
|
+
const v = value as { task?: { id?: string; status?: string; version?: number }; sessionArchive?: SessionArchiveResult }
|
|
558
570
|
const t = v.task
|
|
559
|
-
return [{ type: 'text', text: t === undefined ? '移动失败。' : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}
|
|
571
|
+
return [{ type: 'text', text: t === undefined ? '移动失败。' : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。${v.sessionArchive === undefined ? '' : ` 会话归档结果:${JSON.stringify(v.sessionArchive)}`}` }]
|
|
560
572
|
},
|
|
561
573
|
},
|
|
562
|
-
async execute(args: { id: string; status: string; ifVersion: number }, exec: unknown) {
|
|
574
|
+
async execute(args: { id: string; status: string; ifVersion: number; archiveSessions?: boolean }, exec: unknown) {
|
|
563
575
|
try {
|
|
564
576
|
const { actor } = caller(exec as ToolRunContext)
|
|
565
577
|
const to = asStatus(args.status)
|
|
@@ -571,9 +583,11 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
571
583
|
: undefined
|
|
572
584
|
// R1: every state guard + the write itself run inside the mutation.
|
|
573
585
|
let next: TaskRecord | undefined
|
|
586
|
+
let beforeTask: TaskRecord | undefined
|
|
574
587
|
await store.mutate('task-moved', ledger => {
|
|
575
588
|
const { index, task } = liveTaskAt(ledger, args.id)
|
|
576
589
|
versionGuard(task, args.ifVersion)
|
|
590
|
+
beforeTask = task
|
|
577
591
|
|
|
578
592
|
// Code-level gate: agents never complete a task.
|
|
579
593
|
if (to === 'done') {
|
|
@@ -603,7 +617,10 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
603
617
|
ledger.tasks[index] = next
|
|
604
618
|
return [next]
|
|
605
619
|
})
|
|
606
|
-
|
|
620
|
+
const sessionArchive = to === 'archived' && args.archiveSessions === true
|
|
621
|
+
? await archiveTaskSessions(beforeTask ?? next!, deps.workspaces.archiveSession)
|
|
622
|
+
: undefined
|
|
623
|
+
return json({ task: summarize(next!), ...(sessionArchive !== undefined ? { sessionArchive } : {}) })
|
|
607
624
|
} catch (error) { fail(error) }
|
|
608
625
|
},
|
|
609
626
|
})) as () => void)
|
|
@@ -925,4 +942,4 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
925
942
|
})) as () => void)
|
|
926
943
|
|
|
927
944
|
return disposers
|
|
928
|
-
}
|
|
945
|
+
}
|
package/src/shared/api.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ApiResult<T> = ApiOk<T> | ApiFail
|
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
|
|
40
40
|
/** Full-state response (the reconnect baseline after an SSE gap). */
|
|
41
|
-
export type StateResponse = TaskLedger
|
|
41
|
+
export type StateResponse = TaskLedger & { capabilities?: { archiveSessions: boolean } }
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Workspace listing for the UI pickers. `repoCount` (0.6.3): how many repos a
|
|
@@ -89,7 +89,10 @@ export type UpdateTaskBody = {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** Move-task request body (ifVersion mandatory; the user MAY move to done). */
|
|
92
|
-
export type MoveTaskBody = { ifVersion: number; status: string }
|
|
92
|
+
export type MoveTaskBody = { ifVersion: number; status: string; archiveSessions?: boolean }
|
|
93
|
+
|
|
94
|
+
export type SessionArchiveResult = { archived: string[]; failed: Array<{ sessionId: string; error: string }>; unsupported: string[] }
|
|
95
|
+
export type MoveTaskResponse = TaskSummary & { sessionArchive?: SessionArchiveResult }
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
98
|
* Quick-reject request body (card ✗ button): move back to todo plus an
|
|
@@ -270,4 +273,4 @@ export type ChangeEvent = {
|
|
|
270
273
|
revision: number
|
|
271
274
|
kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded' | 'settings-updated' | 'ledger-replaced'
|
|
272
275
|
tasks: TaskSummary[]
|
|
273
|
-
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in task-template content (0.6.4): the four factory templates seeded
|
|
3
|
+
* into the template side file, in both shipped locales. The host seeds the zh
|
|
4
|
+
* copy as the persisted fallback (the side file is plain data); the client
|
|
5
|
+
* resolves the ACTIVE locale's copy at render / prefill time via
|
|
6
|
+
* {@link builtinTemplateContent}, so an English GUI shows English template
|
|
7
|
+
* text without ever re-seeding the side file.
|
|
8
|
+
*
|
|
9
|
+
* Kept in shared/ (not the client i18n dictionaries) because the content is
|
|
10
|
+
* structured task DATA — a checklist is an array and the prompt is multi-line
|
|
11
|
+
* — rather than UI-chrome strings, and because the host needs the same seed
|
|
12
|
+
* content to write the side file.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-taskboard/shared/builtin-templates
|
|
15
|
+
*/
|
|
16
|
+
import type { TaskTemplateSpec } from './api.ts'
|
|
17
|
+
|
|
18
|
+
/** Shipped built-in template locales (mirrors the client i18n LocaleId). */
|
|
19
|
+
export type BuiltinTemplateLocale = 'zh' | 'en'
|
|
20
|
+
|
|
21
|
+
/** One built-in template's localized name + task spec. */
|
|
22
|
+
export interface BuiltinTemplateContent {
|
|
23
|
+
name: string
|
|
24
|
+
task: TaskTemplateSpec
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Stable ids of the factory templates (persisted in the side file). */
|
|
28
|
+
export const BUILTIN_TEMPLATE_IDS = ['tpl-feature', 'tpl-bugfix', 'tpl-release', 'tpl-patrol'] as const
|
|
29
|
+
|
|
30
|
+
export type BuiltinTemplateId = (typeof BUILTIN_TEMPLATE_IDS)[number]
|
|
31
|
+
|
|
32
|
+
/** Built-in template content per locale (name + prefilled task spec). */
|
|
33
|
+
export const BUILTIN_TEMPLATE_CONTENT: Readonly<Record<BuiltinTemplateLocale, Readonly<Record<BuiltinTemplateId, BuiltinTemplateContent>>>> = {
|
|
34
|
+
zh: {
|
|
35
|
+
'tpl-feature': {
|
|
36
|
+
name: '新增功能',
|
|
37
|
+
task: {
|
|
38
|
+
title: '新增:',
|
|
39
|
+
prompt: [
|
|
40
|
+
'实现以上新功能并按序交接:',
|
|
41
|
+
'1. 明确需求边界与验收标准,列出实现要点',
|
|
42
|
+
'2. 实现功能(含类型定义与错误处理)',
|
|
43
|
+
'3. 补充测试(单测/回归)',
|
|
44
|
+
'4. 运行相关测试套件确认通过',
|
|
45
|
+
].join('\n'),
|
|
46
|
+
urgency: 'normal',
|
|
47
|
+
checklist: ['实现要点已明确(需求边界与验收标准)', '功能已实现并补充测试', '相关测试套件通过'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'tpl-bugfix': {
|
|
51
|
+
name: 'Bug 修复',
|
|
52
|
+
task: {
|
|
53
|
+
title: '修复:',
|
|
54
|
+
prompt: [
|
|
55
|
+
'修复以上问题并按序交接:',
|
|
56
|
+
'1. 复现问题(写最小复现步骤或测试)',
|
|
57
|
+
'2. 定位根因,说明为什么会发生',
|
|
58
|
+
'3. 修复并补回归测试',
|
|
59
|
+
'4. 运行相关测试套件确认无回归',
|
|
60
|
+
].join('\n'),
|
|
61
|
+
urgency: 'urgent',
|
|
62
|
+
checklist: ['已复现并定位根因', '修复已提交到任务分支', '回归测试通过'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
'tpl-release': {
|
|
66
|
+
name: '发布检查',
|
|
67
|
+
task: {
|
|
68
|
+
title: '发布:',
|
|
69
|
+
prompt: '执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。',
|
|
70
|
+
urgency: 'normal',
|
|
71
|
+
checklist: ['版本号已更新(package.json 与版本常量同步)', '构建通过', '全部测试通过', '变更记录已写'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
'tpl-patrol': {
|
|
75
|
+
name: '例行巡检',
|
|
76
|
+
task: {
|
|
77
|
+
title: '巡检:',
|
|
78
|
+
prompt: [
|
|
79
|
+
'例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。',
|
|
80
|
+
'发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。',
|
|
81
|
+
'输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
urgency: 'relaxed',
|
|
84
|
+
execution: { mode: 'scheduled', cron: '0 9 * * 1' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
en: {
|
|
89
|
+
'tpl-feature': {
|
|
90
|
+
name: 'New feature',
|
|
91
|
+
task: {
|
|
92
|
+
title: 'New feature:',
|
|
93
|
+
prompt: [
|
|
94
|
+
'Implement the new feature above and hand off in order:',
|
|
95
|
+
'1. Clarify the requirement boundaries and acceptance criteria; list the implementation points',
|
|
96
|
+
'2. Implement the feature (including type definitions and error handling)',
|
|
97
|
+
'3. Add tests (unit / regression)',
|
|
98
|
+
'4. Run the relevant test suites and confirm they pass',
|
|
99
|
+
].join('\n'),
|
|
100
|
+
urgency: 'normal',
|
|
101
|
+
checklist: ['Implementation points clarified (requirement boundaries and acceptance criteria)', 'Feature implemented with tests added', 'Relevant test suites pass'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
'tpl-bugfix': {
|
|
105
|
+
name: 'Bug fix',
|
|
106
|
+
task: {
|
|
107
|
+
title: 'Fix:',
|
|
108
|
+
prompt: [
|
|
109
|
+
'Fix the issue above and hand off in order:',
|
|
110
|
+
'1. Reproduce the issue (write minimal reproduction steps or a test)',
|
|
111
|
+
'2. Locate the root cause and explain why it happens',
|
|
112
|
+
'3. Fix it and add regression tests',
|
|
113
|
+
'4. Run the relevant test suites and confirm there are no regressions',
|
|
114
|
+
].join('\n'),
|
|
115
|
+
urgency: 'urgent',
|
|
116
|
+
checklist: ['Issue reproduced and root cause located', 'Fix committed to the task branch', 'Regression tests pass'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
'tpl-release': {
|
|
120
|
+
name: 'Release check',
|
|
121
|
+
task: {
|
|
122
|
+
title: 'Release:',
|
|
123
|
+
prompt: 'Run the release process: bump the version, build, test, and update the changelog, then hand off in order (do not actually push / publish — wait for user confirmation).',
|
|
124
|
+
urgency: 'normal',
|
|
125
|
+
checklist: ['Version bumped (package.json and version constants in sync)', 'Build passes', 'All tests pass', 'Changelog written'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
'tpl-patrol': {
|
|
129
|
+
name: 'Routine patrol',
|
|
130
|
+
task: {
|
|
131
|
+
title: 'Patrol:',
|
|
132
|
+
prompt: [
|
|
133
|
+
'Routine patrol: check dependency updates, failing tests, obvious code issues, and unhandled alerts.',
|
|
134
|
+
'List each finding (severity / location / suggestion); fix small issues directly, and only report (do not touch) large ones.',
|
|
135
|
+
'Output a patrol summary (use {{lastComments}} to review the last patrol\u2019s conclusions).',
|
|
136
|
+
].join('\n'),
|
|
137
|
+
urgency: 'relaxed',
|
|
138
|
+
execution: { mode: 'scheduled', cron: '0 9 * * 1' },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve one built-in template's localized content.
|
|
146
|
+
* @param id - the template id.
|
|
147
|
+
* @param locale - the requested locale.
|
|
148
|
+
* @returns the content, or undefined when `id` is not a built-in template id.
|
|
149
|
+
*/
|
|
150
|
+
export function builtinTemplateContent(id: string, locale: BuiltinTemplateLocale): BuiltinTemplateContent | undefined {
|
|
151
|
+
if (!(BUILTIN_TEMPLATE_IDS as readonly string[]).includes(id)) return undefined
|
|
152
|
+
return BUILTIN_TEMPLATE_CONTENT[locale][id as BuiltinTemplateId]
|
|
153
|
+
}
|
package/src/shared/protocol.ts
CHANGED
|
@@ -335,6 +335,15 @@ export type Actor =
|
|
|
335
335
|
| { kind: 'agent'; sessionId: string }
|
|
336
336
|
| { kind: 'system' }
|
|
337
337
|
|
|
338
|
+
/** Structured row of a multi-repo merge system comment (0.6.4). */
|
|
339
|
+
export type SystemCommentRow = {
|
|
340
|
+
/** Repo path relative to the workspace ('' = the workspace root repo). */
|
|
341
|
+
repo: string
|
|
342
|
+
outcome: 'merged' | 'noop' | 'failed'
|
|
343
|
+
/** Failure reason (verbatim) when outcome = 'failed'. */
|
|
344
|
+
error?: string
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
/** A progress/report comment on a task. */
|
|
339
348
|
export type CommentRecord = {
|
|
340
349
|
id: string
|
|
@@ -345,6 +354,16 @@ export type CommentRecord = {
|
|
|
345
354
|
createdAt: number
|
|
346
355
|
/** The session that wrote this comment; absent for user-written ones. */
|
|
347
356
|
threadId?: string
|
|
357
|
+
/**
|
|
358
|
+
* i18n key of a host-generated system message (0.6.4). The GUI localizes it
|
|
359
|
+
* at render time; `body` stays a zh fallback for agent tools / CSV / raw
|
|
360
|
+
* JSON views.
|
|
361
|
+
*/
|
|
362
|
+
systemKey?: string
|
|
363
|
+
/** Flat {name} interpolation params for the system message. */
|
|
364
|
+
systemParams?: Record<string, string>
|
|
365
|
+
/** Structured per-repo rows for the multi-repo merge summary (0.6.4). */
|
|
366
|
+
systemRows?: SystemCommentRow[]
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
/** One commit produced by an isolated execution (hash + subject). */
|
|
@@ -756,6 +775,36 @@ export function syncClaim(task: TaskRecord, to: TaskStatus, now: number, holder?
|
|
|
756
775
|
}
|
|
757
776
|
}
|
|
758
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Collect unique execution session IDs associated with a task:
|
|
780
|
+
* - executions with a non-empty `sessionId`
|
|
781
|
+
* Creator and claim sessions may serve other tasks and are never included.
|
|
782
|
+
* @param task - the task record to inspect.
|
|
783
|
+
* @returns an array of distinct session IDs in stable discovery order.
|
|
784
|
+
*/
|
|
785
|
+
export function taskAssociatedSessionIds(task: TaskRecord): string[] {
|
|
786
|
+
const seen = new Set<string>()
|
|
787
|
+
const result: string[] = []
|
|
788
|
+
const push = (raw: unknown) => {
|
|
789
|
+
if (typeof raw === 'string') {
|
|
790
|
+
const trimmed = raw.trim()
|
|
791
|
+
if (trimmed.length > 0 && !seen.has(trimmed)) {
|
|
792
|
+
seen.add(trimmed)
|
|
793
|
+
result.push(trimmed)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (Array.isArray(task.executions)) {
|
|
799
|
+
for (const ex of task.executions) {
|
|
800
|
+
if (ex !== null && typeof ex === 'object') {
|
|
801
|
+
push((ex as { sessionId?: unknown }).sessionId)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return result
|
|
806
|
+
}
|
|
807
|
+
|
|
759
808
|
/**
|
|
760
809
|
* Validate and normalize a pinned model: `{ provider, model, reasoningEffort? }`,
|
|
761
810
|
* provider and model must be non-empty trimmed strings.
|
|
@@ -1020,6 +1069,21 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
1020
1069
|
version: numOr(ce, 'version', 1),
|
|
1021
1070
|
createdAt: numOr(ce, 'createdAt', now),
|
|
1022
1071
|
...(typeof ce.threadId === 'string' ? { threadId: ce.threadId } : {}),
|
|
1072
|
+
...(typeof ce.systemKey === 'string' && /^sys\.[A-Za-z0-9]+$/.test(ce.systemKey) && ce.systemKey.length <= 100
|
|
1073
|
+
? {
|
|
1074
|
+
systemKey: ce.systemKey,
|
|
1075
|
+
...(typeof ce.systemParams === 'object' && ce.systemParams !== null && !Array.isArray(ce.systemParams)
|
|
1076
|
+
? { systemParams: Object.fromEntries(Object.entries(ce.systemParams).filter(([key, value]) => key.length <= 100 && typeof value === 'string' && value.length <= 4000).slice(0, 20)) as Record<string, string> }
|
|
1077
|
+
: {}),
|
|
1078
|
+
...(Array.isArray(ce.systemRows)
|
|
1079
|
+
? { systemRows: ce.systemRows.filter((row): row is SystemCommentRow => typeof row === 'object' && row !== null
|
|
1080
|
+
&& typeof row.repo === 'string' && (row.repo === '' || isValidRelRepoPath(row.repo))
|
|
1081
|
+
&& ['merged', 'noop', 'failed'].includes(row.outcome)
|
|
1082
|
+
&& (row.error === undefined || typeof row.error === 'string'))
|
|
1083
|
+
.slice(0, MAX_MIRROR_REPOS).map(row => ({ repo: row.repo, outcome: row.outcome, ...(row.error !== undefined ? { error: row.error.slice(0, 4000) } : {}) })) }
|
|
1084
|
+
: {}),
|
|
1085
|
+
}
|
|
1086
|
+
: {}),
|
|
1023
1087
|
})
|
|
1024
1088
|
}
|
|
1025
1089
|
} else return fail('comments must be an array')
|
package/src/shared/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The plugin package version shown in the board UI. Kept in sync with
|
|
3
|
-
* package.json by a regression test (tests lock drift).
|
|
4
|
-
*
|
|
5
|
-
* @module dsh-taskboard/shared/version
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** The package version (must equal package.json "version"). */
|
|
9
|
-
export const PLUGIN_VERSION = '0.6.
|
|
1
|
+
/**
|
|
2
|
+
* The plugin package version shown in the board UI. Kept in sync with
|
|
3
|
+
* package.json by a regression test (tests lock drift).
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-taskboard/shared/version
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The package version (must equal package.json "version"). */
|
|
9
|
+
export const PLUGIN_VERSION = '0.6.7'
|