@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +20 -12
- package/README.zh.md +20 -12
- package/lib/client.js +1327 -229
- package/lib/client.js.map +1 -1
- package/lib/index.js +1909 -348
- package/lib/types/client/TaskBoardSettingsCard.d.ts +12 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +65 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/controller.d.ts +10 -11
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +144 -14
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +82 -7
- package/lib/types/host-runner.d.ts +32 -9
- package/lib/types/host-service.d.ts +117 -9
- package/lib/types/index.d.ts +54 -0
- package/lib/types/protocol.d.ts +50 -12
- package/package.json +2 -6
- package/src/client/TaskBoardSettingsCard.tsx +74 -1
- package/src/client/board/NewTaskModal.tsx +151 -20
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +219 -18
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/status-key.ts +3 -0
- package/src/client/index.ts +43 -50
- package/src/client/locales.ts +137 -4
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/core/controller.ts +12 -8
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +64 -15
- package/src/core/tasks.ts +193 -39
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +3 -3
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +106 -21
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +328 -42
- package/src/host-runner.ts +52 -19
- package/src/host-service.ts +299 -36
- package/src/index.ts +101 -5
- package/src/protocol.ts +132 -28
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
package/src/client/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardS
|
|
|
33
33
|
import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
|
|
34
34
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
35
35
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
36
|
-
import {
|
|
36
|
+
import { installPluginCard } from './plugin-card-seat.ts'
|
|
37
37
|
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
38
38
|
import { TaskBoardClientService } from './service.ts'
|
|
39
39
|
import {
|
|
@@ -133,15 +133,17 @@ declare module '@deepseek-ai/cordis' {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Required services (fiber inject waiting — the runtime must be up first).
|
|
136
|
-
* The generated remote
|
|
137
|
-
* `remote.agentPresets`
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* connection RPC face.
|
|
136
|
+
* The generated remote namespaces are not all hard injects: `remote.session`
|
|
137
|
+
* is one, while `remote.agentPresets` is reached through a scoped inject at
|
|
138
|
+
* use time (see mountUi), because the namespace mounts with the official
|
|
139
|
+
* api-remotes assembly applied beside this entry rather than before it.
|
|
141
140
|
*/
|
|
142
141
|
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace', 'layout']
|
|
143
142
|
|
|
144
|
-
/**
|
|
143
|
+
/** The generated Remote namespace the mode picker reads its roster from. */
|
|
144
|
+
type PresetRemote = ClientRemote['agentPresets']
|
|
145
|
+
|
|
146
|
+
/** One agent-preset row the mode picker consumes. */
|
|
145
147
|
interface PresetRosterRow {
|
|
146
148
|
id: string
|
|
147
149
|
name?: string
|
|
@@ -152,41 +154,17 @@ interface PresetRosterRow {
|
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/**
|
|
155
|
-
* Read the agent-preset roster through
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
157
|
+
* Read the agent-preset roster through the generated api-remotes face. The
|
|
158
|
+
* caller passes the namespace resolved inside its scoped inject, so a host
|
|
159
|
+
* that serves no such namespace never reaches here; a host that serves it
|
|
160
|
+
* answers `ok: false` instead, which leaves the picker options untouched.
|
|
161
|
+
* @param remote - the `remote.agentPresets` namespace.
|
|
162
|
+
* @returns the roster, or a failed read the caller ignores.
|
|
161
163
|
*/
|
|
162
|
-
async function readPresetRoster(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// The cordis `remote` proxy throws on a property that was never injected
|
|
167
|
-
// ("cannot get property X without inject") rather than returning undefined,
|
|
168
|
-
// so the probe must guard the access — a hard read would abort mounting on
|
|
169
|
-
// hosts below the 0.1.2-alpha.2 cohort instead of degrading to the legacy
|
|
170
|
-
// connection RPC face below.
|
|
171
|
-
let remotes: ClientRemote['agentPresets'] | undefined
|
|
172
|
-
try {
|
|
173
|
-
remotes = (remote as Partial<ClientRemote>).agentPresets
|
|
174
|
-
} catch {
|
|
175
|
-
remotes = undefined
|
|
176
|
-
}
|
|
177
|
-
if (remotes !== undefined) {
|
|
178
|
-
const response = await remotes.list()
|
|
179
|
-
if (!response.ok) return { ok: false, presets: [] }
|
|
180
|
-
return { ok: true, presets: response.value.presets }
|
|
181
|
-
}
|
|
182
|
-
const connection = ctx.get('connection') as unknown as {
|
|
183
|
-
api?: { agentPresets?: { list(request: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets?: readonly PresetRosterRow[] } } }> } }
|
|
184
|
-
}
|
|
185
|
-
const legacy = connection.api?.agentPresets
|
|
186
|
-
if (legacy === undefined) return undefined
|
|
187
|
-
const response = await legacy.list({})
|
|
188
|
-
if (!response.result.ok || response.result.value === undefined) return { ok: false, presets: [] }
|
|
189
|
-
return { ok: true, presets: response.result.value.presets ?? [] }
|
|
164
|
+
async function readPresetRoster(remote: PresetRemote): Promise<{ ok: boolean; presets: readonly PresetRosterRow[] }> {
|
|
165
|
+
const response = await remote.list()
|
|
166
|
+
if (!response.ok) return { ok: false, presets: [] }
|
|
167
|
+
return { ok: true, presets: response.value.presets }
|
|
190
168
|
}
|
|
191
169
|
|
|
192
170
|
/**
|
|
@@ -238,7 +216,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
238
216
|
ctx.effect(() => () => { clientService.detach() }, 'task-board: extension service')
|
|
239
217
|
|
|
240
218
|
const settingsCard = new TaskBoardSettingsCardController(settingsForm, request => clientService.dispatch(request))
|
|
241
|
-
|
|
219
|
+
installPluginCard(ctx, {
|
|
242
220
|
bundle: '@linxin666/dsh-client-ui-task-board',
|
|
243
221
|
id: 'task-board',
|
|
244
222
|
order: 110,
|
|
@@ -321,10 +299,10 @@ export function apply(ctx: ClientContext): void {
|
|
|
321
299
|
const created = await workspaces.create({ path })
|
|
322
300
|
return { workspaceId: created.workspaceId }
|
|
323
301
|
})
|
|
324
|
-
const pushPresetOptions = async (): Promise<void> => {
|
|
302
|
+
const pushPresetOptions = async (presetRemote: PresetRemote): Promise<void> => {
|
|
325
303
|
try {
|
|
326
|
-
const roster = await readPresetRoster(
|
|
327
|
-
if (
|
|
304
|
+
const roster = await readPresetRoster(presetRemote)
|
|
305
|
+
if (!roster.ok) return
|
|
328
306
|
controller.setExecutionOptions({
|
|
329
307
|
presets: roster.presets.map(preset => ({
|
|
330
308
|
id: preset.id,
|
|
@@ -340,6 +318,25 @@ export function apply(ctx: ClientContext): void {
|
|
|
340
318
|
console.error('[dsh-task-board] agent preset roster read failed', error)
|
|
341
319
|
}
|
|
342
320
|
}
|
|
321
|
+
// The roster namespace mounts with the official api-remotes assembly,
|
|
322
|
+
// which the page applies beside this entry: at mount time the namespace is
|
|
323
|
+
// usually still absent, and reading it through `remote.agentPresets`
|
|
324
|
+
// before it lands throws ("cannot get property ... without inject"), so a
|
|
325
|
+
// one-shot probe leaves the mode picker with nothing but "inherit" for the
|
|
326
|
+
// page lifetime. A hard inject is no better — it would pend the whole
|
|
327
|
+
// board on a deployment that never mounts the namespace. The scoped inject
|
|
328
|
+
// runs the feed exactly when the namespace exists and re-runs it if the
|
|
329
|
+
// namespace is replaced; the reconnect read inside covers a deployment
|
|
330
|
+
// change behind the same namespace.
|
|
331
|
+
const presetRosterFeed = ctx.inject(['remote.agentPresets'], (inner) => {
|
|
332
|
+
const presetRemote = inner.get('remote.agentPresets') as PresetRemote | undefined
|
|
333
|
+
if (presetRemote === undefined) return
|
|
334
|
+
void pushPresetOptions(presetRemote)
|
|
335
|
+
// Subscribed inside the injected scope, so a namespace that goes away
|
|
336
|
+
// takes its listener with it instead of leaving one per remount.
|
|
337
|
+
return inner.on('connection/reset', () => { void pushPresetOptions(presetRemote) })
|
|
338
|
+
})
|
|
339
|
+
disposers.push(() => { void presetRosterFeed.dispose() })
|
|
343
340
|
const pushModelOptions = async (): Promise<void> => {
|
|
344
341
|
try {
|
|
345
342
|
let models: Array<{ id: string; name?: string; provider?: string }> = []
|
|
@@ -404,12 +401,8 @@ export function apply(ctx: ClientContext): void {
|
|
|
404
401
|
console.error('[dsh-task-board] model options read failed', error)
|
|
405
402
|
}
|
|
406
403
|
}
|
|
407
|
-
void pushPresetOptions()
|
|
408
404
|
void pushModelOptions()
|
|
409
|
-
disposers.push(ctx.on('connection/reset', () => {
|
|
410
|
-
void pushPresetOptions()
|
|
411
|
-
void pushModelOptions()
|
|
412
|
-
}))
|
|
405
|
+
disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
|
|
413
406
|
// Native panel surfaces: one sidebar row and one center-column page, both
|
|
414
407
|
// through the official slot seats the shell itself renders. The layout's
|
|
415
408
|
// panel selection is reconciled back into the controller so the board's own
|
package/src/client/locales.ts
CHANGED
|
@@ -152,6 +152,7 @@ export const zh = {
|
|
|
152
152
|
'detail.noSession': '暂无会话',
|
|
153
153
|
'detail.executionStarted': '已启动',
|
|
154
154
|
'detail.execution.initiator': '发起会话 {session}',
|
|
155
|
+
'detail.execution.external': 'Host 之外完成:{agent}',
|
|
155
156
|
'detail.executionEnded': '已结束',
|
|
156
157
|
'detail.result.succeeded': '成功',
|
|
157
158
|
'detail.result.failed': '失败',
|
|
@@ -194,7 +195,35 @@ export const zh = {
|
|
|
194
195
|
'detail.schedule.duration.hours': '{count} 小时',
|
|
195
196
|
'detail.schedule.duration.minutes': '{count} 分钟',
|
|
196
197
|
'detail.schedule.duration.seconds': '{count} 秒',
|
|
198
|
+
// 调度方式:单次计划与循环规则是两类计划,不是同一 cron 的日期限定写法。
|
|
199
|
+
'detail.schedule.mode': '调度方式',
|
|
200
|
+
'detail.schedule.mode.cron': '循环',
|
|
201
|
+
'detail.schedule.mode.once': '单次',
|
|
202
|
+
'detail.schedule.at': '执行时间',
|
|
203
|
+
'detail.schedule.atHint': '按所选时区解读的计划时刻;必须是未来时间。',
|
|
204
|
+
'detail.schedule.atInvalid': '请选择未来的有效时间',
|
|
205
|
+
'detail.schedule.plannedAt': '计划时间',
|
|
206
|
+
'detail.schedule.zoneOnceHint': '所选时刻按此时区解读。',
|
|
207
|
+
'detail.schedule.maxRuns': '次数上限',
|
|
208
|
+
'detail.schedule.maxRunsUnlimited': '不限次数',
|
|
209
|
+
'detail.schedule.maxRunsValue': '最多 {count} 次',
|
|
210
|
+
'detail.schedule.maxRunsCustom': '自定义',
|
|
211
|
+
'detail.schedule.maxRunsInvalid': '次数上限必须是正整数',
|
|
212
|
+
'detail.schedule.runs': '已执行 {done}/{total} 次',
|
|
213
|
+
'detail.schedule.runsUnlimited': '已执行 {done} 次',
|
|
214
|
+
'detail.schedule.remaining': '剩余 {count} 次',
|
|
215
|
+
'detail.schedule.cancel': '关闭调度',
|
|
216
|
+
'detail.schedule.ended': '调度已结束:{reason}',
|
|
217
|
+
'detail.schedule.skipped': '上次跳过:{reason}',
|
|
218
|
+
'detail.schedule.stop.fired': '单次计划已执行',
|
|
219
|
+
'detail.schedule.stop.limit': '已达次数上限',
|
|
220
|
+
'detail.schedule.stop.no-target': '计划已无可执行时刻',
|
|
221
|
+
'detail.schedule.stop.missed': '错过执行时刻,已跳过',
|
|
222
|
+
'detail.schedule.stop.busy': '触发时任务正在运行,已跳过',
|
|
223
|
+
'detail.schedule.stop.permission': '权限待人工确认,已跳过',
|
|
224
|
+
'detail.schedule.stop.launch-failed': '启动失败未创建执行,次数已退回',
|
|
197
225
|
'card.scheduled': '定时',
|
|
226
|
+
'card.scheduledOnce': '单次',
|
|
198
227
|
'new.workspace': '工作区',
|
|
199
228
|
'new.agentPreset': 'Agent 预设',
|
|
200
229
|
'new.permission': '权限',
|
|
@@ -224,7 +253,7 @@ export const zh = {
|
|
|
224
253
|
'exec.teamRunUnavailable': '当前部署未提供 Agent Teams 服务,无法启用。',
|
|
225
254
|
'exec.reuseSessionHint': '开启后,本任务的后续执行在上一次会话里继续(该会话空闲且仍存在时),不再每次新建对话;每次复用时都会重新应用上面钉住的权限与模型。',
|
|
226
255
|
'exec.goalRun': '以 dsh 内置的 /goal 开始执行任务',
|
|
227
|
-
'exec.goalRunHint': '
|
|
256
|
+
'exec.goalRunHint': '默认关闭:勾选后执行时把任务目标作为持久目标交给 dsh 内置的 /goal,会话会自动续跑多轮,直到 agent 标记目标完成;看板在该目标真正结束后才结算本次执行。不勾选则只执行一轮普通对话。',
|
|
228
257
|
'exec.skipVerification': '跳过本任务的验收',
|
|
229
258
|
'exec.skipVerificationHint': '默认关闭:勾选后本任务以 goal 形式执行时不再拦截 update_goal 标记完成,也不要求通过验收;执行记录会标注「该卡片跳过验收」。仅对之后新开的执行生效,验收总开关关闭时本项无意义。',
|
|
230
259
|
'detail.executionSettings': '执行设置',
|
|
@@ -258,9 +287,16 @@ export const zh = {
|
|
|
258
287
|
'settings.unsaved': '未保存',
|
|
259
288
|
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
260
289
|
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
290
|
+
'settings.sessionPollSeconds': '会话轮询间隔(秒)',
|
|
291
|
+
'settings.sessionPollSecondsHint': '默认 5 秒。仅在看板有需要核对的事情——运行中的卡片或未结算的执行——时才读取会话名册;两者都没有时不再轮询,Host 不会持续扫描持久化会话目录。取值 1–300 秒:间隔越短结算越快、Host 负载越高。',
|
|
261
292
|
'settings.maxSubtaskDepth': '子任务深度上限',
|
|
262
293
|
'settings.maxSubtaskDepthHint': '默认 1:一个任务只允许一层子任务,子任务不能再创建或关联子任务。最大 3。执行父任务会并发执行它的整棵子任务树,层级越深,一次执行开启的会话越多。',
|
|
263
294
|
'settings.maxSubtaskDepthOption': '{depth} 层',
|
|
295
|
+
'settings.runBehaviorTitle': '运行行为',
|
|
296
|
+
'settings.runBehaviorCardHint': '全局原生 /goal 执行开关(总开关,默认关闭)。',
|
|
297
|
+
'settings.goalRunGlobal': '全局启用 DSH 原生 /goal 执行',
|
|
298
|
+
'settings.goalRunGlobalHint': '默认关闭。关闭时所有任务都按普通单回合执行,不调用 dsh 内置的 /goal;只有这里开启、且任务级「以 /goal 开始执行」被勾选时,才执行原生 /goal。任务级勾选始终可操作、也不会被这里的开关改写,总开关只决定它是否生效。开关按执行启动时冻结,只影响之后新开的执行。',
|
|
299
|
+
'settings.goalRunGlobalDisabledTaskOption': '全局「原生 /goal 执行」总开关已关闭:任务级选项仍可随时勾选或取消,但暂不生效,你保存的偏好也不会被改写;开启总开关后即按该偏好执行。本次执行按普通单回合进行,未执行 goal 验收。',
|
|
264
300
|
'settings.goalVerificationTitle': '任务验收',
|
|
265
301
|
'settings.goalVerification': '启用任务验收',
|
|
266
302
|
'settings.goalVerificationCardHint': '验收开关、裁判模型与判据阈值。',
|
|
@@ -290,6 +326,7 @@ export const zh = {
|
|
|
290
326
|
'running.repairing': '验收未通过修复中',
|
|
291
327
|
'running.verificationPassed': '验收通过',
|
|
292
328
|
'running.verificationFailed': '验收未通过',
|
|
329
|
+
'running.verificationInvalid': '验收无效,等待处理',
|
|
293
330
|
'verify.title': '验收报告',
|
|
294
331
|
'verify.status.passed': '通过',
|
|
295
332
|
'verify.status.failed': '未通过',
|
|
@@ -297,6 +334,34 @@ export const zh = {
|
|
|
297
334
|
'verify.status.verifying': '验收中',
|
|
298
335
|
'verify.status.pending': '待验收',
|
|
299
336
|
'verify.status.off': '本次执行未启用验收',
|
|
337
|
+
'verify.status.invalid': '验收无效',
|
|
338
|
+
'verify.status.invalidHeld': '验收无效,等待人工处理',
|
|
339
|
+
'verify.attemptInvalid': '第 {index} 次验收无效',
|
|
340
|
+
'verify.invalidReason.missing-finding': '未达标的判据没有对应的结构化问题反馈',
|
|
341
|
+
'verify.invalidReason.unlocatable-quote': '问题反馈引用的位置不存在于本次裁判所见的证据中',
|
|
342
|
+
'verify.invalidReason.baseline-finding': '问题反馈指向空工作基线,而不是本次执行的工作',
|
|
343
|
+
'verify.invalidReason.vacuous-finding': '问题反馈为空或空泛评价,没有说明实际观测与差异',
|
|
344
|
+
'verify.invalidReason.insufficient-evidence': '裁判看到的证据不足(例如轨迹被截断),无法支撑否决',
|
|
345
|
+
'verify.invalidReason.unknown': '未产生可用的质量判定',
|
|
346
|
+
'verify.invalidCounts': '有效质量判定 {quality}/{max} · 无效验收 {invalid}/{maxInvalid} · 验收异常 {exceptions}/{max}',
|
|
347
|
+
'verify.invalidDetail': '无效原因:{reason}',
|
|
348
|
+
'verify.criterionFindings': '结构化问题反馈(判据 · 引用位置 · 实际引用)',
|
|
349
|
+
'verify.criterionFinding': '{criterion} · {location}:“{quote}”',
|
|
350
|
+
'verify.criterionFindingFull': '{criterion} · {location}:“{quote}” 要求:{requirement} 观测:{observation} 差异:{gap}',
|
|
351
|
+
'verify.location.task': '任务要求',
|
|
352
|
+
'verify.location.trajectory': '执行轨迹',
|
|
353
|
+
'verify.location.workspace': '宿主记录的变更',
|
|
354
|
+
'verify.location.baseline': '空工作基线(不构成否决证据)',
|
|
355
|
+
'verify.location.unknown': '未标明位置',
|
|
356
|
+
'verify.detailRemoved': '本条验收的详细材料已在通过并成功结算后自动清理(不是裁判未举证)。',
|
|
357
|
+
'verify.cleanup.cleaned': '本次执行的验收专用详细材料已在通过并成功结算后自动清理;以下为保留的轻量审计凭据。',
|
|
358
|
+
'verify.cleanup.pending': '验收专用详细材料待清理(将通过并结算后启动)。',
|
|
359
|
+
'verify.cleanup.failed': '验收详细材料清理失败(已重试 {attempts} 次):{error};通过记录仍然有效,宿主重启后会补清。',
|
|
360
|
+
'verify.cleanup.audit': '保留凭据:第 {index} 次 · {stage} · 分数 {score} · 证据哈希 {hash}',
|
|
361
|
+
'verify.cleanup.stage.quality': '质量判定',
|
|
362
|
+
'verify.cleanup.stage.invalid': '无效验收',
|
|
363
|
+
'verify.cleanup.stage.exception': '验收异常',
|
|
364
|
+
'verify.cleanup.stage.budget': '时间预算耗尽',
|
|
300
365
|
'verify.summary': '总分 {score}(阈值 {threshold})· 空工作基线 {baseline}',
|
|
301
366
|
'verify.criteria': '逐项判据',
|
|
302
367
|
'verify.criterion': '{name}:{score}(阈值 {threshold})',
|
|
@@ -315,9 +380,10 @@ export const zh = {
|
|
|
315
380
|
'verify.usageIncomplete': '(至少一次请求的用量未知,以上为下限)',
|
|
316
381
|
'verify.finalFailure': '判定依据:{reason}',
|
|
317
382
|
'verify.applicability.disabled': '本次执行启动时验收开关关闭,按原有回合判定结算。',
|
|
318
|
-
'verify.applicability.goalUnavailable': '本次执行未成为 goal
|
|
383
|
+
'verify.applicability.goalUnavailable': '本次执行未成为 goal 执行(卡片未勾选「以 /goal 开始执行任务」,或 /goal 被拒绝/不可用),验收未强制执行。',
|
|
319
384
|
'verify.applicability.skipped': '该任务卡勾选了「跳过验收」,本次执行未经验收判定。',
|
|
320
385
|
'verify.applicability.teamMember': '团队执行成员:由 Lead 的团队汇总证据统一验收。',
|
|
386
|
+
'verify.applicability.goalDisabled': '本次执行启动时全局「原生 /goal 执行」总开关关闭,按普通单回合执行:本次未执行 goal 验收,既不算已验收通过,也不因此判失败。',
|
|
321
387
|
'verify.effortFallback': '推理强度回退:{requested} → {resolved}',
|
|
322
388
|
'verify.thresholdValue': '{value}%',
|
|
323
389
|
// 子任务(父子关系、级联执行、关联/解除关联)。
|
|
@@ -515,6 +581,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
515
581
|
'detail.noSession': 'No session',
|
|
516
582
|
'detail.executionStarted': 'Started',
|
|
517
583
|
'detail.execution.initiator': 'Initiated by session {session}',
|
|
584
|
+
'detail.execution.external': 'Completed outside this Host by {agent}',
|
|
518
585
|
'detail.executionEnded': 'Ended',
|
|
519
586
|
'detail.result.succeeded': 'Succeeded',
|
|
520
587
|
'detail.result.failed': 'Failed',
|
|
@@ -557,7 +624,36 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
557
624
|
'detail.schedule.duration.hours': '{count}h',
|
|
558
625
|
'detail.schedule.duration.minutes': '{count}m',
|
|
559
626
|
'detail.schedule.duration.seconds': '{count}s',
|
|
627
|
+
// Scheduling mode: a one-shot and a recurring rule are two kinds of plan,
|
|
628
|
+
// not the same cron expression restricted to a date.
|
|
629
|
+
'detail.schedule.mode': 'Mode',
|
|
630
|
+
'detail.schedule.mode.cron': 'Recurring',
|
|
631
|
+
'detail.schedule.mode.once': 'Once',
|
|
632
|
+
'detail.schedule.at': 'Run at',
|
|
633
|
+
'detail.schedule.atHint': 'Planned instant, read in the selected zone; it must be in the future.',
|
|
634
|
+
'detail.schedule.atInvalid': 'Choose a valid future time',
|
|
635
|
+
'detail.schedule.plannedAt': 'Planned time',
|
|
636
|
+
'detail.schedule.zoneOnceHint': 'The chosen instant is read in this zone.',
|
|
637
|
+
'detail.schedule.maxRuns': 'Run limit',
|
|
638
|
+
'detail.schedule.maxRunsUnlimited': 'Unlimited',
|
|
639
|
+
'detail.schedule.maxRunsValue': 'At most {count} runs',
|
|
640
|
+
'detail.schedule.maxRunsCustom': 'Custom',
|
|
641
|
+
'detail.schedule.maxRunsInvalid': 'The run limit must be a positive whole number',
|
|
642
|
+
'detail.schedule.runs': 'Ran {done}/{total}',
|
|
643
|
+
'detail.schedule.runsUnlimited': 'Ran {done}',
|
|
644
|
+
'detail.schedule.remaining': '{count} left',
|
|
645
|
+
'detail.schedule.cancel': 'Cancel schedule',
|
|
646
|
+
'detail.schedule.ended': 'Schedule ended: {reason}',
|
|
647
|
+
'detail.schedule.skipped': 'Last skip: {reason}',
|
|
648
|
+
'detail.schedule.stop.fired': 'the one-shot ran',
|
|
649
|
+
'detail.schedule.stop.limit': 'the run limit was reached',
|
|
650
|
+
'detail.schedule.stop.no-target': 'the plan has no reachable occurrence',
|
|
651
|
+
'detail.schedule.stop.missed': 'the instant was missed and skipped',
|
|
652
|
+
'detail.schedule.stop.busy': 'the task was already running, so the occurrence was skipped',
|
|
653
|
+
'detail.schedule.stop.permission': 'a permission awaited human confirmation, so the occurrence was skipped',
|
|
654
|
+
'detail.schedule.stop.launch-failed': 'the launch failed before any execution existed, so the run was refunded',
|
|
560
655
|
'card.scheduled': 'scheduled',
|
|
656
|
+
'card.scheduledOnce': 'once',
|
|
561
657
|
'new.workspace': 'Workspace',
|
|
562
658
|
'new.agentPreset': 'Agent preset',
|
|
563
659
|
'new.permission': 'Permission',
|
|
@@ -587,7 +683,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
587
683
|
'exec.teamRunUnavailable': 'This deployment serves no Agent Teams service, so team runs cannot be enabled.',
|
|
588
684
|
'exec.reuseSessionHint': 'When on, later runs continue in the previous session (when that session is idle and still exists) instead of starting a new conversation each time; the pinned permission and model above are re-applied on every reuse.',
|
|
589
685
|
'exec.goalRun': "Start the run with dsh's built-in /goal",
|
|
590
|
-
'exec.goalRunHint': '
|
|
686
|
+
'exec.goalRunHint': 'Off by default: when checked, the run arms dsh built-in /goal with the task objective, so the session keeps working automatic continuation rounds until the agent marks the goal complete, and the board settles the execution only when that goal really ends. Unchecked: one plain turn.',
|
|
591
687
|
'exec.skipVerification': 'Skip acceptance for this task',
|
|
592
688
|
'exec.skipVerificationHint': 'Off by default: when checked, a goal-form run of this task no longer gates update_goal on a passing acceptance, and the execution record is marked as skipped. It applies only to executions started afterwards, and means nothing while the board-wide acceptance switch is off.',
|
|
593
689
|
'detail.executionSettings': 'Execution Settings',
|
|
@@ -621,9 +717,16 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
621
717
|
'settings.unsaved': 'Unsaved',
|
|
622
718
|
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
623
719
|
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
720
|
+
'settings.sessionPollSeconds': 'Session poll interval (seconds)',
|
|
721
|
+
'settings.sessionPollSecondsHint': 'Default 5. The Host reads the DSH session roster only while the board has something to reconcile: a running card or an open execution. With neither it stops polling and never re-scans the persisted session directory. Range 1 to 300 seconds: a shorter interval settles runs sooner and costs the Host more.',
|
|
624
722
|
'settings.maxSubtaskDepth': 'Subtask depth limit',
|
|
625
723
|
'settings.maxSubtaskDepthHint': 'Default 1: a task may carry one level of subtasks, and a subtask cannot create or link further subtasks. Maximum 3. Running a task also runs its whole subtask tree concurrently, and every extra level opens more sessions per run.',
|
|
626
724
|
'settings.maxSubtaskDepthOption': '{depth} levels',
|
|
725
|
+
'settings.runBehaviorTitle': 'Run behavior',
|
|
726
|
+
'settings.runBehaviorCardHint': 'The global native /goal switch (master control, off by default).',
|
|
727
|
+
'settings.goalRunGlobal': 'Global: run tasks with dsh native /goal',
|
|
728
|
+
'settings.goalRunGlobalHint': 'Off by default. While off, every task runs one plain turn and never calls dsh built-in /goal; a run uses native /goal only when this is on AND the task-level option is checked (see the task option below). The task-level checkbox always stays operable and this switch never rewrites it: the master switch only decides whether that choice takes effect. The switch is frozen when an execution starts, so it only affects runs opened afterwards.',
|
|
729
|
+
'settings.goalRunGlobalDisabledTaskOption': 'The global native /goal switch is off: the task-level option can still be checked or cleared, but it has no effect right now and the preference you save is never rewritten; turning the global switch back on applies it. This execution runs one plain turn and performs no goal acceptance.',
|
|
627
730
|
'settings.goalVerificationTitle': 'Task acceptance',
|
|
628
731
|
'settings.goalVerification': 'Enable task acceptance',
|
|
629
732
|
'settings.goalVerificationCardHint': 'The switch, the judge model and the criteria threshold.',
|
|
@@ -653,6 +756,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
653
756
|
'running.repairing': 'Fixing a failed acceptance',
|
|
654
757
|
'running.verificationPassed': 'Acceptance passed',
|
|
655
758
|
'running.verificationFailed': 'Acceptance failed',
|
|
759
|
+
'running.verificationInvalid': 'Invalid acceptance, waiting for a human',
|
|
656
760
|
'verify.title': 'Acceptance report',
|
|
657
761
|
'verify.status.passed': 'Passed',
|
|
658
762
|
'verify.status.failed': 'Failed',
|
|
@@ -660,6 +764,34 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
660
764
|
'verify.status.verifying': 'Verifying',
|
|
661
765
|
'verify.status.pending': 'Not yet verified',
|
|
662
766
|
'verify.status.off': 'Acceptance was off for this execution',
|
|
767
|
+
'verify.status.invalid': 'Invalid acceptance',
|
|
768
|
+
'verify.status.invalidHeld': 'Invalid acceptance, waiting for a human',
|
|
769
|
+
'verify.attemptInvalid': 'Invalid acceptance {index}',
|
|
770
|
+
'verify.invalidReason.missing-finding': 'A failing criterion carries no structured finding of its own',
|
|
771
|
+
'verify.invalidReason.unlocatable-quote': 'A finding cites a passage that is not in the evidence the judge saw',
|
|
772
|
+
'verify.invalidReason.baseline-finding': 'A finding points at the empty-work baseline instead of this execution\'s work',
|
|
773
|
+
'verify.invalidReason.vacuous-finding': 'A finding is empty or a generic evaluation with no observation or difference',
|
|
774
|
+
'verify.invalidReason.insufficient-evidence': 'The judge saw too little evidence (for example a truncated trace) to support a veto',
|
|
775
|
+
'verify.invalidReason.unknown': 'No usable quality verdict was produced',
|
|
776
|
+
'verify.invalidCounts': 'Valid quality verdicts {quality}/{max} · invalid acceptances {invalid}/{maxInvalid} · anomalies {exceptions}/{max}',
|
|
777
|
+
'verify.invalidDetail': 'Invalid because: {reason}',
|
|
778
|
+
'verify.criterionFindings': 'Structured findings (criterion · cited location · verbatim citation)',
|
|
779
|
+
'verify.criterionFinding': '{criterion} · {location}: "{quote}"',
|
|
780
|
+
'verify.criterionFindingFull': '{criterion} · {location}: "{quote}" requirement: {requirement} observed: {observation} gap: {gap}',
|
|
781
|
+
'verify.location.task': 'task statement',
|
|
782
|
+
'verify.location.trajectory': 'execution trajectory',
|
|
783
|
+
'verify.location.workspace': 'host-recorded changes',
|
|
784
|
+
'verify.location.baseline': 'empty-work baseline (not evidence against the work)',
|
|
785
|
+
'verify.location.unknown': 'location not named',
|
|
786
|
+
'verify.detailRemoved': 'This attempt detail was cleaned up automatically after the execution passed and settled (the judge did report evidence).',
|
|
787
|
+
'verify.cleanup.cleaned': 'The acceptance-only detail of this execution was cleaned up automatically after it passed and settled; what remains is the lightweight audit credential.',
|
|
788
|
+
'verify.cleanup.pending': 'The acceptance-only detail is pending cleanup (started once the execution passes and settles).',
|
|
789
|
+
'verify.cleanup.failed': 'Cleaning the acceptance detail failed after {attempts} attempts: {error}. The pass record is still valid and the host retries on its next start.',
|
|
790
|
+
'verify.cleanup.audit': 'Kept credential: attempt {index} · {stage} · score {score} · evidence hash {hash}',
|
|
791
|
+
'verify.cleanup.stage.quality': 'quality verdict',
|
|
792
|
+
'verify.cleanup.stage.invalid': 'invalid acceptance',
|
|
793
|
+
'verify.cleanup.stage.exception': 'acceptance anomaly',
|
|
794
|
+
'verify.cleanup.stage.budget': 'time budget exhausted',
|
|
663
795
|
'verify.summary': 'Total {score} (threshold {threshold}) · empty-work baseline {baseline}',
|
|
664
796
|
'verify.criteria': 'Per-criterion scores',
|
|
665
797
|
'verify.criterion': '{name}: {score} (threshold {threshold})',
|
|
@@ -678,9 +810,10 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
678
810
|
'verify.usageIncomplete': ' (at least one request usage is unknown; these counts are a floor)',
|
|
679
811
|
'verify.finalFailure': 'Verdict basis: {reason}',
|
|
680
812
|
'verify.applicability.disabled': 'Acceptance was off when this execution started; it settles on the historical verdict.',
|
|
681
|
-
'verify.applicability.goalUnavailable': 'This execution never became a goal run (/goal was refused or unavailable), so acceptance was not enforced.',
|
|
813
|
+
'verify.applicability.goalUnavailable': 'This execution never became a goal run (the card did not check Start with /goal, or /goal was refused or unavailable), so acceptance was not enforced.',
|
|
682
814
|
'verify.applicability.skipped': 'This card checked Skip acceptance, so the execution was never judged.',
|
|
683
815
|
'verify.applicability.teamMember': 'Team member: the Lead team-summary evidence is accepted as one execution.',
|
|
816
|
+
'verify.applicability.goalDisabled': 'The global native /goal switch was off when this execution started, so it ran one plain turn: no goal acceptance was performed, which neither counts as a pass nor fails the run.',
|
|
684
817
|
'verify.effortFallback': 'Reasoning effort fallback: {requested} → {resolved}',
|
|
685
818
|
'verify.thresholdValue': '{value}%',
|
|
686
819
|
// Subtasks (parent/child links, cascade runs, link/detach).
|
|
@@ -86,6 +86,11 @@ export interface PluginCardSeat {
|
|
|
86
86
|
label?: () => string
|
|
87
87
|
/** Locale namespace the card renders with. */
|
|
88
88
|
locale: string
|
|
89
|
+
/**
|
|
90
|
+
* Child slots this card declares and renders. Forwarded to whichever seat
|
|
91
|
+
* the host renders; absent for a card that declares none.
|
|
92
|
+
*/
|
|
93
|
+
children?: Record<string, unknown>
|
|
89
94
|
/** Business-face factory of the registration. */
|
|
90
95
|
inject?: () => object
|
|
91
96
|
/**
|
|
@@ -162,12 +167,14 @@ export function installPluginCard(ctx: PluginCardContext, seat: PluginCardSeat):
|
|
|
162
167
|
...(seat.order === undefined ? {} : { order: seat.order }),
|
|
163
168
|
...(seat.label === undefined ? {} : { label: seat.label }),
|
|
164
169
|
locale: seat.locale,
|
|
170
|
+
...(seat.children === undefined ? {} : { children: seat.children }),
|
|
165
171
|
...(seat.inject === undefined ? {} : { inject }),
|
|
166
172
|
}
|
|
167
173
|
: {
|
|
168
174
|
name: OFFICIAL_PLUGIN_CARD_SEAT,
|
|
169
175
|
key: seat.bundle,
|
|
170
176
|
locale: seat.locale,
|
|
177
|
+
...(seat.children === undefined ? {} : { children: seat.children }),
|
|
171
178
|
...(seat.inject === undefined ? {} : { inject }),
|
|
172
179
|
}) as never, component) as () => void
|
|
173
180
|
current = target
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* the Host schedules with (`core/schedule.ts`), so the preview a user sees is
|
|
8
8
|
* computed by the code that will actually arm the rule.
|
|
9
9
|
*/
|
|
10
|
-
import { isValidTimeZone } from '../core/schedule.ts'
|
|
11
|
-
import {
|
|
10
|
+
import { isValidTimeZone, partsInZone, resolveHostTimeZone, zonedEpoch } from '../core/schedule.ts'
|
|
11
|
+
import { scheduleRunBudget, type ScheduleRule, type ScheduleStopReason } from '../core/tasks.ts'
|
|
12
|
+
import { t, type TaskBoardKey } from './locales.ts'
|
|
12
13
|
|
|
13
14
|
/** One selectable zone, in the order the picker shows it. */
|
|
14
15
|
export interface ZoneChoice {
|
|
@@ -112,6 +113,103 @@ export function relativeTimeLabel(targetMs: number, now: number): string {
|
|
|
112
113
|
return `${durationParts(delta)}`
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/** Zero-pad a calendar/clock field for a `datetime-local` value. */
|
|
117
|
+
function pad(value: number): string {
|
|
118
|
+
return String(value).padStart(2, '0')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The `datetime-local` input value for an instant, read in an explicit zone:
|
|
123
|
+
* `YYYY-MM-DDTHH:mm`. Uses the shared engine's zone reader, so the editor
|
|
124
|
+
* shows the same wall clock the Host schedules by.
|
|
125
|
+
* @param epochMs - the instant to render.
|
|
126
|
+
* @param timeZone - zone to read it in; absent uses the Host zone.
|
|
127
|
+
* @returns the input value.
|
|
128
|
+
*/
|
|
129
|
+
export function zonedInputValue(epochMs: number, timeZone?: string): string {
|
|
130
|
+
const parts = partsInZone(usableZone(timeZone), epochMs)
|
|
131
|
+
return `${parts.year}-${pad(parts.month)}-${pad(parts.day)}T${pad(parts.hour)}:${pad(parts.minute)}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a `datetime-local` input value back to an instant, read in an explicit
|
|
136
|
+
* zone. Returns undefined when the text is not a whole calendar wall clock or
|
|
137
|
+
* names one the zone skips (a spring-forward gap), so the editor can refuse an
|
|
138
|
+
* unusable instant instead of silently moving it.
|
|
139
|
+
* @param value - the input text.
|
|
140
|
+
* @param timeZone - zone to interpret it in; absent uses the Host zone.
|
|
141
|
+
* @returns the instant, or undefined when it is not a usable wall clock.
|
|
142
|
+
*/
|
|
143
|
+
export function parseZonedInput(value: string, timeZone?: string): number | undefined {
|
|
144
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value.trim())
|
|
145
|
+
if (match === null) return undefined
|
|
146
|
+
const [year, month, day, hour, minute] = match.slice(1).map(Number)
|
|
147
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59) return undefined
|
|
148
|
+
return zonedEpoch(usableZone(timeZone), year, month, day, hour, minute)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The default instant a new one-shot editor opens on: the next whole hour,
|
|
153
|
+
* rendered in the zone the rule will be read in.
|
|
154
|
+
* @param now - reference instant.
|
|
155
|
+
* @param timeZone - zone the rule is read in.
|
|
156
|
+
* @returns the input value for a near-future default.
|
|
157
|
+
*/
|
|
158
|
+
export function defaultZonedInputValue(now: number, timeZone?: string): string {
|
|
159
|
+
const nextHour = Math.ceil(now / 3_600_000) * 3_600_000
|
|
160
|
+
return zonedInputValue(nextHour, timeZone)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The zone the input helpers resolve against: an explicit resolved zone, or the Host zone. */
|
|
164
|
+
function usableZone(timeZone: string | undefined): string {
|
|
165
|
+
return timeZone !== undefined && isValidTimeZone(timeZone) ? timeZone : resolveHostTimeZone()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The editor's run-cap option for a stored cap: the two named presets, the
|
|
170
|
+
* unlimited entry, or a custom value the editor shows in its own field.
|
|
171
|
+
* @param maxRuns - the stored cap, when any.
|
|
172
|
+
* @returns the select option value.
|
|
173
|
+
*/
|
|
174
|
+
export function runCapOption(maxRuns: number | undefined): string {
|
|
175
|
+
if (maxRuns === undefined) return 'unlimited'
|
|
176
|
+
if (maxRuns === 1) return '1'
|
|
177
|
+
if (maxRuns === 2) return '2'
|
|
178
|
+
return 'custom'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The locale key describing why a rule stopped or skipped an occurrence. */
|
|
182
|
+
export function stopReasonKey(reason: ScheduleStopReason): TaskBoardKey {
|
|
183
|
+
return `detail.schedule.stop.${reason}` as TaskBoardKey
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The run-budget line: how many scheduled executions the Host opened and how
|
|
188
|
+
* many remain. Derived from the counter and the cap rather than stored, so the
|
|
189
|
+
* display cannot drift from the ledger.
|
|
190
|
+
* @param rule - the schedule rule.
|
|
191
|
+
* @returns localized wording.
|
|
192
|
+
*/
|
|
193
|
+
export function budgetLabel(rule: ScheduleRule): string {
|
|
194
|
+
const budget = scheduleRunBudget(rule)
|
|
195
|
+
const done = String(rule.runCount)
|
|
196
|
+
if (budget === undefined) return t('detail.schedule.runsUnlimited', { done })
|
|
197
|
+
const remaining = Math.max(budget - rule.runCount, 0)
|
|
198
|
+
return `${t('detail.schedule.runs', { done, total: String(budget) })} · ${t('detail.schedule.remaining', { count: String(remaining) })}`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The line describing a rule that stopped itself, or undefined while it may still fire. */
|
|
202
|
+
export function endedLabel(rule: ScheduleRule): string | undefined {
|
|
203
|
+
if (rule.endedReason === undefined) return undefined
|
|
204
|
+
return t('detail.schedule.ended', { reason: t(stopReasonKey(rule.endedReason)) })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The line describing the last skipped occurrence, or undefined when none. */
|
|
208
|
+
export function skippedLabel(rule: ScheduleRule): string | undefined {
|
|
209
|
+
if (rule.skippedReason === undefined) return undefined
|
|
210
|
+
return t('detail.schedule.skipped', { reason: t(stopReasonKey(rule.skippedReason)) })
|
|
211
|
+
}
|
|
212
|
+
|
|
115
213
|
/**
|
|
116
214
|
* The full next-run label: the absolute wall clock in the schedule's own zone,
|
|
117
215
|
* plus the relative distance in parentheses. Showing both matters because a
|
package/src/core/controller.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { hasOpenExecution, withStatus, type NewTaskInput, type TaskRecord, type
|
|
|
16
16
|
import { applyArchiveTask, applyRestoreTask } from './use-cases/task-archive.ts'
|
|
17
17
|
import { applyCreateTask } from './use-cases/task-create.ts'
|
|
18
18
|
import { applyDeleteTask } from './use-cases/task-delete.ts'
|
|
19
|
-
import { applyScheduleNextRun as applyScheduleRollForward, applySetSchedule } from './use-cases/task-schedule.ts'
|
|
19
|
+
import { applyScheduleNextRun as applyScheduleRollForward, applySetSchedule, type SetSchedulePatch } from './use-cases/task-schedule.ts'
|
|
20
20
|
import { resolveHostTimeZone } from './schedule.ts'
|
|
21
21
|
import { applySetParent } from './use-cases/task-parent.ts'
|
|
22
22
|
import { applyDeleteTag, applyRenameTag } from './use-cases/task-tag.ts'
|
|
@@ -141,7 +141,7 @@ export interface ExecutionOptionsSnapshot {
|
|
|
141
141
|
* (revision/scheduler/power); a full snapshot also carries the deployment
|
|
142
142
|
* constants the UI reads (session default permission, subtask depth).
|
|
143
143
|
*/
|
|
144
|
-
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable' | 'extensions'>
|
|
144
|
+
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable' | 'goalRunEnabled' | 'extensions'>
|
|
145
145
|
|
|
146
146
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
147
147
|
export interface ControllerSnapshot {
|
|
@@ -597,15 +597,17 @@ export class BoardController {
|
|
|
597
597
|
// --- scheduling ---------------------------------------------------------------
|
|
598
598
|
|
|
599
599
|
/**
|
|
600
|
-
* Update a task's schedule rule.
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
600
|
+
* Update a task's schedule rule. An unusable plan (blank or invalid cron, a
|
|
601
|
+
* one-shot with no future instant, a non-positive run cap) is rejected
|
|
602
|
+
* (returns false, state untouched). When the rule ends up enabled the next
|
|
603
|
+
* run instant is computed immediately — a one-shot's next instant IS its
|
|
604
|
+
* planned instant — and a disabled rule carries none. Delegates the domain
|
|
605
|
+
* transition to the schedule use case.
|
|
604
606
|
* @param id - the task to schedule.
|
|
605
607
|
* @param patch - fields to change (absent fields keep their current value).
|
|
606
|
-
* @returns true when applied, false when rejected (
|
|
608
|
+
* @returns true when applied, false when rejected (unknown task / unusable plan).
|
|
607
609
|
*/
|
|
608
|
-
setSchedule(id: string, patch:
|
|
610
|
+
setSchedule(id: string, patch: SetSchedulePatch): boolean {
|
|
609
611
|
// The Host zone is the fallback a rule with no stored zone follows: the
|
|
610
612
|
// mirrored snapshot carries it, and a controller without a Host mirror
|
|
611
613
|
// (pure client tests) falls back to the process zone.
|
|
@@ -813,6 +815,7 @@ export class BoardController {
|
|
|
813
815
|
const sessionDefaultPermission = snapshot.sessionDefaultPermission ?? this.hostState?.sessionDefaultPermission
|
|
814
816
|
const maxSubtaskDepth = snapshot.maxSubtaskDepth ?? this.hostState?.maxSubtaskDepth
|
|
815
817
|
const teamRunAvailable = snapshot.teamRunAvailable ?? this.hostState?.teamRunAvailable
|
|
818
|
+
const goalRunEnabled = snapshot.goalRunEnabled ?? this.hostState?.goalRunEnabled
|
|
816
819
|
const extensions = snapshot.extensions ?? this.hostState?.extensions
|
|
817
820
|
return {
|
|
818
821
|
revision: snapshot.revision,
|
|
@@ -821,6 +824,7 @@ export class BoardController {
|
|
|
821
824
|
...(sessionDefaultPermission === undefined ? {} : { sessionDefaultPermission }),
|
|
822
825
|
...(maxSubtaskDepth === undefined ? {} : { maxSubtaskDepth }),
|
|
823
826
|
...(teamRunAvailable === undefined ? {} : { teamRunAvailable }),
|
|
827
|
+
...(goalRunEnabled === undefined ? {} : { goalRunEnabled }),
|
|
824
828
|
...(extensions === undefined ? {} : { extensions }),
|
|
825
829
|
}
|
|
826
830
|
}
|