dsh-taskboard 0.6.6 → 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.
Files changed (45) hide show
  1. package/README.md +301 -294
  2. package/lib/client.js +937 -931
  3. package/lib/host/archive-sessions.js +30 -0
  4. package/lib/host/archive-sessions.js.map +1 -0
  5. package/lib/host/execution.js +3 -0
  6. package/lib/host/execution.js.map +1 -1
  7. package/lib/host/locale.js +17 -0
  8. package/lib/host/locale.js.map +1 -0
  9. package/lib/host/routes.js +45 -6
  10. package/lib/host/routes.js.map +1 -1
  11. package/lib/host/scheduler.js +2 -0
  12. package/lib/host/scheduler.js.map +1 -1
  13. package/lib/host/session-sync.js +4 -1
  14. package/lib/host/session-sync.js.map +1 -1
  15. package/lib/host/templates.js +11 -75
  16. package/lib/host/templates.js.map +1 -1
  17. package/lib/host/tools.js +17 -4
  18. package/lib/host/tools.js.map +1 -1
  19. package/lib/shared/api.js.map +1 -1
  20. package/lib/shared/builtin-templates.js +155 -0
  21. package/lib/shared/builtin-templates.js.map +1 -0
  22. package/lib/shared/protocol.js +39 -2
  23. package/lib/shared/protocol.js.map +1 -1
  24. package/package.json +90 -90
  25. package/src/client/api.ts +5 -1
  26. package/src/client/board/TaskBoard.tsx +16 -11
  27. package/src/client/board/TaskDetail.tsx +116 -10
  28. package/src/client/board/TemplateManager.tsx +22 -10
  29. package/src/client/controller.ts +23 -3
  30. package/src/client/i18n/en.ts +18 -0
  31. package/src/client/i18n/templates.ts +25 -0
  32. package/src/client/i18n/zh.ts +18 -0
  33. package/src/client/styles.ts +1 -1
  34. package/src/host/archive-sessions.ts +18 -0
  35. package/src/host/execution.ts +4 -1
  36. package/src/host/locale.ts +44 -0
  37. package/src/host/routes.ts +40 -7
  38. package/src/host/scheduler.ts +2 -0
  39. package/src/host/session-sync.ts +4 -1
  40. package/src/host/templates.ts +8 -59
  41. package/src/host/tools.ts +20 -7
  42. package/src/shared/api.ts +6 -3
  43. package/src/shared/builtin-templates.ts +153 -0
  44. package/src/shared/protocol.ts +64 -0
  45. package/src/shared/version.ts +9 -9
@@ -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
  })
@@ -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
- void this.handleSessionEvent(sessionId, event, sessionMeta)
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
  })
@@ -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
- /** The built-in templates seeded when the side file does not exist yet. */
15
- export const BUILTIN_TEMPLATES: ReadonlyArray<{ id: string; name: string; task: TaskTemplate['task'] }> = [
16
- {
17
- id: 'tpl-feature',
18
- name: '新增功能',
19
- task: {
20
- title: '新增:',
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
@@ -162,6 +164,8 @@ export interface WorkspaceFace {
162
164
  get(id: string): { id: string; path: string; title: string } | undefined
163
165
  /** List all workspaces. */
164
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>
165
169
  }
166
170
 
167
171
  /** Adapt the real registry to the narrow face. */
@@ -178,6 +182,9 @@ export function workspaceFace(registry: WorkspaceRegistry): WorkspaceFace {
178
182
  return ws === undefined ? undefined : { id: ws.id, path: ws.path, title: ws.title }
179
183
  },
180
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
+ : {}),
181
188
  }
182
189
  }
183
190
 
@@ -411,7 +418,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
411
418
  output: {
412
419
  schema: JSON_OUT,
413
420
  render: (_args, value) => {
414
- 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 }
415
422
  const t = v.task
416
423
  return [{ type: 'text', text: t === undefined ? '创建失败。' : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。` }]
417
424
  },
@@ -501,7 +508,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
501
508
  output: {
502
509
  schema: JSON_OUT,
503
510
  render: (_args, value) => {
504
- 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 }
505
512
  const t = v.task
506
513
  return [{ type: 'text', text: t === undefined ? '更新失败。' : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。` }]
507
514
  },
@@ -554,16 +561,17 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
554
561
  id: { type: 'string', required: true, description: 'Task id.' },
555
562
  status: { type: 'string', required: true, description: 'Target status.' },
556
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.' },
557
565
  },
558
566
  output: {
559
567
  schema: JSON_OUT,
560
568
  render: (_args, value) => {
561
- 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 }
562
570
  const t = v.task
563
- 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)}`}` }]
564
572
  },
565
573
  },
566
- 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) {
567
575
  try {
568
576
  const { actor } = caller(exec as ToolRunContext)
569
577
  const to = asStatus(args.status)
@@ -575,9 +583,11 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
575
583
  : undefined
576
584
  // R1: every state guard + the write itself run inside the mutation.
577
585
  let next: TaskRecord | undefined
586
+ let beforeTask: TaskRecord | undefined
578
587
  await store.mutate('task-moved', ledger => {
579
588
  const { index, task } = liveTaskAt(ledger, args.id)
580
589
  versionGuard(task, args.ifVersion)
590
+ beforeTask = task
581
591
 
582
592
  // Code-level gate: agents never complete a task.
583
593
  if (to === 'done') {
@@ -607,7 +617,10 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
607
617
  ledger.tasks[index] = next
608
618
  return [next]
609
619
  })
610
- return json({ task: summarize(next!) })
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 } : {}) })
611
624
  } catch (error) { fail(error) }
612
625
  },
613
626
  })) as () => void)
@@ -929,4 +942,4 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
929
942
  })) as () => void)
930
943
 
931
944
  return disposers
932
- }
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
+ }
@@ -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.6.7'