@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.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 (83) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -18
  3. package/README.zh.md +34 -18
  4. package/lib/client.js +2804 -1024
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2576 -416
  7. package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
  9. package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
  10. package/lib/types/client/board/model-options.d.ts +67 -0
  11. package/lib/types/client/board/preset-label.d.ts +21 -0
  12. package/lib/types/client/board/preset-roster.d.ts +45 -0
  13. package/lib/types/client/board/team-run-promotion.d.ts +26 -0
  14. package/lib/types/client/index.d.ts +4 -5
  15. package/lib/types/client/locales.d.ts +81 -0
  16. package/lib/types/client/plugin-card-seat.d.ts +5 -0
  17. package/lib/types/client/schedule-zone.d.ts +50 -0
  18. package/lib/types/core/card-defaults.d.ts +78 -0
  19. package/lib/types/core/controller.d.ts +41 -17
  20. package/lib/types/core/launch-width.d.ts +31 -0
  21. package/lib/types/core/poll-cadence.d.ts +31 -0
  22. package/lib/types/core/reasoning-effort.d.ts +57 -0
  23. package/lib/types/core/subtask.d.ts +30 -8
  24. package/lib/types/core/tasks.d.ts +163 -14
  25. package/lib/types/core/use-cases/task-create.d.ts +10 -5
  26. package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
  27. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  28. package/lib/types/core/verification-budget.d.ts +52 -0
  29. package/lib/types/core/verification.d.ts +376 -9
  30. package/lib/types/host/launch-admission.d.ts +72 -0
  31. package/lib/types/host/verification-gate.d.ts +24 -0
  32. package/lib/types/host/verification-runner.d.ts +38 -7
  33. package/lib/types/host-ledger.d.ts +91 -11
  34. package/lib/types/host-runner.d.ts +111 -13
  35. package/lib/types/host-service.d.ts +199 -8
  36. package/lib/types/index.d.ts +141 -0
  37. package/lib/types/protocol.d.ts +63 -12
  38. package/package.json +29 -33
  39. package/src/client/CardDefaultPresetField.tsx +101 -0
  40. package/src/client/TaskBoardSettingsCard.tsx +288 -17
  41. package/src/client/board/LinkSubtaskModal.tsx +8 -0
  42. package/src/client/board/NewTaskModal.tsx +237 -60
  43. package/src/client/board/ReasoningEffortField.tsx +50 -0
  44. package/src/client/board/TaskCard.tsx +3 -3
  45. package/src/client/board/TaskDetail.tsx +261 -24
  46. package/src/client/board/VerificationReport.tsx +132 -13
  47. package/src/client/board/model-options.ts +178 -0
  48. package/src/client/board/preset-label.ts +32 -0
  49. package/src/client/board/preset-roster.ts +58 -0
  50. package/src/client/board/status-key.ts +3 -0
  51. package/src/client/board/team-run-promotion.ts +39 -0
  52. package/src/client/index.ts +105 -108
  53. package/src/client/locales.ts +171 -6
  54. package/src/client/plugin-card-seat.ts +7 -0
  55. package/src/client/schedule-zone.ts +100 -2
  56. package/src/client/settings-entry-form.ts +0 -1
  57. package/src/core/card-defaults.ts +132 -0
  58. package/src/core/controller.ts +55 -16
  59. package/src/core/launch-width.ts +38 -0
  60. package/src/core/poll-cadence.ts +41 -0
  61. package/src/core/reasoning-effort.ts +92 -0
  62. package/src/core/store.ts +69 -15
  63. package/src/core/subtask.ts +48 -13
  64. package/src/core/tasks.ts +213 -39
  65. package/src/core/use-cases/task-create.ts +57 -25
  66. package/src/core/use-cases/task-schedule.ts +190 -25
  67. package/src/core/use-cases/task-update.ts +9 -4
  68. package/src/core/verification-budget.ts +115 -0
  69. package/src/core/verification.ts +785 -17
  70. package/src/host/agent-tools.ts +127 -29
  71. package/src/host/launch-admission.ts +140 -0
  72. package/src/host/verification-gate.ts +172 -16
  73. package/src/host/verification-runner.ts +207 -40
  74. package/src/host-ledger.ts +346 -48
  75. package/src/host-runner.ts +234 -35
  76. package/src/host-service.ts +539 -52
  77. package/src/index.ts +250 -7
  78. package/src/protocol.ts +160 -32
  79. package/lib/invariant.js +0 -6
  80. package/lib/types/client/board-card-seat.d.ts +0 -34
  81. package/lib/types/invariant.d.ts +0 -3
  82. package/src/client/board-card-seat.ts +0 -102
  83. package/src/invariant.ts +0 -4
