@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.
Files changed (120) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +59 -20
  3. package/README.zh.md +59 -20
  4. package/lib/client.js +6208 -624
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +4774 -493
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
  9. package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
  10. package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
  11. package/lib/types/client/board/EditTaskModal.d.ts +5 -2
  12. package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
  13. package/lib/types/client/board/NewTaskModal.d.ts +4 -1
  14. package/lib/types/client/board/TagManagerModal.d.ts +26 -0
  15. package/lib/types/client/board/TaskBoard.d.ts +10 -1
  16. package/lib/types/client/board/TaskCard.d.ts +14 -1
  17. package/lib/types/client/board/TaskDetail.d.ts +3 -1
  18. package/lib/types/client/board/TaskForm.d.ts +4 -1
  19. package/lib/types/client/board/VerificationReport.d.ts +23 -0
  20. package/lib/types/client/board/card-view.d.ts +113 -0
  21. package/lib/types/client/board/icons.d.ts +32 -0
  22. package/lib/types/client/board/overlay.d.ts +69 -0
  23. package/lib/types/client/board/status-key.d.ts +15 -0
  24. package/lib/types/client/board/task-markdown.d.ts +19 -0
  25. package/lib/types/client/index.d.ts +23 -5
  26. package/lib/types/client/locales.d.ts +168 -1
  27. package/lib/types/client/native-panel.d.ts +6 -20
  28. package/lib/types/client/plugin-card-seat.d.ts +5 -0
  29. package/lib/types/client/schedule-zone.d.ts +50 -0
  30. package/lib/types/client/seats.d.ts +18 -0
  31. package/lib/types/client/service.d.ts +32 -0
  32. package/lib/types/client/settings-form.d.ts +16 -0
  33. package/lib/types/core/controller.d.ts +46 -11
  34. package/lib/types/core/extension.d.ts +283 -0
  35. package/lib/types/core/handover.d.ts +13 -1
  36. package/lib/types/core/issue-form.d.ts +16 -0
  37. package/lib/types/core/poll-cadence.d.ts +31 -0
  38. package/lib/types/core/tasks.d.ts +228 -17
  39. package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
  40. package/lib/types/core/use-cases/task-tag.d.ts +48 -0
  41. package/lib/types/core/use-cases/task-update.d.ts +5 -3
  42. package/lib/types/core/verification-budget.d.ts +52 -0
  43. package/lib/types/core/verification.d.ts +753 -0
  44. package/lib/types/core/workspace-target.d.ts +41 -0
  45. package/lib/types/host/agent-tools.d.ts +1 -1
  46. package/lib/types/host/extension-registry.d.ts +111 -0
  47. package/lib/types/host/llm-dispatch.d.ts +63 -0
  48. package/lib/types/host/verification-gate.d.ts +95 -0
  49. package/lib/types/host/verification-runner.d.ts +139 -0
  50. package/lib/types/host/workspace-evidence.d.ts +53 -0
  51. package/lib/types/host-ledger.d.ts +133 -14
  52. package/lib/types/host-runner.d.ts +55 -10
  53. package/lib/types/host-service.d.ts +205 -11
  54. package/lib/types/index.d.ts +134 -26
  55. package/lib/types/protocol.d.ts +92 -10
  56. package/lib/types/tool-surface.d.ts +71 -0
  57. package/package.json +28 -31
  58. package/src/client/PluginSettingsCard.tsx +9 -0
  59. package/src/client/TaskBoardSettingsCard.tsx +378 -66
  60. package/src/client/board/ClampedMarkdown.tsx +69 -0
  61. package/src/client/board/ConfirmDialog.tsx +7 -3
  62. package/src/client/board/EditTaskModal.tsx +5 -2
  63. package/src/client/board/LinkSubtaskModal.tsx +12 -7
  64. package/src/client/board/NewTaskModal.tsx +168 -21
  65. package/src/client/board/TagManagerModal.tsx +235 -0
  66. package/src/client/board/TaskBoard.tsx +259 -103
  67. package/src/client/board/TaskCard.tsx +214 -81
  68. package/src/client/board/TaskDetail.tsx +289 -43
  69. package/src/client/board/TaskForm.tsx +13 -3
  70. package/src/client/board/VerificationReport.tsx +322 -0
  71. package/src/client/board/card-view.ts +220 -0
  72. package/src/client/board/icons.tsx +89 -0
  73. package/src/client/board/overlay.tsx +189 -0
  74. package/src/client/board/status-key.ts +30 -0
  75. package/src/client/board/task-markdown.module.css +137 -0
  76. package/src/client/board/task-markdown.tsx +106 -0
  77. package/src/client/board-settings.module.css +66 -0
  78. package/src/client/board.module.css +488 -63
  79. package/src/client/index.ts +82 -51
  80. package/src/client/locales.ts +343 -6
  81. package/src/client/native-panel.tsx +18 -2
  82. package/src/client/plugin-card-seat.ts +7 -0
  83. package/src/client/schedule-zone.ts +100 -2
  84. package/src/client/seats.tsx +58 -0
  85. package/src/client/service.ts +94 -0
  86. package/src/client/settings-card.module.css +31 -5
  87. package/src/client/settings-form.ts +27 -1
  88. package/src/core/controller.ts +105 -10
  89. package/src/core/extension.ts +376 -0
  90. package/src/core/freeze-snapshot.ts +2 -2
  91. package/src/core/handover.ts +16 -1
  92. package/src/core/issue-form.ts +19 -0
  93. package/src/core/poll-cadence.ts +41 -0
  94. package/src/core/store.ts +80 -15
  95. package/src/core/tasks.ts +293 -45
  96. package/src/core/use-cases/task-create.ts +30 -9
  97. package/src/core/use-cases/task-parent.ts +3 -2
  98. package/src/core/use-cases/task-schedule.ts +190 -25
  99. package/src/core/use-cases/task-tag.ts +110 -0
  100. package/src/core/use-cases/task-update.ts +13 -7
  101. package/src/core/verification-budget.ts +115 -0
  102. package/src/core/verification.ts +1594 -0
  103. package/src/core/workspace-target.ts +59 -0
  104. package/src/host/agent-tools.ts +125 -38
  105. package/src/host/extension-registry.ts +509 -0
  106. package/src/host/llm-dispatch.ts +87 -0
  107. package/src/host/verification-gate.ts +487 -0
  108. package/src/host/verification-runner.ts +702 -0
  109. package/src/host/workspace-evidence.ts +176 -0
  110. package/src/host-ai.ts +15 -9
  111. package/src/host-ledger.ts +468 -71
  112. package/src/host-routes.ts +15 -2
  113. package/src/host-runner.ts +106 -24
  114. package/src/host-service.ts +546 -40
  115. package/src/index.ts +305 -21
  116. package/src/protocol.ts +226 -28
  117. package/src/tool-surface.ts +95 -0
  118. package/lib/invariant.js +0 -6
  119. package/lib/types/invariant.d.ts +0 -3
  120. package/src/invariant.ts +0 -4
