@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +28 -3
  3. package/README.zh.md +28 -3
  4. package/lib/client.js +1142 -467
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2066 -1475
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
  8. package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
  9. package/lib/types/client/board/NewTaskModal.d.ts +7 -1
  10. package/lib/types/client/board/TaskCard.d.ts +11 -1
  11. package/lib/types/client/index.d.ts +15 -1
  12. package/lib/types/client/locales.d.ts +33 -0
  13. package/lib/types/client/native-panel.d.ts +62 -0
  14. package/lib/types/client/settings-entry-form.d.ts +49 -0
  15. package/lib/types/core/controller.d.ts +71 -3
  16. package/lib/types/core/subtask.d.ts +122 -0
  17. package/lib/types/core/tasks.d.ts +46 -3
  18. package/lib/types/core/use-cases/task-archive.d.ts +10 -13
  19. package/lib/types/core/use-cases/task-create.d.ts +13 -4
  20. package/lib/types/core/use-cases/task-delete.d.ts +7 -1
  21. package/lib/types/core/use-cases/task-parent.d.ts +21 -0
  22. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  23. package/lib/types/host/agent-tools.d.ts +27 -0
  24. package/lib/types/host-ledger.d.ts +97 -4
  25. package/lib/types/host-runner.d.ts +39 -1
  26. package/lib/types/host-service.d.ts +98 -5
  27. package/lib/types/index.d.ts +93 -5
  28. package/lib/types/mount-once.d.ts +38 -7
  29. package/lib/types/protocol.d.ts +14 -0
  30. package/package.json +26 -21
  31. package/src/client/TaskBoardSettingsCard.tsx +48 -3
  32. package/src/client/board/LinkSubtaskModal.tsx +82 -0
  33. package/src/client/board/NewTaskModal.tsx +40 -6
  34. package/src/client/board/TaskBoard.tsx +88 -8
  35. package/src/client/board/TaskCard.tsx +52 -2
  36. package/src/client/board/TaskDetail.tsx +124 -1
  37. package/src/client/board.module.css +112 -131
  38. package/src/client/host-api.ts +14 -4
  39. package/src/client/index.ts +56 -18
  40. package/src/client/locales.ts +70 -2
  41. package/src/client/native-panel.tsx +124 -0
  42. package/src/client/settings-entry-form.ts +142 -0
  43. package/src/core/controller.ts +128 -10
  44. package/src/core/store.ts +15 -0
  45. package/src/core/subtask.ts +286 -0
  46. package/src/core/tasks.ts +50 -2
  47. package/src/core/use-cases/task-archive.ts +46 -12
  48. package/src/core/use-cases/task-create.ts +42 -7
  49. package/src/core/use-cases/task-delete.ts +11 -1
  50. package/src/core/use-cases/task-parent.ts +64 -0
  51. package/src/core/use-cases/task-update.ts +3 -1
  52. package/src/host/agent-tools.ts +643 -0
  53. package/src/host-ledger.ts +343 -45
  54. package/src/host-routes.ts +37 -1
  55. package/src/host-runner.ts +141 -12
  56. package/src/host-service.ts +265 -32
  57. package/src/index.ts +203 -9
  58. package/src/mount-once.ts +107 -13
  59. package/src/protocol.ts +26 -2
  60. package/lib/types/client/board-mount.d.ts +0 -23
  61. package/lib/types/client/body-mutations.d.ts +0 -45
  62. package/lib/types/client/panel-mount-core.d.ts +0 -56
  63. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  64. package/lib/types/client/sidebar-entry.d.ts +0 -29
  65. package/src/client/board-mount.tsx +0 -43
  66. package/src/client/body-mutations.ts +0 -133
  67. package/src/client/panel-mount-core.ts +0 -155
  68. package/src/client/sidebar-entry-core.ts +0 -237
  69. package/src/client/sidebar-entry.ts +0 -53
@@ -1,17 +1,26 @@
1
1
  import { type NewTaskInput, type TaskRecord } from '../tasks.ts';
2
2
  /** Result of a create transition: the new task (when accepted) + the next ledger. */
3
3
  export interface CreateTaskResult {
4
- /** The minted task, or undefined when the input was rejected (blank title). */
4
+ /** The minted task, or undefined when the input was rejected. */
5
5
  task: TaskRecord | undefined;
6
6
  /** The next ledger; identical reference when rejected. */
7
7
  tasks: readonly TaskRecord[];
8
+ /** Why the input was rejected; absent when a task was minted. */
9
+ error?: string;
8
10
  }
