dsh-taskboard 0.6.6 → 0.7.0

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.
Files changed (64) hide show
  1. package/README.md +305 -288
  2. package/lib/client.js +985 -931
  3. package/lib/host/archive-sessions.js +30 -0
  4. package/lib/host/archive-sessions.js.map +1 -0
  5. package/lib/host/assets.js +139 -0
  6. package/lib/host/assets.js.map +1 -0
  7. package/lib/host/execution.js +3 -0
  8. package/lib/host/execution.js.map +1 -1
  9. package/lib/host/locale.js +17 -0
  10. package/lib/host/locale.js.map +1 -0
  11. package/lib/host/routes.js +132 -6
  12. package/lib/host/routes.js.map +1 -1
  13. package/lib/host/scheduler.js +2 -0
  14. package/lib/host/scheduler.js.map +1 -1
  15. package/lib/host/session-sync.js +4 -1
  16. package/lib/host/session-sync.js.map +1 -1
  17. package/lib/host/storage-queue.js +14 -0
  18. package/lib/host/storage-queue.js.map +1 -0
  19. package/lib/host/storage.js +249 -0
  20. package/lib/host/storage.js.map +1 -0
  21. package/lib/host/store.js +34 -7
  22. package/lib/host/store.js.map +1 -1
  23. package/lib/host/templates.js +73 -113
  24. package/lib/host/templates.js.map +1 -1
  25. package/lib/host/tools.js +23 -8
  26. package/lib/host/tools.js.map +1 -1
  27. package/lib/index.js +83 -27
  28. package/lib/index.js.map +1 -1
  29. package/lib/shared/api.js.map +1 -1
  30. package/lib/shared/builtin-templates.js +155 -0
  31. package/lib/shared/builtin-templates.js.map +1 -0
  32. package/lib/shared/protocol.js +39 -2
  33. package/lib/shared/protocol.js.map +1 -1
  34. package/package.json +90 -89
  35. package/src/client/api.ts +35 -1
  36. package/src/client/board/SettingsModal.tsx +67 -4
  37. package/src/client/board/SlashPromptInput.tsx +80 -1
  38. package/src/client/board/TaskBoard.tsx +16 -11
  39. package/src/client/board/TaskDetail.tsx +186 -17
  40. package/src/client/board/TaskFormModal.tsx +8 -5
  41. package/src/client/board/TemplateManager.tsx +22 -10
  42. package/src/client/controller.ts +67 -5
  43. package/src/client/i18n/en.ts +35 -3
  44. package/src/client/i18n/templates.ts +25 -0
  45. package/src/client/i18n/zh.ts +35 -3
  46. package/src/client/image-insert.ts +29 -0
  47. package/src/client/styles.ts +35 -2
  48. package/src/host/archive-sessions.ts +18 -0
  49. package/src/host/assets.ts +120 -0
  50. package/src/host/execution.ts +4 -1
  51. package/src/host/locale.ts +44 -0
  52. package/src/host/routes.ts +132 -7
  53. package/src/host/scheduler.ts +2 -0
  54. package/src/host/session-sync.ts +4 -1
  55. package/src/host/storage-queue.ts +10 -0
  56. package/src/host/storage.ts +212 -0
  57. package/src/host/store.ts +40 -12
  58. package/src/host/templates.ts +38 -66
  59. package/src/host/tools.ts +28 -11
  60. package/src/index.ts +88 -33
  61. package/src/shared/api.ts +32 -3
  62. package/src/shared/builtin-templates.ts +153 -0
  63. package/src/shared/protocol.ts +64 -0
  64. package/src/shared/version.ts +9 -9
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Host loader entry for dsh-taskboard.
3
3
  *
4
- * Wiring: the ledger store (one JSON file under the DSH home), the ten
4
+ * Wiring: the configurable local data stores, the ten
5
5
  * `taskboard_*` agent tools, the agent workflow-protocol system-prompt
6
6
  * section, the /taskboard JSON+SSE routes (when a webServer is served),