package/src/index.ts CHANGED
@@ -19,15 +19,31 @@ import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
19
19
  import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
20
20
  import { TaskBoardHostService, type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts'
21
21
  import { parseTaskDraft, TaskParseError } from './host-ai.ts'
22
- import { TASK_PERMISSIONS, type TaskPermission } from './core/tasks.ts'
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
- import { buildTaskBoardTools } from './host/agent-tools.ts'
36
+ import { buildTaskBoardTools, TASK_BOARD_TOOL_NAMES } from './host/agent-tools.ts'
37
+ import { PLUGIN_TOOL_SECTION_ORDERS, visibleToolText } from './tool-surface.ts'
26
38
  import { makeTaskBoardRoutes } from './host-routes.ts'
39
+ import { TASK_BOARD_SERVICE_NAME, type TaskBoardExtension } from './core/extension.ts'
27
40
  import { mountOnce } from './mount-once.ts'
41
+ import { createGoalVerificationGate, type GoalFace } from './host/verification-gate.ts'
42
+ import { normalizeCatalog, type ModelCatalogView, type VerificationSettings } from './core/verification.ts'
43
+ import { probeWorkspaceChanges } from './host/workspace-evidence.ts'
28
44
 
29
- /** Order of the announcement section within the tool-guidance band. */
30
- const SECTION_ORDER = 200
45
+ /** Order of the announcement section within the shared tool-guidance band. */
46
+ const SECTION_ORDER = PLUGIN_TOOL_SECTION_ORDERS['task-board']
31
47
 
32
48
  /** Default environment variable holding the authenticated proxy token. */
33
49
  export const DEFAULT_PROXY_TOKEN_ENV = 'DSH_TASK_BOARD_PROXY_TOKEN'
@@ -68,9 +84,12 @@ export interface Config {
68
84
  /** Environment variable whose value the authenticated proxy injects upstream. */
69
85
  proxyTokenEnv?: string
70
86
  /**
71
- * The deployment's session-default permission. A card whose effective
72
- * permission (handover bundle or pin) is above this value requires a human
73
- * confirmation before it may run; cron refuses unconfirmed cards.
87
+ * An explicit baseline for the permission confirmation gate. Left unset, the
88
+ * board follows the Host's own default permission preset — the value new DSH
89
+ * sessions start at — and falls back to `read-only` when the deployment
90
+ * serves no permission catalog. A card whose effective permission (handover
91
+ * bundle or pin) is above the baseline requires a human confirmation before
92
+ * it may run; cron refuses unconfirmed cards.
74
93
  */
75
94
  sessionDefaultPermission?: TaskPermission
76
95
  /**
@@ -80,12 +99,77 @@ export interface Config {
80
99
  * of the root task opens, so the ceiling is deliberately small.
81
100
  */
82
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>
83
109
  /**
84
110
  * Continuable-subagent provider the Agent Teams service uses to compose a
85
111
  * teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
86
112
  * only team-mode runs use it.
87
113
  */
88
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>
131
+ /**
132
+ * Goal acceptance for executions this board starts (default ON). When on,
133
+ * update_goal(action: complete) inside a task execution is refused until an
134
+ * acceptance pass is recorded for that execution: the three coding criteria,
135
+ * a 0.65 threshold, two rounds per criterion with the A/B slots swapped, and
136
+ * at most two acceptances per execution — the first failure returns its
137
+ * findings to the fixing agent, the second ends the cycle. The switch only
138
+ * affects goal-form executions of the board; plain chat and a task pinned to
139
+ * goalRun: false are untouched. When a separate third-party verifier also
140
+ * runs its own automatic acceptance, both judges score independently: this
141
+ * one gates completion, the other only steers, and no public interface lets
142
+ * the two share a verdict.
143
+ */
144
+ goalVerification?: Volatile<boolean>
145
+ /**
146
+ * Judge model for goal acceptance as provider/model. Blank inherits the HOST's
147
+ * own model catalog default — never the card's pinned execution model.
148
+ */
149
+ goalVerificationModel?: Volatile<string>
150
+ /**
151
+ * Reasoning effort for the judge. Blank inherits the host default level; an
152
+ * effort the resolved model does not declare is dropped instead of being
153
+ * sent, and the fallback to the model's own default is reported.
154
+ */
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>
89
173
  }
90
174
 
91
175
  /**
@@ -95,22 +179,76 @@ export interface Config {
95
179
  export const DEFAULT_TEAM_PROVIDER = 'spawn'
96
180
 
97
181
  /**
98
- * The schema is left to inference rather than annotated with `z<Config>`: a
99
- * volatile field's parsed output is a `Volatile` reference while its accepted
100
- * input stays the plain value, so the two sides no longer share one shape and
101
- * the annotation would reject the schema the Host must be given.
182
+ * Profile-patch shape of {@link Config}: what the Host validates the row's
183
+ * config against, before the schema turns volatile fields into live references
184
+ * and applies defaults. Declared separately because the two sides no longer
185
+ * share one shape, so the schema is annotated `z<ConfigInput, Config>` — the
186
+ * same split `dsh-git-graph` uses. The annotation is also what keeps the
187
+ * emitted declaration portable: an unannotated schema holding a nested object
188
+ * array infers a type that cannot be named without reaching into a transitive
189
+ * dependency's own types.
102
190
  */
103
- export const Config = z.object({
191
+ export interface ConfigInput {
192
+ /** Announce the board in the system prompt. */
193
+ announceToAgent?: boolean
194
+ /** Master switch for the plugin. */
195
+ enabled?: boolean
196
+ /** Prevent idle system sleep while sessions run or schedules are armed. */
197
+ preventIdleSleep?: boolean
198
+ /** Canonical reverse-proxy Host authorities. */
199
+ trustedProxyHosts?: string[]
200
+ /** Environment variable whose value the authenticated proxy injects. */
201
+ proxyTokenEnv?: string
202
+ /** Explicit baseline for the permission confirmation gate; unset follows the Host default. */
203
+ sessionDefaultPermission?: TaskPermission
204
+ /** Subtask depth limit, 1..3. */
205
+ maxSubtaskDepth?: number
206
+ /** Roster-poll cadence in seconds while the board has work to reconcile. */
207
+ sessionPollSeconds?: number
208
+ /** Continuable-subagent provider the Agent Teams service composes a teammate from. */
209
+ teamProvider?: string
210
+ /** Global native /goal switch; default off, frozen per execution. */
211
+ goalRunEnabled?: boolean
212
+ /** Goal acceptance switch. */
213
+ goalVerification?: boolean
214
+ /** Judge model route for goal acceptance; blank inherits the host default. */
215
+ goalVerificationModel?: string
216
+ /** Judge reasoning effort; blank inherits the host default. */
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
222
+ }
223
+
224
+ export const Config: z<ConfigInput, Config> = z.object({
104
225
  announceToAgent: z.boolean().default(false).volatile(),
105
226
  enabled: z.boolean().default(true).volatile(),
106
227
  preventIdleSleep: z.boolean().default(false).volatile(),
107
228
  trustedProxyHosts: z.array(z.string()).default([]),
108
229
  proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
109
- sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
230
+ sessionDefaultPermission: z.union(TASK_PERMISSIONS),
110
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(),
111
233
  teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
234
+ goalRunEnabled: z.boolean().default(false).volatile(),
235
+ goalVerification: z.boolean().default(true).volatile(),
236
+ goalVerificationModel: z.string().default('').volatile(),
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(),
112
240
  })
113
241
 
242
+ /** Schema default for the goal-acceptance switch, re-read for hand-built contexts. */
243
+ export const DEFAULT_GOAL_VERIFICATION = true
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
+
114
252
  declare module '@deepseek-ai/cordis' {
115
253
  interface Events {
116
254
  /**
@@ -160,6 +298,13 @@ export function resolveProxyAccess(config: Config | undefined, env: NodeJS.Proce
160
298
  /** The registry face the agent tools register into. */
161
299
  interface AgentToolRegistry {
162
300
  register(definition: ToolDefinition): () => void
301
+ /** Scoped lookup; present on the real registry, absent on a stub that only registers. */
302
+ get?(name: string, scope?: unknown): unknown
303
+ }
304
+
305
+ /** The lookup half of a resolved registry, when it exposes one. */
306
+ function lookupOf(registry: AgentToolRegistry | undefined): { get: (name: string, scope?: unknown) => unknown } | undefined {
307
+ return registry !== undefined && typeof registry.get === 'function' ? { get: registry.get.bind(registry) } : undefined
163
308
  }
164
309
 
165
310
  /**
@@ -203,6 +348,33 @@ export function resolveHostTimers(ctx: Context): HostTimerFace | undefined {
203
348
  /** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
204
349
  const DEFAULT_ANNOUNCE = false
205
350
 
351
+ /** The subset of the official permission-preset service the board reads. */
352
+ interface PermissionPresetsFace {
353
+ catalog(): { defaultPreset?: unknown }
354
+ }
355
+
356
+ /**
357
+ * The Host's own default permission preset, mapped into the board's vocabulary.
358
+ *
359
+ * This is the value new DSH sessions start at (the deployment's `permission`
360
+ * settings row), so a board that follows it treats an ordinary card exactly as
361
+ * the Host would. Absent when the deployment serves no permission catalog, or
362
+ * its default is a current-session identity (`auto`) that names no fixed
363
+ * sandbox mode; the caller then keeps the fail-safe baseline.
364
+ * @param ctx - the plugin context.
365
+ * @returns the Host default as a board permission, or undefined when unreadable.
366
+ */
367
+ export function resolveHostDefaultPermission(ctx: Context): TaskPermission | undefined {
368
+ try {
369
+ const presets = ctx.get('permissionPresets') as PermissionPresetsFace | undefined
370
+ if (presets === undefined || typeof presets.catalog !== 'function') return undefined
371
+ const preset = presets.catalog()?.defaultPreset
372
+ return isTaskPermission(preset) ? preset : undefined
373
+ } catch {
374
+ return undefined
375
+ }
376
+ }
377
+
206
378
  /**
207
379
  * How long one teammate may take to reach its durable active or failed edge.
208
380
  * Agent Teams provisioning normally resolves in seconds; the bound exists so a
@@ -317,11 +489,53 @@ function applyImpl(ctx: Context, config?: Config): void {
317
489
  const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
318
490
  /** Current subtask depth limit (read live: the settings card edits it in place). */
319
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)
494
+ /**
495
+ * The baseline the permission confirmation gate judges against: the row's own
496
+ * `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
497
+ * default preset. Read per call through the ledger, so a Host Settings change
498
+ * reaches the running board without remounting the row; the fail-safe default
499
+ * still applies when no catalog is available.
500
+ */
501
+ const sessionDefaultPermission = (): TaskPermission =>
502
+ config?.sessionDefaultPermission ?? resolveHostDefaultPermission(ctx) ?? DEFAULT_SESSION_PERMISSION
320
503
 
504
+ /** Live acceptance settings the contract of each new execution freezes. */
505
+ const verificationSettings = (): VerificationSettings => ({
506
+ enabled: readConfigField(config?.goalVerification, DEFAULT_GOAL_VERIFICATION),
507
+ model: readConfigField(config?.goalVerificationModel, ''),
508
+ reasoningEffort: readConfigField(config?.goalVerificationReasoningEffort, ''),
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))
519
+ // The catalog reader needs the service, and the service needs the reader;
520
+ // the reader only runs after start(), so a late holder is enough.
521
+ let hostForCatalog: TaskBoardHostService | undefined
522
+ const verificationCatalog = async (): Promise<ModelCatalogView | undefined> => {
523
+ const runner = hostForCatalog?.runner
524
+ return runner === undefined ? undefined : normalizeCatalog(await runner.modelCatalog())
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)
321
532
  const host = new TaskBoardHostService(ctx.typertGateway, {
322
533
  workspaceRegistry: ctx.workspaceRegistry,
323
- sessionDefaultPermission: config?.sessionDefaultPermission ?? DEFAULT_SESSION_PERMISSION,
534
+ sessionDefaultPermission,
324
535
  maxSubtaskDepth: maxSubtaskDepth(),
536
+ goalRunEnabled,
537
+ verificationSettings,
538
+ verificationCatalog,
325
539
  timers: resolveHostTimers(ctx),
326
540
  team: buildTeamDispatcher(ctx, config?.teamProvider ?? DEFAULT_TEAM_PROVIDER),
327
541
  commandDispatcher: {
@@ -332,22 +546,41 @@ function applyImpl(ctx: Context, config?: Config): void {
332
546
  },
333
547
  },
334
548
  })
549
+ hostForCatalog = host
335
550
  // Configuration before start(): a disabled row must not take the first
336
551
  // scheduler tick, which would roll schedules the board is not running.
337
- host.setConfiguration(enabled(), preventIdleSleep())
552
+ host.setConfiguration(enabled(), preventIdleSleep(), sessionPollSeconds())
338
553
  host.start()
339
554
 
555
+ // External providers. The board publishes its registration service so any
556
+ // provider package can resolve it and admit itself; the board itself knows
557
+ // no provider vocabulary. A capture-only test context implements no service
558
+ // registry and the board still serves its own surfaces.
559
+ if (typeof (ctx as { provide?: unknown }).provide === 'function') {
560
+ ctx.provide(TASK_BOARD_SERVICE_NAME, {
561
+ registerExtension: (extension: TaskBoardExtension) => host.registerExtension(extension),
562
+ isExtensionEnabled: (extensionId: string) => host.extensions.isActive(extensionId),
563
+ })
564
+ }
565
+
340
566
  // Agent tools: the same Host ledger the browser drives, so any session can
341
567
  // list, create, link, run and settle board work. Registration follows the
342
568
  // master switch (a disabled board answers no tool call), and the mount
343
- // effect below owns the disposers.
569
+ // effect below owns the disposers. Provider tools register through the
570
+ // extension registry, which follows the same gate and rebinds when the tool
571
+ // registry appears late.
344
572
  let disposeTools: (() => void) | undefined
345
573
  const setToolsEnabled = (active: boolean): void => {
346
574
  if (!active) {
575
+ // Releasing is the whole job on the way down. Rebinding the extension
576
+ // registry here would re-register every provider tool, and the teardown
577
+ // path runs this while the fiber is already unloading, so each of those
578
+ // re-registrations is a request the host can only refuse.
347
579
  disposeTools?.()
348
580
  disposeTools = undefined
349
581
  return
350
582
  }
583
+ host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
351
584
  if (disposeTools !== undefined) return
352
585
  const registry = resolveToolRegistry(ctx)
353
586
  if (registry === undefined) return
@@ -364,6 +597,9 @@ function applyImpl(ctx: Context, config?: Config): void {
364
597
  if (typeof scopedInject === 'function') {
365
598
  scopedInject.call(ctx, ['tools'], () => {
366
599
  setToolsEnabled(enabled())
600
+ // The extension registry follows the same rebinding: a registry that
601
+ // appears (or is replaced) after a provider started adopts its tools.
602
+ host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
367
603
  // Cordis unloads and re-runs this callback when the injected service's
368
604
  // provider fiber changes, and the old registry dies with its provider.
369
605
  // Releasing the guard here is what lets the callback register into the
@@ -378,6 +614,44 @@ function applyImpl(ctx: Context, config?: Config): void {
378
614
  ctx.effect(() => {
379
615
  const disposers: Array<() => void> = []
380
616
  try {
617
+ // The goal acceptance gate: the OFFICIAL tool pre-execution lifecycle,
618
+ // installed before the tool body runs, so a completion claim cannot take
619
+ // effect without a matching acceptance pass record.
620
+ const gate = createGoalVerificationGate({
621
+ ledger: host.ledger,
622
+ llm: () => resolveLlmRuntime(ctx),
623
+ goals: () => {
624
+ try {
625
+ return ctx.get('goals') as GoalFace | undefined
626
+ } catch {
627
+ return undefined
628
+ }
629
+ },
630
+ // The host's own observation of what a turn changed on disk, when this
631
+ // deployment records it; the acceptance degrades to the trajectory alone
632
+ // when it does not.
633
+ workspaceChanges: () => probeWorkspaceChanges(ctx),
634
+ callTimeoutMs: verificationCallTimeoutMsLive,
635
+ budgetMs: verificationBudgetMsLive,
636
+ logger: {
637
+ warn: (message: string, ...rest: unknown[]) => {
638
+ const logger = (ctx as { logger?: { warn?: (...args: unknown[]) => void } }).logger
639
+ if (typeof logger?.warn === 'function') logger.warn(message, ...rest)
640
+ else console.warn(message, ...rest)
641
+ },
642
+ },
643
+ })
644
+ // A capture-only context may implement no listener registry (it returns
645
+ // no disposer), so the handle is pushed only when it really is one.
646
+ const offGate = ctx.on('tools/pre-execute', async (exec, next) => {
647
+ const decision = await gate({ name: exec.name, arguments: exec.arguments, agent: exec.agent, signal: exec.signal })
648
+ return decision ?? next()
649
+ })
650
+ if (typeof offGate === 'function') disposers.push(offGate)
651
+ else if (typeof (offGate as { dispose?: unknown } | undefined)?.dispose === 'function') {
652
+ const handle = offGate as { dispose(): void }
653
+ disposers.push(() => { handle.dispose() })
654
+ }
381
655
  const routes = makeTaskBoardRoutes(host, resolveProxyAccess(config), {
382
656
  parseTask: async (request, signal) => {
383
657
  const llm = resolveLlmRuntime(ctx)
@@ -402,7 +676,7 @@ function applyImpl(ctx: Context, config?: Config): void {
402
676
  }, 'task-board: host ledger, scheduler, and routes')
403
677
 
404
678
  let disposeSection: (() => void) | undefined
405
- 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
406
680
 
407
681
  // Apply the current values to the host service and the announcement. A
408
682
  // commit that changes nothing visible is a no-op, so following a coarse
@@ -410,13 +684,19 @@ function applyImpl(ctx: Context, config?: Config): void {
410
684
  // under one disposer: re-registering first tears the old one down so a
411
685
  // duplicate-name registration never throws.
412
686
  const sync = (): void => {
413
- const next = { enabled: enabled(), announceToAgent: announceToAgent(), preventIdleSleep: preventIdleSleep(), maxSubtaskDepth: maxSubtaskDepth() }
414
- if (applied !== undefined && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) {
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) {
415
695
  return
416
696
  }
417
697
  applied = next
418
698
  host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth)
419
- host.setConfiguration(next.enabled, next.preventIdleSleep)
699
+ host.setConfiguration(next.enabled, next.preventIdleSleep, next.sessionPollSeconds)
420
700
  setToolsEnabled(next.enabled)
421
701
  if (disposeSection !== undefined) {
422
702
  disposeSection()
@@ -426,7 +706,11 @@ function applyImpl(ctx: Context, config?: Config): void {
426
706
  disposeSection = ctx.systemPrompt.section({
427
707
  name: 'plugin:task-board',
428
708
  order: SECTION_ORDER,
429
- text: TASK_BOARD_GUIDANCE,
709
+ // The announcement names the task_board_* tools, so it renders only while
710
+ // at least one of them is reachable: a runtime that serves no tool
711
+ // registry, or a restriction that withholds them, must not leave guidance
712
+ // telling the model to call tools this session cannot see.
713
+ text: visibleToolText(lookupOf(resolveToolRegistry(ctx)), TASK_BOARD_TOOL_NAMES, TASK_BOARD_GUIDANCE),
430
714
  })
431
715
  }
432
716