9
11
  /**
10
12
  * Apply a create against the current ledger. Returns the new task and the
11
- * appended ledger, or the unchanged ledger when the title is blank.
13
+ * appended ledger, or the unchanged ledger when the title is blank or the
14
+ * requested parent link is refused.
15
+ *
16
+ * A subtask inherits the execution targets its parent already pins (workspace,
17
+ * agent preset, permission, model) unless the form supplied its own; the
18
+ * parent human permission confirmation travels with an inherited permission,
19
+ * so a subtask of an already-confirmed elevated card is runnable at once.
12
20
  * @param tasks - current ledger.
13
- * @param input - raw user input (title/description/prompt + optional schedule).
21
+ * @param input - raw user input (title/description/prompt + optional schedule/parent).
14
22
  * @param now - clock instant (ms epoch).
15
23
  * @param id - minted task id.
24
+ * @param maxSubtaskDepth - deployment subtask depth limit.
16
25
  */
17
- export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string): CreateTaskResult;
26
+ export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string, maxSubtaskDepth?: number): CreateTaskResult;
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Delete-task use case: drop a task from the ledger and clear the selection
3
3
  * when it pointed at the removed task. Pure ledger transition (no persistence
4
- * or notify — the controller orchestrates those).
4
+ * or notify - the controller orchestrates those).
5
+ *
6
+ * A task that still has subtasks is refused: deleting the parent would leave
7
+ * dangling child links, and silently detaching (or cascading) the children is
8
+ * a data-loss decision the user must make explicitly.
5
9
  */
6
10
  import type { TaskRecord } from '../tasks.ts';
7
11
  /** Result of a delete transition. */
@@ -10,6 +14,8 @@ export interface DeleteTaskResult {
10
14
  tasks: readonly TaskRecord[];
11
15
  /** Whether the previous selection referenced the removed task. */
12
16
  selectionCleared: boolean;
17
+ /** True when the task still carries subtasks, so nothing was removed. */
18
+ blocked: boolean;
13
19
  }
14
20
  /**
15
21
  * Apply a delete across the ledger. The selection (a task id) is cleared when
@@ -0,0 +1,21 @@
1
+ import type { TaskRecord } from '../tasks.ts';
2
+ /** Result of a parent-link transition. */
3
+ export interface SetParentResult {
4
+ /** The next ledger; the input reference when refused. */
5
+ tasks: readonly TaskRecord[];
6
+ /** Whether the link (or the detachment) was stored. */
7
+ applied: boolean;
8
+ /** User-facing refusal reason; absent when applied. */
9
+ error?: string;
10
+ }
11
+ /**
12
+ * Attach or detach one task. A null (or blank) parent detaches, which is always
13
+ * allowed for a known on-board task; attaching validates the whole chain so a
14
+ * cyclic or too-deep ledger can never be stored.
15
+ * @param tasks - current ledger.
16
+ * @param id - the task being moved.
17
+ * @param parentId - the new parent, or null to detach.
18
+ * @param now - clock instant (ms epoch).
19
+ * @param maxSubtaskDepth - deployment subtask depth limit.
20
+ */
21
+ export declare function applySetParent(tasks: readonly TaskRecord[], id: string, parentId: string | null, now: number, maxSubtaskDepth?: number): SetParentResult;
@@ -15,7 +15,7 @@ import type { TaskHandoverInput } from '../handover.ts';
15
15
  * Editable fields on a task (the update patch surface). `freeze` replaces the
16
16
  * continuation-card snapshot (restamping frozenAt); an explicit null clears it.
17
17
  */
18
- export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession'>> & {
18
+ export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession' | 'teamRun'>> & {
19
19
  freeze?: FreezeSnapshot & {
20
20
  redacted?: boolean;
21
21
  } | null;