7
7
  * the host execution service (fresh in-project sessions, pinned models), and
@@ -29,14 +29,19 @@ import { dshHomePath } from './host/sdk.ts'
29
29
  import { TaskStore } from './host/store.ts'
30
30
  import { TemplateStore } from './host/templates.ts'
31
31
  import { ExternalSessionSyncService } from './host/session-sync.ts'
32
- import { registerTaskboardTools, workspaceFace } from './host/tools.ts'
32
+ import { ERR, ToolError, registerTaskboardTools, workspaceFace, type WorkspaceFace } from './host/tools.ts'
33
+ import { AssetStore } from './host/assets.ts'
34
+ import { STORAGE_CONFIG_FILE, StorageCoordinator } from './host/storage.ts'
33
35
 
34
- /** Ledger file name under the DSH home. */
36
+ /** Ledger file name under the active taskboard data directory. */
35
37
  export const LEDGER_FILE = 'dsh-taskboard.json'
36
38
 
37
- /** Task-template side file name under the DSH home (0.4.0). */
39
+ /** Task-template side file name under the active data directory. */
38
40
  export const TEMPLATES_FILE = 'dsh-taskboard-templates.json'
39
41
 
42
+ /** Content-addressed image attachment directory under the active data directory. */
43
+ export const ASSETS_DIR = 'dsh-taskboard-assets'
44
+
40
45
  /** Cordis plugin name. */
41
46
  export const name = 'dsh-taskboard'
42
47
 
@@ -48,14 +53,24 @@ export const inject = ['tools', 'systemPrompt']
48
53
  * @param ctx - the plugin context (tools + systemPrompt injected).
49
54
  */
50
55
  export function apply(ctx: Context): void {
51
- const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })
52
- const templates = new TemplateStore(dshHomePath(TEMPLATES_FILE))
56
+ const storage = new StorageCoordinator({
57
+ defaultDirectory: dshHomePath(),
58
+ configFile: dshHomePath(STORAGE_CONFIG_FILE),
59
+ ledgerName: LEDGER_FILE,
60
+ templatesName: TEMPLATES_FILE,
61
+ assetsName: ASSETS_DIR,
62
+ })
63
+ const store = new TaskStore({ file: storage.ledgerPath(), queue: storage.queue })
64
+ const templates = new TemplateStore(storage.templatesPath(), storage.queue)
65
+ const assets = new AssetStore(storage.assetsPath(), () => Date.now(), storage.queue)
66
+ storage.attach({ ledger: store, templates, assets })
53
67
  // Eager first load: the tools and most routes read snapshot()/get() without
54
68
  // triggering the lazy load, so a fresh boot used to serve an EMPTY board to
55
69
  // taskboard_list/get until the scheduler catchup tick or the first
56
70
  // GET /state happened to load the file (review P0). load() never throws —
57
71
  // a corrupt ledger is quarantined instead.
58
- void store.load()
72
+ const storeReady = storage.ready().then(() => store.load())
73
+ void storeReady.then(() => assets.cleanup(JSON.stringify(store.snapshot())))
59
74
  const now = () => Date.now()
60
75
  // Global execution concurrency cap (DSH_TASKBOARD_MAX_CONCURRENT overrides).
61
76
  const maxConcurrent = Math.max(1, Number.parseInt(process.env.DSH_TASKBOARD_MAX_CONCURRENT ?? '', 10) || DEFAULT_MAX_CONCURRENT)
@@ -68,29 +83,60 @@ export function apply(ctx: Context): void {
68
83
  })
69
84
  ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')
70
85
 