package/src/protocol.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
2
- import { TAG_NAME_MAX_LENGTH, isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
2
+ import type { SetSchedulePatch } from './core/use-cases/task-schedule.ts'
3
+ import { SCHEDULE_STOP_REASONS, TAG_NAME_MAX_LENGTH, isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type ScheduleStopReason, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
3
4
  import { parseLedger } from './core/store.ts'
4
5
  import { isValidTimeZone } from './core/schedule.ts'
5
6
  import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
@@ -10,26 +11,42 @@ import { normalizeVerification, type ModelCatalogView, type VerificationContract
10
11
  /** Freeze payload carried by create/update actions after the gate (redacted in place). */
11
12
  type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
12
13
 
13
- export const TASK_BOARD_SCHEMA_VERSION = 5 as const
14
- /** Ledger documents written before v5; loaded once and migrated on startup. */
15
- export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 4 as const
14
+ export const TASK_BOARD_SCHEMA_VERSION = 6 as const
15
+ /** Ledger documents written before v6; loaded once and migrated on startup. */
16
+ export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 5 as const
17
+ /** Ledger documents written before v5; migrated through the v5 normalization too. */
18
+ export const TASK_BOARD_PRE_ACCEPTANCE_SCHEMA_VERSION = 4 as const
16
19
  /** Ledger documents written before v4; migrated through the v4 normalization too. */
17
20
  export const TASK_BOARD_OLDER_SCHEMA_VERSION = 3 as const
18
21
  /** Ledger documents written before v3; migrated through every later normalization too. */
19
22
  export const TASK_BOARD_OLDEST_SCHEMA_VERSION = 2 as const
20
23
  /**
21
- * Every older generation the loader migrates in place. v5 only ADDS the
22
- * per-execution acceptance block, so the migration is the normalization pass
23
- * that re-validates each row; a document from any listed generation upgrades
24
- * losslessly.
24
+ * The last generation whose rules still need the Host zone stamped on them at
25
+ * migration: v4 introduced the stored zone but a rule written without one must
26
+ * stop following a later TZ change. A v5 rule already carries its zone (or
27
+ * deliberately follows the Host), so it is NOT stamped again.
28
+ */
29
+ export const TASK_BOARD_ZONE_STAMP_SCHEMA_VERSION = 4 as const
30
+ /**
31
+ * Every older generation the loader migrates in place. v5 only added the
32
+ * per-execution acceptance block and v6 only added the scheduling mode and run
33
+ * budget, so the migration is the normalization pass that re-validates each
34
+ * row; a document from any listed generation upgrades losslessly.
25
35
  */
26
36
  export const TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS: readonly number[] = [
27
37
  TASK_BOARD_LEGACY_SCHEMA_VERSION,
38
+ TASK_BOARD_PRE_ACCEPTANCE_SCHEMA_VERSION,
28
39
  TASK_BOARD_OLDER_SCHEMA_VERSION,
29
40
  TASK_BOARD_OLDEST_SCHEMA_VERSION,
30
41
  ]
31
42
  export const TASK_BOARD_API_PREFIX = '/api/task-board'
32
43
 
44
+ /** Longest accepted caller identity on an external outcome record. */
45
+ export const EXTERNAL_INITIATOR_MAX_LENGTH = 200
46
+
47
+ /** Longest accepted free-text summary on an external outcome record. */
48
+ export const EXTERNAL_SUMMARY_MAX_LENGTH = 1_000
49
+
33
50
  export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
34
51
 
35
52
  export interface TaskBoardPowerSnapshot {
@@ -68,6 +85,22 @@ export interface TaskBoardSnapshot {
68
85
  * into team execution (Team Lead session plus one teammate per subtask).
69
86
  */
70
87
  teamRunAvailable?: boolean
88
+ /**
89
+ * Live GLOBAL native-/goal switch. The browser uses it to disable the
90
+ * task-level option (and explain that the global switch must be turned on
91
+ * first) WITHOUT rewriting anyone's stored per-task preference; the Host
92
+ * freezes its own read per execution, so this mirror never governs a run in
93
+ * flight.
94
+ */
95
+ goalRunEnabled?: boolean
96
+ /**
97
+ * New-card defaults this deployment configures: what the create dialog opens
98
+ * with, and what the create path fills an omitted target from when the caller
99
+ * opts in (`applyCardDefaults`). A target the board configures nothing for is
100
+ * omitted rather than published blank, so the browser can tell "no default
101
+ * model" from "a blank one".
102
+ */
103
+ cardDefaults?: { mode?: string; permission?: TaskPermission; model?: string; reasoningEffort?: string }
71
104
  /**
72
105
  * Read-only summaries published by running extensions, keyed by extension id.
73
106
  * The board forwards them verbatim; it never interprets their shape.
@@ -139,7 +172,18 @@ export type TaskBoardAction =
139
172
  | { kind: 'archive'; taskId: string }
140
173
  | { kind: 'restore'; taskId: string }
141
174
  | { kind: 'settle'; taskId: string }
142
- | { kind: 'set-schedule'; taskId: string; patch: { enabled?: boolean; cron?: string; timeZone?: string | null } }
175
+ /** Clear the acceptance anomalies recorded on a card's open execution (issue #1828). */
176
+ | { kind: 'reset-verification'; taskId: string }
177
+ /**
178
+ * Record an outcome an agent OUTSIDE the Host produced (issue #1826).
179
+ *
180
+ * It writes a real execution record, so the terminal column and the run
181
+ * history both derive from that one record instead of a bare column edit.
182
+ * `cancelled` is deliberately absent: nothing outside a DSH session may
183
+ * claim the Host closed a run, and `running` must stay Host-only.
184
+ */
185
+ | { kind: 'record-external-outcome'; taskId: string; result: 'succeeded' | 'failed'; initiatedBy: string; summary?: string }
186
+ | { kind: 'set-schedule'; taskId: string; patch: SetSchedulePatch }
143
187
  | { kind: 'run'; taskId: string }
144
188
  | { kind: 'rerun'; taskId: string }
145
189
  | { kind: 'confirm-permission'; taskId: string }
@@ -187,19 +231,51 @@ function optionalFiniteNumber(value: unknown): boolean {
187
231
  return value === undefined || (typeof value === 'number' && Number.isFinite(value))
188
232
  }
189
233
 
234
+ /** Whether a stop-reason field names a reason this build understands. */
235
+ function validStopReason(value: unknown): boolean {
236
+ return value === undefined || (typeof value === 'string' && SCHEDULE_STOP_REASONS.includes(value as ScheduleStopReason))
237
+ }
238
+
239
+ /**
240
+ * Whether an imported schedule rule is structurally usable. A one-shot must
241
+ * carry its planned instant and a recurring rule its cron expression; the
242
+ * counter and stop record are optional (a rule written before them simply has
243
+ * none), but a value that IS present must be usable so the import cannot smuggle
244
+ * a rule the scheduler would misread.
245
+ */
246
+ function validImportedSchedule(value: unknown): boolean {
247
+ const schedule = record(value)
248
+ if (schedule === undefined || typeof schedule.enabled !== 'boolean') return false
249
+ if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
250
+ if (schedule.mode === 'once') {
251
+ if (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at)) return false
252
+ } else if (typeof schedule.cron !== 'string') {
253
+ return false
254
+ }
255
+ if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
256
+ if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at))) return false
257
+ if (schedule.maxRuns !== undefined
258
+ && (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
259
+ if (schedule.runCount !== undefined
260
+ && (typeof schedule.runCount !== 'number' || !Number.isInteger(schedule.runCount) || schedule.runCount < 0)) return false
261
+ if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
262
+ if (!optionalFiniteNumber(schedule.endedAt) || !optionalFiniteNumber(schedule.skippedAt)) return false
263
+ if (!validStopReason(schedule.endedReason) || !validStopReason(schedule.skippedReason)) return false
264
+ if (schedule.timeZone !== undefined
265
+ && (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
266
+ return true
267
+ }
268
+
190
269
  function validImportedKnownFields(value: Record<string, unknown>): boolean {
191
270
  // Tags are validated (not repaired) on the import path: an imported task
192
271
  // must carry a well-formed list or none at all, so a hand-edited export
193
272
  // cannot smuggle a malformed tag past the gate.
194
273
  if (value.tags !== undefined && !isTaskTagList(value.tags)) return false
274
+ // The model pin is carried by the import (with the level bound to it), so a
275
+ // non-string value is refused here like the other carried known fields.
276
+ if (value.model !== undefined && typeof value.model !== 'string') return false
195
277
  if (value.integrations !== undefined && normalizeTaskIntegrations(value.integrations) === undefined) return false
196
- if (value.schedule !== undefined) {
197
- const schedule = record(value.schedule)
198
- if (schedule === undefined || typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
199
- if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
200
- if (schedule.timeZone !== undefined
201
- && (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
202
- }
278
+ if (value.schedule !== undefined && !validImportedSchedule(value.schedule)) return false
203
279
  if (value.executions !== undefined) {
204
280
  if (!Array.isArray(value.executions)) return false
205
281
  for (const item of value.executions) {
@@ -209,6 +285,10 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
209
285
  if (!optionalFiniteNumber(execution.endedAt) || !optionalString(execution.error)) return false
210
286
  if (execution.result !== undefined && !['succeeded', 'failed', 'cancelled'].includes(String(execution.result))) return false
211
287
  if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
288
+ // An imported external marker is accepted (it is a declaration an
289
+ // operator made about their own board), but it is still just a boolean:
290
+ // the acceptance block below is what must never arrive from an import.
291
+ if (execution.external !== undefined && typeof execution.external !== 'boolean') return false
212
292
  if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
213
293
  if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
214
294
  // A well-formed acceptance block may be imported for inspection, but the
@@ -241,26 +321,31 @@ function importedTask(value: unknown): TaskRecord | undefined {
241
321
  result: execution.result,
242
322
  error: execution.error,
243
323
  ...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
324
+ ...(execution.external === true ? { external: true } : {}),
244
325
  ...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
245
326
  ...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
246
327
  // 安全门(对抗场景 d):验收报告是 Host 的判定,不接受 import 携带——
247
328
  // 一条伪造的通过记录会让导入的执行被当成已验收。
248
329
  })),
249
- ...(task.schedule === undefined ? {} : {
250
- schedule: {
251
- enabled: task.schedule.enabled,
252
- cron: task.schedule.cron,
253
- ...(task.schedule.timeZone === undefined ? {} : { timeZone: task.schedule.timeZone }),
254
- nextRunAt: task.schedule.nextRunAt,
255
- lastTriggeredAt: task.schedule.lastTriggeredAt,
256
- },
257
- }),
330
+ // The rule was already reconstructed field by field by parseLedger (which
331
+ // is what drops an unknown or malformed field), so the import keeps that
332
+ // normalized shape: mode, planned instant, run counter and stop record all
333
+ // travel with it.
334
+ ...(task.schedule === undefined ? {} : { schedule: { ...task.schedule } }),
258
335
  // The lineage link rides the import; the Host repairs a link whose parent
259
336
  // is missing from the merged ledger instead of trusting the export.
260
337
  ...(task.parentId === undefined ? {} : { parentId: task.parentId }),
261
338
  ...(task.workspaceId === undefined ? {} : { workspaceId: task.workspaceId }),
262
339
  ...(task.mode === undefined ? {} : { mode: task.mode }),
340
+ // The model pin rides the import together with the level bound to it: the
341
+ // two are one pair, and an imported level without its model would run at
342
+ // whatever route the session happens to be on. Blank text means "no pin",
343
+ // like every other target, so it is not carried as a pin of its own.
344
+ ...(typeof task.model !== 'string' || task.model.trim() === '' ? {} : { model: task.model }),
263
345
  ...(task.permission === undefined ? {} : { permission: task.permission }),
346
+ // The reasoning effort rides the import like the other execution targets: it
347
+ // is the user's own card preference, not a verdict.
348
+ ...(task.reasoningEffort === undefined ? {} : { reasoningEffort: task.reasoningEffort }),
264
349
  ...(task.reuseSession === undefined ? {} : { reuseSession: task.reuseSession }),
265
350
  ...(task.goalRun === undefined ? {} : { goalRun: task.goalRun }),
266
351
  // The per-card acceptance opt-out rides the import like the /goal opt-in:
@@ -306,10 +391,15 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
306
391
 
307
392
  function createInput(value: unknown): value is NewTaskInput {
308
393
  const input = record(value)
309
- if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags', 'integrations'])) return false
394
+ if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reasoningEffort', 'applyCardDefaults', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags', 'integrations'])) return false
310
395
  if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
311
396
  if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
312
- if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
397
+ // The reasoning effort is an adapter-owned id the Host resolves against the
398
+ // live model catalog, so this gate only keeps a malformed value (a number, an
399
+ // object, null) off the ledger; blank text means "inherit" like every other
400
+ // pinned target.
401
+ if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model) || !optionalString(input.reasoningEffort)) return false
402
+ if (input.applyCardDefaults !== undefined && typeof input.applyCardDefaults !== 'boolean') return false
313
403
  if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
314
404
  if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
315
405
  if (input.goalRun !== undefined && typeof input.goalRun !== 'boolean') return false
@@ -321,8 +411,13 @@ function createInput(value: unknown): value is NewTaskInput {
321
411
  if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
322
412
  if (input.schedule !== undefined) {
323
413
  const schedule = record(input.schedule)
324
- if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron', 'timeZone'])) return false
325
- if (typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
414
+ if (schedule === undefined || !exactKeys(schedule, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])) return false
415
+ if (typeof schedule.enabled !== 'boolean') return false
416
+ if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
417
+ if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
418
+ if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isInteger(schedule.at))) return false
419
+ if (schedule.maxRuns !== undefined
420
+ && (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
326
421
  if (schedule.timeZone !== undefined
327
422
  && (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
328
423
  }
@@ -331,7 +426,7 @@ function createInput(value: unknown): value is NewTaskInput {
331
426
 
332
427
  function updatePatch(value: unknown): boolean {
333
428
  const patch = record(value)
334
- if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags'])) return false
429
+ if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reasoningEffort', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags'])) return false
335
430
  // null (or false) clears the reuse opt-in; only a real boolean is accepted.
336
431
  if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
337
432
  if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
@@ -341,7 +436,7 @@ function updatePatch(value: unknown): boolean {
341
436
  // The acceptance opt-out is the mirror tri-state: null/false return the card
342
437
  // to inheriting the board-wide switch, true pins it out of the gate.
343
438
  if (patch.skipVerification !== undefined && patch.skipVerification !== null && typeof patch.skipVerification !== 'boolean') return false
344
- for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
439
+ for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model', 'reasoningEffort'] as const) {
345
440
  if (!optionalString(patch[key])) return false
346
441
  }
347
442
  if (patch.permission !== undefined && !isTaskPermission(patch.permission)) return false
@@ -365,13 +460,21 @@ function isTagName(value: unknown): value is string {
365
460
  function schedulePatch(value: unknown): boolean {
366
461
  const patch = record(value)
367
462
  return patch !== undefined
368
- && exactKeys(patch, ['enabled', 'cron', 'timeZone'])
463
+ && exactKeys(patch, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])
369
464
  && (patch.enabled === undefined || typeof patch.enabled === 'boolean')
465
+ && (patch.mode === undefined || patch.mode === 'cron' || patch.mode === 'once')
370
466
  && (patch.cron === undefined || typeof patch.cron === 'string')
467
+ // A one-shot's planned instant is a whole millisecond: a fractional or
468
+ // non-numeric value would make the armed target ambiguous.
469
+ && (patch.at === undefined || (typeof patch.at === 'number' && Number.isInteger(patch.at)))
371
470
  // `null` clears the stored zone back to the Host zone; an unknown name is
372
471
  // rejected here so the Host never has to guess at an unusable zone.
373
472
  && (patch.timeZone === undefined || patch.timeZone === null
374
473
  || (typeof patch.timeZone === 'string' && isValidTimeZone(patch.timeZone)))
474
+ // `null` clears the run cap back to unlimited; any other value must be a
475
+ // positive whole number of runs.
476
+ && (patch.maxRuns === undefined || patch.maxRuns === null
477
+ || (typeof patch.maxRuns === 'number' && Number.isInteger(patch.maxRuns) && patch.maxRuns >= 1))
375
478
  }
376
479
 
377
480
  export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
@@ -472,11 +575,36 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
472
575
  },
473
576
  }
474
577
  }
578
+ case 'record-external-outcome': {
579
+ if (!exactKeys(action, ['kind', 'taskId', 'result', 'initiatedBy', 'summary'])) return undefined
580
+ if (taskId === undefined) return undefined
581
+ // Only a real outside verdict is accepted: `running` is Host-only and
582
+ // `cancelled` is the Host's own bookkeeping for a run it closed.
583
+ if (action.result !== 'succeeded' && action.result !== 'failed') return undefined
584
+ if (typeof action.initiatedBy !== 'string') return undefined
585
+ const initiatedBy = action.initiatedBy.trim()
586
+ // The caller must be identifiable: an unattributed external outcome is
587
+ // the same 'state without history' the record exists to avoid.
588
+ if (initiatedBy === '' || initiatedBy.length > EXTERNAL_INITIATOR_MAX_LENGTH) return undefined
589
+ if (action.summary !== undefined && typeof action.summary !== 'string') return undefined
590
+ const summary = typeof action.summary === 'string' ? action.summary.trim().slice(0, EXTERNAL_SUMMARY_MAX_LENGTH) : ''
591
+ return {
592
+ requestId: envelope.requestId,
593
+ action: {
594
+ kind: 'record-external-outcome',
595
+ taskId,
596
+ result: action.result,
597
+ initiatedBy,
598
+ ...(summary === '' ? {} : { summary }),
599
+ },
600
+ }
601
+ }
475
602
  case 'confirm-permission':
476
603
  case 'delete':
477
604
  case 'archive':
478
605
  case 'restore':
479
606
  case 'settle':
607
+ case 'reset-verification':
480
608
  case 'run':
481
609
  case 'rerun':
482
610
  if (!exactKeys(action, ['kind', 'taskId'])) return undefined
package/lib/invariant.js DELETED
@@ -1,6 +0,0 @@
1
- //#region src/invariant.ts
2
- /** Invariant companion plugin (no assertions — nothing to check at runtime). */
3
- /** Provides no assertions: the board owns no cross-package runtime invariants. */
4
- function apply() {}
5
- //#endregion
6
- export { apply };
@@ -1,34 +0,0 @@
1
- /**
2
- * Board-owned variant of the family plugin-card seat that can declare child
3
- * slots on the registration.
4
- *
5
- * The shared `installPluginCard` helper owns seat selection but cannot forward
6
- * a `children` declaration, and the task board's settings card declares the
7
- * `task-board.settings.section` seat a provider renders into. This wrapper
8
- * reuses the shared seat decision (`familyGroupLoaded` and the two seat keys)
9
- * and adds the declaration; it should collapse back into the shared helper once
10
- * that helper accepts children.
11
- *
12
- * @module dsh-task-board/client/board-card-seat
13
- */
14
- import { type PluginCardContext } from './plugin-card-seat.ts';
15
- /** One board card contribution, with the child seats it declares. */
16
- export interface BoardCardSeat {
17
- bundle: string;
18
- id: string;
19
- order?: number;
20
- label?: () => string;
21
- locale: string;
22
- /** Child slots this entry declares and renders. */
23
- children: Record<string, unknown>;
24
- inject?: () => object;
25
- component: unknown;
26
- }
27
- /**
28
- * Contribute the board's settings card to the seat this host renders, declaring
29
- * the child slots it owns. Mirrors the shared helper's reconcile discipline: the
30
- * entry moves when the family group appears, and is never in two seats at once.
31
- * @param ctx - client context (its slot registry decides the seat).
32
- * @param seat - the card contribution and its children declaration.
33
- */
34
- export declare function installBoardCard(ctx: PluginCardContext, seat: BoardCardSeat): void;
@@ -1,3 +0,0 @@
1
- /** Invariant companion plugin (no assertions — nothing to check at runtime). */
2
- /** Provides no assertions: the board owns no cross-package runtime invariants. */
3
- export declare function apply(): void;
@@ -1,102 +0,0 @@
1
- /**
2
- * Board-owned variant of the family plugin-card seat that can declare child
3
- * slots on the registration.
4
- *
5
- * The shared `installPluginCard` helper owns seat selection but cannot forward
6
- * a `children` declaration, and the task board's settings card declares the
7
- * `task-board.settings.section` seat a provider renders into. This wrapper
8
- * reuses the shared seat decision (`familyGroupLoaded` and the two seat keys)
9
- * and adds the declaration; it should collapse back into the shared helper once
10
- * that helper accepts children.
11
- *
12
- * @module dsh-task-board/client/board-card-seat
13
- */
14
- import {
15
- FAMILY_PLUGIN_CARD_SEAT,
16
- OFFICIAL_PLUGIN_CARD_SEAT,
17
- familyGroupLoaded,
18
- type PluginCardContext,
19
- } from './plugin-card-seat.ts'
20
-
21
- /** One board card contribution, with the child seats it declares. */
22
- export interface BoardCardSeat {
23
- bundle: string
24
- id: string
25
- order?: number
26
- label?: () => string
27
- locale: string
28
- /** Child slots this entry declares and renders. */
29
- children: Record<string, unknown>
30
- inject?: () => object
31
- component: unknown
32
- }
33
-
34
- function warnRefusedSeat(seat: string, error: unknown): void {
35
- try {
36
- console.warn(`[dsh-web] plugin card registration into "${seat}" was refused; the card will not render`, error)
37
- } catch {
38
- // Best-effort console write; a failed warning must not break the plugin.
39
- }
40
- }
41
-
42
- /**
43
- * Contribute the board's settings card to the seat this host renders, declaring
44
- * the child slots it owns. Mirrors the shared helper's reconcile discipline: the
45
- * entry moves when the family group appears, and is never in two seats at once.
46
- * @param ctx - client context (its slot registry decides the seat).
47
- * @param seat - the card contribution and its children declaration.
48
- */
49
- export function installBoardCard(ctx: PluginCardContext, seat: BoardCardSeat): void {
50
- const slots = ctx.slots
51
- const component = seat.component as never
52
- const inject = seat.inject as never
53
-
54
- let dispose: (() => void) | undefined
55
- let current: string | undefined
56
- let reconciling = false
57
-
58
- const reconcile = (): void => {
59
- if (reconciling) return
60
- const target = familyGroupLoaded(ctx) ? FAMILY_PLUGIN_CARD_SEAT : OFFICIAL_PLUGIN_CARD_SEAT
61
- if (current === target) return
62
- reconciling = true
63
- const previous = dispose
64
- dispose = undefined
65
- current = undefined
66
- previous?.()
67
- try {
68
- dispose = slots.register((target === FAMILY_PLUGIN_CARD_SEAT
69
- ? {
70
- name: FAMILY_PLUGIN_CARD_SEAT,
71
- id: seat.id,
72
- ...(seat.order === undefined ? {} : { order: seat.order }),
73
- ...(seat.label === undefined ? {} : { label: seat.label }),
74
- locale: seat.locale,
75
- children: seat.children,
76
- ...(seat.inject === undefined ? {} : { inject }),
77
- }
78
- : {
79
- name: OFFICIAL_PLUGIN_CARD_SEAT,
80
- key: seat.bundle,
81
- locale: seat.locale,
82
- children: seat.children,
83
- ...(seat.inject === undefined ? {} : { inject }),
84
- }) as never, component) as () => void
85
- current = target
86
- } catch (error) {
87
- warnRefusedSeat(target, error)
88
- } finally {
89
- reconciling = false
90
- }
91
- }
92
-
93
- if (typeof ctx.on === 'function') {
94
- try {
95
- ctx.on('slots/changed', () => { reconcile() })
96
- } catch {
97
- // An event seat that refuses the subscription leaves the initial
98
- // decision in place; the card still renders in the seat chosen below.
99
- }
100
- }
101
- reconcile()
102
- }
package/src/invariant.ts DELETED
@@ -1,4 +0,0 @@
1
- /** Invariant companion plugin (no assertions — nothing to check at runtime). */
2
-
3
- /** Provides no assertions: the board owns no cross-package runtime invariants. */
4
- export function apply(): void {}