@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.
- package/README.i18n.yaml +2 -2
- package/README.md +34 -18
- package/README.zh.md +34 -18
- package/lib/client.js +2804 -1024
- package/lib/client.js.map +1 -1
- package/lib/index.js +2576 -416
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +81 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +41 -17
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +163 -14
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +91 -11
- package/lib/types/host-runner.d.ts +111 -13
- package/lib/types/host-service.d.ts +199 -8
- package/lib/types/index.d.ts +141 -0
- package/lib/types/protocol.d.ts +63 -12
- package/package.json +29 -33
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +288 -17
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +237 -60
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +261 -24
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/status-key.ts +3 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +105 -108
- package/src/client/locales.ts +171 -6
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +55 -16
- package/src/core/launch-width.ts +38 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +69 -15
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +213 -39
- package/src/core/use-cases/task-create.ts +57 -25
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +9 -4
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +127 -29
- package/src/host/launch-admission.ts +140 -0
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +346 -48
- package/src/host-runner.ts +234 -35
- package/src/host-service.ts +539 -52
- package/src/index.ts +250 -7
- package/src/protocol.ts +160 -32
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- 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
|
|
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 =
|
|
14
|
-
/** Ledger documents written before
|
|
15
|
-
export const TASK_BOARD_LEGACY_SCHEMA_VERSION =
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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'
|
|
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,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;
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -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