71
- // Tools, routes, execution, and the scheduler all come up with the
72
- // workspace registry (claim boundary + project execution need it).
73
- ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {
74
- const disposers: Array<() => void> = []
75
-
76
- // Registered model provider routes (from the host llm runtime), read
77
- // lazily at call time so late availability still applies; undefined when
78
- // the runtime is absent only structural model validation runs.
79
- const modelProviders = (): string[] | undefined => {
80
- try {
81
- const llm = wsCtx.get('llm') as { listProviders?: () => Array<{ id: string }> } | undefined
82
- return llm === undefined || typeof llm.listProviders !== 'function'
83
- ? undefined
84
- : llm.listProviders().map(p => p.id)
85
- } catch { return undefined }
86
+ // Register the complete tool schema in the same synchronous mount as the
87
+ // protocol. Keeping schemas stable from the first request preserves the
88
+ // provider's prefix cache; calls use the live workspace service below.
89
+ let activeWorkspaces: WorkspaceFace | undefined
90
+ let activeWorkspaceContext: Context | undefined
91
+ const requireWorkspaces = (): WorkspaceFace => {
92
+ if (activeWorkspaces === undefined) {
93
+ throw new ToolError(ERR.notReady, 'workspace service is not ready; retry after host startup completes')
86
94
  }
95
+ return activeWorkspaces
96
+ }
97
+ const workspaces: WorkspaceFace = {
98
+ resolveByPath: path => requireWorkspaces().resolveByPath(path),
99
+ get: id => requireWorkspaces().get(id),
100
+ list: () => requireWorkspaces().list(),
101
+ }
102
+ // Preserve the optional archive capability without replacing the stable
103
+ // facade captured by the tool definitions.
104
+ Object.defineProperty(workspaces, 'archiveSession', {
105
+ enumerable: true,
106
+ get: () => activeWorkspaces?.archiveSession === undefined
107
+ ? undefined
108
+ : (sessionId: string) => requireWorkspaces().archiveSession!(sessionId),
109
+ })
110
+ const modelProviders = (): string[] | undefined => {
111
+ try {
112
+ const llm = activeWorkspaceContext?.get('llm') as { listProviders?: () => Array<{ id: string }> } | undefined
113
+ return llm === undefined || typeof llm.listProviders !== 'function'
114
+ ? undefined
115
+ : llm.listProviders().map(p => p.id)
116
+ } catch { return undefined }
117
+ }
118
+ const disposeTools = registerTaskboardTools(ctx, {
119
+ store,
120
+ workspaces,
121
+ now,
122
+ modelProviders,
123
+ ready: async () => {
124
+ if (activeWorkspaces === undefined) {
125
+ throw new ToolError(ERR.notReady, 'workspace service is not ready; retry after host startup completes')
126
+ }
127
+ await storeReady
128
+ },
129
+ })
130
+ ctx.effect(() => () => {
131
+ for (const dispose of disposeTools.splice(0)) dispose()
132
+ }, 'dsh-taskboard: tools')
87
133
 
88
- disposers.push(...registerTaskboardTools(wsCtx, {
89
- store,
90
- workspaces: workspaceFace(wsCtx.workspaceRegistry),
91
- now,
92
- modelProviders,
93
- }))
134
+ // Runtime services come and go with the workspace registry. Tool schemas
135
+ // remain mounted and resolve this current service only when called.
136
+ ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {
137
+ const workspaceDisposers: Array<() => void> = []
138
+ activeWorkspaces = workspaceFace(wsCtx.workspaceRegistry)
139
+ activeWorkspaceContext = wsCtx
94
140
 
95
141
  // Settlement listener over the session event bus.
96
142
  const events: EventsFace = {
@@ -122,7 +168,7 @@ export function apply(ctx: Context): void {
122
168
  },
123
169
  now,
124
170
  })
125
- disposers.push(() => sessionSync.dispose())
171
+ workspaceDisposers.push(() => sessionSync.dispose())
126
172
 
127
173
  // The narrow git face shared by execution (worktree isolation) and the
128
174
  // routes (merge / remove / workspace detection), plus the shared
@@ -131,6 +177,7 @@ export function apply(ctx: Context): void {
131
177
  const scanner = createRepoScanner()
132
178
 
133
179
  wsCtx.inject(['agents'], (agentCtx: Context) => {
180
+ const agentDisposers: Array<() => void> = []
134
181
  agentSessions = agentCtx.get('sessions') as { get?: (id: string) => unknown; list?: () => unknown[] } | undefined
135
182
  const execution = new ExecutionService({
136
183
  store,
@@ -207,6 +254,9 @@ export function apply(ctx: Context): void {
207
254
  git,
208
255
  scanner,
209
256
  templates,
257
+ assets,
258
+ storage,
259
+ ready: async () => { await storeReady },
210
260
  promptCompletions: async () => {
211
261
  try {
212
262
  const skillsService = agentCtx.get('skills') as { list?(options?: unknown): Promise<Array<{ name: string; description?: string }>> } | undefined
@@ -307,19 +357,24 @@ export function apply(ctx: Context): void {
307
357
  // browser open. Shares the execution concurrency cap.
308
358
  const scheduler = new SchedulerService({ store, execution, now, maxConcurrent })
309
359
  scheduler.start()
310
- disposers.push(() => scheduler.dispose())
360
+ agentDisposers.push(() => scheduler.dispose())
311
361
  // Detach the settlement listener with the plugin — a hot reload must
312
362
  // not leave stale services reacting to turn/end errors (review P1).
313
- disposers.push(() => execution.dispose())
363
+ agentDisposers.push(() => execution.dispose())
314
364
 
315
365
  return () => {
316
366
  disposeRoutes?.()
317
- for (const dispose of disposers.splice(0)) dispose()
367
+ agentSessions = undefined
368
+ for (const dispose of agentDisposers.splice(0)) dispose()
318
369
  }
319
370
  })
320
371
 
321
372
  return () => {
322
- for (const dispose of disposers.splice(0)) dispose()
373
+ if (activeWorkspaceContext === wsCtx) {
374
+ activeWorkspaceContext = undefined
375
+ activeWorkspaces = undefined
376
+ }
377
+ for (const dispose of workspaceDisposers.splice(0)) dispose()
323
378
  }
324
379
  })
325
380
  }
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
@@ -101,6 +104,16 @@ export type RejectTaskBody = { ifVersion: number; body?: string }
101
104
  /** Comment request body. */
102
105
  export type CommentBody = { body: string }
103
106
 
107
+ /** One content-addressed image uploaded outside the ledger. */
108
+ export type AttachmentUpload = {
109
+ id: string
110
+ name: string
111
+ size: number
112
+ url: string
113
+ extension: 'png' | 'jpg' | 'gif' | 'webp'
114
+ mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'
115
+ }
116
+
104
117
  /** Delete request body (purge=true physically removes a trashed task). */
105
118
  export type DeleteTaskBody = { ifVersion?: number; purge?: boolean }
106
119
 
@@ -184,6 +197,22 @@ export type TemplatesResponse = { templates: TaskTemplate[] }
184
197
  /** Board-settings response (0.5.0; absent fields follow factory defaults). */
185
198
  export type SettingsResponse = BoardSettings
186
199
 
200
+ /** Current host-side location of all durable taskboard data. */
201
+ export type StorageStatus = {
202
+ currentDirectory: string
203
+ defaultDirectory: string
204
+ isDefault: boolean
205
+ configured: boolean
206
+ writable: boolean
207
+ assetCount: number
208
+ assetBytes: number
209
+ checkedDirectory?: string
210
+ error?: string
211
+ }
212
+
213
+ /** Completed storage relocation, including non-fatal old-file cleanup failures. */
214
+ export type StorageMigrationResult = StorageStatus & { migrated: boolean; warnings: string[] }
215
+
187
216
  /** Update-board-settings request body (0.5.0; whole-object replace semantics). */
188
217
  export type UpdateSettingsBody = {
189
218
  /** Default code isolation for NEW tasks ('worktree' | 'none'). */
@@ -270,4 +299,4 @@ export type ChangeEvent = {
270
299
  revision: number
271
300
  kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded' | 'settings-updated' | 'ledger-replaced'
272
301
  tasks: TaskSummary[]
273
- }
302
+ }
@@ -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
+ }
@@ -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')
@@ -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.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.7.0'