@@ -0,0 +1,27 @@
1
+ import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
2
+ import type { TaskBoardAction, TaskBoardSnapshot } from '../protocol.ts';
3
+ /**
4
+ * The narrow Host face the tools need. TaskBoardHostService satisfies it
5
+ * structurally, so tests and a future host can drive the same surface.
6
+ */
7
+ export interface TaskBoardToolHost {
8
+ /** Current board snapshot (tasks + scheduler + power + deployment constants). */
9
+ snapshot(): TaskBoardSnapshot;
10
+ /** Submit one confirmed Host action; returns the resulting snapshot. */
11
+ apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot;
12
+ }
13
+ /** Registered tool names, in registration order. */
14
+ export declare const TASK_BOARD_TOOL_NAMES: readonly ["task_board_list", "task_board_get", "task_board_create", "task_board_update", "task_board_set_parent", "task_board_run", "task_board_manage", "task_board_schedule"];
15
+ /**
16
+ * The calling session id, for the execution audit trail (the same stamp the
17
+ * browser asserts). Absent when a call has no agent behind it.
18
+ */
19
+ export declare function callingSessionId(exec: {
20
+ agent?: unknown;
21
+ }): string | undefined;
22
+ /**
23
+ * Build every task-board tool bound to one Host service.
24
+ * @param host - the Host ledger/service face (TaskBoardHostService).
25
+ * @returns the tool definitions, in TASK_BOARD_TOOL_NAMES order.
26
+ */
27
+ export declare function buildTaskBoardTools(host: TaskBoardToolHost): ToolDefinition[];
@@ -1,4 +1,4 @@
1
- import { type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
1
+ import { type ExecutionOutcome, type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
2
2
  import { type TaskBoardAction, type TaskBoardSchedulerSnapshot } from './protocol.ts';
3
3
  import { type TaskPermission } from './core/handover.ts';
4
4
  export interface LedgerState {
@@ -9,6 +9,13 @@ export interface LedgerState {
9
9
  export interface OpenedRun {
10
10
  task: TaskRecord;
11
11
  execution: ExecutionRecord;
12
+ /**
13
+ * How the Host obtains this run's session. Absent or `session` launches a
14
+ * fresh (or reused) execution session; `teammate` means the run belongs to a
15
+ * team-mode cascade, where the Host asks the root's Lead session to spawn a
16
+ * teammate instead of launching a session of its own.
17
+ */
18
+ dispatch?: 'session' | 'teammate';
12
19
  }
13
20
  /** Minimal value copy used by the Host session monitor. */
14
21
  export interface OpenExecutionReference {
@@ -52,6 +59,16 @@ export type PowerShellProbe = (script: string, timeoutMs: number) => string | un
52
59
  * probe is injectable so the fallback chain is testable off Windows.
53
60
  */
54
61
  export declare function win32StartTimeMs(pid: number, probe?: PowerShellProbe): number | undefined;
62
+ /**
63
+ * Repair parent links after a load or an import: a link whose parent row is
64
+ * missing, cyclic, or names an archived parent while the child is on board is
65
+ * dropped (the task becomes a root) instead of dropping the task. A chain
66
+ * deeper than the current `maxSubtaskDepth` is deliberately KEPT: the limit
67
+ * gates new links, it never deletes a stored one, and every cascade walk is
68
+ * bounded by the limit anyway. The Host is the only writer of these links, so
69
+ * this is a last-resort guard against a hand-edited or imported document.
70
+ */
71
+ export declare function repairParentLinks(tasks: readonly TaskRecord[]): TaskRecord[];
55
72
  export declare class HostTaskLedger {
56
73
  private readonly now;
57
74
  private document;
@@ -65,9 +82,24 @@ export declare class HostTaskLedger {
65
82
  readonly schedulerFile: string;
66
83
  /** Session-default permission the confirmation gate compares against. */
67
84
  readonly sessionDefaultPermission: TaskPermission;
85
+ /**
86
+ * Deployment subtask depth limit (1..3): the lineage gate every write obeys.
87
+ * The settings card edits it live, so {@link setMaxSubtaskDepth} mutates it
88
+ * instead of remounting the row.
89
+ */
90
+ private depthLimit;
68
91
  constructor(dir?: string, now?: () => number, options?: {
69
92
  sessionDefaultPermission?: TaskPermission;
93
+ maxSubtaskDepth?: number;
70
94
  });
95
+ /** Current subtask depth limit (1..3). */
96
+ get maxSubtaskDepth(): number;
97
+ /**
98
+ * Apply a live settings edit of the subtask depth limit. A no-op when the
99
+ * normalized limit is unchanged, so a coarse volatile invalidation that
100
+ * changed nothing emits nothing.
101
+ */
102
+ setMaxSubtaskDepth(depth: number): void;
71
103
  /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
72
104
  private cleanStaleTemporaryFiles;
73
105
  /** Revision + scheduler without any task cloning; feeds the SSE event frame. */
@@ -84,20 +116,81 @@ export declare class HostTaskLedger {
84
116
  runtimeView(): LedgerRuntimeView;
85
117
  /** Count armed, non-archived schedules without cloning task histories. */
86
118
  armedScheduleCount(): number;
119
+ /**
120
+ * The earliest armed `nextRunAt` strictly after the supplied Host time, or
121
+ * undefined when no schedule is armed in the future. This is the instant the
122
+ * Host arms its native timer at, so the board wakes exactly when work is due
123
+ * instead of polling a fixed heartbeat.
124
+ * @param now - current Host time in ms epoch.
125
+ * @returns the nearest future trigger, or undefined when none is armed.
126
+ */
127
+ nextArmedRunAt(now: number): number | undefined;
87
128
  /** Return value-only references for schedules due at the supplied Host time. */
88
129
  dueSchedules(now: number): DueScheduleReference[];
89
130
  subscribe(listener: () => void): () => void;
90
131
  dispose(): void;
91
132
  applyRequest(requestId: string, action: TaskBoardAction, initiator?: string): {
92
133
  state: LedgerState;
93
- run?: OpenedRun;
134
+ runs?: OpenedRun[];
94
135
  };
95
- openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun | undefined;
136
+ /**
137
+ * Open the cascade one due schedule triggers: an empty array means nothing
138
+ * ran (already running, or a participant whose elevated permission is still
139
+ * unconfirmed), and the rule rolls to its next occurrence either way.
140
+ */
141
+ openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun[];
96
142
  skipMissed(now: number): void;
97
143
  setScheduler(patch: Partial<TaskBoardSchedulerSnapshot>): void;
144
+ /**
145
+ * Settle one execution. A cascade participant first records its OWN outcome
146
+ * and only finalizes once every child execution in its run group has
147
+ * settled, so a parent card leaves 'running' with the whole tree's verdict
148
+ * rather than its own turn alone.
149
+ */
150
+ settle(taskId: string, executionId: string, outcome: ExecutionOutcome, error?: string): void;
98
151
  attachSession(taskId: string, executionId: string, sessionId: string): void;
99
- settle(taskId: string, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', error?: string): void;
100
152
  private apply;
153
+ /**
154
+ * The binding that makes a run illegal, if any.
155
+ *
156
+ * A plain cascade launches one session per participant, so every participant
157
+ * carries its own resolved binding and each one gates the run. A team run
158
+ * launches only the Lead session: the Lead's binding gates it, while a
159
+ * subtask's OWN above-default pin cannot be applied to a teammate and is
160
+ * refused instead of being silently dropped (an inherited binding is the
161
+ * Lead's own and stays allowed once the Lead is confirmed).
162
+ * @param root - the task being run.
163
+ * @returns the first refusal, or undefined when the run may start.
164
+ */
165
+ private bindingRefusal;
166
+ /**
167
+ * The tasks a cascade from `rootId` would actually open executions for, with
168
+ * their effective execution targets resolved. Archived members never run, and
169
+ * a member that already has an open execution is skipped (a task cannot run
170
+ * twice), so the result is the participant set the launch will use.
171
+ */
172
+ private cascadeParticipants;
173
+ /** Whether a task or any of its subtasks still has an open execution. */
174
+ private subtreeHasOpenExecution;
175
+ /**
176
+ * Open one execution per cascade participant (the requested task and its
177
+ * on-board descendants, within the depth limit) under a single run group, and
178
+ * commit the ledger once. The returned runs are ordered root-first, so the
179
+ * Host launches the parent before its subtasks.
180
+ * @param root - the task the user ran.
181
+ * @param now - clock instant (ms epoch).
182
+ * @param initiator - the DSH session that asked for the run (audit only).
183
+ * @param rerun - true to reset the root to 'todo' before starting it.
184
+ * @param nextRunAt - when set, the root's schedule also rolls forward.
185
+ */
186
+ private startCascade;
187
+ /**
188
+ * Finalize every cascade parent that is ready, walking from `taskId` up the
189
+ * lineage: a member settles once its own outcome is recorded and none of its
190
+ * group children is still open, and its verdict folds those children in
191
+ * (failure dominates, then cancellation).
192
+ */
193
+ private settleCascade;
101
194
  private repairSchedules;
102
195
  private reconcileInterruptedStarts;
103
196
  /**
@@ -49,7 +49,29 @@ export declare class SessionLaunchError extends Error {
49
49
  * first, the provenance wrap then encloses the instruction. Plain tasks (no
50
50
  * freeze) keep the bare handover preamble + prompt.
51
51
  */
52
- export declare function promptText(task: TaskRecord): string;
52
+ /** One other execution member of a run, as named in the launched task's prompt. */
53
+ export interface PromptPeer {
54
+ /** Task id of the member. */
55
+ id: string;
56
+ /** Member title. */
57
+ title: string;
58
+ /** Teammate name when this member runs as a teammate. */
59
+ name?: string;
60
+ }
61
+ /** Execution-shape context appended to the launched task's prompt. */
62
+ export interface PromptContext {
63
+ /** The other members this run opens, excluding the launched task itself. */
64
+ peers?: readonly PromptPeer[];
65
+ /** True when those members run as teammates inside this session (Team Lead). */
66
+ team?: boolean;
67
+ }
68
+ /**
69
+ * Build the execution prompt for one task.
70
+ * @param task - the task being launched.
71
+ * @param context - the run's other members, for the execution-shape section.
72
+ * @returns the prompt text.
73
+ */
74
+ export declare function promptText(task: TaskRecord, context?: PromptContext): string;
53
75
  export declare class HostExecutionRunner {
54
76
  private readonly gateway;
55
77
  private readonly commands?;
@@ -77,7 +99,23 @@ export declare class HostExecutionRunner {
77
99
  */
78
100
  launch(task: TaskRecord, options?: {
79
101
  reuseSessionId?: string;
102
+ promptContext?: PromptContext;
80
103
  }): Promise<string>;
104
+ /**
105
+ * Fail closed when a reused session records a different Agent preset than
106
+ * the one this task pins.
107
+ *
108
+ * The fresh branch asserts the pin by passing `agentPreset` to
109
+ * `session/create`; the reuse branch must not call that method (it would
110
+ * create or re-adopt the very session the board only meant to continue), so
111
+ * the recorded value is read through `session/projections` — the documented
112
+ * read that resolves no Agent — and compared here (issue #1708). A session
113
+ * whose preset cannot be read is not treated as a match: silently continuing
114
+ * under an unknown composition is the failure this guards.
115
+ * @param sessionId - the session this execution continues in.
116
+ * @param pinned - the preset id the task pins, when it pins one.
117
+ */
118
+ private assertReusedPreset;
81
119
  /**
82
120
  * Re-assert the pinned execution contract on a session and queue the task
83
121
  * prompt. Shared by the fresh-session and reuse paths so both apply exactly
@@ -4,16 +4,57 @@ import { HostExecutionRunner, type SessionCommandDispatcher, type TaskBoardWorks
4
4
  import { PowerInhibitor } from './power-inhibitor.ts';
5
5
  import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from './protocol.ts';
6
6
  import type { TaskPermission } from './core/handover.ts';
7
+ /** One teammate the Host asks the Agent Teams service to spawn for a team run. */
8
+ export interface TeamSpawnInput {
9
+ /** Session id of the run's Team Lead (the root task's execution session). */
10
+ leadSessionId: string;
11
+ /** Immutable lower-kebab-case teammate name, unique inside the Team. */
12
+ name: string;
13
+ /** Short description of the delegated responsibility. */
14
+ description: string;
15
+ /** The subtask's execution prompt. */
16
+ prompt: string;
17
+ }
18
+ /** Result of one teammate spawn attempt. */
19
+ export interface TeamSpawnResult {
20
+ /** The teammate's session id when it reached the active edge. */
21
+ sessionId?: string;
22
+ /** Why the spawn failed: a thrown call, or a member that settled as failed. */
23
+ error?: string;
24
+ }
25
+ /**
26
+ * The Agent Teams capability the Host needs for team-mode runs. The plugin
27
+ * builds it from the optional `agentTeams` service; it is absent when this
28
+ * deployment does not serve that service, in which case a team run is refused
29
+ * instead of silently degrading into a plain cascade.
30
+ */
31
+ export interface TaskBoardTeamDispatcher {
32
+ spawn(input: TeamSpawnInput): Promise<TeamSpawnResult>;
33
+ }
34
+ /**
35
+ * The native timer face the Host arms through. The cordis `timer` service
36
+ * (dsh-base's own `cordis-plugin-timer` row) provides it: its handles are
37
+ * registered on the owning fiber, so unloading the board clears every armed
38
+ * timer without this service tracking handles by hand. A composition that
39
+ * serves no timer service falls back to the process globals.
40
+ */
41
+ export interface HostTimerFace {
42
+ timeout(callback: () => void, delay: number): () => void;
43
+ interval(callback: () => void, delay: number): () => void;
44
+ }
7
45
  export declare class TaskBoardHostService {
8
46
  readonly ledger: HostTaskLedger;
9
47
  readonly runner: HostExecutionRunner;
10
48
  readonly power: PowerInhibitor;
11
49
  private readonly listeners;
12
- private timers;
13
- private lastScheduleTick;
50
+ /** The one recurring timer: the session-roster poll. */
51
+ private pollTimer;
52
+ /** The armed schedule timer, if a trigger is pending. */
53
+ private scheduleTimer;
54
+ /** The instant the armed schedule timer targets (ms epoch), for resume detection. */
55
+ private scheduleTarget;
14
56
  private disposed;
15
57
  private pollInFlight;
16
- private tickInFlight;
17
58
  private active;
18
59
  /**
19
60
  * Ids the last roster poll saw as present and idle; undefined while the
@@ -23,6 +64,8 @@ export declare class TaskBoardHostService {
23
64
  */
24
65
  private idleSessionIds;
25
66
  private preventIdleSleep;
67
+ private readonly team;
68
+ private readonly timers;
26
69
  private lastPowerJson;
27
70
  private readonly now;
28
71
  constructor(gateway: TypertGateway, options?: {
@@ -32,6 +75,9 @@ export declare class TaskBoardHostService {
32
75
  commandDispatcher?: SessionCommandDispatcher;
33
76
  workspaceRegistry?: TaskBoardWorkspaceRegistry;
34
77
  sessionDefaultPermission?: TaskPermission;
78
+ maxSubtaskDepth?: number;
79
+ team?: TaskBoardTeamDispatcher;
80
+ timers?: HostTimerFace;
35
81
  });
36
82
  start(): void;
37
83
  setConfiguration(active: boolean, preventIdleSleep: boolean): void;
@@ -42,14 +88,61 @@ export declare class TaskBoardHostService {
42
88
  apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot;
43
89
  dispose(): void;
44
90
  private launch;
91
+ private scheduleTeammate;
92
+ /**
93
+ * Spawn one teammate inside the Lead session and attach the teammate's
94
+ * session to the subtask execution, so the existing session monitor settles
95
+ * it from the teammate's own turn like any other execution. A spawn that
96
+ * fails settles the subtask as failed immediately, which then folds into the
97
+ * Lead's cascade verdict.
98
+ */
99
+ private spawnTeammate;
45
100
  private pollSessions;
46
101
  /** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
47
102
  private reconcileExecutions;
48
- private tickSchedule;
103
+ /** Drop the armed schedule timer and forget its target. */
104
+ private clearScheduleTimer;
105
+ /**
106
+ * Boot / resume recovery: skip every occurrence that came due while the
107
+ * board was not running and roll each schedule to its next future target,
108
+ * then arm the timer for the nearest one. Rendering the occurrence is
109
+ * deliberately not attempted: the ACL of a card that fired hours ago is
110
+ * stale, and the board's own recovery contract is "missed triggers are
111
+ * skipped, never replayed".
112
+ */
113
+ private recoverSchedule;
114
+ /**
115
+ * Arm the native timer at the nearest armed future trigger. One timer serves
116
+ * every schedule: the ledger's next target is the only instant the Host has
117
+ * to wake for. A target beyond the platform's timer ceiling re-arms in
118
+ * segments, and a target already past (the wall clock jumped, or the process
119
+ * was suspended) is handled immediately as a recovery.
120
+ */
121
+ private armSchedule;
122
+ /**
123
+ * One armed target became due. A fire landing well past its target is a
124
+ * resume (suspend, throttle, forward clock jump) rather than a normal
125
+ * occurrence, so it takes the recovery path instead of launching a run for a
126
+ * long-stale instant.
127
+ */
128
+ private onScheduleFire;
49
129
  private armedSchedules;
130
+ /**
131
+ * Launch one run set. The root goes first and receives the run shape in its
132
+ * prompt (which members this run opens, and how they run); every plain-cascade
133
+ * member then launches on its own so one refused participant cannot hold the
134
+ * others back. A team run's members are spawned inside the root's Lead
135
+ * session instead, once that session exists.
136
+ */
137
+ private dispatchRuns;
50
138
  private scheduleLaunch;
51
139
  private schedulePoll;
52
- private scheduleTick;
140
+ /**
141
+ * Re-arm from the ledger's current targets. Callers that just changed a
142
+ * schedule (the host routes, the agent tools) invoke this after the write
143
+ * commits, so a new or edited trigger arms without waiting for the next fire.
144
+ */
145
+ refreshSchedule(): void;
53
146
  private syncPowerReasons;
54
147
  private emit;
55
148
  }
@@ -7,21 +7,25 @@
7
7
  */
8
8
  import type { Context, Volatile } from '@deepseek-ai/cordis';
9
9
  import z from '@deepseek-ai/schemastery';
10
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
10
11
  import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
12
+ import { type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts';
11
13
  import { type TaskPermission } from './core/tasks.ts';
12
14
  /** Default environment variable holding the authenticated proxy token. */
13
15
  export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
14
16
  export declare const inject: string[];
15
17
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
16
- export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002\u82E5\u4F60\u540C\u65F6\u7528 todo_write \u7EF4\u62A4\u4F1A\u8BDD\u9876\u90E8\u7684\u53EF\u89C1\u8BA1\u5212\u5217\u8868\uFF0C\u6700\u7EC8\u56DE\u590D\u524D\u5FC5\u987B\u518D\u6B21\u8C03\u7528 todo_write \u6536\u5C3E\uFF1A\u6CA1\u6709\u5269\u4F59\u5DE5\u4F5C\u65F6\u4E0D\u8981\u4FDD\u7559 in_progress\uFF0C\u5DF2\u5B8C\u6210\u7684\u6700\u540E\u4E00\u6B65\u8981\u6807\u4E3A completed\u3002";
18
+ export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u4EFB\u52A1\u53EF\u5EFA\u5B50\u4EFB\u52A1\uFF08\u6DF1\u5EA6\u4E0A\u9650\u53EF\u914D 1..3\uFF0C\u9ED8\u8BA4 1 \u5C42\uFF0C\u5B50\u4EFB\u52A1\u4E0D\u80FD\u518D\u5E26\u5B50\u4EFB\u52A1\uFF09\uFF0C\u6267\u884C\u7236\u4EFB\u52A1\u4F1A\u5E76\u53D1\u6267\u884C\u5176\u5B50\u4EFB\u52A1\u6811\uFF0C\u5B50\u4EFB\u52A1\u53EF\u5355\u72EC\u8986\u76D6\u6743\u9650\u4E0E\u6A21\u578B\uFF1B\u53E6\u6CE8\u518C task_board_* agent \u5DE5\u5177\uFF08list/get/create/update/set_parent/run/manage/schedule\uFF09\uFF0C\u4EFB\u4F55\u4F1A\u8BDD\u90FD\u53EF\u76F4\u63A5\u8BFB\u5199\u770B\u677F\u3001\u5B50\u4EFB\u52A1\u4E0E\u5B9A\u65F6\u8BA1\u5212\uFF0C\u4F46\u8FD0\u884C\u4EFB\u52A1\u4F1A\u771F\u5B9E\u6267\u884C\u5E76\u6D88\u8017\u989D\u5EA6\uFF0C\u9AD8\u4E8E\u4F1A\u8BDD\u9ED8\u8BA4\u6743\u9650\u7684\u7ED1\u5B9A\u4ECD\u5FC5\u987B\u7531\u7528\u6237\u5728 GUI \u4EBA\u5DE5\u786E\u8BA4\uFF08\u5DE5\u5177\u523B\u610F\u4E0D\u63D0\u4F9B\u786E\u8BA4\u80FD\u529B\uFF09\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002\u82E5\u4F60\u540C\u65F6\u7528 todo_write \u7EF4\u62A4\u4F1A\u8BDD\u9876\u90E8\u7684\u53EF\u89C1\u8BA1\u5212\u5217\u8868\uFF0C\u6700\u7EC8\u56DE\u590D\u524D\u5FC5\u987B\u518D\u6B21\u8C03\u7528 todo_write \u6536\u5C3E\uFF1A\u6CA1\u6709\u5269\u4F59\u5DE5\u4F5C\u65F6\u4E0D\u8981\u4FDD\u7559 in_progress\uFF0C\u5DF2\u5B8C\u6210\u7684\u6700\u540E\u4E00\u6B65\u8981\u6807\u4E3A completed\u3002";
17
19
  /**
18
20
  * Plugin config, validated by the same-named schemastery schema.
19
21
  *
20
22
  * This schema IS the board's settings page: the 0.1.7 Host serves one
21
23
  * configuration form per profile entry from the entry's own Config, so the
22
- * three fields the browser card edits are marked volatile — the Loader commits
24
+ * fields the browser card edits are marked volatile — the Loader commits
23
25
  * an edit into the running fiber's references without remounting the row (no
24
- * settings document of the plugin's own exists any more).
26
+ * settings document of the plugin's own exists any more). A field without the
27
+ * marker is refused by the settings write path, which fences writes on
28
+ * volatility.
25
29
  */
26
30
  export interface Config {
27
31
  /**
@@ -48,7 +52,25 @@ export interface Config {
48
52
  * confirmation before it may run; cron refuses unconfirmed cards.
49
53
  */
50
54
  sessionDefaultPermission?: TaskPermission;
55
+ /**
56
+ * Subtask depth limit, 1..3 (default 1). At 1 a task may carry one level of
57
+ * subtasks and a subtask cannot be given subtasks of its own; raising it
58
+ * allows deeper trees. Every extra level multiplies the sessions one run
59
+ * of the root task opens, so the ceiling is deliberately small.
60
+ */
61
+ maxSubtaskDepth?: Volatile<number>;
62
+ /**
63
+ * Continuable-subagent provider the Agent Teams service uses to compose a
64
+ * teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
65
+ * only team-mode runs use it.
66
+ */
67
+ teamProvider?: string;
51
68
  }
69
+ /**
70
+ * Continuable-subagent provider for fresh teammates, matching the Agent Teams
71
+ * tool plugin's own `freshProvider` default.
72
+ */
73
+ export declare const DEFAULT_TEAM_PROVIDER = "spawn";
52
74
  /**
53
75
  * The schema is left to inference rather than annotated with `z<Config>`: a
54
76
  * volatile field's parsed output is a `Volatile` reference while its accepted
@@ -62,6 +84,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
62
84
  trustedProxyHosts: z<string[], string[], "defined">;
63
85
  proxyTokenEnv: z<string, string, "defined">;
64
86
  sessionDefaultPermission: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "defined">;
87
+ maxSubtaskDepth: z<number, number, "volatile-defined">;
88
+ teamProvider: z<string, string, "defined">;
65
89
  }>>, Schemastery.ObjectT<NoInfer<{
66
90
  announceToAgent: z<boolean, boolean, "volatile-defined">;
67
91
  enabled: z<boolean, boolean, "volatile-defined">;
@@ -69,6 +93,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
69
93
  trustedProxyHosts: z<string[], string[], "defined">;
70
94
  proxyTokenEnv: z<string, string, "defined">;
71
95
  sessionDefaultPermission: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "defined">;
96
+ maxSubtaskDepth: z<number, number, "volatile-defined">;
97
+ teamProvider: z<string, string, "defined">;
72
98
  }>>, "plain">;
73
99
  declare module '@deepseek-ai/cordis' {
74
100
  interface Events {
@@ -100,6 +126,68 @@ export declare function resolveProxyAccess(config: Config | undefined, env?: Nod
100
126
  trustedProxyHosts: string[];
101
127
  proxyToken?: string;
102
128
  };
129
+ /** The registry face the agent tools register into. */
130
+ interface AgentToolRegistry {
131
+ register(definition: ToolDefinition): () => void;
132
+ }
133
+ /**
134
+ * Resolve the optional agent-tool registry. The board deliberately does not
135
+ * inject it: a deployment whose runtime serves no registry must still mount the
136
+ * whole board and lose only the agent-tool surface, the same tolerance the
137
+ * optional llm service gets.
138
+ * @param ctx - the plugin context.
139
+ * @returns the registry, or undefined when this deployment serves none.
140
+ */
141
+ export declare function resolveToolRegistry(ctx: Context): AgentToolRegistry | undefined;
142
+ /**
143
+ * Resolve the native cordis timer service (dsh-base's own `cordis-plugin-timer`
144
+ * row). The board arms its schedule through it so unloading the row clears
145
+ * every pending timer with the owning fiber, instead of the row tracking raw
146
+ * process handles by hand. A composition that serves no timer service keeps
147
+ * working on the process globals.
148
+ * @param ctx - the plugin context.
149
+ * @returns the timer face, or undefined when this deployment serves none.
150
+ */
151
+ export declare function resolveHostTimers(ctx: Context): HostTimerFace | undefined;
152
+ /** The subset of the Agent Teams service this plugin drives. */
153
+ interface AgentTeamsFace {
154
+ spawnTeammate(caller: unknown, request: {
155
+ name: string;
156
+ description: string;
157
+ prompt: Array<{
158
+ type: 'text';
159
+ text: string;
160
+ }>;
161
+ context: 'fresh' | 'fork';
162
+ provider: string;
163
+ signal: AbortSignal;
164
+ }): Promise<{
165
+ member?: {
166
+ id?: string;
167
+ phase?: string;
168
+ error?: string;
169
+ };
170
+ }>;
171
+ }
172
+ /**
173
+ * Resolve the optional Agent Teams service. Like the tool registry and the llm
174
+ * runtime, it is deliberately not injected: a deployment without Agent Teams
175
+ * must still mount the whole board and only lose team-mode runs.
176
+ * @param ctx - the plugin context.
177
+ * @returns the service, or undefined when this deployment serves none.
178
+ */
179
+ export declare function resolveAgentTeams(ctx: Context): AgentTeamsFace | undefined;
180
+ /**
181
+ * Build the team dispatcher: the run's Lead session spawns one teammate per
182
+ * subtask through the Agent Teams service, and the teammate's session id is
183
+ * attached to that subtask's execution so the ordinary session monitor settles
184
+ * it. Returns undefined when the deployment serves no Agent Teams service or
185
+ * no live-agent registry, which makes a team run fail closed.
186
+ * @param ctx - the plugin context.
187
+ * @param provider - the continuable-subagent provider for fresh teammates.
188
+ * @returns the dispatcher, or undefined when team runs cannot be served.
189
+ */
190
+ export declare function buildTeamDispatcher(ctx: Context, provider: string): TaskBoardTeamDispatcher | undefined;
103
191
  /**
104
192
  * Read the optional `llm` service. The board deliberately does not inject it:
105
193
  * a deployment without a model must still mount the board, and the parse route
@@ -112,8 +200,8 @@ export declare function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined;
112
200
  * Activate the board's host half: ledger, routes, cron scheduler, power
113
201
  * inhibitor, and the model-facing announcement.
114
202
  *
115
- * The effective settings are the config the Host hands this row. The three
116
- * fields the browser card edits are schema-volatile, so the Loader commits an
203
+ * The effective settings are the config the Host hands this row. The fields
204
+ * the browser card edits are schema-volatile, so the Loader commits an
117
205
  * edit into the running fiber's references instead of remounting the row —
118
206
  * `sync` therefore reads them at use time and follows
119
207
  * `loader/volatile-update`, the event the Loader emits once it has committed