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

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 (53) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -10
  3. package/README.zh.md +17 -10
  4. package/lib/client.js +1350 -552
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +655 -134
  7. package/lib/types/client/board/TaskForm.d.ts +19 -1
  8. package/lib/types/client/board/preset-label.d.ts +22 -0
  9. package/lib/types/client/host-api.d.ts +24 -1
  10. package/lib/types/client/locales.d.ts +38 -2
  11. package/lib/types/client/schedule-zone.d.ts +47 -0
  12. package/lib/types/core/controller.d.ts +1 -0
  13. package/lib/types/core/degraded-reason.d.ts +46 -0
  14. package/lib/types/core/package-name.d.ts +9 -0
  15. package/lib/types/core/schedule.d.ts +95 -20
  16. package/lib/types/core/subtask.d.ts +30 -11
  17. package/lib/types/core/tasks.d.ts +26 -2
  18. package/lib/types/core/use-cases/task-create.d.ts +2 -1
  19. package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
  20. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  21. package/lib/types/host-ledger.d.ts +56 -4
  22. package/lib/types/host-runner.d.ts +76 -3
  23. package/lib/types/host-service.d.ts +26 -0
  24. package/lib/types/protocol.d.ts +9 -3
  25. package/package.json +25 -25
  26. package/src/client/board/LinkSubtaskModal.tsx +21 -11
  27. package/src/client/board/NewTaskModal.tsx +271 -150
  28. package/src/client/board/TaskCard.tsx +24 -5
  29. package/src/client/board/TaskDetail.tsx +83 -33
  30. package/src/client/board/TaskForm.tsx +48 -3
  31. package/src/client/board/preset-label.ts +44 -0
  32. package/src/client/board.module.css +114 -5
  33. package/src/client/host-api.ts +109 -6
  34. package/src/client/index.ts +5 -1
  35. package/src/client/locales.ts +82 -8
  36. package/src/client/schedule-zone.ts +135 -0
  37. package/src/core/controller.ts +7 -2
  38. package/src/core/degraded-reason.ts +81 -0
  39. package/src/core/package-name.ts +9 -0
  40. package/src/core/schedule.ts +211 -55
  41. package/src/core/store.ts +10 -1
  42. package/src/core/subtask.ts +77 -22
  43. package/src/core/tasks.ts +38 -4
  44. package/src/core/use-cases/task-create.ts +14 -2
  45. package/src/core/use-cases/task-schedule.ts +17 -6
  46. package/src/core/use-cases/task-update.ts +4 -1
  47. package/src/host/agent-tools.ts +40 -8
  48. package/src/host-ledger.ts +203 -23
  49. package/src/host-runner.ts +164 -14
  50. package/src/host-service.ts +94 -17
  51. package/src/protocol.ts +27 -8
  52. package/lib/types/host/run-guarded.d.ts +0 -39
  53. package/src/host/run-guarded.ts +0 -76
@@ -4,7 +4,7 @@
4
4
  * NewTaskModal and the EditTaskModal. State stays in the owning modal; these
5
5
  * are controlled components.
6
6
  */
7
- import type { ReactNode } from 'react';
7
+ import { type ReactNode } from 'react';
8
8
  import { type TaskTag } from '../../core/tasks.ts';
9
9
  /** Modal overlay: closes on backdrop press, submits through the form. */
10
10
  export declare function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, secondaryAction, children, }: {
@@ -22,6 +22,24 @@ export declare function ModalShell({ ariaLabel, title, error, pending, submitLab
22
22
  };
23
23
  children: ReactNode;
24
24
  }): import("react").JSX.Element;
25
+ /**
26
+ * One collapsible region of a task form (create/edit dialog UX): the form
27
+ * groups its configuration into a few regions instead of stacking every field
28
+ * in one long column, so a normal-sized dialog needs no scrollbar. A collapsed
29
+ * region keeps a one-line summary of the values it holds, so nothing is hidden
30
+ * without a trace; a region holding a blocking error is forced open by its
31
+ * owner (forceOpen) rather than reporting the error out of sight.
32
+ */
33
+ export declare function CollapsibleSection({ title, summary, defaultOpen, forceOpen, children, }: {
34
+ title: string;
35
+ /** One-line value preview rendered only while the region is collapsed. */
36
+ summary?: string | undefined;
37
+ /** Whether the region starts expanded (the owner's default layout). */
38
+ defaultOpen?: boolean;
39
+ /** Open regardless of the stored state: an error inside must stay visible. */
40
+ forceOpen?: boolean;
41
+ children: ReactNode;
42
+ }): import("react").JSX.Element;
25
43
  /** Title + description + prompt fields shared by the new and edit task forms. */
