@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.6
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 +20 -12
- package/README.zh.md +20 -12
- package/lib/client.js +1327 -229
- package/lib/client.js.map +1 -1
- package/lib/index.js +1909 -348
- package/lib/types/client/TaskBoardSettingsCard.d.ts +12 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +65 -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/controller.d.ts +10 -11
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +144 -14
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- 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 +82 -7
- package/lib/types/host-runner.d.ts +32 -9
- package/lib/types/host-service.d.ts +117 -9
- package/lib/types/index.d.ts +54 -0
- package/lib/types/protocol.d.ts +50 -12
- package/package.json +2 -6
- package/src/client/TaskBoardSettingsCard.tsx +74 -1
- package/src/client/board/NewTaskModal.tsx +151 -20
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +219 -18
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/status-key.ts +3 -0
- package/src/client/index.ts +43 -50
- package/src/client/locales.ts +137 -4
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/core/controller.ts +12 -8
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +64 -15
- package/src/core/tasks.ts +193 -39
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +3 -3
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +106 -21
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +328 -42
- package/src/host-runner.ts +52 -19
- package/src/host-service.ts +299 -36
- package/src/index.ts +101 -5
- package/src/protocol.ts +132 -28
- 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/index.ts
CHANGED
|
@@ -21,6 +21,17 @@ import { TaskBoardHostService, type HostTimerFace, type TaskBoardTeamDispatcher
|
|
|
21
21
|
import { parseTaskDraft, TaskParseError } from './host-ai.ts'
|
|
22
22
|
import { TASK_PERMISSIONS, isTaskPermission, type TaskPermission } from './core/tasks.ts'
|
|
23
23
|
import { DEFAULT_SUBTASK_DEPTH, SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from './core/subtask.ts'
|
|
24
|
+
import { DEFAULT_SESSION_POLL_SECONDS, SESSION_POLL_MAX_SECONDS, SESSION_POLL_MIN_SECONDS } from './core/poll-cadence.ts'
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_VERIFICATION_BUDGET_SECONDS,
|
|
27
|
+
DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS,
|
|
28
|
+
VERIFICATION_BUDGET_MAX_SECONDS,
|
|
29
|
+
VERIFICATION_BUDGET_MIN_SECONDS,
|
|
30
|
+
VERIFICATION_CALL_TIMEOUT_MAX_SECONDS,
|
|
31
|
+
VERIFICATION_CALL_TIMEOUT_MIN_SECONDS,
|
|
32
|
+
verificationBudgetMs,
|
|
33
|
+
verificationCallTimeoutMs,
|
|
34
|
+
} from './core/verification-budget.ts'
|
|
24
35
|
import { DEFAULT_SESSION_PERMISSION } from './core/handover.ts'
|
|
25
36
|
import { buildTaskBoardTools, TASK_BOARD_TOOL_NAMES } from './host/agent-tools.ts'
|
|
26
37
|
import { PLUGIN_TOOL_SECTION_ORDERS, visibleToolText } from './tool-surface.ts'
|
|
@@ -88,12 +99,35 @@ export interface Config {
|
|
|
88
99
|
* of the root task opens, so the ceiling is deliberately small.
|
|
89
100
|
*/
|
|
90
101
|
maxSubtaskDepth?: Volatile<number>
|
|
102
|
+
/**
|
|
103
|
+
* How often, in seconds, the Host re-reads the DSH session roster while the
|
|
104
|
+
* board has something to reconcile: a running card, an open execution, or an
|
|
105
|
+
* active provider extension. An idle board with none of those does not poll
|
|
106
|
+
* at all, so this cadence only bounds how quickly a running card settles.
|
|
107
|
+
*/
|
|
108
|
+
sessionPollSeconds?: Volatile<number>
|
|
91
109
|
/**
|
|
92
110
|
* Continuable-subagent provider the Agent Teams service uses to compose a
|
|
93
111
|
* teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
|
|
94
112
|
* only team-mode runs use it.
|
|
95
113
|
*/
|
|
96
114
|
teamProvider?: string
|
|
115
|
+
/**
|
|
116
|
+
* GLOBAL native /goal switch (default OFF), the master control over whether
|
|
117
|
+
* this board starts a run with dsh's built-in /goal at all. A run uses native
|
|
118
|
+
* /goal only when THIS is on AND the task's own `goalRun` is not off; while
|
|
119
|
+
* it is off every task runs one plain turn.
|
|
120
|
+
*
|
|
121
|
+
* The value is frozen when an execution starts, so toggling it only affects
|
|
122
|
+
* executions that begin afterwards and never re-writes a task's stored
|
|
123
|
+
* `goalRun` preference. A run that consequently does not execute native
|
|
124
|
+
* /goal is marked as such (its acceptance applicability is `goal-disabled`)
|
|
125
|
+
* and is never reported as a passed goal acceptance or failed for lacking a
|
|
126
|
+
* completion call. Independent of `goalVerification`; acceptance governs the
|
|
127
|
+
* completing call of a goal-form run, this switch governs whether the run
|
|
128
|
+
* becomes a goal run.
|
|
129
|
+
*/
|
|
130
|
+
goalRunEnabled?: Volatile<boolean>
|
|
97
131
|
/**
|
|
98
132
|
* Goal acceptance for executions this board starts (default ON). When on,
|
|
99
133
|
* update_goal(action: complete) inside a task execution is refused until an
|
|
@@ -119,6 +153,23 @@ export interface Config {
|
|
|
119
153
|
* sent, and the fallback to the model's own default is reported.
|
|
120
154
|
*/
|
|
121
155
|
goalVerificationReasoningEffort?: Volatile<string>
|
|
156
|
+
/**
|
|
157
|
+
* Ceiling of ONE judge request of a goal acceptance, in seconds
|
|
158
|
+
* (30..600, default 150). One acceptance issues six judge requests (three
|
|
159
|
+
* criteria times two rounds), so a ceiling that is too small turns a slow but
|
|
160
|
+
* healthy judge route into a wall of timeouts, and every timeout was charged
|
|
161
|
+
* as an acceptance anomaly. Raise it for a reasoning model reading a long
|
|
162
|
+
* trace.
|
|
163
|
+
*/
|
|
164
|
+
goalVerificationCallTimeoutSeconds?: Volatile<number>
|
|
165
|
+
/**
|
|
166
|
+
* Total ceiling of ONE goal acceptance attempt, in seconds (120..1800,
|
|
167
|
+
* default eight times the per-call default). Keep it at or above
|
|
168
|
+
* `goalVerificationCallTimeoutSeconds` times six or an acceptance can never
|
|
169
|
+
* finish: the attempt then ends early with a budget stop that judges nothing
|
|
170
|
+
* and spends no acceptance budget.
|
|
171
|
+
*/
|
|
172
|
+
goalVerificationBudgetSeconds?: Volatile<number>
|
|
122
173
|
}
|
|
123
174
|
|
|
124
175
|
/**
|
|
@@ -152,14 +203,22 @@ export interface ConfigInput {
|
|
|
152
203
|
sessionDefaultPermission?: TaskPermission
|
|
153
204
|
/** Subtask depth limit, 1..3. */
|
|
154
205
|
maxSubtaskDepth?: number
|
|
206
|
+
/** Roster-poll cadence in seconds while the board has work to reconcile. */
|
|
207
|
+
sessionPollSeconds?: number
|
|
155
208
|
/** Continuable-subagent provider the Agent Teams service composes a teammate from. */
|
|
156
209
|
teamProvider?: string
|
|
210
|
+
/** Global native /goal switch; default off, frozen per execution. */
|
|
211
|
+
goalRunEnabled?: boolean
|
|
157
212
|
/** Goal acceptance switch. */
|
|
158
213
|
goalVerification?: boolean
|
|
159
214
|
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
160
215
|
goalVerificationModel?: string
|
|
161
216
|
/** Judge reasoning effort; blank inherits the host default. */
|
|
162
217
|
goalVerificationReasoningEffort?: string
|
|
218
|
+
/** Ceiling of one judge request of a goal acceptance, in seconds. */
|
|
219
|
+
goalVerificationCallTimeoutSeconds?: number
|
|
220
|
+
/** Total ceiling of one goal acceptance attempt, in seconds. */
|
|
221
|
+
goalVerificationBudgetSeconds?: number
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
export const Config: z<ConfigInput, Config> = z.object({
|
|
@@ -170,15 +229,26 @@ export const Config: z<ConfigInput, Config> = z.object({
|
|
|
170
229
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
171
230
|
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
172
231
|
maxSubtaskDepth: z.number().min(SUBTASK_DEPTH_MIN).max(SUBTASK_DEPTH_MAX).default(DEFAULT_SUBTASK_DEPTH).volatile(),
|
|
232
|
+
sessionPollSeconds: z.number().min(SESSION_POLL_MIN_SECONDS).max(SESSION_POLL_MAX_SECONDS).default(DEFAULT_SESSION_POLL_SECONDS).volatile(),
|
|
173
233
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
234
|
+
goalRunEnabled: z.boolean().default(false).volatile(),
|
|
174
235
|
goalVerification: z.boolean().default(true).volatile(),
|
|
175
236
|
goalVerificationModel: z.string().default('').volatile(),
|
|
176
237
|
goalVerificationReasoningEffort: z.string().default('').volatile(),
|
|
238
|
+
goalVerificationCallTimeoutSeconds: z.number().min(VERIFICATION_CALL_TIMEOUT_MIN_SECONDS).max(VERIFICATION_CALL_TIMEOUT_MAX_SECONDS).default(DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS).volatile(),
|
|
239
|
+
goalVerificationBudgetSeconds: z.number().min(VERIFICATION_BUDGET_MIN_SECONDS).max(VERIFICATION_BUDGET_MAX_SECONDS).default(DEFAULT_VERIFICATION_BUDGET_SECONDS).volatile(),
|
|
177
240
|
})
|
|
178
241
|
|
|
179
242
|
/** Schema default for the goal-acceptance switch, re-read for hand-built contexts. */
|
|
180
243
|
export const DEFAULT_GOAL_VERIFICATION = true
|
|
181
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Schema default for the GLOBAL native-/goal switch, re-read for hand-built
|
|
247
|
+
* contexts. Deliberately false: a deployment that never configured it (and
|
|
248
|
+
* every card written before the option existed) runs plain single turns.
|
|
249
|
+
*/
|
|
250
|
+
export const DEFAULT_GOAL_RUN = false
|
|
251
|
+
|
|
182
252
|
declare module '@deepseek-ai/cordis' {
|
|
183
253
|
interface Events {
|
|
184
254
|
/**
|
|
@@ -419,6 +489,8 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
419
489
|
const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
|
|
420
490
|
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
421
491
|
const maxSubtaskDepth = (): number => readConfigField(config?.maxSubtaskDepth, DEFAULT_SUBTASK_DEPTH)
|
|
492
|
+
/** Current roster-poll cadence in seconds (read live: the settings card edits it in place). */
|
|
493
|
+
const sessionPollSeconds = (): number => readConfigField(config?.sessionPollSeconds, DEFAULT_SESSION_POLL_SECONDS)
|
|
422
494
|
/**
|
|
423
495
|
* The baseline the permission confirmation gate judges against: the row's own
|
|
424
496
|
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
@@ -435,6 +507,15 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
435
507
|
model: readConfigField(config?.goalVerificationModel, ''),
|
|
436
508
|
reasoningEffort: readConfigField(config?.goalVerificationReasoningEffort, ''),
|
|
437
509
|
})
|
|
510
|
+
/**
|
|
511
|
+
* The acceptance time ceilings, read live: raising one must unblock a card
|
|
512
|
+
* whose acceptance is stuck on a slow judge route without a remount, so
|
|
513
|
+
* these deliberately are NOT frozen into the per-execution contract.
|
|
514
|
+
*/
|
|
515
|
+
const verificationCallTimeoutMsLive = (): number =>
|
|
516
|
+
verificationCallTimeoutMs(readConfigField(config?.goalVerificationCallTimeoutSeconds, DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS))
|
|
517
|
+
const verificationBudgetMsLive = (): number =>
|
|
518
|
+
verificationBudgetMs(readConfigField(config?.goalVerificationBudgetSeconds, DEFAULT_VERIFICATION_BUDGET_SECONDS))
|
|
438
519
|
// The catalog reader needs the service, and the service needs the reader;
|
|
439
520
|
// the reader only runs after start(), so a late holder is enough.
|
|
440
521
|
let hostForCatalog: TaskBoardHostService | undefined
|
|
@@ -442,10 +523,17 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
442
523
|
const runner = hostForCatalog?.runner
|
|
443
524
|
return runner === undefined ? undefined : normalizeCatalog(await runner.modelCatalog())
|
|
444
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Live reader of the GLOBAL native-/goal switch. The service calls it exactly
|
|
528
|
+
* once per execution and carries the value, so a settings edit only reaches
|
|
529
|
+
* executions that start afterwards.
|
|
530
|
+
*/
|
|
531
|
+
const goalRunEnabled = (): boolean => readConfigField(config?.goalRunEnabled, DEFAULT_GOAL_RUN)
|
|
445
532
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
446
533
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
447
534
|
sessionDefaultPermission,
|
|
448
535
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
536
|
+
goalRunEnabled,
|
|
449
537
|
verificationSettings,
|
|
450
538
|
verificationCatalog,
|
|
451
539
|
timers: resolveHostTimers(ctx),
|
|
@@ -461,7 +549,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
461
549
|
hostForCatalog = host
|
|
462
550
|
// Configuration before start(): a disabled row must not take the first
|
|
463
551
|
// scheduler tick, which would roll schedules the board is not running.
|
|
464
|
-
host.setConfiguration(enabled(), preventIdleSleep())
|
|
552
|
+
host.setConfiguration(enabled(), preventIdleSleep(), sessionPollSeconds())
|
|
465
553
|
host.start()
|
|
466
554
|
|
|
467
555
|
// External providers. The board publishes its registration service so any
|
|
@@ -543,6 +631,8 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
543
631
|
// deployment records it; the acceptance degrades to the trajectory alone
|
|
544
632
|
// when it does not.
|
|
545
633
|
workspaceChanges: () => probeWorkspaceChanges(ctx),
|
|
634
|
+
callTimeoutMs: verificationCallTimeoutMsLive,
|
|
635
|
+
budgetMs: verificationBudgetMsLive,
|
|
546
636
|
logger: {
|
|
547
637
|
warn: (message: string, ...rest: unknown[]) => {
|
|
548
638
|
const logger = (ctx as { logger?: { warn?: (...args: unknown[]) => void } }).logger
|
|
@@ -586,7 +676,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
586
676
|
}, 'task-board: host ledger, scheduler, and routes')
|
|
587
677
|
|
|
588
678
|
let disposeSection: (() => void) | undefined
|
|
589
|
-
let applied: { enabled: boolean; announceToAgent: boolean; preventIdleSleep: boolean; maxSubtaskDepth: number } | undefined
|
|
679
|
+
let applied: { enabled: boolean; announceToAgent: boolean; preventIdleSleep: boolean; maxSubtaskDepth: number; sessionPollSeconds: number } | undefined
|
|
590
680
|
|
|
591
681
|
// Apply the current values to the host service and the announcement. A
|
|
592
682
|
// commit that changes nothing visible is a no-op, so following a coarse
|
|
@@ -594,13 +684,19 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
594
684
|
// under one disposer: re-registering first tears the old one down so a
|
|
595
685
|
// duplicate-name registration never throws.
|
|
596
686
|
const sync = (): void => {
|
|
597
|
-
const next = {
|
|
598
|
-
|
|
687
|
+
const next = {
|
|
688
|
+
enabled: enabled(),
|
|
689
|
+
announceToAgent: announceToAgent(),
|
|
690
|
+
preventIdleSleep: preventIdleSleep(),
|
|
691
|
+
maxSubtaskDepth: maxSubtaskDepth(),
|
|
692
|
+
sessionPollSeconds: sessionPollSeconds(),
|
|
693
|
+
}
|
|
694
|
+
if (applied !== undefined && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth && applied.sessionPollSeconds === next.sessionPollSeconds) {
|
|
599
695
|
return
|
|
600
696
|
}
|
|
601
697
|
applied = next
|
|
602
698
|
host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth)
|
|
603
|
-
host.setConfiguration(next.enabled, next.preventIdleSleep)
|
|
699
|
+
host.setConfiguration(next.enabled, next.preventIdleSleep, next.sessionPollSeconds)
|
|
604
700
|
setToolsEnabled(next.enabled)
|
|
605
701
|
if (disposeSection !== undefined) {
|
|
606
702
|
disposeSection()
|
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,14 @@ 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
|
|
71
96
|
/**
|
|
72
97
|
* Read-only summaries published by running extensions, keyed by extension id.
|
|
73
98
|
* The board forwards them verbatim; it never interprets their shape.
|
|
@@ -139,7 +164,18 @@ export type TaskBoardAction =
|
|
|
139
164
|
| { kind: 'archive'; taskId: string }
|
|
140
165
|
| { kind: 'restore'; taskId: string }
|
|
141
166
|
| { kind: 'settle'; taskId: string }
|
|
142
|
-
|
|
167
|
+
/** Clear the acceptance anomalies recorded on a card's open execution (issue #1828). */
|
|
168
|
+
| { kind: 'reset-verification'; taskId: string }
|
|
169
|
+
/**
|
|
170
|
+
* Record an outcome an agent OUTSIDE the Host produced (issue #1826).
|
|
171
|
+
*
|
|
172
|
+
* It writes a real execution record, so the terminal column and the run
|
|
173
|
+
* history both derive from that one record instead of a bare column edit.
|
|
174
|
+
* `cancelled` is deliberately absent: nothing outside a DSH session may
|
|
175
|
+
* claim the Host closed a run, and `running` must stay Host-only.
|
|
176
|
+
*/
|
|
177
|
+
| { kind: 'record-external-outcome'; taskId: string; result: 'succeeded' | 'failed'; initiatedBy: string; summary?: string }
|
|
178
|
+
| { kind: 'set-schedule'; taskId: string; patch: SetSchedulePatch }
|
|
143
179
|
| { kind: 'run'; taskId: string }
|
|
144
180
|
| { kind: 'rerun'; taskId: string }
|
|
145
181
|
| { kind: 'confirm-permission'; taskId: string }
|
|
@@ -187,19 +223,48 @@ function optionalFiniteNumber(value: unknown): boolean {
|
|
|
187
223
|
return value === undefined || (typeof value === 'number' && Number.isFinite(value))
|
|
188
224
|
}
|
|
189
225
|
|
|
226
|
+
/** Whether a stop-reason field names a reason this build understands. */
|
|
227
|
+
function validStopReason(value: unknown): boolean {
|
|
228
|
+
return value === undefined || (typeof value === 'string' && SCHEDULE_STOP_REASONS.includes(value as ScheduleStopReason))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether an imported schedule rule is structurally usable. A one-shot must
|
|
233
|
+
* carry its planned instant and a recurring rule its cron expression; the
|
|
234
|
+
* counter and stop record are optional (a rule written before them simply has
|
|
235
|
+
* none), but a value that IS present must be usable so the import cannot smuggle
|
|
236
|
+
* a rule the scheduler would misread.
|
|
237
|
+
*/
|
|
238
|
+
function validImportedSchedule(value: unknown): boolean {
|
|
239
|
+
const schedule = record(value)
|
|
240
|
+
if (schedule === undefined || typeof schedule.enabled !== 'boolean') return false
|
|
241
|
+
if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
|
|
242
|
+
if (schedule.mode === 'once') {
|
|
243
|
+
if (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at)) return false
|
|
244
|
+
} else if (typeof schedule.cron !== 'string') {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
|
|
248
|
+
if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isFinite(schedule.at))) return false
|
|
249
|
+
if (schedule.maxRuns !== undefined
|
|
250
|
+
&& (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
|
|
251
|
+
if (schedule.runCount !== undefined
|
|
252
|
+
&& (typeof schedule.runCount !== 'number' || !Number.isInteger(schedule.runCount) || schedule.runCount < 0)) return false
|
|
253
|
+
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
|
|
254
|
+
if (!optionalFiniteNumber(schedule.endedAt) || !optionalFiniteNumber(schedule.skippedAt)) return false
|
|
255
|
+
if (!validStopReason(schedule.endedReason) || !validStopReason(schedule.skippedReason)) return false
|
|
256
|
+
if (schedule.timeZone !== undefined
|
|
257
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
258
|
+
return true
|
|
259
|
+
}
|
|
260
|
+
|
|
190
261
|
function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
191
262
|
// Tags are validated (not repaired) on the import path: an imported task
|
|
192
263
|
// must carry a well-formed list or none at all, so a hand-edited export
|
|
193
264
|
// cannot smuggle a malformed tag past the gate.
|
|
194
265
|
if (value.tags !== undefined && !isTaskTagList(value.tags)) return false
|
|
195
266
|
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
|
-
}
|
|
267
|
+
if (value.schedule !== undefined && !validImportedSchedule(value.schedule)) return false
|
|
203
268
|
if (value.executions !== undefined) {
|
|
204
269
|
if (!Array.isArray(value.executions)) return false
|
|
205
270
|
for (const item of value.executions) {
|
|
@@ -209,6 +274,10 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
209
274
|
if (!optionalFiniteNumber(execution.endedAt) || !optionalString(execution.error)) return false
|
|
210
275
|
if (execution.result !== undefined && !['succeeded', 'failed', 'cancelled'].includes(String(execution.result))) return false
|
|
211
276
|
if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
|
|
277
|
+
// An imported external marker is accepted (it is a declaration an
|
|
278
|
+
// operator made about their own board), but it is still just a boolean:
|
|
279
|
+
// the acceptance block below is what must never arrive from an import.
|
|
280
|
+
if (execution.external !== undefined && typeof execution.external !== 'boolean') return false
|
|
212
281
|
if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
|
|
213
282
|
if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
|
|
214
283
|
// A well-formed acceptance block may be imported for inspection, but the
|
|
@@ -241,20 +310,17 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
241
310
|
result: execution.result,
|
|
242
311
|
error: execution.error,
|
|
243
312
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
313
|
+
...(execution.external === true ? { external: true } : {}),
|
|
244
314
|
...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
|
|
245
315
|
...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
|
|
246
316
|
// 安全门(对抗场景 d):验收报告是 Host 的判定,不接受 import 携带——
|
|
247
317
|
// 一条伪造的通过记录会让导入的执行被当成已验收。
|
|
248
318
|
})),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
nextRunAt: task.schedule.nextRunAt,
|
|
255
|
-
lastTriggeredAt: task.schedule.lastTriggeredAt,
|
|
256
|
-
},
|
|
257
|
-
}),
|
|
319
|
+
// The rule was already reconstructed field by field by parseLedger (which
|
|
320
|
+
// is what drops an unknown or malformed field), so the import keeps that
|
|
321
|
+
// normalized shape: mode, planned instant, run counter and stop record all
|
|
322
|
+
// travel with it.
|
|
323
|
+
...(task.schedule === undefined ? {} : { schedule: { ...task.schedule } }),
|
|
258
324
|
// The lineage link rides the import; the Host repairs a link whose parent
|
|
259
325
|
// is missing from the merged ledger instead of trusting the export.
|
|
260
326
|
...(task.parentId === undefined ? {} : { parentId: task.parentId }),
|
|
@@ -321,8 +387,13 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
321
387
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
322
388
|
if (input.schedule !== undefined) {
|
|
323
389
|
const schedule = record(input.schedule)
|
|
324
|
-
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron', 'timeZone'])) return false
|
|
325
|
-
if (typeof schedule.enabled !== 'boolean'
|
|
390
|
+
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])) return false
|
|
391
|
+
if (typeof schedule.enabled !== 'boolean') return false
|
|
392
|
+
if (schedule.mode !== undefined && schedule.mode !== 'cron' && schedule.mode !== 'once') return false
|
|
393
|
+
if (schedule.cron !== undefined && typeof schedule.cron !== 'string') return false
|
|
394
|
+
if (schedule.at !== undefined && (typeof schedule.at !== 'number' || !Number.isInteger(schedule.at))) return false
|
|
395
|
+
if (schedule.maxRuns !== undefined
|
|
396
|
+
&& (typeof schedule.maxRuns !== 'number' || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false
|
|
326
397
|
if (schedule.timeZone !== undefined
|
|
327
398
|
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
328
399
|
}
|
|
@@ -365,13 +436,21 @@ function isTagName(value: unknown): value is string {
|
|
|
365
436
|
function schedulePatch(value: unknown): boolean {
|
|
366
437
|
const patch = record(value)
|
|
367
438
|
return patch !== undefined
|
|
368
|
-
&& exactKeys(patch, ['enabled', 'cron', 'timeZone'])
|
|
439
|
+
&& exactKeys(patch, ['enabled', 'mode', 'cron', 'at', 'timeZone', 'maxRuns'])
|
|
369
440
|
&& (patch.enabled === undefined || typeof patch.enabled === 'boolean')
|
|
441
|
+
&& (patch.mode === undefined || patch.mode === 'cron' || patch.mode === 'once')
|
|
370
442
|
&& (patch.cron === undefined || typeof patch.cron === 'string')
|
|
443
|
+
// A one-shot's planned instant is a whole millisecond: a fractional or
|
|
444
|
+
// non-numeric value would make the armed target ambiguous.
|
|
445
|
+
&& (patch.at === undefined || (typeof patch.at === 'number' && Number.isInteger(patch.at)))
|
|
371
446
|
// `null` clears the stored zone back to the Host zone; an unknown name is
|
|
372
447
|
// rejected here so the Host never has to guess at an unusable zone.
|
|
373
448
|
&& (patch.timeZone === undefined || patch.timeZone === null
|
|
374
449
|
|| (typeof patch.timeZone === 'string' && isValidTimeZone(patch.timeZone)))
|
|
450
|
+
// `null` clears the run cap back to unlimited; any other value must be a
|
|
451
|
+
// positive whole number of runs.
|
|
452
|
+
&& (patch.maxRuns === undefined || patch.maxRuns === null
|
|
453
|
+
|| (typeof patch.maxRuns === 'number' && Number.isInteger(patch.maxRuns) && patch.maxRuns >= 1))
|
|
375
454
|
}
|
|
376
455
|
|
|
377
456
|
export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
@@ -472,11 +551,36 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
472
551
|
},
|
|
473
552
|
}
|
|
474
553
|
}
|
|
554
|
+
case 'record-external-outcome': {
|
|
555
|
+
if (!exactKeys(action, ['kind', 'taskId', 'result', 'initiatedBy', 'summary'])) return undefined
|
|
556
|
+
if (taskId === undefined) return undefined
|
|
557
|
+
// Only a real outside verdict is accepted: `running` is Host-only and
|
|
558
|
+
// `cancelled` is the Host's own bookkeeping for a run it closed.
|
|
559
|
+
if (action.result !== 'succeeded' && action.result !== 'failed') return undefined
|
|
560
|
+
if (typeof action.initiatedBy !== 'string') return undefined
|
|
561
|
+
const initiatedBy = action.initiatedBy.trim()
|
|
562
|
+
// The caller must be identifiable: an unattributed external outcome is
|
|
563
|
+
// the same 'state without history' the record exists to avoid.
|
|
564
|
+
if (initiatedBy === '' || initiatedBy.length > EXTERNAL_INITIATOR_MAX_LENGTH) return undefined
|
|
565
|
+
if (action.summary !== undefined && typeof action.summary !== 'string') return undefined
|
|
566
|
+
const summary = typeof action.summary === 'string' ? action.summary.trim().slice(0, EXTERNAL_SUMMARY_MAX_LENGTH) : ''
|
|
567
|
+
return {
|
|
568
|
+
requestId: envelope.requestId,
|
|
569
|
+
action: {
|
|
570
|
+
kind: 'record-external-outcome',
|
|
571
|
+
taskId,
|
|
572
|
+
result: action.result,
|
|
573
|
+
initiatedBy,
|
|
574
|
+
...(summary === '' ? {} : { summary }),
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
}
|
|
475
578
|
case 'confirm-permission':
|
|
476
579
|
case 'delete':
|
|
477
580
|
case 'archive':
|
|
478
581
|
case 'restore':
|
|
479
582
|
case 'settle':
|
|
583
|
+
case 'reset-verification':
|
|
480
584
|
case 'run':
|
|
481
585
|
case 'rerun':
|
|
482
586
|
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