@linxin666/dsh-client-ui-task-board 0.4.4 → 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 +59 -20
- package/README.zh.md +59 -20
- package/lib/client.js +6208 -624
- package/lib/client.js.map +1 -1
- package/lib/index.js +4774 -493
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/index.d.ts +23 -5
- package/lib/types/client/locales.d.ts +168 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +46 -11
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +228 -17
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +753 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +95 -0
- package/lib/types/host/verification-runner.d.ts +139 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +133 -14
- package/lib/types/host-runner.d.ts +55 -10
- package/lib/types/host-service.d.ts +205 -11
- package/lib/types/index.d.ts +134 -26
- package/lib/types/protocol.d.ts +92 -10
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +28 -31
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +378 -66
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +168 -21
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +289 -43
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +322 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +30 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +82 -51
- package/src/client/locales.ts +343 -6
- package/src/client/native-panel.tsx +18 -2
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +105 -10
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +80 -15
- package/src/core/tasks.ts +293 -45
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -7
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +1594 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +125 -38
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +487 -0
- package/src/host/verification-runner.ts +702 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +468 -71
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +106 -24
- package/src/host-service.ts +546 -40
- package/src/index.ts +305 -21
- package/src/protocol.ts +226 -28
- package/src/tool-surface.ts +95 -0
- package/lib/invariant.js +0 -6
- package/lib/types/invariant.d.ts +0 -3
- package/src/invariant.ts +0 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace targeting for cards that pin none.
|
|
3
|
+
*
|
|
4
|
+
* The Host answers two questions here instead of leaving them to
|
|
5
|
+
* `session.create`, whose own fallback is the Host process working directory
|
|
6
|
+
* (the desktop app's profile directory): which workspace a root card inherits
|
|
7
|
+
* from the session that created it, and which workspace an unpinned run lands
|
|
8
|
+
* in when nothing could be inherited.
|
|
9
|
+
*
|
|
10
|
+
* Structural types only — the Host's `Workspace` entity satisfies them — so the
|
|
11
|
+
* browser program compiles this module without importing an SDK value.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-task-board/core/workspace-target
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The workspace facts these rules read (a `Workspace` entity subset). */
|
|
17
|
+
export interface WorkspaceCandidate {
|
|
18
|
+
/** Stable workspace record id. */
|
|
19
|
+
readonly id: string
|
|
20
|
+
/** ISO-8601 instant of the workspace's last durable mutation. */
|
|
21
|
+
readonly updatedAt: string
|
|
22
|
+
/** Header-validated session ids the workspace owns, most recent first. */
|
|
23
|
+
readonly sessionIds: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The workspace that owns one session.
|
|
28
|
+
* @param items - the deployment's workspaces.
|
|
29
|
+
* @param sessionId - the session whose workspace is requested.
|
|
30
|
+
* @returns the owning workspace id, or undefined when none claims the session.
|
|
31
|
+
*/
|
|
32
|
+
export function workspaceOwningSession(
|
|
33
|
+
items: readonly WorkspaceCandidate[],
|
|
34
|
+
sessionId: string,
|
|
35
|
+
): string | undefined {
|
|
36
|
+
return items.find(item => item.sessionIds.includes(sessionId))?.id
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The workspace the board treats as most recently used: the one whose record
|
|
41
|
+
* changed last. Attaching a session stamps that instant, so the newest stamp
|
|
42
|
+
* tracks where the user most recently opened or resumed a session. The
|
|
43
|
+
* deployment's own list order breaks ties, and a stamp that does not parse
|
|
44
|
+
* never outranks one that does; a registry whose stamps are all unreadable
|
|
45
|
+
* falls back to list order rather than to "no workspace at all".
|
|
46
|
+
* @param items - the deployment's workspaces.
|
|
47
|
+
* @returns the most recently used workspace id, or undefined when there is none.
|
|
48
|
+
*/
|
|
49
|
+
export function mostRecentWorkspaceId(items: readonly WorkspaceCandidate[]): string | undefined {
|
|
50
|
+
let best: WorkspaceCandidate | undefined
|
|
51
|
+
let bestAt = Number.NEGATIVE_INFINITY
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const at = Date.parse(item.updatedAt)
|
|
54
|
+
if (!Number.isFinite(at) || at <= bestAt) continue
|
|
55
|
+
best = item
|
|
56
|
+
bestAt = at
|
|
57
|
+
}
|
|
58
|
+
return best?.id ?? items[0]?.id
|
|
59
|
+
}
|
package/src/host/agent-tools.ts
CHANGED
|
@@ -23,11 +23,13 @@ import {
|
|
|
23
23
|
TASK_PERMISSIONS,
|
|
24
24
|
type ExecutionRecord,
|
|
25
25
|
type NewTaskInput,
|
|
26
|
+
type ScheduleMode,
|
|
26
27
|
type ScheduleRule,
|
|
27
28
|
type TaskPermission,
|
|
28
29
|
type TaskRecord,
|
|
29
30
|
type TaskTag,
|
|
30
31
|
} from '../core/tasks.ts'
|
|
32
|
+
import type { SetSchedulePatch } from '../core/use-cases/task-schedule.ts'
|
|
31
33
|
import type { TaskUpdatePatch } from '../core/use-cases/task-update.ts'
|
|
32
34
|
import type { TaskBoardAction, TaskBoardSnapshot } from '../protocol.ts'
|
|
33
35
|
|
|
@@ -39,7 +41,7 @@ export interface TaskBoardToolHost {
|
|
|
39
41
|
/** Current board snapshot (tasks + scheduler + power + deployment constants). */
|
|
40
42
|
snapshot(): TaskBoardSnapshot
|
|
41
43
|
/** Submit one confirmed Host action; returns the resulting snapshot. */
|
|
42
|
-
apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot
|
|
44
|
+
apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot | Promise<TaskBoardSnapshot>
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** Registered tool names, in registration order. */
|
|
@@ -93,11 +95,22 @@ export function callingSessionId(exec: { agent?: unknown }): string | undefined
|
|
|
93
95
|
function scheduleView(schedule: ScheduleRule): Record<string, unknown> {
|
|
94
96
|
return {
|
|
95
97
|
enabled: schedule.enabled,
|
|
96
|
-
|
|
98
|
+
mode: schedule.mode,
|
|
99
|
+
// A one-shot has no cron, and carries its planned instant instead.
|
|
100
|
+
...(schedule.cron === undefined ? {} : { cron: schedule.cron }),
|
|
101
|
+
...(schedule.at === undefined ? {} : { at: schedule.at }),
|
|
97
102
|
// Absent means the rule follows the Host zone, which the board view reports.
|
|
98
103
|
...(schedule.timeZone === undefined ? {} : { timeZone: schedule.timeZone }),
|
|
99
104
|
...(schedule.nextRunAt === undefined ? {} : { nextRunAt: schedule.nextRunAt }),
|
|
100
105
|
...(schedule.lastTriggeredAt === undefined ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }),
|
|
106
|
+
// The run budget: what the scheduler has already opened, its cap (absent =
|
|
107
|
+
// unlimited), and how the rule stopped or what it last skipped.
|
|
108
|
+
runCount: schedule.runCount,
|
|
109
|
+
...(schedule.maxRuns === undefined ? {} : { maxRuns: schedule.maxRuns }),
|
|
110
|
+
...(schedule.endedAt === undefined ? {} : { endedAt: schedule.endedAt }),
|
|
111
|
+
...(schedule.endedReason === undefined ? {} : { endedReason: schedule.endedReason }),
|
|
112
|
+
...(schedule.skippedAt === undefined ? {} : { skippedAt: schedule.skippedAt }),
|
|
113
|
+
...(schedule.skippedReason === undefined ? {} : { skippedReason: schedule.skippedReason }),
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -114,8 +127,11 @@ function executionView(execution: ExecutionRecord): Record<string, unknown> {
|
|
|
114
127
|
...(execution.result === undefined ? {} : { result: execution.result }),
|
|
115
128
|
...(execution.error === undefined ? {} : { error: execution.error }),
|
|
116
129
|
// The session that asked for the run (audit only; client-asserted by the
|
|
117
|
-
// browser, the calling session here).
|
|
130
|
+
// browser, the calling session here). On an external record it names the
|
|
131
|
+
// outside agent instead, which is what distinguishes it in the history.
|
|
118
132
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
133
|
+
// Set only by record-external-outcome: the Host observed no session for it.
|
|
134
|
+
...(execution.external === true ? { external: true } : {}),
|
|
119
135
|
...(execution.runGroupId === undefined ? {} : { cascade: true }),
|
|
120
136
|
// A deferred cascade parent has settled its own turn but still waits for
|
|
121
137
|
// its subtasks; that is not a stuck card and the model must not retry it.
|
|
@@ -152,8 +168,10 @@ function taskSummary(
|
|
|
152
168
|
...(task.model === undefined ? {} : { model: task.model }),
|
|
153
169
|
...(task.reuseSession === true ? { reuseSession: true } : {}),
|
|
154
170
|
...(task.teamRun === true ? { teamRun: true } : {}),
|
|
155
|
-
// Default-
|
|
156
|
-
...(task.goalRun ===
|
|
171
|
+
// Default-OFF opt-in: only a card that asked for the goal run reports it.
|
|
172
|
+
...(task.goalRun === true ? { goalRun: true } : {}),
|
|
173
|
+
// Default-OFF opt-out: the deviation is what a reader must be told.
|
|
174
|
+
...(task.skipVerification === true ? { skipVerification: true } : {}),
|
|
157
175
|
...(task.schedule === undefined ? {} : { schedule: scheduleView(task.schedule) }),
|
|
158
176
|
...(task.freeze === undefined ? {} : { continuationCard: true }),
|
|
159
177
|
...(task.permissionConfirmedAt === undefined ? {} : { permissionConfirmedAt: task.permissionConfirmedAt }),
|
|
@@ -260,7 +278,6 @@ function buildSetParentTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
260
278
|
'Pass an empty parentId to detach.',
|
|
261
279
|
'The Host refuses the link when the parent is unknown or archived, when the moved task is one of its own descendants, when the resulting depth exceeds the deployment subtask limit (default one level, maximum three), or while the task has an unsettled execution (its link may still be a running cascade parent pointer).',
|
|
262
280
|
'Running a task runs its whole subtree concurrently; a parent card settles only after its own turn and every direct subtask settle.',
|
|
263
|
-
'Triggers: 任务看板, task board, 看板, 子任务, subtask, 关联任务, link task, 解除关联, detach.',
|
|
264
281
|
].join(' '),
|
|
265
282
|
parameters: {
|
|
266
283
|
taskId: { type: 'string', required: true, description: 'The task whose parent link changes.' },
|
|
@@ -269,7 +286,7 @@ function buildSetParentTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
269
286
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
270
287
|
async execute(args, exec) {
|
|
271
288
|
try {
|
|
272
|
-
const snapshot = host.apply(
|
|
289
|
+
const snapshot = await host.apply(
|
|
273
290
|
crypto.randomUUID(),
|
|
274
291
|
{ kind: 'set-parent', taskId: args.taskId, parentId: args.parentId === '' ? null : args.parentId },
|
|
275
292
|
callingSessionId(exec),
|
|
@@ -294,7 +311,6 @@ function buildRunTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
294
311
|
'A card whose effective permission is above the deployment session default is refused with confirmation-required until a HUMAN confirms the binding in the board UI; this tool cannot confirm it.',
|
|
295
312
|
'A card whose task carries teamRun instead starts ONE Team Lead session and the Host spawns a teammate per subtask inside it, so those cards run as an Agent Team rather than as independent sessions; that mode needs the deployment Agent Teams service and is refused when it is missing.',
|
|
296
313
|
'Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.',
|
|
297
|
-
'Triggers: 任务看板, task board, 看板, 执行任务, run task, 重新执行, rerun, 子任务, subtask.',
|
|
298
314
|
].join(' '),
|
|
299
315
|
parameters: {
|
|
300
316
|
taskId: { type: 'string', required: true, description: 'The task to run; running a root task runs its whole subtree.' },
|
|
@@ -304,7 +320,7 @@ function buildRunTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
304
320
|
async execute(args, exec) {
|
|
305
321
|
const before = host.snapshot()
|
|
306
322
|
try {
|
|
307
|
-
const snapshot = host.apply(
|
|
323
|
+
const snapshot = await host.apply(
|
|
308
324
|
crypto.randomUUID(),
|
|
309
325
|
{ kind: args.rerun === true ? 'rerun' : 'run', taskId: args.taskId },
|
|
310
326
|
callingSessionId(exec),
|
|
@@ -339,29 +355,81 @@ function buildManageTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
339
355
|
name: 'task_board_manage',
|
|
340
356
|
description: [
|
|
341
357
|
'Move, archive, restore, delete, or settle one task board card.',
|
|
342
|
-
'move-todo and move-
|
|
358
|
+
'move-backlog, move-todo, move-running, move-done and move-failed are the manual column moves and cover every column; a card the runner is executing (one with an open execution) cannot be moved.',
|
|
359
|
+
'A manual move writes the card column only and never fabricates an execution record, so the card reports a declaration rather than evidence of a run: done/failed declare work finished (or failed) outside a Host-run execution — human work, an external system, a decision made elsewhere — and running says the work is under way without a tracked session. Use task_board_run when the work should actually run in a session here.',
|
|
360
|
+
'record-external-outcome is the record-backed version of move-done and move-failed: use it when YOU, or another agent outside this Host, actually finished or failed the card with your own model, so the terminal column and the run history both derive from one execution record. It takes result (succeeded or failed) and initiatedBy (who completed it, for example your own agent name) plus an optional summary, and it is refused while the card still has an open execution: the Host stays the only authority over a run it is executing. It cannot record running or cancelled — those remain Host-only.',
|
|
343
361
|
'settle force-closes the open execution of a card the board can no longer observe (a stuck running card) and records it cancelled with the caller as the reason, so the card returns to the todo column and can be run again.',
|
|
362
|
+
'reset-verification clears the acceptance anomalies (timeouts, authentication failures, an unresolvable judge route, or an attempt the time budget ended) recorded on a card open execution, so the next update_goal(action: complete) runs a fresh acceptance. It is refused unless that execution really carries such anomalies, it never clears a quality verdict, and a spent anomaly budget otherwise HOLDS the card open instead of failing it: tell the user the acceptance environment is unusable and that this action is how they clear it once they have fixed it.',
|
|
344
363
|
'archive takes the whole subtask tree off the board and is refused while any member has an unsettled execution; restore brings the task, its ancestors and its subtree back; delete removes one card and is refused while it still has subtasks (detach or delete them first) or while it runs.',
|
|
345
364
|
'It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.',
|
|
346
|
-
'Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task, 卡住, stuck, 强制结算, settle.',
|
|
347
365
|
].join(' '),
|
|
348
366
|
parameters: {
|
|
349
367
|
taskId: { type: 'string', required: true, description: 'The task to act on.' },
|
|
350
368
|
action: {
|
|
351
369
|
type: 'string',
|
|
352
370
|
required: true,
|
|
353
|
-
enum: ['move-todo', 'move-backlog', 'archive', 'restore', 'delete', 'settle'],
|
|
371
|
+
enum: ['move-todo', 'move-backlog', 'move-running', 'move-done', 'move-failed', 'archive', 'restore', 'delete', 'settle', 'reset-verification', 'record-external-outcome'],
|
|
354
372
|
description: 'The lifecycle operation to perform.',
|
|
355
373
|
},
|
|
374
|
+
// Only record-external-outcome reads these two; every other action
|
|
375
|
+
// ignores them, so the model can pass them without a second tool call.
|
|
376
|
+
result: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
enum: ['succeeded', 'failed'],
|
|
379
|
+
description: 'record-external-outcome only: the verdict the outside agent actually reached.',
|
|
380
|
+
},
|
|
381
|
+
initiatedBy: {
|
|
382
|
+
type: 'string',
|
|
383
|
+
description: 'record-external-outcome only: who finished the work outside this Host (for example your own agent name).',
|
|
384
|
+
},
|
|
385
|
+
summary: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
description: 'record-external-outcome only: optional free-text note stored with the recorded outcome.',
|
|
388
|
+
},
|
|
356
389
|
},
|
|
357
390
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
358
391
|
async execute(args, exec) {
|
|
359
392
|
const initiator = callingSessionId(exec)
|
|
393
|
+
if (args.action === 'record-external-outcome') {
|
|
394
|
+
const result = args.result
|
|
395
|
+
const by = typeof args.initiatedBy === 'string' ? args.initiatedBy.trim() : ''
|
|
396
|
+
if (result !== 'succeeded' && result !== 'failed') {
|
|
397
|
+
return refused('missing-result', 'record-external-outcome needs result=succeeded or result=failed')
|
|
398
|
+
}
|
|
399
|
+
if (by === '') {
|
|
400
|
+
return refused('missing-initiator', 'record-external-outcome needs initiatedBy naming who completed the work outside this Host')
|
|
401
|
+
}
|
|
402
|
+
if (!host.snapshot().tasks.some(task => task.id === args.taskId)) {
|
|
403
|
+
return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const snapshot = await host.apply(crypto.randomUUID(), {
|
|
407
|
+
kind: 'record-external-outcome',
|
|
408
|
+
taskId: args.taskId,
|
|
409
|
+
result,
|
|
410
|
+
initiatedBy: by,
|
|
411
|
+
...(typeof args.summary === 'string' && args.summary.trim() !== '' ? { summary: args.summary } : {}),
|
|
412
|
+
}, initiator)
|
|
413
|
+
const task = snapshot.tasks.find(item => item.id === args.taskId)
|
|
414
|
+
return json({
|
|
415
|
+
ok: true,
|
|
416
|
+
action: args.action,
|
|
417
|
+
...(task === undefined ? {} : { task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission) }),
|
|
418
|
+
note: 'The outcome is recorded as its own execution, so the terminal column and the run history agree. Read it back with task_board_get.',
|
|
419
|
+
})
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return refused('refused', messageOf(error))
|
|
422
|
+
}
|
|
423
|
+
}
|
|
360
424
|
const actions: Record<string, TaskBoardAction> = {
|
|
361
425
|
'move-todo': { kind: 'move', taskId: args.taskId, status: 'todo' },
|
|
362
426
|
'move-backlog': { kind: 'move', taskId: args.taskId, status: 'backlog' },
|
|
427
|
+
'move-running': { kind: 'move', taskId: args.taskId, status: 'running' },
|
|
428
|
+
'move-done': { kind: 'move', taskId: args.taskId, status: 'done' },
|
|
429
|
+
'move-failed': { kind: 'move', taskId: args.taskId, status: 'failed' },
|
|
363
430
|
archive: { kind: 'archive', taskId: args.taskId },
|
|
364
431
|
settle: { kind: 'settle', taskId: args.taskId },
|
|
432
|
+
'reset-verification': { kind: 'reset-verification', taskId: args.taskId },
|
|
365
433
|
restore: { kind: 'restore', taskId: args.taskId },
|
|
366
434
|
delete: { kind: 'delete', taskId: args.taskId },
|
|
367
435
|
}
|
|
@@ -371,7 +439,7 @@ function buildManageTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
371
439
|
return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
372
440
|
}
|
|
373
441
|
try {
|
|
374
|
-
const snapshot = host.apply(crypto.randomUUID(), action, initiator)
|
|
442
|
+
const snapshot = await host.apply(crypto.randomUUID(), action, initiator)
|
|
375
443
|
if (args.action === 'delete') return json({ ok: true, action: args.action, taskId: args.taskId })
|
|
376
444
|
const task = snapshot.tasks.find(item => item.id === args.taskId)
|
|
377
445
|
if (task === undefined) return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
@@ -387,41 +455,49 @@ function buildScheduleTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
387
455
|
return defineTool({
|
|
388
456
|
name: 'task_board_schedule',
|
|
389
457
|
description: [
|
|
390
|
-
'Arm, change, or disarm a task scheduled runs
|
|
391
|
-
'
|
|
458
|
+
'Arm, change, or disarm a task scheduled runs. Two kinds of plan exist: a recurring 5-field cron rule (day-of-month and day-of-week follow Vixie semantics: both restricted means OR, otherwise AND), and a one-shot that runs once at a future instant.',
|
|
459
|
+
'For a one-shot pass mode "once" with at (the planned instant, ms since epoch UTC). For a recurring rule pass mode "cron" (or omit mode) with a cron expression, optionally capped by maxRuns (a positive whole number of scheduled executions; omit for unlimited, pass 0 to clear a cap).',
|
|
460
|
+
'The wall clock is read in the rule stored IANA time zone; omit timeZone to use the Host zone reported by task_board_list/task_board_get. DST gaps are skipped and an ambiguous fall-back time fires once, at the earlier instant.',
|
|
392
461
|
'A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.',
|
|
393
|
-
'A schedule whose tree contains an unconfirmed above-default permission is refused and
|
|
394
|
-
'Missed occurrences during Host downtime are skipped, never queued; a task that is already running skips its occurrence.',
|
|
395
|
-
'Triggers: 任务看板, task board, 看板, 定时任务, schedule, cron, 每天执行.',
|
|
462
|
+
'The scheduler counts executions it actually starts; once a rule reaches its run budget it stops itself in the ledger, and a manual run never consumes the budget. A schedule whose tree contains an unconfirmed above-default permission is refused and does not consume the budget; the reason is reported in the list board summary as schedulerError.',
|
|
463
|
+
'Missed occurrences during Host downtime are skipped, never queued (a one-shot whose instant passed while the Host was down is stopped as missed); a task that is already running skips its occurrence without consuming the budget.',
|
|
396
464
|
].join(' '),
|
|
397
465
|
parameters: {
|
|
398
466
|
taskId: { type: 'string', required: true, description: 'The task whose schedule changes.' },
|
|
399
467
|
enabled: { type: 'boolean', description: 'Arm (true) or disarm (false) the schedule.' },
|
|
400
|
-
|
|
468
|
+
mode: { type: 'string', enum: ['cron', 'once'], description: 'The kind of plan: "cron" for a recurring rule, "once" for a single future instant. Omit to keep the stored kind.' },
|
|
469
|
+
cron: { type: 'string', description: '5-field cron expression of a recurring rule: minute hour day-of-month month day-of-week.' },
|
|
470
|
+
at: { type: 'integer', description: 'Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future.' },
|
|
401
471
|
timeZone: {
|
|
402
472
|
type: 'string',
|
|
403
|
-
description: 'IANA zone the
|
|
473
|
+
description: 'IANA zone the wall clock is read in (for example Asia/Shanghai). Omit to keep the stored zone, or pass an empty string to clear it back to the Host zone.',
|
|
404
474
|
},
|
|
475
|
+
maxRuns: { type: 'integer', description: 'Maximum scheduled executions of a recurring rule (positive whole number). Omit to keep the stored cap; pass 0 to clear it back to unlimited.' },
|
|
405
476
|
},
|
|
406
477
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
407
478
|
async execute(args, exec) {
|
|
408
|
-
if (args.enabled === undefined && args.cron === undefined && args.
|
|
409
|
-
|
|
479
|
+
if (args.enabled === undefined && args.mode === undefined && args.cron === undefined && args.at === undefined
|
|
480
|
+
&& args.timeZone === undefined && args.maxRuns === undefined) {
|
|
481
|
+
return refused('nothing-to-change', 'pass enabled, mode, cron, at, timeZone and/or maxRuns')
|
|
410
482
|
}
|
|
411
483
|
// A missing card must read as a missing card: the ledger reports an
|
|
412
484
|
// unknown id through the same refusal as a malformed cron.
|
|
413
485
|
if (!host.snapshot().tasks.some(task => task.id === args.taskId)) {
|
|
414
486
|
return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
415
487
|
}
|
|
416
|
-
const patch:
|
|
488
|
+
const patch: SetSchedulePatch = {
|
|
417
489
|
...(args.enabled === undefined ? {} : { enabled: args.enabled }),
|
|
490
|
+
...(args.mode === undefined ? {} : { mode: args.mode as ScheduleMode }),
|
|
418
491
|
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
492
|
+
...(args.at === undefined ? {} : { at: args.at }),
|
|
419
493
|
// An empty string is the documented way to clear the stored zone, so
|
|
420
494
|
// the model never has to emit an explicit null through JSON args.
|
|
421
495
|
...(args.timeZone === undefined ? {} : { timeZone: args.timeZone === '' ? null : args.timeZone }),
|
|
496
|
+
// 0 is the documented way to clear a run cap back to unlimited.
|
|
497
|
+
...(args.maxRuns === undefined ? {} : { maxRuns: args.maxRuns <= 0 ? null : args.maxRuns }),
|
|
422
498
|
}
|
|
423
499
|
try {
|
|
424
|
-
const snapshot = host.apply(crypto.randomUUID(), { kind: 'set-schedule', taskId: args.taskId, patch }, callingSessionId(exec))
|
|
500
|
+
const snapshot = await host.apply(crypto.randomUUID(), { kind: 'set-schedule', taskId: args.taskId, patch }, callingSessionId(exec))
|
|
425
501
|
const task = snapshot.tasks.find(item => item.id === args.taskId)
|
|
426
502
|
if (task === undefined) return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
427
503
|
return json({
|
|
@@ -445,7 +521,6 @@ function buildListTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
445
521
|
'Filters: status column, parentId (direct subtasks), rootsOnly, tag, free-text query, and archived inclusion.',
|
|
446
522
|
'Returns each task id, title, column, parent/subtask link, execution targets, schedule and latest run outcome, plus the board summary (revision, time zone, subtask depth limit, session-default permission).',
|
|
447
523
|
'Use task_board_get for one task in full, task_board_create to add work, and task_board_run to execute it.',
|
|
448
|
-
'Triggers: 任务看板, task board, 看板, 子任务, subtask, task list, 定时任务, scheduled task.',
|
|
449
524
|
].join(' '),
|
|
450
525
|
parameters: {
|
|
451
526
|
status: { type: 'string', enum: ['backlog', 'todo', 'running', 'done', 'failed'], description: 'Only cards in this column.' },
|
|
@@ -491,7 +566,6 @@ function buildGetTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
491
566
|
description: [
|
|
492
567
|
'Read one task board card in full: prompt, description, labels, column, parent link, direct subtasks, execution targets, schedule, permission gate state, and the last ten execution attempts with their session ids and outcomes.',
|
|
493
568
|
'Use it before running or editing a card, and after a run to read the settled outcome.',
|
|
494
|
-
'Triggers: 任务看板, task board, 看板, 子任务, subtask, task detail.',
|
|
495
569
|
].join(' '),
|
|
496
570
|
parameters: {
|
|
497
571
|
taskId: { type: 'string', required: true, description: 'Task id, as reported by task_board_list or task_board_create.' },
|
|
@@ -515,20 +589,20 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
515
589
|
'A subtask inherits every execution target it does not override (workspace, agent preset, permission, model) from its parent, and inherits the parent human permission confirmation together with an inherited binding.',
|
|
516
590
|
'The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.',
|
|
517
591
|
'The task lands in the todo column and does NOT run; call task_board_run to execute it.',
|
|
518
|
-
'Triggers: 任务看板, task board, 看板, 新建任务, create task, 子任务, subtask, 定时任务.',
|
|
519
592
|
].join(' '),
|
|
520
593
|
parameters: {
|
|
521
594
|
title: { type: 'string', required: true, description: 'Short display title (one line).' },
|
|
522
595
|
description: { type: 'string', description: 'Longer human description shown in the detail view.' },
|
|
523
596
|
prompt: { type: 'string', description: 'The instruction sent to the execution agent; the title is used when blank.' },
|
|
524
597
|
parentId: { type: 'string', description: 'Parent task id, making this a subtask. Omit for a root task.' },
|
|
525
|
-
workspaceId: { type: 'string', description: 'Workspace id the execution must run in; omit to inherit the
|
|
598
|
+
workspaceId: { type: 'string', description: 'Workspace id the execution must run in; omit to inherit the workspace this session is in (root task) or the parent value (subtask).' },
|
|
526
599
|
mode: { type: 'string', description: 'Agent preset id the execution session is composed from; omit for the deployment default or the parent value.' },
|
|
527
600
|
permission: { type: 'string', enum: [...TASK_PERMISSIONS], description: 'Permission preset for the execution session. Omit to inherit the parent binding; a value above the session default needs a human confirmation in the board UI before the card can run.' },
|
|
528
601
|
model: { type: 'string', description: 'Pinned model as provider/model (or a model id); omit for the host default or the parent value.' },
|
|
529
602
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session instead of a fresh conversation.' },
|
|
530
603
|
teamRun: { type: 'boolean', description: 'Run this task as an Agent Team: running it starts one Team Lead session and the Host spawns a teammate per subtask inside it. Omit or false for one independent session per member. Refused when the deployment serves no Agent Teams service.' },
|
|
531
|
-
goalRun: { type: 'boolean', description: 'Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes.
|
|
604
|
+
goalRun: { type: 'boolean', description: 'Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes. Off by default: omit (or pass false) for a single plain turn, and pass true to opt this card in. It only takes effect while the deployment global native /goal switch is on.' },
|
|
605
|
+
skipVerification: { type: 'boolean', description: 'Run this task without the board acceptance gate: update_goal may mark the goal complete without a passing acceptance, and the execution record is marked skipped. Default false (omit to inherit the board-wide acceptance switch). Only affects executions started afterwards.' },
|
|
532
606
|
tags: {
|
|
533
607
|
type: 'array',
|
|
534
608
|
description: 'Labels: the name renders as a badge and drives the board filter; promptPrefix is injected ahead of the execution prompt on every run.',
|
|
@@ -544,10 +618,13 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
544
618
|
schedule: {
|
|
545
619
|
type: 'object',
|
|
546
620
|
additionalProperties: false,
|
|
547
|
-
description: 'Arm a
|
|
621
|
+
description: 'Arm a schedule at creation time. Use mode "once" with at for a single future run, or mode "cron" (or omit mode) with a 5-field cron expression, optionally capped by maxRuns.',
|
|
548
622
|
properties: {
|
|
549
623
|
enabled: { type: 'boolean', required: true, description: 'Whether the schedule is armed.' },
|
|
550
|
-
|
|
624
|
+
mode: { type: 'string', enum: ['cron', 'once'], description: 'The kind of plan: "cron" for a recurring rule, "once" for a single future instant.' },
|
|
625
|
+
cron: { type: 'string', description: '5-field cron of a recurring rule: minute hour day-of-month month day-of-week.' },
|
|
626
|
+
at: { type: 'integer', description: 'Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future.' },
|
|
627
|
+
maxRuns: { type: 'integer', description: 'Maximum scheduled executions of a recurring rule (positive whole number); omit for unlimited.' },
|
|
551
628
|
},
|
|
552
629
|
},
|
|
553
630
|
},
|
|
@@ -570,12 +647,21 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
570
647
|
...(args.model === undefined || args.model === '' ? {} : { model: args.model }),
|
|
571
648
|
...(args.reuseSession === true ? { reuseSession: true } : {}),
|
|
572
649
|
...(args.teamRun === true ? { teamRun: true } : {}),
|
|
573
|
-
...(args.goalRun ===
|
|
650
|
+
...(args.goalRun === true ? { goalRun: true } : {}),
|
|
651
|
+
...(args.skipVerification === true ? { skipVerification: true } : {}),
|
|
574
652
|
...(tags.length === 0 ? {} : { tags }),
|
|
575
|
-
...(args.schedule === undefined ? {} : {
|
|
653
|
+
...(args.schedule === undefined ? {} : {
|
|
654
|
+
schedule: {
|
|
655
|
+
enabled: args.schedule.enabled,
|
|
656
|
+
...(args.schedule.mode === undefined ? {} : { mode: args.schedule.mode as ScheduleMode }),
|
|
657
|
+
...(args.schedule.cron === undefined ? {} : { cron: args.schedule.cron }),
|
|
658
|
+
...(args.schedule.at === undefined ? {} : { at: args.schedule.at }),
|
|
659
|
+
...(args.schedule.maxRuns === undefined ? {} : { maxRuns: args.schedule.maxRuns }),
|
|
660
|
+
},
|
|
661
|
+
}),
|
|
576
662
|
}
|
|
577
663
|
try {
|
|
578
|
-
const snapshot = host.apply(crypto.randomUUID(), { kind: 'create', id, input }, callingSessionId(exec))
|
|
664
|
+
const snapshot = await host.apply(crypto.randomUUID(), { kind: 'create', id, input }, callingSessionId(exec))
|
|
579
665
|
const task = snapshot.tasks.find(item => item.id === id)
|
|
580
666
|
if (task === undefined) return refused('create-failed', 'the Host did not confirm the new task')
|
|
581
667
|
return json({ ok: true, task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission) })
|
|
@@ -594,7 +680,6 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
594
680
|
'Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.',
|
|
595
681
|
'Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.',
|
|
596
682
|
'Changing the pinned permission re-arms the permission confirmation gate.',
|
|
597
|
-
'Triggers: 任务看板, task board, 看板, 修改任务, edit task, 子任务, 标签, tags.',
|
|
598
683
|
].join(' '),
|
|
599
684
|
parameters: {
|
|
600
685
|
taskId: { type: 'string', required: true, description: 'Task id to edit.' },
|
|
@@ -621,7 +706,8 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
621
706
|
model: { type: 'string', description: 'New pinned model; an empty string clears it.' },
|
|
622
707
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session.' },
|
|
623
708
|
teamRun: { type: 'boolean', description: 'Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask).' },
|
|
624
|
-
goalRun: { type: 'boolean', description: 'Whether each run starts with dsh built-in /goal (default
|
|
709
|
+
goalRun: { type: 'boolean', description: 'Whether each run starts with dsh built-in /goal (default false, one plain turn). Pass true to opt this card in, or false to return it to the default.' },
|
|
710
|
+
skipVerification: { type: 'boolean', description: 'Whether this task runs without the board acceptance gate (default false, inherit the board-wide switch). Pass true to opt out; pass false to opt back in. Only affects executions started afterwards.' },
|
|
625
711
|
tags: {
|
|
626
712
|
type: 'array',
|
|
627
713
|
description: 'Replacement label set; an empty array clears all labels.',
|
|
@@ -650,7 +736,8 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
650
736
|
}
|
|
651
737
|
if (has(args, 'reuseSession')) patch.reuseSession = args.reuseSession === true
|
|
652
738
|
if (has(args, 'teamRun')) patch.teamRun = args.teamRun === true
|
|
653
|
-
if (has(args, 'goalRun')) patch.goalRun = args.goalRun ===
|
|
739
|
+
if (has(args, 'goalRun')) patch.goalRun = args.goalRun === true ? true : false
|
|
740
|
+
if (has(args, 'skipVerification')) patch.skipVerification = args.skipVerification === true ? true : false
|
|
654
741
|
if (has(args, 'tags')) {
|
|
655
742
|
const tags = args.tags ?? []
|
|
656
743
|
patch.tags = tags.length === 0
|
|
@@ -662,7 +749,7 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
662
749
|
}
|
|
663
750
|
if (Object.keys(patch).length === 0) return refused('nothing-to-change', 'pass at least one field to update')
|
|
664
751
|
try {
|
|
665
|
-
const snapshot = host.apply(crypto.randomUUID(), { kind: 'update', taskId: args.taskId, patch }, callingSessionId(exec))
|
|
752
|
+
const snapshot = await host.apply(crypto.randomUUID(), { kind: 'update', taskId: args.taskId, patch }, callingSessionId(exec))
|
|
666
753
|
const task = snapshot.tasks.find(item => item.id === args.taskId)
|
|
667
754
|
if (task === undefined) return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
668
755
|
return json({ ok: true, task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission) })
|