26
44
  export declare function TaskContentFields({ title, description, prompt, onTitleChange, onDescriptionChange, onPromptChange, }: {
27
45
  title: string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Display-only labels for the agent-preset roster, shared by the task form and
3
+ * the detail view.
4
+ *
5
+ * The deployment's four built-in presets (the `standard` / `ptc` / `minimal` /
6
+ * `cordis` rows the shipped web-app bundle declares) carry no display name in
7
+ * the registry roster, so a picker would list bare ids beside the user presets.
8
+ * The map below only supplies names for those four; any other id — a renamed
9
+ * built-in, a community preset, a user preset — falls back to the roster name
10
+ * and then to the id, so this table can never hide or mislabel a preset.
11
+ */
12
+ import type { ExecutionPresetOption } from '../../core/controller.ts';
13
+ /** Whether one roster row is a shipped built-in preset. */
14
+ export declare function isBuiltinPreset(id: string): boolean;
15
+ /** Display label of one preset row: built-in label, roster name, or raw id. */
16
+ export declare function presetLabel(preset: Pick<ExecutionPresetOption, 'id' | 'name'>): string;
17
+ /**
18
+ * Label of the empty ("inherit") choice: names the preset a run actually lands
19
+ * on when the card pins nothing, which is the deployment default the roster
20
+ * marks with `isDefault`.
21
+ */
22
+ export declare function inheritPresetLabel(presets: readonly ExecutionPresetOption[]): string;
@@ -7,13 +7,36 @@ import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardParseDr
7
7
  * "Unexpected token 'o', \"not found\" is not valid JSON" is what a missing
8
8
  * Host half looks like today.
9
9
  */
10
- export type HostApiFailure = 'not-mounted' | 'unauthorized' | 'locked' | 'rejected' | 'timeout' | 'unreachable' | 'unexpected';
10
+ export type HostApiFailure =
11
+ /** The board's own routes are not registered and nothing explains why. */
12
+ 'not-mounted'
13
+ /** Another live DSH process owns the ledger, so this instance registered no board routes (#1730). */
14
+ | 'ledger-locked'
15
+ /** This row degraded during start for some other reason the family health route reports (#1730). */
16
+ | 'degraded' | 'unauthorized' | 'forbidden' | 'locked' | 'rejected' | 'timeout' | 'unreachable' | 'unexpected';
11
17
  /** Transport failure carrying a stable class next to its user-facing message. */
12
18
  export declare class HostApiError extends Error {
13
19
  readonly failure: HostApiFailure;
14
20
  readonly status?: number | undefined;
15
21
  constructor(failure: HostApiFailure, message: string, status?: number | undefined);
16
22
  }
23
+ /**
24
+ * Name the real reason this board's routes are missing (issue #1730).
25
+ *
26
+ * The core webserver answers an unmounted route with a bare 404, which cannot
27
+ * distinguish "this deployment never loaded the plugin" from "this row
28
+ * degraded during start" — a second DSH process holding `ledger-v2.lock` is
29
+ * the case that matters, because the panel's advice ("restart the service")
30
+ * is then false: the lock belongs to the other process, and restarting cannot
31
+ * win it. The family health route knows the difference, so the 404 is
32
+ * disambiguated there before anything is shown.
33
+ * @param fallback - copy for the plain "routes are not there" failure, which
34
+ * is route-specific (the board's state route and the AI-parse route phrase
35
+ * their own absence); it is also what is shown when the ledger names nothing.
36
+ * @param fetchImpl - fetch implementation (injected for tests).
37
+ * @returns the HostApiError to throw for a missing board route.
38
+ */
39
+ export declare function missingRouteFailure(fallback?: string, fetchImpl?: typeof fetch): Promise<HostApiError>;
17
40
  export interface TaskBoardHostTransport {
18
41
  bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
19
42
  state(): Promise<TaskBoardSnapshot>;
@@ -29,7 +29,10 @@ export declare const zh: {
29
29
  'board.created': string;
30
30
  'board.hostError': string;
31
31
  'board.hostError.notMounted': string;
32
+ 'board.hostError.ledgerLocked': string;
33
+ 'board.hostError.degraded': string;
32
34
  'board.hostError.unauthorized': string;
35
+ 'board.hostError.forbidden': string;
33
36
  'board.hostError.locked': string;
34
37
  'board.hostError.timeout': string;
35
38
  'board.hostError.unreachable': string;
@@ -54,6 +57,19 @@ export declare const zh: {
54
57
  'new.aiParseFailed': string;
55
58
  'board.retryHost': string;
56
59
  'board.hostMeta': string;
60
+ 'new.section.parse': string;
61
+ 'new.section.content': string;
62
+ 'new.section.labels': string;
63
+ 'new.section.execution': string;
64
+ 'new.section.run': string;
65
+ 'new.section.handover': string;
66
+ 'new.section.schedule': string;
67
+ 'new.summary.none': string;
68
+ 'new.summary.labelCount': string;
69
+ 'new.summary.filled': string;
70
+ 'new.summary.scheduleOff': string;
71
+ 'new.summary.multiRound': string;
72
+ 'new.summary.singleRound': string;
57
73
  'new.title': string;
58
74
  'new.titlePlaceholder': string;
59
75
  'new.description': string;
@@ -145,13 +161,31 @@ export declare const zh: {
145
161
  'detail.schedule.invalid': string;
146
162
  'detail.schedule.notScheduled': string;
147
163
  'detail.schedule.dueSoon': string;
164
+ 'detail.schedule.timeZone': string;
165
+ 'detail.schedule.timeZoneHost': string;
166
+ 'detail.schedule.timeZoneHostUnknown': string;
167
+ 'detail.schedule.timeZoneHint': string;
168
+ 'detail.schedule.timeZoneSearch': string;
169
+ 'detail.schedule.timeZoneNone': string;
170
+ 'detail.schedule.nextRunOverdue': string;
171
+ 'detail.schedule.duration.days': string;
172
+ 'detail.schedule.duration.hours': string;
173
+ 'detail.schedule.duration.minutes': string;
174
+ 'detail.schedule.duration.seconds': string;
148
175
  'card.scheduled': string;
149
176
  'new.workspace': string;
150
- 'new.mode': string;
177
+ 'new.agentPreset': string;
151
178
  'new.permission': string;
152
179
  'new.model': string;
153
180
  'exec.workspace.recent': string;
154
- 'exec.mode.default': string;
181
+ 'exec.mode.inherit': string;
182
+ 'exec.mode.inheritWith': string;
183
+ 'exec.mode.builtinGroup': string;
184
+ 'exec.mode.customGroup': string;
185
+ 'preset.builtin.standard': string;
186
+ 'preset.builtin.ptc': string;
187
+ 'preset.builtin.minimal': string;
188
+ 'preset.builtin.cordis': string;
155
189
  'exec.mode.defaultSuffix': string;
156
190
  'exec.mode.brokenSuffix': string;
157
191
  'exec.mode.removed': string;
@@ -167,6 +201,8 @@ export declare const zh: {
167
201
  'exec.teamRunHint': string;
168
202
  'exec.teamRunUnavailable': string;
169
203
  'exec.reuseSessionHint': string;
204
+ 'exec.goalRun': string;
205
+ 'exec.goalRunHint': string;
170
206
  'detail.executionSettings': string;
171
207
  'exec.hint': string;
172
208
  'settings.title': string;
@@ -0,0 +1,47 @@
1
+ /** One selectable zone, in the order the picker shows it. */
2
+ export interface ZoneChoice {
3
+ /** Canonical IANA id, or `''` for the Host-zone entry. */
4
+ id: string;
5
+ /** Display label. */
6
+ label: string;
7
+ }
8
+ /**
9
+ * Zones offered by the picker: the Host zone first (the default, and the entry
10
+ * a rule with no stored zone follows), then the runtime's own inventory.
11
+ *
12
+ * The Host entry is always present, even before the Host snapshot reports a
13
+ * zone: it is the entry that stores no zone, so dropping it would leave a rule
14
+ * unable to express "follow the Host" and would let the control silently
15
+ * display an unrelated zone.
16
+ *
17
+ * A stored zone the inventory does not list (a rule written on another machine
18
+ * or under a different ICU build) is appended, so opening the editor can never
19
+ * silently rewrite the rule's zone.
20
+ * @param hostTimeZone - zone the Host reported for this deployment, when known.
21
+ * @param storedTimeZone - zone the edited rule already stores, when any.
22
+ * @returns the de-duplicated choice list.
23
+ */
24
+ export declare function zoneChoices(hostTimeZone: string | undefined, storedTimeZone?: string): ZoneChoice[];
25
+ /**
26
+ * The relative half of a next-run label: how far away the instant is, or how
27
+ * far past it the board is running. `now` is passed in rather than sampled so
28
+ * callers can share one clock reading across a row, and tests stay
29
+ * deterministic.
30
+ * @param targetMs - the instant being described.
31
+ * @param now - the reference instant.
32
+ * @returns localized relative wording.
33
+ */
34
+ export declare function relativeTimeLabel(targetMs: number, now: number): string;
35
+ /**
36
+ * The full next-run label: the absolute wall clock in the schedule's own zone,
37
+ * plus the relative distance in parentheses. Showing both matters because a
38
+ * rule's zone can differ from the reader's, so the wall clock alone is
39
+ * ambiguous and the distance alone loses the actual moment.
40
+ * @param targetMs - the instant being described.
41
+ * @param timeZone - zone to render the absolute half in.
42
+ * @param formatAbsolute - formatter for the absolute half (keeps Intl caching in the caller).
43
+ * @param now - the reference instant.
44
+ * @param translate - translate function, bound so this module stays dependency-light.
45
+ * @returns the combined label.
46
+ */
47
+ export declare function nextRunLabel(targetMs: number, timeZone: string | undefined, formatAbsolute: (ms: number, timeZone?: string) => string, now: number): string;
@@ -262,6 +262,7 @@ export declare class BoardController {
262
262
  setSchedule(id: string, patch: {
263
263
  enabled?: boolean;
264
264
  cron?: string;
265
+ timeZone?: string | null;
265
266
  }): boolean;
266
267
  /**
267
268
  * Legacy pure-controller seam retained for migration-focused tests. The
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The family health ledger's degraded-record contract (issues #1528, #1730).
3
+ *
4
+ * A family plugin row that fails during apply is recorded by the dsh-web-all
5
+ * shell as a degraded record; the shell serves the ledger through its
6
+ * loopback-only `GET /api/dsh-web-all/degraded` route because the failed
7
+ * plugin could not register any route of its own — the task board whose ledger
8
+ * lock is held by another DSH process is exactly that case, and without this
9
+ * the panel could only report "the Host API is not mounted".
10
+ *
11
+ * Both halves define this shape: the shell WRITES it (packages/dsh-web-all,
12
+ * src/degraded.ts — the two packages have no import edge, one is bundled into
13
+ * the other's profile) and a plugin's browser half PARSES it. The edits move
14
+ * together.
15
+ */
16
+ /** Where a degraded row failed: module import, plugin shape, or fiber start. */
17
+ export type DegradedStage = 'import' | 'shape' | 'start';
18
+ /** One degraded family row as the shell's health route serves it. */
19
+ export interface DegradedRow {
20
+ /** Real plugin package name from the shell row config. */
21
+ plugin: string;
22
+ /** Where the failure happened. */
23
+ stage: DegradedStage;
24
+ /** The shell's one-line reason, for display. */
25
+ reason: string;
26
+ }
27
+ /**
28
+ * The one degraded record naming `plugin`, or undefined when that row is not
29
+ * degraded (or the payload does not come from the shell's own shape). A
30
+ * plugin may appear once: the shell keys the ledger by package name and
31
+ * refreshes the record on every new failure.
32
+ * @param body - parsed JSON body of the health route.
33
+ * @param plugin - the real plugin package name to look for.
34
+ * @returns the degraded row, or undefined when the ledger does not name it.
35
+ */
36
+ export declare function degradedRowOf(body: unknown, plugin: string): DegradedRow | undefined;
37
+ /**
38
+ * Extract the pid of the LIVE process a ledger-lock failure blames, when the
39
+ * reason identifies one. The task board's lock refusal is the one degraded
40
+ * reason that carries an owner the user can act on, so the panel renders the
41
+ * pid in its own sentence and adds the guidance that actually applies.
42
+ * @param reason - the shell's one-line failure reason.
43
+ * @returns the confirmed owner pid, or undefined when the reason names none or
44
+ * warns that the PID may have been reused (its own text owns the recovery).
45
+ */
46
+ export declare function ledgerOwnerPid(reason: string): number | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * npm identity of this plugin. The dsh-web-all degraded ledger is keyed by the
3
+ * real plugin package name of a family row, and this plugin's browser half
4
+ * must name its OWN row there to learn why its Host API is missing (issue
5
+ * #1730): the served bundle cannot derive the name, because a family row
6
+ * mounts the package from a versioned profile path. The host half spells the
7
+ * same name in its single-mount guard (src/index.ts); the two move together.
8
+ */
9
+ export declare const TASK_BOARD_PACKAGE = "@linxin666/dsh-client-ui-task-board";
@@ -1,15 +1,36 @@
1
1
  /**
2
- * Minimal 5-field cron parsing and next-run computation for scheduled task
3
- * runs. Framework-free and dependency-free so the scheduler and controller
4
- * share one tiny pure module.
2
+ * Zone-aware 5-field cron parsing and next-run computation for scheduled task
3
+ * runs. Framework-free and dependency-free (Intl only) so the Host scheduler
4
+ * and the browser next-run preview share one tiny pure module.
5
5
  *
6
6
  * Grammar: five whitespace-separated fields, 分 时 日 月 周. Every field
7
7
  * supports the wildcard, step (wildcard or range + "/n"), single value,
8
8
  * inclusive range a-b, and comma lists mixing any of those. Ranges: minutes
9
9
  * 0-59, hours 0-23, days 1-31, months 1-12, weekdays 0-7 (0 and 7 both mean
10
- * Sunday). When both the day and weekday fields are restricted they combine
11
- * with OR semantics (standard cron). Invalid expressions parse to null and
12
- * are rejected by the UI/controller.
10
+ * Sunday). Invalid expressions parse to null and are rejected by the UI and
11
+ * the Host.
12
+ *
13
+ * Day semantics follow Vixie cron: a field is *starred* when its field text
14
+ * starts with `*`. When day-of-month and day-of-week are both restricted the
15
+ * fields combine with OR; otherwise they combine with AND. The distinction
16
+ * matters for a stepped star such as `*&#47;2`, which still restricts the dates it
17
+ * matches, so `0 0 *&#47;2 * 1` means "the Mondays that fall on an even day of
18
+ * month" rather than "every even day, or every Monday".
19
+ *
20
+ * Time zone: every candidate wall clock is interpreted in an explicit IANA
21
+ * zone (the schedule's stored zone, or the Host zone when none is given).
22
+ * A wall clock that does not exist (spring-forward gap) is skipped, and an
23
+ * ambiguous wall clock (fall-back overlap) resolves to the earlier instant, so
24
+ * a repeated local minute fires exactly once. The Host-scheduler semantics are
25
+ * byte-identical to `@deepseek-ai/dsh-schedule`'s resolver, which was verified
26
+ * against it across twelve IANA zones (including 30-minute and 45-minute DST
27
+ * offsets and a date-line jump) and 300k decision instants.
28
+ *
29
+ * That upstream is on the experimental track and is NOT a dependency here (no
30
+ * import, no inject, no peer range), so a change to it fails no test. Re-run the
31
+ * differential check on any `dsh-schedule` release or cohort bump that moves it,
32
+ * and keep this comment honest; the re-verification trigger is recorded in
33
+ * [the owning Agent Note](../../../../.agents/notes/implemented/feature/2026-09-29-task-board-schedule-time-zone.md).
13
34
  */
14
35
  /** The parsed match sets of one cron expression. */
15
36
  export interface CronSchedule {
@@ -19,11 +40,61 @@ export interface CronSchedule {
19
40
  months: ReadonlySet<number>;
20
41
  /** Weekdays 0-6, 0 = Sunday (input 7 normalized to 0). */
21
42
  weekdays: ReadonlySet<number>;
22
- /** Whether the day-of-month field was the literal '*' (unrestricted). */
23
- dayWildcard: boolean;
24
- /** Whether the weekday field was the literal '*' (unrestricted). */
25
- weekdayWildcard: boolean;
43
+ /** Whether the day-of-month field text starts with `*` (Vixie starred). */
44
+ dayStarred: boolean;
45
+ /** Whether the weekday field text starts with `*` (Vixie starred). */
46
+ weekdayStarred: boolean;
47
+ }
48
+ /** Fallback zone when the process reports no usable IANA zone. */
49
+ export declare const HOST_TIME_ZONE_FALLBACK = "UTC";
50
+ /** One resolved wall-clock reading in some zone. */
51
+ export interface ZonedParts {
52
+ year: number;
53
+ month: number;
54
+ day: number;
55
+ hour: number;
56
+ minute: number;
26
57
  }
58
+ /**
59
+ * The wall-clock reading of one instant in an explicit zone. Reading through
60
+ * Intl (rather than the process `Date` accessors) is what makes the result
61
+ * independent of the Host's own TZ.
62
+ * @param timeZone - explicit IANA zone.
63
+ * @param epochMs - instant to read.
64
+ * @returns the zone-local calendar and clock fields.
65
+ */
66
+ export declare function partsInZone(timeZone: string, epochMs: number): ZonedParts;
67
+ /**
68
+ * Resolve a wall clock in an explicit zone to its instant.
69
+ *
70
+ * The offset is probed a day either side of the naive guess so both DST
71
+ * transitions are covered, then every candidate offset whose inverse maps back
72
+ * to the requested wall clock is kept. No survivor means the wall clock does
73
+ * not exist (a spring-forward gap); several means it is ambiguous (a fall-back
74
+ * overlap), and the earliest instant wins so the repeated clock is taken once.
75
+ * @param timeZone - explicit IANA zone.
76
+ * @param year - full calendar year.
77
+ * @param month - 1-12.
78
+ * @param day - day of month.
79
+ * @param hour - 0-23.
80
+ * @param minute - 0-59.
81
+ * @returns the instant in ms epoch, or undefined when the wall clock is skipped.
82
+ */
83
+ export declare function zonedEpoch(timeZone: string, year: number, month: number, day: number, hour: number, minute: number): number | undefined;
84
+ /**
85
+ * The Host process's IANA zone, used for schedules that store no zone of their
86
+ * own (a ledger written before zones were persisted).
87
+ * @returns a zone name Intl accepts, never empty.
88
+ */
89
+ export declare function resolveHostTimeZone(): string;
90
+ /**
91
+ * Whether a value names a zone this runtime can actually resolve. A stored
92
+ * zone is trusted only after it round-trips through Intl, so a typo or a zone
93
+ * that this ICU build does not know can never arm a schedule.
94
+ * @param value - candidate zone name.
95
+ * @returns true when the name is usable.
96
+ */
97
+ export declare function isValidTimeZone(value: string): boolean;
27
98
  /**
28
99
  * Parse a 5-field cron expression.
29
100
  * @returns the match sets, or null when the expression is invalid.
@@ -32,18 +103,22 @@ export declare function parseCron(expr: string): CronSchedule | null;
32
103
  /** Whether the expression parses. */
33
104
  export declare function isValidCron(expr: string): boolean;
34
105
  /**
35
- * Compute the next matching instant after `fromMs` (ms epoch), in local time,
36
- * at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
37
- * of the matching minute's start, or undefined when the calendar constraint
38
- * can never match (for example `0 0 30 2 *`). The five-year horizon includes
39
- * a full leap cycle, so a valid February 29 schedule remains reachable from
40
- * every non-leap year.
106
+ * Compute the next matching instant after `fromMs` strictly greater than it,
107
+ * interpreting the expression's wall clock in `timeZone` (the Host zone when
108
+ * omitted). Returns the ms epoch of the matching minute's start, or undefined
109
+ * when the calendar constraint can never match (for example `0 0 30 2 *`).
110
+ * The five-year horizon includes a full leap cycle, so a valid February 29
111
+ * schedule remains reachable from every non-leap year.
41
112
  *
42
113
  * Walks candidate year/month/day/hour/minute values straight from the parsed
43
114
  * field sets instead of scanning every minute: a sparse expression such as
44
115
  * `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
45
- * next leap day. Wall-clock field construction + the final `matches` re-check
46
- * preserve the old minute scan's DST semantics exactly (nonexistent spring
47
- * minutes normalize forward and the repeated fall-back hour is never visited).
116
+ * next leap day. A candidate wall clock that the zone skips (spring-forward
117
+ * gap) is passed over, which is what makes the gap semantics fall out of the
118
+ * candidate walk rather than needing a separate rule.
119
+ * @param expr - 5-field cron expression.
120
+ * @param fromMs - exclusive lower bound, ms epoch.
121
+ * @param timeZone - explicit IANA zone; defaults to the Host zone.
122
+ * @returns the next trigger instant, or undefined when unreachable.
48
123
  */
49
- export declare function nextRunAtMs(expr: string, fromMs: number): number | undefined;
124
+ export declare function nextRunAtMs(expr: string, fromMs: number, timeZone?: string): number | undefined;
@@ -24,28 +24,40 @@ export declare const DEFAULT_SUBTASK_DEPTH = 1;
24
24
  export declare function normalizeSubtaskDepth(value: unknown): number;
25
25
  /** Whether an unknown value is a settled execution outcome. */
26
26
  export declare function isExecutionOutcome(value: unknown): value is ExecutionOutcome;
27
+ /**
28
+ * Reusable lineage index over ONE task list: the id lookup and the direct
29
+ * children per parent. A caller that runs several lineage queries over the same
30
+ * ledger (the link-subtask candidate gate runs one per task) builds this once
31
+ * instead of rescanning the list per query.
32
+ */
33
+ export interface LineageIndex {
34
+ readonly byId: ReadonlyMap<string, TaskRecord>;
35
+ readonly childrenByParent: ReadonlyMap<string, readonly TaskRecord[]>;
36
+ }
37
+ /** Build the reusable index for a task list (ledger order preserved). */
38
+ export declare function buildLineageIndex(tasks: readonly TaskRecord[]): LineageIndex;
27
39
  /**
28
40
  * Nearest-first ancestor chain of a task. The walk is cycle-safe and bounded
29
41
  * by the ledger size rather than by the deployment maximum: a chain stored
30
42
  * under a larger limit survives that limit being lowered, and restore and
31
43
  * inheritance must still see all of it.
32
44
  */
33
- export declare function ancestorChain(tasks: readonly TaskRecord[], task: TaskRecord): TaskRecord[];
45
+ export declare function ancestorChain(tasks: readonly TaskRecord[], task: TaskRecord, lineage?: LineageIndex): TaskRecord[];
34
46
  /** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
35
- export declare function taskDepth(tasks: readonly TaskRecord[], id: string): number;
47
+ export declare function taskDepth(tasks: readonly TaskRecord[], id: string, lineage?: LineageIndex): number;
36
48
  /** Direct subtasks of a task, in ledger order. */
37
- export declare function directSubtasks(tasks: readonly TaskRecord[], id: string): TaskRecord[];
49
+ export declare function directSubtasks(tasks: readonly TaskRecord[], id: string, lineage?: LineageIndex): TaskRecord[];
38
50
  /**
39
51
  * Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
40
52
  * total depth. The visited set keeps a malformed ledger from looping.
41
53
  */
42
- export declare function descendantTasks(tasks: readonly TaskRecord[], id: string, maxSubtaskDepth: number): TaskRecord[];
54
+ export declare function descendantTasks(tasks: readonly TaskRecord[], id: string, maxSubtaskDepth: number, lineage?: LineageIndex): TaskRecord[];
43
55
  /**
44
56
  * Height of a task's own subtree (0 for a leaf), bounded by the hard maximum.
45
57
  * The visited set makes the walk terminate on a cyclic hand-edited ledger
46
58
  * instead of recursing until the stack overflows.
47
59
  */
48
- export declare function subtreeHeight(tasks: readonly TaskRecord[], id: string, visited?: ReadonlySet<string>): number;
60
+ export declare function subtreeHeight(tasks: readonly TaskRecord[], id: string, visited?: ReadonlySet<string>, lineage?: LineageIndex): number;
49
61
  /** Why a parent link was refused; also the source of the Host error text. */
50
62
  export type ParentLinkRejection = 'unknown-task' | 'unknown-parent' | 'archived-parent' | 'self-parent' | 'cycle' | 'depth-exceeded';
51
63
  export interface ParentLinkCheck {
@@ -61,7 +73,7 @@ export interface ParentLinkCheck {
61
73
  * and the resulting depth (parent depth + 1 + the child own subtree height)
62
74
  * must stay within maxSubtaskDepth.
63
75
  */
64
- export declare function checkParentLink(tasks: readonly TaskRecord[], childId: string, parentId: string | null, maxSubtaskDepth: number): ParentLinkCheck;
76
+ export declare function checkParentLink(tasks: readonly TaskRecord[], childId: string, parentId: string | null, maxSubtaskDepth: number, lineage?: LineageIndex): ParentLinkCheck;
65
77
  /**
66
78
  * The tasks one run request opens executions for: the requested task first,
67
79
  * then its on-board descendants within the configured depth. Archived tasks
@@ -110,13 +122,20 @@ export declare function effectiveTaskPermission(task: TaskRecord): TaskRecord['p
110
122
  */
111
123
  /**
112
124
  * The immutable teammate name for one team-mode member. Teammate names are
113
- * lower-kebab-case and must stay unique for the lifetime of the Team, so the
114
- * run-group token is appended: two runs of the same tree never collide. A title
115
- * with no ASCII word characters (a CJK title, for example) keeps a stable
116
- * generic prefix and is still distinguished by the token.
125
+ * lower-kebab-case and must stay unique for the lifetime of the Team: the
126
+ * run-group token separates two runs of the same tree, and the member
127
+ * discriminator separates two members of one run that reduce to the same slug.
128
+ *
129
+ * The discriminator is not cosmetic. A title with no ASCII word characters (a
130
+ * CJK title, for example) carries no slug at all, so every such title of one
131
+ * run group collides on the generic prefix; Agent Teams answers every member
132
+ * after the first with `teammate name "..." was already used in this Team`,
133
+ * and those subtasks never start. Two ASCII titles that share a slug
134
+ * (`AI 强度预测` next to `AI 配比优化`) collide the same way.
117
135
  * @param title - the member task's title.
118
136
  * @param token - the run group id (the task id when no group is recorded).
137
+ * @param memberId - the member task's id, which makes the name unique in the run.
119
138
  * @returns the teammate name handed to the Agent Teams service.
120
139
  */
121
- export declare function teammateName(title: string, token: string): string;
140
+ export declare function teammateName(title: string, token: string, memberId?: string): string;
122
141
  export declare function resolveExecutionTargets(task: TaskRecord, tasks: readonly TaskRecord[], maxSubtaskDepth?: number): TaskRecord;
@@ -75,6 +75,13 @@ export interface ScheduleRule {
75
75
  enabled: boolean;
76
76
  /** 5-field cron expression: `分 时 日 月 周`. */
77
77
  cron: string;
78
+ /**
79
+ * IANA zone the cron wall clock is read in (issue #1722). Absent means the
80
+ * Host process zone, which is what a rule written before zones were
81
+ * persisted keeps using; the Host stamps its own zone at ledger migration so
82
+ * a later `TZ` change cannot silently move an existing rule.
83
+ */
84
+ timeZone?: string;
78
85
  /** Next due instant (ms epoch); maintained by the scheduler/controller. */
79
86
  nextRunAt: number | undefined;
80
87
  /** Instant of the latest scheduled trigger (ms epoch). */
@@ -214,6 +221,16 @@ export interface TaskRecord {
214
221
  * and a subtask's own permission pin cannot be honored in this mode.
215
222
  */
216
223
  teamRun?: boolean;
224
+ /**
225
+ * Start every execution of this task with dsh's built-in `/goal` command,
226
+ * armed with the composed execution prompt as its objective. The session then
227
+ * keeps working automatic continuation rounds until the agent marks the goal
228
+ * complete, and this board settles the execution on the goal's own end
229
+ * instead of at the first turn end. Absent means ON — the option is checked
230
+ * by default, so only an explicit `false` runs the task as a single plain
231
+ * turn. A session whose `/goal` command is refused still runs the prompt.
232
+ */
233
+ goalRun?: boolean;
217
234
  /**
218
235
  * Frozen context snapshot for a continuation card; absent on plain tasks.
219
236
  * Sanitized before it enters the ledger (redaction, slash-command taint,
@@ -282,14 +299,21 @@ export interface NewTaskInput {
282
299
  reuseSession?: boolean;
283
300
  /** Run the subtree as an Agent Team (Team Lead session plus one teammate per direct subtask). */
284
301
  teamRun?: boolean;
302
+ /**
303
+ * Start the execution with dsh's built-in `/goal`. Absent/true keeps the
304
+ * default (goal run); an explicit false requests a single plain turn.
305
+ */
306
+ goalRun?: boolean;
285
307
  /**
286
308
  * Optional scheduled-run rule requested at creation time (the new-task
287
- * dialog): an enable flag plus a 5-field cron expression. The create use
288
- * case arms it only when enabled and the expression is valid.
309
+ * dialog): an enable flag, a 5-field cron expression, and the IANA zone its
310
+ * wall clock is read in (absent means the Host zone). The create use case
311
+ * arms it only when enabled and the expression is valid.
289
312
  */
290
313
  schedule?: {
291
314
  enabled: boolean;
292
315
  cron: string;
316
+ timeZone?: string;
293
317
  };
294
318
  /**
295
319
  * Optional frozen context snapshot (goal/progress/next, sanitized by the
@@ -22,5 +22,6 @@ export interface CreateTaskResult {
22
22
  * @param now - clock instant (ms epoch).
23
23
  * @param id - minted task id.
24
24
  * @param maxSubtaskDepth - deployment subtask depth limit.
25
+ * @param hostTimeZone - zone a rule that stores no zone of its own follows.
25
26
  */
26
- export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string, maxSubtaskDepth?: number): CreateTaskResult;
27
+ export declare function applyCreateTask(tasks: readonly TaskRecord[], input: NewTaskInput, now: number, id: string, maxSubtaskDepth?: number, hostTimeZone?: string): CreateTaskResult;
@@ -3,6 +3,8 @@ import { type TaskRecord } from '../tasks.ts';
3
3
  export interface SetSchedulePatch {
4
4
  enabled?: boolean;
5
5
  cron?: string;
6
+ /** IANA zone for the cron wall clock; `null` clears it back to the Host zone. */
7
+ timeZone?: string | null;
6
8
  }
7
9
  /** Result of arming/disarming a rule. */
8
10
  export interface SetScheduleResult {
@@ -12,15 +14,17 @@ export interface SetScheduleResult {
12
14
  applied: boolean;
13
15
  }
14
16
  /**
15
- * Set an on-board task's schedule rule. A blank or invalid cron, or an
16
- * archived task, is rejected (state untouched); an enabled rule computes the
17
- * next run instant immediately, a disabled one carries no next-run instant.
17
+ * Set an on-board task's schedule rule. A blank or invalid cron, an unknown or
18
+ * archived task, or an unusable zone is rejected (state untouched); an enabled
19
+ * rule computes the next run instant immediately in the rule's own zone, a
20
+ * disabled one carries no next-run instant.
18
21
  * @param tasks - current ledger.
19
22
  * @param id - the task to schedule.
20
23
  * @param patch - rule fields to change (absent fields keep their current value).
21
24
  * @param now - clock instant (ms epoch).
25
+ * @param hostTimeZone - zone a rule with no stored zone follows.
22
26
  */
23
- export declare function applySetSchedule(tasks: readonly TaskRecord[], id: string, patch: SetSchedulePatch, now: number): SetScheduleResult;
27
+ export declare function applySetSchedule(tasks: readonly TaskRecord[], id: string, patch: SetSchedulePatch, now: number, hostTimeZone: string): SetScheduleResult;
24
28
  /**
25
29
  * Roll a task's schedule rule forward (scheduler callback): persist the next
26
30
  * due instant and the trigger instant. No-op for tasks without a rule (deleted
@@ -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' | 'teamRun'>> & {
18
+ export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession' | 'teamRun' | 'goalRun'>> & {
19
19
  freeze?: FreezeSnapshot & {
20
20
  redacted?: boolean;
21
21
  } | null;