dsh-taskboard 0.7.2 → 0.7.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.
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","names":[],"sources":["../../src/shared/protocol.ts"],"sourcesContent":["/**\n * Task domain model, state machine, urgency classes, and cron math — the\n * framework-free core shared verbatim by the host half (tools, store, routes,\n * scheduler) and, from P2 on, the browser half (board view).\n *\n * Everything here is a pure function over plain data: no imports beyond the\n * standard library, no I/O, no globals. Tests drive it directly.\n *\n * @module dsh-taskboard/shared/protocol\n */\n\n// ---------------------------------------------------------------------------\n// Status vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Task lifecycle states. Main board columns render `backlog → todo →\n * in_progress → in_review → done`; `canceled` and `archived` are secondary\n * states collected under an \"other tasks\" tab. `blocked` is NOT a status —\n * it is a horizontal marker any non-terminal state may carry.\n */\nexport type TaskStatus =\n | 'backlog'\n | 'todo'\n | 'in_progress'\n | 'in_review'\n | 'done'\n | 'canceled'\n | 'archived'\n\n/** Statuses shown as the five main board columns, in order. */\nexport const MAIN_STATUSES: readonly TaskStatus[] = [\n 'backlog',\n 'todo',\n 'in_progress',\n 'in_review',\n 'done',\n]\n\n/** Statuses collected under the secondary tab. */\nexport const SECONDARY_STATUSES: readonly TaskStatus[] = ['canceled', 'archived']\n\n/** Every valid status, main first. */\nexport const ALL_STATUSES: readonly TaskStatus[] = [...MAIN_STATUSES, ...SECONDARY_STATUSES]\n\n/**\n * Legal forward/sideways transitions. Anything not listed is rejected with\n * `invalid_transition`. `archived` is terminal.\n */\nconst TRANSITIONS: Readonly<Record<TaskStatus, readonly TaskStatus[]>> = {\n backlog: ['todo', 'canceled'],\n todo: ['in_progress', 'backlog', 'canceled'],\n in_progress: ['in_review', 'todo', 'canceled'],\n in_review: ['in_progress', 'todo', 'done', 'canceled'],\n done: ['archived'],\n canceled: ['archived', 'todo'],\n archived: [],\n}\n\n/**\n * Whether a status move is legal per the state machine.\n * @param from - current status.\n * @param to - requested status.\n * @returns true when the transition is allowed.\n */\nexport function canTransition(from: TaskStatus, to: TaskStatus): boolean {\n return TRANSITIONS[from].includes(to)\n}\n\n/**\n * The claim move: the one transition that transfers ownership of a task to\n * the calling session. Guarded by the project (workspace) boundary in the\n * tool layer.\n */\nexport function isClaim(from: TaskStatus, to: TaskStatus): boolean {\n return from === 'todo' && to === 'in_progress'\n}\n\n/** Statuses a `done` move may depart from (user confirmation only). */\nexport function canCompleteFrom(from: TaskStatus): boolean {\n return from === 'in_review'\n}\n\n// ---------------------------------------------------------------------------\n// Urgency\n// ---------------------------------------------------------------------------\n\n/** Urgency classes with fixed UI colors. */\nexport type Urgency = 'urgent' | 'normal' | 'relaxed'\n\n/** All valid urgency values. */\nexport const URGENCIES: readonly Urgency[] = ['urgent', 'normal', 'relaxed']\n\n/** CSS color token per urgency: red / purple / blue. */\nexport const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {\n urgent: '#e5484d',\n normal: '#8e4ec6',\n relaxed: '#3e63dd',\n}\n\n// ---------------------------------------------------------------------------\n// Execution\n// ---------------------------------------------------------------------------\n\n/**\n * Per-task code isolation mode (0.3.0).\n * - `worktree`: each execution runs in a fresh `git worktree` on a dedicated\n * task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.\n * - `none`: run in the workspace directory as before, zero git interaction.\n * Omitted = {@link DEFAULT_ISOLATION}; since 0.5.0 creation materializes the\n * board default onto the record (看板设置 → 执行隔离), and non-git projects\n * still auto-degrade at run time (the execution record carries an\n * `isolationNote` explaining why).\n */\nexport type IsolationMode = 'worktree' | 'none'\n\n/**\n * Factory-default isolation (0.5.0): 原目录执行. Applies when neither the\n * task record nor the board setting (`BoardSettings.defaultIsolation`)\n * says otherwise. Before 0.5.0 the implicit default was 'worktree'.\n */\nexport const DEFAULT_ISOLATION: IsolationMode = 'none'\n\n/** Validate an isolation value. */\nexport function asIsolation(raw: string): IsolationMode {\n if (raw !== 'worktree' && raw !== 'none') {\n throw new Error(\"isolation must be 'worktree' or 'none'\")\n }\n return raw\n}\n\n/** Resolve a task's effective isolation (omitted → the factory default). */\nexport function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {\n return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation\n}\n\n/**\n * Execution permission preset (0.5.5). Matches DSH permission presets:\n * - 'workspace-write': 可写入工作区 (factory default)\n * - 'read-only': 仅可查看\n * - 'danger-full-access': 完全权限\n */\nexport type PermissionMode = 'workspace-write' | 'read-only' | 'danger-full-access'\n\n/** Factory default permission preset (0.5.5). */\nexport const DEFAULT_PERMISSION: PermissionMode = 'workspace-write'\n\n/** Canonical list of supported permission modes. */\nexport const ALL_PERMISSIONS: readonly PermissionMode[] = ['workspace-write', 'read-only', 'danger-full-access']\n\n/** Validate and normalize a permission string into a valid {@link PermissionMode}. */\nexport function asPermission(raw: unknown): PermissionMode {\n if (typeof raw !== 'string') return DEFAULT_PERMISSION\n const normalized = raw.trim()\n if (normalized === 'workspace-write' || normalized === 'workspaceWrite') return 'workspace-write'\n if (normalized === 'read-only' || normalized === 'readOnly') return 'read-only'\n if (normalized === 'danger-full-access' || normalized === 'fullAccess') return 'danger-full-access'\n throw new Error(\"permission must be 'workspace-write', 'read-only', or 'danger-full-access'\")\n}\n\n/**\n * Board-level settings persisted with the ledger (0.5.0). Only fields the\n * user explicitly set are present; absent fields follow factory defaults.\n */\nexport type BoardSettings = {\n /** Default code isolation applied when a NEW task is created without an explicit choice. */\n defaultIsolation?: IsolationMode\n /** Automatically capture external workspace sessions into the taskboard (default: false). */\n syncExternalSessions?: boolean\n /** Default permission preset applied when a NEW task is created without an explicit choice (0.5.5, default: 'workspace-write'). */\n defaultPermission?: PermissionMode\n}\n\n/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */\nexport function asBoardSettings(raw: unknown): BoardSettings {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('board settings must be an object')\n }\n const e = raw as Record<string, unknown>\n const out: BoardSettings = {}\n if (e.defaultIsolation !== undefined) {\n if (typeof e.defaultIsolation !== 'string') {\n throw new Error(\"defaultIsolation must be 'worktree' or 'none'\")\n }\n out.defaultIsolation = asIsolation(e.defaultIsolation)\n }\n if (e.syncExternalSessions !== undefined) {\n if (typeof e.syncExternalSessions !== 'boolean') {\n throw new Error('syncExternalSessions must be a boolean')\n }\n out.syncExternalSessions = e.syncExternalSessions\n }\n if (e.defaultPermission !== undefined) {\n out.defaultPermission = asPermission(e.defaultPermission)\n }\n return out\n}\n\n/** The effective default isolation for NEW tasks (board setting → factory default). */\nexport function defaultIsolationOf(settings?: BoardSettings): IsolationMode {\n return settings?.defaultIsolation ?? DEFAULT_ISOLATION\n}\n\n/** The effective external session sync switch (board setting → factory default false). */\nexport function defaultSyncExternalSessionsOf(settings?: BoardSettings): boolean {\n return settings?.syncExternalSessions ?? false\n}\n\n/** The effective default permission preset for NEW tasks (board setting → factory default 'workspace-write'). */\nexport function defaultPermissionOf(settings?: BoardSettings): PermissionMode {\n return settings?.defaultPermission ?? DEFAULT_PERMISSION\n}\n\n/** How a task may run. */\nexport type ExecutionMode = 'claim' | 'scheduled'\n\n/**\n * Per-task execution configuration. `claim` tasks wait for an in-project\n * session to claim them; `scheduled` tasks run on the host cron scheduler.\n */\nexport interface ExecutionConfig {\n mode: ExecutionMode\n /** Five-field cron expression (minute hour day month weekday); required for `scheduled`. */\n cron?: string\n /** Next due time (epoch ms); maintained by the host scheduler. */\n nextRunAt?: number\n /** Last time the scheduler triggered this task (epoch ms). */\n lastTriggeredAt?: number\n}\n\n/**\n * Parse a five-field cron expression. Supported field syntax: star, star/step\n * (`* / n` without spaces), a single number, an `a-b` range, and comma lists\n * of those. Day-of-week accepts both 0 and 7 as Sunday (normalized to 0).\n *\n * @param expr - the expression to parse.\n * @returns the match sets per field, or null when invalid.\n */\nexport function parseCron(expr: string): CronMatch | null {\n const fields = expr.trim().split(/\\s+/)\n if (fields.length !== 5) return null\n const ranges: ReadonlyArray<readonly [number, number]> = [\n [0, 59],\n [0, 23],\n [1, 31],\n [1, 12],\n [0, 7],\n ]\n const sets: Array<Set<number>> = []\n for (let i = 0; i < 5; i++) {\n const [min, max] = ranges[i]!\n const set = new Set<number>()\n if (!parseCronField(fields[i]!, min, max, set)) return null\n sets.push(set)\n }\n const weekdays = new Set<number>()\n for (const day of sets[4]!) weekdays.add(day === 7 ? 0 : day)\n return { minutes: sets[0]!, hours: sets[1]!, days: sets[2]!, months: sets[3]!, weekdays }\n}\n\n/** Parsed cron field match sets. */\nexport type CronMatch = {\n minutes: ReadonlySet<number>\n hours: ReadonlySet<number>\n days: ReadonlySet<number>\n months: ReadonlySet<number>\n weekdays: ReadonlySet<number>\n}\n\n/** Parse one cron field into a match set; false on any syntax error. */\nfunction parseCronField(field: string, min: number, max: number, out: Set<number>): boolean {\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10)\n if (!Number.isInteger(step) || step < 1) return false\n let lo: number\n let hi: number\n if (range === undefined || range === '') return false\n if (range === '*') {\n lo = min\n hi = max\n } else if (range.includes('-')) {\n const [a, b] = range.split('-')\n lo = Number.parseInt(a ?? '', 10)\n hi = Number.parseInt(b ?? '', 10)\n if (!Number.isInteger(lo) || !Number.isInteger(hi)) return false\n } else {\n lo = Number.parseInt(range, 10)\n if (!Number.isInteger(lo)) return false\n hi = stepRaw === undefined ? lo : max\n }\n if (lo < min || hi > max || lo > hi) return false\n for (let v = lo; v <= hi; v += step) out.add(v)\n }\n return out.size > 0\n}\n\n/**\n * The next time at or after `from` matching the cron sets (local time),\n * or null when no match exists within four years (e.g. Feb 30).\n * @param match - parsed cron sets.\n * @param from - epoch ms start point (inclusive match candidate).\n * @returns the next match's epoch ms, or null.\n */\nexport function nextCronTime(match: CronMatch, from: number): number | null {\n // Walk minute by minute from the next whole minute, capped at ~4 years.\n const start = new Date(from)\n start.setSeconds(0, 0)\n start.setMinutes(start.getMinutes() + 1)\n const cap = from + 4 * 366 * 24 * 60 * 60 * 1000\n let t = start.getTime()\n while (t <= cap) {\n const d = new Date(t)\n if (\n match.months.has(d.getMonth() + 1)\n && match.days.has(d.getDate())\n && match.weekdays.has(d.getDay())\n && match.hours.has(d.getHours())\n && match.minutes.has(d.getMinutes())\n ) {\n return t\n }\n t += 60_000\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Records\n// ---------------------------------------------------------------------------\n\n/** Who performed a write. */\nexport type Actor =\n | { kind: 'user' }\n | { kind: 'agent'; sessionId: string }\n | { kind: 'system' }\n\n/** Structured row of a multi-repo merge system comment (0.6.4). */\nexport type SystemCommentRow = {\n /** Repo path relative to the workspace ('' = the workspace root repo). */\n repo: string\n outcome: 'merged' | 'noop' | 'failed'\n /** Failure reason (verbatim) when outcome = 'failed'. */\n error?: string\n}\n\n/** A progress/report comment on a task. */\nexport type CommentRecord = {\n id: string\n /** Comment body (plain text; UI renders as pre-wrapped). */\n body: string\n /** Optimistic-concurrency version of this comment. */\n version: number\n createdAt: number\n /** The session that wrote this comment; absent for user-written ones. */\n threadId?: string\n /**\n * i18n key of a host-generated system message (0.6.4). The GUI localizes it\n * at render time; `body` stays a zh fallback for agent tools / CSV / raw\n * JSON views.\n */\n systemKey?: string\n /** Flat {name} interpolation params for the system message. */\n systemParams?: Record<string, string>\n /** Structured per-repo rows for the multi-repo merge summary (0.6.4). */\n systemRows?: SystemCommentRow[]\n}\n\n/** One commit produced by an isolated execution (hash + subject). */\nexport type CommitInfo = { hash: string; subject: string }\n\n// ---------------------------------------------------------------------------\n// Multi-repo mirror isolation (0.6.3)\n// ---------------------------------------------------------------------------\n\n/**\n * Max repos ONE task mirror may cover (0.6.3): the workspace root repo plus\n * its parallel nested repos. A workspace discovering more degrades the whole\n * worktree run to the original directory (plan §4.1 — predictable, and never\n * a half-built mirror).\n */\nexport const MAX_MIRROR_REPOS = 8\n\n/**\n * Whether `rel` is a legal repo-path key (0.6.3): `''` = the workspace root\n * repo; otherwise a relative forward-slash path with no traversal/absolute\n * shape and no dot segments. These keys ride into filesystem joins\n * (`<workspace>/<rel>`) and ledger maps — the same R4 paranoia as task ids.\n */\nexport function isValidRelRepoPath(rel: string): boolean {\n if (rel === '') return true\n if (rel.length === 0 || rel.length > 300) return false\n if (/^[A-Za-z]:[\\\\/]/.test(rel) || rel.startsWith('\\\\\\\\') || rel.startsWith('/')) return false\n const parts = rel.split('/')\n return parts.every(p =>\n p.length > 0 && p !== '.' && p !== '..' && !p.startsWith('.') && !/[\\\\:*?\"<>|]/.test(p))\n}\n\n/**\n * Per-repo isolation + evidence facts for one execution (0.6.3 multi-repo\n * mirror). Present only when the mirror covers MORE than the workspace root\n * repo alone (or has skipped repos) — a plain single-repo run keeps the\n * legacy flat fields above, untouched, so old ledgers/clients stay blind to\n * this. The root repo (when mirrored) ALSO dual-writes the flat fields.\n */\nexport type ExecutionRepoEvidence = {\n /** Repo path relative to the workspace ('' = the workspace root repo). */\n repo: string\n /** The task branch checked out in this repo's worktree. */\n branch: string\n /** Absolute path of the repo's worktree inside the task mirror. */\n worktreePath: string\n /** HEAD of the task branch before the execution started. */\n baseCommit?: string\n /** HEAD at settlement. */\n headCommit?: string\n /** Commits between baseCommit and headCommit (capped, newest first). */\n commits?: CommitInfo[]\n commitsTotal?: number\n /** Uncommitted changes at settlement (`status --porcelain` lines, capped). */\n dirtyFiles?: string[]\n dirtyFilesTotal?: number\n diffStat?: string\n changedFiles?: number\n}\n\n/**\n * The structured execution report an agent submits at handoff (0.4.0).\n * Commits/dirty/diff facts are host-collected git evidence — the report\n * covers the BUSINESS side the host cannot see.\n */\nexport type ExecutionReport = {\n /** What was done (1..2000 chars, required). */\n summary: string\n /** Files the agent changed (paths, ≤50 × 300 chars). */\n changedFiles: string[]\n /** How the work was self-verified (≤50 × 300 chars). */\n checks: string[]\n /** Produced artifacts worth reviewing (≤30 × 300 chars). */\n artifacts: string[]\n /** Known remaining risks / follow-ups (≤2000 chars, '' allowed). */\n risk: string\n}\n\n/** One Definition-of-Done checklist item (0.4.0). */\nexport type ChecklistItem = {\n id: string\n /** What must be true for acceptance (1..200 chars). */\n text: string\n checked: boolean\n /** Who checked it: an agent session id, or 'user' for GUI toggles. */\n checkedBy?: string\n /** When it was checked (epoch ms). */\n checkedAt?: number\n /** Evidence note attached when checking (≤400 chars). */\n note?: string\n}\n\n/** One execution attempt of a task. */\nexport type ExecutionRecord = {\n id: string\n /** The session this execution ran in; set once the session is really started. */\n sessionId?: string\n /** Effective configuration of a scheduled session; used only for compatible reuse. */\n sessionReuseKey?: string\n /** Trigger: manual button or the host scheduler. */\n trigger: 'manual' | 'scheduled'\n startedAt?: number\n endedAt?: number\n outcome: 'running' | 'succeeded' | 'failed' | 'cancelled'\n error?: string\n /** Code isolation actually used (`none` also covers degraded worktree runs). */\n isolation?: IsolationMode\n /** Why worktree isolation degraded to running in the original directory. */\n isolationNote?: string\n /** The task branch this execution worked on (worktree runs only). */\n branch?: string\n /** Absolute path of the dedicated worktree (worktree runs only). */\n worktreePath?: string\n /** HEAD of the task branch before the execution started. */\n baseCommit?: string\n /** HEAD at settlement. */\n headCommit?: string\n /** Commits between baseCommit and headCommit (hash + subject; capped at 50, newest first). */\n commits?: CommitInfo[]\n /** Total commits before the evidence cap (equals commits.length when under it). */\n commitsTotal?: number\n /** Uncommitted changes present at settlement (`git status --porcelain` lines; capped at 100). */\n dirtyFiles?: string[]\n /** Total uncommitted lines before the evidence cap. */\n dirtyFilesTotal?: number\n /** Aggregate diff stat between baseCommit and headCommit. */\n diffStat?: string\n /** How many files differ between baseCommit and headCommit. */\n changedFiles?: number\n /** Per-repo facts of a multi-repo mirror run (0.6.3; absent on single-repo runs). */\n repos?: ExecutionRepoEvidence[]\n /** The agent's structured report, submitted via taskboard_execution_report. */\n report?: ExecutionReport\n}\n\n/** The per-model override a task may carry; absent = session default model. */\nexport type TaskModel = {\n provider: string\n model: string\n /** Thinking intensity / reasoning effort (optional; e.g. 'low', 'medium', 'high', 'none'). */\n reasoningEffort?: string\n}\n\n/** One task on the board. */\nexport type TaskRecord = {\n id: string\n title: string\n description: string\n /** Extra execution prompt; the execution session receives title+description+prompt. */\n prompt: string\n /** Owning project: a DSH workspace id. */\n workspaceId: string\n urgency: Urgency\n status: TaskStatus\n /** Horizontal marker: work cannot continue right now (any non-terminal status). */\n blocked: boolean\n execution: ExecutionConfig\n model?: TaskModel\n /** Code isolation for executions (omitted = the worktree default; see {@link IsolationMode}). */\n isolation?: IsolationMode\n /**\n * The agent preset execution sessions are composed from (omitted = the\n * deployment default preset). Recorded on the session header and mounted\n * via the presets service at creation — this is what hands the session its\n * tool set. Editable any time (each run composes fresh).\n */\n presetId?: string\n /**\n * Execution permission preset (0.5.5; see {@link PermissionMode}).\n */\n permission?: PermissionMode\n /**\n * Definition-of-Done acceptance checklist (0.4.0). Agents may append items\n * and check/uncheck them (with evidence); the GUI may edit the whole list.\n * Unchecked items highlight at review time; done stays user-only.\n */\n checklist?: ChecklistItem[]\n /**\n * The task branch fixed at the FIRST worktree creation (`task/<标题>+<taskId>`).\n * Renaming the task afterwards never changes it (history preservation).\n * Multi-repo mirror runs (0.6.3): this stays the WORKSPACE ROOT repo's\n * branch; every nested repo pins its own under {@link branches}.\n */\n branch?: string\n /**\n * Per-repo task branches of a multi-repo mirror task (0.6.3): repo path\n * relative to the workspace (never `''` — the root uses {@link branch})\n * → branch name. Pinned at the repo's FIRST successful worktree creation,\n * same rename-proof semantics as {@link branch}.\n */\n branches?: Record<string, string>\n /**\n * The session currently holding the in-progress claim (explicit claim or a\n * live execution). Present only while `status === 'in_progress'`: any move\n * out of in_progress releases it. `updatedBy` is audit-only — user edits no\n * longer erase the holder.\n */\n claimedBy?: string\n /** When the current holder claimed the task (epoch ms). */\n claimedAt?: number\n version: number\n createdAt: number\n updatedAt: number\n createdBy: Actor\n updatedBy: Actor\n comments: CommentRecord[]\n executions: ExecutionRecord[]\n /** How many older execution records were pruned by the retention cap. */\n executionsPruned?: number\n /** Soft-delete marker set by agent `taskboard_delete`; user confirms the purge. */\n trashedAt?: number\n}\n\n/** Retention cap: how many execution records each task keeps (oldest pruned). */\nexport const MAX_EXECUTIONS = 20\n\n/**\n * Enforce the execution-record retention cap on one task (in place): keep the\n * newest {@link MAX_EXECUTIONS} records, count the dropped ones in\n * `executionsPruned`. Running records are always the newest, never dropped.\n * @param task - the task to prune.\n */\nexport function pruneExecutions(task: TaskRecord): void {\n if (task.executions.length <= MAX_EXECUTIONS) return\n const dropped = task.executions.length - MAX_EXECUTIONS\n task.executions = task.executions.slice(-MAX_EXECUTIONS)\n task.executionsPruned = (task.executionsPruned ?? 0) + dropped\n}\n\n/** The whole durable ledger. */\nexport type TaskLedger = {\n schemaVersion: number\n /** Global monotonic revision; every mutation bumps it. */\n revision: number\n tasks: TaskRecord[]\n /** Board-level settings (0.5.0); absent on ledgers never touched by 设置. */\n settings?: BoardSettings\n}\n\n/** Current ledger format version. */\nexport const LEDGER_SCHEMA_VERSION = 1\n\n/** An empty ledger. */\nexport function emptyLedger(): TaskLedger {\n return { schemaVersion: LEDGER_SCHEMA_VERSION, revision: 0, tasks: [] }\n}\n\n// ---------------------------------------------------------------------------\n// ids\n// ---------------------------------------------------------------------------\n\n/** Random base36 suffix. */\nfunction suffix(): string {\n return Math.random().toString(36).slice(2, 8)\n}\n\n/**\n * Legal task id charset (R4): `t-<base36>-<base36>` from {@link newTaskId},\n * and the ONLY shape accepted from the outside (import) or used to build\n * filesystem paths (worktree dirs). Ids ride into `join(ws, '.dsh-worktrees',\n * id)` — a lax charset here is an arbitrary-directory delete primitive.\n */\nexport function isValidTaskId(id: string): boolean {\n return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/.test(id)\n}\n\n/** Mint a task id. */\nexport function newTaskId(): string {\n return `t-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint a comment id. */\nexport function newCommentId(): string {\n return `c-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint a checklist item id. */\nexport function newChecklistItemId(): string {\n return `k-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint an execution id. */\nexport function newExecutionId(): string {\n return `e-${Date.now().toString(36)}-${suffix()}`\n}\n\n// ---------------------------------------------------------------------------\n// validation helpers (input shaping for tools and routes)\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and normalize a title: trimmed, 1..200 chars.\n * @param raw - the raw input.\n * @returns the normalized title.\n * @throws when empty or too long.\n */\nexport function normalizeTitle(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > 200) {\n throw new Error('title must be 1..200 characters')\n }\n return t\n}\n\n/**\n * Validate a task prompt: trimmed, at most 8000 chars; empty becomes ''.\n * @param raw - the raw input.\n */\nexport function normalizePrompt(raw: string | undefined): string {\n const t = (raw ?? '').trim()\n if (t.length > 8000) throw new Error('prompt must be at most 8000 characters')\n return t\n}\n\n/**\n * Validate and normalize a comment body: trimmed, 1..4000 chars.\n * @param raw - the raw input.\n */\nexport function normalizeBody(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > 4000) {\n throw new Error('comment body must be 1..4000 characters')\n }\n return t\n}\n\n/**\n * Validate an urgency value.\n * @param raw - the raw input.\n */\nexport function asUrgency(raw: string): Urgency {\n if (!URGENCIES.includes(raw as Urgency)) {\n throw new Error(`urgency must be one of: ${URGENCIES.join(', ')}`)\n }\n return raw as Urgency\n}\n\n/**\n * Validate a status value.\n * @param raw - the raw input.\n */\nexport function asStatus(raw: string): TaskStatus {\n if (!ALL_STATUSES.includes(raw as TaskStatus)) {\n throw new Error(`status must be one of: ${ALL_STATUSES.join(', ')}`)\n }\n return raw as TaskStatus\n}\n\n/**\n * Validate an execution config request from raw tool/route input.\n * `scheduled` requires a valid cron; computes the first `nextRunAt` from\n * `now`.\n * @param raw - raw execution input ({@link ExecutionConfig} fields, untyped).\n * @param now - current epoch ms.\n * @returns the normalized config.\n */\nexport function normalizeExecution(\n raw: { mode?: string; cron?: string },\n now: number,\n): ExecutionConfig {\n const mode = raw.mode ?? 'claim'\n if (mode !== 'claim' && mode !== 'scheduled') {\n throw new Error(\"execution.mode must be 'claim' or 'scheduled'\")\n }\n if (mode === 'claim') return { mode }\n const cron = (raw.cron ?? '').trim()\n const match = parseCron(cron)\n if (match === null) throw new Error('execution.cron is not a valid 5-field cron expression')\n const next = nextCronTime(match, now)\n if (next === null) throw new Error('execution.cron never matches within 4 years')\n return { mode, cron, nextRunAt: next }\n}\n\n/**\n * The effective prompt of a task: title+description, with the explicit\n * prompt appended when set — title+description+prompt.\n * @param task - the task.\n */\nexport function effectivePrompt(task: TaskRecord): string {\n const head = task.title\n const body = task.description.length > 0 ? `${head}\\n\\n${task.description}` : head\n return task.prompt.length > 0 ? `${body}\\n\\n${task.prompt}` : body\n}\n\n/**\n * Whether the task is currently claimed by a session (running state).\n * @param task - the task.\n */\nexport function isClaimedBy(task: TaskRecord): string | undefined {\n return task.status === 'in_progress' && task.claimedBy !== undefined ? task.claimedBy : undefined\n}\n\n/**\n * Maintain the explicit claim fields around a status change: entering\n * in_progress under a session records the holder (an execution-start or an\n * agent claim); every move out of in_progress releases the claim (handoff,\n * give-back, cancel). A user-driven move into in_progress records no holder —\n * no session works on it yet.\n * @param task - the task being written (mutated in place).\n * @param to - the target status.\n * @param now - current epoch ms.\n * @param holder - the session id claiming the task, when applicable.\n */\nexport function syncClaim(task: TaskRecord, to: TaskStatus, now: number, holder?: string): void {\n if (to !== 'in_progress') {\n delete task.claimedBy\n delete task.claimedAt\n } else if (holder !== undefined) {\n task.claimedBy = holder\n task.claimedAt = now\n }\n}\n\n/**\n * Collect unique execution session IDs associated with a task:\n * - executions with a non-empty `sessionId`\n * Creator and claim sessions may serve other tasks and are never included.\n * @param task - the task record to inspect.\n * @returns an array of distinct session IDs in stable discovery order.\n */\nexport function taskAssociatedSessionIds(task: TaskRecord): string[] {\n const seen = new Set<string>()\n const result: string[] = []\n const push = (raw: unknown) => {\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (trimmed.length > 0 && !seen.has(trimmed)) {\n seen.add(trimmed)\n result.push(trimmed)\n }\n }\n }\n\n if (Array.isArray(task.executions)) {\n for (const ex of task.executions) {\n if (ex !== null && typeof ex === 'object') {\n push((ex as { sessionId?: unknown }).sessionId)\n }\n }\n }\n return result\n}\n\n/**\n * Validate and normalize a pinned model: `{ provider, model, reasoningEffort? }`,\n * provider and model must be non-empty trimmed strings.\n * @param raw - the raw input.\n * @returns the normalized model.\n * @throws when the shape or the fields are invalid.\n */\nexport function normalizeModel(raw: unknown): TaskModel {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('model must be { provider: string, model: string }')\n }\n const { provider, model, reasoningEffort } = raw as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }\n if (typeof provider !== 'string' || typeof model !== 'string') {\n throw new Error('model must be { provider: string, model: string }')\n }\n const p = provider.trim()\n const m = model.trim()\n if (p.length === 0 || m.length === 0) {\n throw new Error('model.provider and model.model must be non-empty strings')\n }\n const eff = typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0 ? reasoningEffort.trim() : undefined\n return { provider: p, model: m, ...(eff !== undefined ? { reasoningEffort: eff } : {}) }\n}\n\n// ---------------------------------------------------------------------------\n// checklist + report validation (0.4.0)\n// ---------------------------------------------------------------------------\n\n/** Checklist size cap per task. */\nexport const MAX_CHECKLIST_ITEMS = 30\n\n/** Checklist item text cap (chars). */\nexport const MAX_CHECKLIST_TEXT = 200\n\n/**\n * Validate and normalize one checklist text line: trimmed, 1..200 chars.\n * @param raw - the raw text.\n * @throws when empty or too long.\n */\nexport function normalizeChecklistText(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > MAX_CHECKLIST_TEXT) {\n throw new Error(`checklist item text must be 1..${MAX_CHECKLIST_TEXT} characters`)\n }\n return t\n}\n\n/**\n * Build a fresh unchecked checklist from plain text lines (create route /\n * templates / tool adds).\n * @param texts - the item texts (validated individually).\n */\nexport function checklistFromTexts(texts: readonly string[]): ChecklistItem[] {\n const items = texts.map(text => ({ id: newChecklistItemId(), text: normalizeChecklistText(text), checked: false }))\n if (items.length > MAX_CHECKLIST_ITEMS) {\n throw new Error(`checklist may hold at most ${MAX_CHECKLIST_ITEMS} items`)\n }\n return items\n}\n\n/**\n * Validate and normalize a full checklist array (GUI update route, import):\n * missing ids are minted, text is checked, checked flags must be booleans,\n * checkedBy/checkedAt are kept only on checked items.\n * @param raw - untyped array from the wire.\n * @throws with a readable reason on any invalid entry.\n */\nexport function normalizeChecklist(raw: unknown): ChecklistItem[] {\n if (!Array.isArray(raw)) throw new Error('checklist must be an array')\n if (raw.length > MAX_CHECKLIST_ITEMS) {\n throw new Error(`checklist may hold at most ${MAX_CHECKLIST_ITEMS} items`)\n }\n return raw.map((entry): ChecklistItem => {\n if (typeof entry !== 'object' || entry === null) throw new Error('checklist item must be an object')\n const e = entry as Record<string, unknown>\n const text = normalizeChecklistText(typeof e.text === 'string' ? e.text : '')\n const id = typeof e.id === 'string' && e.id.trim().length > 0 ? e.id.trim() : newChecklistItemId()\n const checked = e.checked === true\n const checkedBy = typeof e.checkedBy === 'string' ? e.checkedBy.trim().slice(0, 100) : undefined\n const checkedAt = typeof e.checkedAt === 'number' && Number.isFinite(e.checkedAt) ? e.checkedAt : undefined\n const note = typeof e.note === 'string' && e.note.trim().length > 0 ? e.note.trim().slice(0, 400) : undefined\n if (!checked) return { id, text, checked: false }\n return {\n id,\n text,\n checked: true,\n ...(checkedBy !== undefined && checkedBy.length > 0 ? { checkedBy } : {}),\n ...(checkedAt !== undefined ? { checkedAt } : {}),\n ...(note !== undefined ? { note } : {}),\n }\n })\n}\n\n/** Checklist progress: how many items are checked (absent checklist → 0/0). */\nexport function checklistProgress(task: Pick<TaskRecord, 'checklist'>): { done: number; total: number } {\n const items = task.checklist ?? []\n return { done: items.filter(i => i.checked).length, total: items.length }\n}\n\n/** Report string-list caps. */\nconst REPORT_LIST_CAPS = { changedFiles: 50, checks: 50, artifacts: 30 } as const\n\n/** Per-entry cap for report lists (chars). */\nconst REPORT_ENTRY_MAX = 300\n\n/** Validate one report string list: strings trimmed 1..300 chars. */\nfunction normalizeReportList(raw: unknown, field: keyof typeof REPORT_LIST_CAPS): string[] {\n if (raw === undefined) return []\n if (!Array.isArray(raw)) throw new Error(`report.${field} must be an array of strings`)\n const out = raw.map(entry => {\n if (typeof entry !== 'string') throw new Error(`report.${field} must be an array of strings`)\n const t = entry.trim()\n if (t.length === 0 || t.length > REPORT_ENTRY_MAX) {\n throw new Error(`report.${field} entries must be 1..${REPORT_ENTRY_MAX} characters`)\n }\n return t\n })\n if (out.length > REPORT_LIST_CAPS[field]) {\n throw new Error(`report.${field} may hold at most ${REPORT_LIST_CAPS[field]} entries`)\n }\n return out\n}\n\n/**\n * Validate and normalize a structured execution report.\n * @param raw - untyped tool/route input.\n * @throws with a readable reason on any invalid field.\n */\nexport function normalizeExecutionReport(raw: unknown): ExecutionReport {\n if (typeof raw !== 'object' || raw === null) throw new Error('report must be an object')\n const e = raw as Record<string, unknown>\n const summary = typeof e.summary === 'string' ? e.summary.trim() : ''\n if (summary.length === 0 || summary.length > 2000) {\n throw new Error('report.summary must be 1..2000 characters')\n }\n const risk = typeof e.risk === 'string' ? e.risk.trim().slice(0, 2000) : ''\n return {\n summary,\n changedFiles: normalizeReportList(e.changedFiles, 'changedFiles'),\n checks: normalizeReportList(e.checks, 'checks'),\n artifacts: normalizeReportList(e.artifacts, 'artifacts'),\n risk,\n }\n}\n\n// ---------------------------------------------------------------------------\n// ledger import validation (0.4.0)\n// ---------------------------------------------------------------------------\n\n/** Result classifying every task in an import file against the live ledger. */\nexport type ImportPlan = {\n /** Structurally valid tasks whose ids are new (merge adds them). */\n create: TaskRecord[]\n /** Structurally valid tasks whose ids already exist (merge replaces them). */\n overwrite: TaskRecord[]\n /** Invalid entries with a human-readable reason (never imported). */\n invalid: Array<{ id?: string; reason: string }>\n /** The file's board settings (0.5.0); replace-mode swaps them, merge keeps the live ones. */\n settings?: BoardSettings\n}\n\n/** One unknown-value read helper: string fields with defaults. */\nfunction strOr(raw: Record<string, unknown>, key: string, fallback: string): string {\n const v = raw[key]\n return typeof v === 'string' ? v : fallback\n}\n\n/** One unknown-value read helper: finite numbers with defaults. */\nfunction numOr(raw: Record<string, unknown>, key: string, fallback: number): number {\n const v = raw[key]\n return typeof v === 'number' && Number.isFinite(v) ? v : fallback\n}\n\n/** Evidence caps for imported per-repo facts (aligns the host's collect caps). */\nconst IMPORT_COMMIT_CAP = 50\nconst IMPORT_DIRTY_CAP = 100\n\n/**\n * Sanitize an imported per-task branches map (0.6.3): legal repo keys only,\n * non-empty branch strings, capped at {@link MAX_MIRROR_REPOS} entries.\n * @returns undefined when nothing legal remains.\n */\nexport function normalizeBranchesMap(raw: unknown): Record<string, string> | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (Object.keys(out).length >= MAX_MIRROR_REPOS) break\n if (typeof value !== 'string' || value.trim().length === 0 || value.length > 200) continue\n if (key === '' || !isValidRelRepoPath(key)) continue\n out[key] = value.trim()\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Sanitize ONE imported per-repo evidence entry (0.6.3): rebuilds the record\n * field by field, capping evidence arrays like the host's own collection.\n * @returns undefined for a structurally illegal entry (dropped, not fatal).\n */\nexport function normalizeRepoEvidence(raw: unknown): ExecutionRepoEvidence | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined\n const e = raw as Record<string, unknown>\n if (typeof e.repo !== 'string' || !isValidRelRepoPath(e.repo)) return undefined\n if (typeof e.branch !== 'string' || e.branch.length === 0 || e.branch.length > 200) return undefined\n if (typeof e.worktreePath !== 'string' || e.worktreePath.length === 0 || e.worktreePath.length > 1000) return undefined\n const commits = Array.isArray(e.commits)\n ? e.commits.filter((c): c is CommitInfo =>\n typeof c === 'object' && c !== null && typeof (c as CommitInfo).hash === 'string'\n && typeof (c as CommitInfo).subject === 'string').slice(0, IMPORT_COMMIT_CAP)\n : undefined\n const dirtyFiles = Array.isArray(e.dirtyFiles)\n ? e.dirtyFiles.filter((l): l is string => typeof l === 'string').slice(0, IMPORT_DIRTY_CAP)\n : undefined\n return {\n repo: e.repo,\n branch: e.branch,\n worktreePath: e.worktreePath,\n ...(typeof e.baseCommit === 'string' ? { baseCommit: e.baseCommit.slice(0, 100) } : {}),\n ...(typeof e.headCommit === 'string' ? { headCommit: e.headCommit.slice(0, 100) } : {}),\n ...(commits !== undefined && commits.length > 0 ? { commits } : {}),\n ...(typeof e.commitsTotal === 'number' && Number.isFinite(e.commitsTotal) ? { commitsTotal: e.commitsTotal } : {}),\n ...(dirtyFiles !== undefined && dirtyFiles.length > 0 ? { dirtyFiles } : {}),\n ...(typeof e.dirtyFilesTotal === 'number' && Number.isFinite(e.dirtyFilesTotal) ? { dirtyFilesTotal: e.dirtyFilesTotal } : {}),\n ...(typeof e.diffStat === 'string' ? { diffStat: e.diffStat.slice(0, 500) } : {}),\n ...(typeof e.changedFiles === 'number' && Number.isFinite(e.changedFiles) ? { changedFiles: e.changedFiles } : {}),\n }\n}\n\n/**\n * Validate ONE imported task record (pure): rebuilds it field by field with\n * the normal validators, minting missing ids and re-arming cron. Executions\n * left `running` by the exporting machine are marked failed — their\n * settlement watchers died there and can never settle here.\n * @param raw - the untyped record.\n * @param now - current epoch ms (defaults for timestamps).\n * @returns the rebuilt record, or a rejection reason.\n */\nexport function validateImportedTask(raw: unknown, now: number): { ok: true; task: TaskRecord } | { ok: false; reason: string } {\n if (typeof raw !== 'object' || raw === null) return { ok: false, reason: 'not an object' }\n const e = raw as Record<string, unknown>\n const id = typeof e.id === 'string' ? e.id.trim() : ''\n const fail = (reason: string): { ok: false; reason: string } => ({ ok: false, reason })\n // R4①: length alone let traversal-shaped ids (`../../x`, `..\\..\\x`) into\n // the ledger; the charset gate is the primary defense for every downstream\n // filesystem use of a task id.\n if (!isValidTaskId(id)) return fail('missing/invalid id (must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$)')\n try {\n const execution = normalizeExecution(\n typeof e.execution === 'object' && e.execution !== null ? e.execution as { mode?: string; cron?: string } : {},\n now,\n )\n const comments: CommentRecord[] = []\n if (Array.isArray(e.comments)) {\n for (const c of e.comments) {\n if (typeof c !== 'object' || c === null) return fail('invalid comment entry')\n const ce = c as Record<string, unknown>\n const body = typeof ce.body === 'string' ? ce.body : ''\n if (body.trim().length === 0 || body.length > 4000) return fail('invalid comment body')\n comments.push({\n id: typeof ce.id === 'string' && ce.id.length > 0 ? ce.id : newCommentId(),\n body,\n version: numOr(ce, 'version', 1),\n createdAt: numOr(ce, 'createdAt', now),\n ...(typeof ce.threadId === 'string' ? { threadId: ce.threadId } : {}),\n ...(typeof ce.systemKey === 'string' && /^sys\\.[A-Za-z0-9]+$/.test(ce.systemKey) && ce.systemKey.length <= 100\n ? {\n systemKey: ce.systemKey,\n ...(typeof ce.systemParams === 'object' && ce.systemParams !== null && !Array.isArray(ce.systemParams)\n ? { systemParams: Object.fromEntries(Object.entries(ce.systemParams).filter(([key, value]) => key.length <= 100 && typeof value === 'string' && value.length <= 4000).slice(0, 20)) as Record<string, string> }\n : {}),\n ...(Array.isArray(ce.systemRows)\n ? { systemRows: ce.systemRows.filter((row): row is SystemCommentRow => typeof row === 'object' && row !== null\n && typeof row.repo === 'string' && (row.repo === '' || isValidRelRepoPath(row.repo))\n && ['merged', 'noop', 'failed'].includes(row.outcome)\n && (row.error === undefined || typeof row.error === 'string'))\n .slice(0, MAX_MIRROR_REPOS).map(row => ({ repo: row.repo, outcome: row.outcome, ...(row.error !== undefined ? { error: row.error.slice(0, 4000) } : {}) })) }\n : {}),\n }\n : {}),\n })\n }\n } else return fail('comments must be an array')\n const executions: ExecutionRecord[] = []\n if (Array.isArray(e.executions)) {\n for (const x of e.executions) {\n if (typeof x !== 'object' || x === null) return fail('invalid execution entry')\n const xe = x as Record<string, unknown>\n const trigger = xe.trigger === 'scheduled' ? 'scheduled' : 'manual'\n const outcomeRaw = xe.outcome\n if (outcomeRaw !== 'running' && outcomeRaw !== 'succeeded' && outcomeRaw !== 'failed' && outcomeRaw !== 'cancelled') {\n return fail('invalid execution outcome')\n }\n // A running execution from the exporting machine can never settle\n // here — import it as failed with the reason recorded.\n const outcome = outcomeRaw === 'running' ? 'failed' as const : outcomeRaw\n executions.push({\n id: typeof xe.id === 'string' && xe.id.length > 0 ? xe.id : newExecutionId(),\n ...(typeof xe.sessionId === 'string' ? { sessionId: xe.sessionId } : {}),\n trigger,\n ...(typeof xe.startedAt === 'number' ? { startedAt: xe.startedAt } : {}),\n ...(typeof xe.endedAt === 'number' ? { endedAt: xe.endedAt } : {}),\n outcome,\n ...(outcomeRaw === 'running' ? { error: 'imported while still running (settlement watcher died with the exporting host)' } : (typeof xe.error === 'string' ? { error: xe.error } : {})),\n ...(typeof xe.isolation === 'string' && (xe.isolation === 'worktree' || xe.isolation === 'none') ? { isolation: xe.isolation } : {}),\n ...(typeof xe.isolationNote === 'string' ? { isolationNote: xe.isolationNote } : {}),\n ...(typeof xe.branch === 'string' ? { branch: xe.branch } : {}),\n ...(typeof xe.worktreePath === 'string' ? { worktreePath: xe.worktreePath } : {}),\n ...(typeof xe.baseCommit === 'string' ? { baseCommit: xe.baseCommit } : {}),\n ...(typeof xe.headCommit === 'string' ? { headCommit: xe.headCommit } : {}),\n ...(Array.isArray(xe.commits) ? { commits: xe.commits.filter((c): c is CommitInfo =>\n typeof c === 'object' && c !== null && typeof (c as CommitInfo).hash === 'string' && typeof (c as CommitInfo).subject === 'string') } : {}),\n ...(typeof xe.commitsTotal === 'number' ? { commitsTotal: xe.commitsTotal } : {}),\n ...(Array.isArray(xe.dirtyFiles) ? { dirtyFiles: xe.dirtyFiles.filter((l): l is string => typeof l === 'string') } : {}),\n ...(typeof xe.dirtyFilesTotal === 'number' ? { dirtyFilesTotal: xe.dirtyFilesTotal } : {}),\n ...(typeof xe.diffStat === 'string' ? { diffStat: xe.diffStat } : {}),\n ...(typeof xe.changedFiles === 'number' ? { changedFiles: xe.changedFiles } : {}),\n ...(Array.isArray(xe.repos) ? { repos: xe.repos.slice(0, MAX_MIRROR_REPOS).map(normalizeRepoEvidence).filter((r): r is ExecutionRepoEvidence => r !== undefined) } : {}),\n ...(typeof xe.report === 'object' && xe.report !== null ? { report: normalizeExecutionReport(xe.report) } : {}),\n })\n }\n } else return fail('executions must be an array')\n const status = asStatus(strOr(e, 'status', 'todo'))\n const actorOf = (v: unknown): Actor => (typeof v === 'object' && v !== null && (v as Actor).kind === 'agent' && typeof (v as { sessionId?: unknown }).sessionId === 'string'\n ? { kind: 'agent', sessionId: (v as { sessionId: string }).sessionId }\n : { kind: 'user' })\n const branchesMap = normalizeBranchesMap(e.branches)\n const task: TaskRecord = {\n id,\n title: normalizeTitle(strOr(e, 'title', '')),\n description: strOr(e, 'description', '').trim(),\n prompt: normalizePrompt(strOr(e, 'prompt', '')),\n workspaceId: strOr(e, 'workspaceId', ''),\n urgency: asUrgency(strOr(e, 'urgency', 'normal')),\n status,\n blocked: e.blocked === true,\n execution,\n ...(typeof e.model === 'object' && e.model !== null ? { model: normalizeModel(e.model) } : {}),\n ...(typeof e.isolation === 'string' && (e.isolation === 'worktree' || e.isolation === 'none') ? { isolation: e.isolation } : {}),\n ...(typeof e.presetId === 'string' && e.presetId.trim().length > 0 ? { presetId: e.presetId.trim() } : {}),\n ...(typeof e.permission === 'string' ? { permission: asPermission(e.permission) } : {}),\n ...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),\n ...(typeof e.branch === 'string' ? { branch: e.branch } : {}),\n ...(branchesMap !== undefined ? { branches: branchesMap } : {}),\n ...(status === 'in_progress' && typeof e.claimedBy === 'string' ? { claimedBy: e.claimedBy } : {}),\n ...(status === 'in_progress' && typeof e.claimedAt === 'number' ? { claimedAt: e.claimedAt } : {}),\n version: Math.max(1, Math.trunc(numOr(e, 'version', 1))),\n createdAt: numOr(e, 'createdAt', now),\n updatedAt: numOr(e, 'updatedAt', now),\n createdBy: actorOf(e.createdBy),\n updatedBy: actorOf(e.updatedBy),\n comments,\n executions,\n ...(typeof e.executionsPruned === 'number' ? { executionsPruned: e.executionsPruned } : {}),\n ...(typeof e.trashedAt === 'number' ? { trashedAt: e.trashedAt } : {}),\n }\n if (task.workspaceId.length === 0) return fail('missing workspaceId')\n return { ok: true, task }\n } catch (error) {\n return fail(error instanceof Error ? error.message : String(error))\n }\n}\n\n/**\n * Minimal structural check for ONE ledger record at load time (S11): unlike\n * {@link validateImportedTask} this REBUILDS NOTHING (cron state, ids and\n * timestamps must survive a load untouched) — it only rejects entries whose\n * shape would break downstream consumers, including the R4 id charset.\n * @param raw - the untyped record.\n */\nexport function isPlausibleTaskRecord(raw: unknown): boolean {\n if (typeof raw !== 'object' || raw === null) return false\n const t = raw as Record<string, unknown>\n return typeof t.id === 'string' && isValidTaskId(t.id)\n && typeof t.title === 'string' && t.title.length > 0\n && typeof t.workspaceId === 'string' && t.workspaceId.length > 0\n && ALL_STATUSES.includes(t.status as TaskStatus)\n && typeof t.version === 'number' && Number.isFinite(t.version) && t.version >= 1\n && Array.isArray(t.comments) && Array.isArray(t.executions)\n && typeof t.execution === 'object' && t.execution !== null\n && (t.execution as { mode?: unknown }).mode !== undefined\n}\n\n/**\n * Validate a whole imported ledger and classify its tasks against the live\n * one (pure). Duplicate ids INSIDE the file are invalid (first wins, later\n * copies reported); schemaVersion must match {@link LEDGER_SCHEMA_VERSION}.\n * @param raw - the parsed import file.\n * @param knownIds - live ledger task ids.\n * @param now - current epoch ms.\n * @throws when the file is not a ledger or the schemaVersion is unsupported.\n */\nexport function validateLedgerImport(raw: unknown, knownIds: ReadonlySet<string>, now: number): ImportPlan {\n if (typeof raw !== 'object' || raw === null) throw new Error('导入文件不是 JSON 对象')\n const e = raw as Record<string, unknown>\n if (e.schemaVersion !== LEDGER_SCHEMA_VERSION) {\n throw new Error(`不支持的 schemaVersion ${String(e.schemaVersion)}(当前支持 ${LEDGER_SCHEMA_VERSION})`)\n }\n if (!Array.isArray(e.tasks)) throw new Error('导入文件的 tasks 不是数组')\n const plan: ImportPlan = { create: [], overwrite: [], invalid: [], ...(e.settings !== undefined ? { settings: asBoardSettings(e.settings) } : {}) }\n const seen = new Set<string>()\n for (const entry of e.tasks) {\n const id = typeof (entry as { id?: unknown })?.id === 'string' ? (entry as { id: string }).id : undefined\n const result = validateImportedTask(entry, now)\n if (!result.ok) {\n plan.invalid.push({ ...(id !== undefined ? { id } : {}), reason: result.reason })\n continue\n }\n if (seen.has(result.task.id)) {\n plan.invalid.push({ id: result.task.id, reason: '文件内重复 id' })\n continue\n }\n seen.add(result.task.id)\n if (knownIds.has(result.task.id)) plan.overwrite.push(result.task)\n else plan.create.push(result.task)\n }\n return plan\n}\n\n/**\n * Compact list-projection of a task (token-friendly for `taskboard_list`).\n * @param task - the task.\n */\nexport type TaskSummary = {\n id: string\n title: string\n workspaceId: string\n urgency: Urgency\n status: TaskStatus\n blocked: boolean\n executionMode: ExecutionMode\n nextRunAt?: number\n model?: TaskModel\n permission?: PermissionMode\n version: number\n claimOwner?: string\n commentCount: number\n lastExecutionOutcome?: ExecutionRecord['outcome']\n /** Checklist progress (present only when the task has a checklist). */\n checklist?: { done: number; total: number }\n trashed: boolean\n}\n\n/**\n * Build the compact summary of a task.\n * @param task - the task.\n */\nexport function summarize(task: TaskRecord): TaskSummary {\n const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : undefined\n const checklist = task.checklist !== undefined && task.checklist.length > 0 ? checklistProgress(task) : undefined\n return {\n id: task.id,\n title: task.title,\n workspaceId: task.workspaceId,\n urgency: task.urgency,\n status: task.status,\n blocked: task.blocked,\n executionMode: task.execution.mode,\n nextRunAt: task.execution.nextRunAt,\n model: task.model,\n permission: task.permission,\n version: task.version,\n claimOwner: isClaimedBy(task),\n commentCount: task.comments.length,\n lastExecutionOutcome: last?.outcome,\n ...(checklist !== undefined ? { checklist } : {}),\n trashed: task.trashedAt !== undefined,\n }\n}\n"],"mappings":";;AA+BA,MAAa,gBAAuC;CAClD;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAa,qBAA4C,CAAC,YAAY,UAAU;;AAGhF,MAAa,eAAsC,CAAC,GAAG,eAAe,GAAG,kBAAkB;;;;;AAM3F,MAAM,cAAmE;CACvE,SAAS,CAAC,QAAQ,UAAU;CAC5B,MAAM;EAAC;EAAe;EAAW;CAAU;CAC3C,aAAa;EAAC;EAAa;EAAQ;CAAU;CAC7C,WAAW;EAAC;EAAe;EAAQ;EAAQ;CAAU;CACrD,MAAM,CAAC,UAAU;CACjB,UAAU,CAAC,YAAY,MAAM;CAC7B,UAAU,CAAC;AACb;;;;;;;AAQA,SAAgB,cAAc,MAAkB,IAAyB;CACvE,OAAO,YAAY,KAAK,CAAC,SAAS,EAAE;AACtC;;;;;;AAOA,SAAgB,QAAQ,MAAkB,IAAyB;CACjE,OAAO,SAAS,UAAU,OAAO;AACnC;;AAeA,MAAa,YAAgC;CAAC;CAAU;CAAU;AAAS;;;;;;AA8B3E,MAAa,oBAAmC;;AAGhD,SAAgB,YAAY,KAA4B;CACtD,IAAI,QAAQ,cAAc,QAAQ,QAChC,MAAM,IAAI,MAAM,wCAAwC;CAE1D,OAAO;AACT;;AAGA,SAAgB,mBAAmB,MAAoD;CACrF,OAAO,KAAK,cAAc,KAAA,IAAY,oBAAoB,KAAK;AACjE;;AAWA,MAAa,qBAAqC;;AAMlD,SAAgB,aAAa,KAA8B;CACzD,IAAI,OAAO,QAAQ,UAAU,OAAO;CACpC,MAAM,aAAa,IAAI,KAAK;CAC5B,IAAI,eAAe,qBAAqB,eAAe,kBAAkB,OAAO;CAChF,IAAI,eAAe,eAAe,eAAe,YAAY,OAAO;CACpE,IAAI,eAAe,wBAAwB,eAAe,cAAc,OAAO;CAC/E,MAAM,IAAI,MAAM,4EAA4E;AAC9F;;AAgBA,SAAgB,gBAAgB,KAA6B;CAC3D,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,MAAM,IAAI,MAAM,kCAAkC;CAEpD,MAAM,IAAI;CACV,MAAM,MAAqB,CAAC;CAC5B,IAAI,EAAE,qBAAqB,KAAA,GAAW;EACpC,IAAI,OAAO,EAAE,qBAAqB,UAChC,MAAM,IAAI,MAAM,+CAA+C;EAEjE,IAAI,mBAAmB,YAAY,EAAE,gBAAgB;CACvD;CACA,IAAI,EAAE,yBAAyB,KAAA,GAAW;EACxC,IAAI,OAAO,EAAE,yBAAyB,WACpC,MAAM,IAAI,MAAM,wCAAwC;EAE1D,IAAI,uBAAuB,EAAE;CAC/B;CACA,IAAI,EAAE,sBAAsB,KAAA,GAC1B,IAAI,oBAAoB,aAAa,EAAE,iBAAiB;CAE1D,OAAO;AACT;;AAGA,SAAgB,mBAAmB,UAAyC;CAC1E,OAAO,UAAU,oBAAA;AACnB;;AAGA,SAAgB,8BAA8B,UAAmC;CAC/E,OAAO,UAAU,wBAAwB;AAC3C;;AAGA,SAAgB,oBAAoB,UAA0C;CAC5E,OAAO,UAAU,qBAAA;AACnB;;;;;;;;;AA2BA,SAAgB,UAAU,MAAgC;CACxD,MAAM,SAAS,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK;CACtC,IAAI,OAAO,WAAW,GAAG,OAAO;CAChC,MAAM,SAAmD;EACvD,CAAC,GAAG,EAAE;EACN,CAAC,GAAG,EAAE;EACN,CAAC,GAAG,EAAE;EACN,CAAC,GAAG,EAAE;EACN,CAAC,GAAG,CAAC;CACP;CACA,MAAM,OAA2B,CAAC;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EAC1B,MAAM,CAAC,KAAK,OAAO,OAAO;EAC1B,MAAM,sBAAM,IAAI,IAAY;EAC5B,IAAI,CAAC,eAAe,OAAO,IAAK,KAAK,KAAK,GAAG,GAAG,OAAO;EACvD,KAAK,KAAK,GAAG;CACf;CACA,MAAM,2BAAW,IAAI,IAAY;CACjC,KAAK,MAAM,OAAO,KAAK,IAAK,SAAS,IAAI,QAAQ,IAAI,IAAI,GAAG;CAC5D,OAAO;EAAE,SAAS,KAAK;EAAK,OAAO,KAAK;EAAK,MAAM,KAAK;EAAK,QAAQ,KAAK;EAAK;CAAS;AAC1F;;AAYA,SAAS,eAAe,OAAe,KAAa,KAAa,KAA2B;CAC1F,KAAK,MAAM,QAAQ,MAAM,MAAM,GAAG,GAAG;EACnC,MAAM,CAAC,OAAO,WAAW,KAAK,MAAM,GAAG;EACvC,MAAM,OAAO,YAAY,KAAA,IAAY,IAAI,OAAO,SAAS,SAAS,EAAE;EACpE,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,GAAG,OAAO;EAChD,IAAI;EACJ,IAAI;EACJ,IAAI,UAAU,KAAA,KAAa,UAAU,IAAI,OAAO;EAChD,IAAI,UAAU,KAAK;GACjB,KAAK;GACL,KAAK;EACP,OAAO,IAAI,MAAM,SAAS,GAAG,GAAG;GAC9B,MAAM,CAAC,GAAG,KAAK,MAAM,MAAM,GAAG;GAC9B,KAAK,OAAO,SAAS,KAAK,IAAI,EAAE;GAChC,KAAK,OAAO,SAAS,KAAK,IAAI,EAAE;GAChC,IAAI,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,OAAO,UAAU,EAAE,GAAG,OAAO;EAC7D,OAAO;GACL,KAAK,OAAO,SAAS,OAAO,EAAE;GAC9B,IAAI,CAAC,OAAO,UAAU,EAAE,GAAG,OAAO;GAClC,KAAK,YAAY,KAAA,IAAY,KAAK;EACpC;EACA,IAAI,KAAK,OAAO,KAAK,OAAO,KAAK,IAAI,OAAO;EAC5C,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC;CAChD;CACA,OAAO,IAAI,OAAO;AACpB;;;;;;;;AASA,SAAgB,aAAa,OAAkB,MAA6B;CAE1E,MAAM,QAAQ,IAAI,KAAK,IAAI;CAC3B,MAAM,WAAW,GAAG,CAAC;CACrB,MAAM,WAAW,MAAM,WAAW,IAAI,CAAC;CACvC,MAAM,MAAM,OAAO,IAAI,MAAM,KAAK,KAAK,KAAK;CAC5C,IAAI,IAAI,MAAM,QAAQ;CACtB,OAAO,KAAK,KAAK;EACf,MAAM,IAAI,IAAI,KAAK,CAAC;EACpB,IACE,MAAM,OAAO,IAAI,EAAE,SAAS,IAAI,CAAC,KAC9B,MAAM,KAAK,IAAI,EAAE,QAAQ,CAAC,KAC1B,MAAM,SAAS,IAAI,EAAE,OAAO,CAAC,KAC7B,MAAM,MAAM,IAAI,EAAE,SAAS,CAAC,KAC5B,MAAM,QAAQ,IAAI,EAAE,WAAW,CAAC,GAEnC,OAAO;EAET,KAAK;CACP;CACA,OAAO;AACT;;;;;;;AAgEA,SAAgB,mBAAmB,KAAsB;CACvD,IAAI,QAAQ,IAAI,OAAO;CACvB,IAAI,IAAI,WAAW,KAAK,IAAI,SAAS,KAAK,OAAO;CACjD,IAAI,kBAAkB,KAAK,GAAG,KAAK,IAAI,WAAW,MAAM,KAAK,IAAI,WAAW,GAAG,GAAG,OAAO;CAEzF,OADc,IAAI,MAAM,GACb,CAAC,CAAC,OAAM,MACjB,EAAE,SAAS,KAAK,MAAM,OAAO,MAAM,QAAQ,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC,cAAc,KAAK,CAAC,CAAC;AAC3F;;;;;;;AAgMA,SAAgB,gBAAgB,MAAwB;CACtD,IAAI,KAAK,WAAW,UAAA,IAA0B;CAC9C,MAAM,UAAU,KAAK,WAAW,SAAA;CAChC,KAAK,aAAa,KAAK,WAAW,MAAM,GAAe;CACvD,KAAK,oBAAoB,KAAK,oBAAoB,KAAK;AACzD;;AAgBA,SAAgB,cAA0B;CACxC,OAAO;EAAE,eAAA;EAAsC,UAAU;EAAG,OAAO,CAAC;CAAE;AACxE;;AAOA,SAAS,SAAiB;CACxB,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AAC9C;;;;;;;AAQA,SAAgB,cAAc,IAAqB;CACjD,OAAO,mCAAmC,KAAK,EAAE;AACnD;;AAGA,SAAgB,YAAoB;CAClC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO;AAChD;;AAGA,SAAgB,eAAuB;CACrC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO;AAChD;;AAGA,SAAgB,qBAA6B;CAC3C,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO;AAChD;;AAGA,SAAgB,iBAAyB;CACvC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO;AAChD;;;;;;;AAYA,SAAgB,eAAe,KAAqB;CAClD,MAAM,IAAI,IAAI,KAAK;CACnB,IAAI,EAAE,WAAW,KAAK,EAAE,SAAS,KAC/B,MAAM,IAAI,MAAM,iCAAiC;CAEnD,OAAO;AACT;;;;;AAMA,SAAgB,gBAAgB,KAAiC;CAC/D,MAAM,KAAK,OAAO,GAAA,CAAI,KAAK;CAC3B,IAAI,EAAE,SAAS,KAAM,MAAM,IAAI,MAAM,wCAAwC;CAC7E,OAAO;AACT;;;;;AAMA,SAAgB,cAAc,KAAqB;CACjD,MAAM,IAAI,IAAI,KAAK;CACnB,IAAI,EAAE,WAAW,KAAK,EAAE,SAAS,KAC/B,MAAM,IAAI,MAAM,yCAAyC;CAE3D,OAAO;AACT;;;;;AAMA,SAAgB,UAAU,KAAsB;CAC9C,IAAI,CAAC,UAAU,SAAS,GAAc,GACpC,MAAM,IAAI,MAAM,2BAA2B,UAAU,KAAK,IAAI,GAAG;CAEnE,OAAO;AACT;;;;;AAMA,SAAgB,SAAS,KAAyB;CAChD,IAAI,CAAC,aAAa,SAAS,GAAiB,GAC1C,MAAM,IAAI,MAAM,0BAA0B,aAAa,KAAK,IAAI,GAAG;CAErE,OAAO;AACT;;;;;;;;;AAUA,SAAgB,mBACd,KACA,KACiB;CACjB,MAAM,OAAO,IAAI,QAAQ;CACzB,IAAI,SAAS,WAAW,SAAS,aAC/B,MAAM,IAAI,MAAM,+CAA+C;CAEjE,IAAI,SAAS,SAAS,OAAO,EAAE,KAAK;CACpC,MAAM,QAAQ,IAAI,QAAQ,GAAA,CAAI,KAAK;CACnC,MAAM,QAAQ,UAAU,IAAI;CAC5B,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,uDAAuD;CAC3F,MAAM,OAAO,aAAa,OAAO,GAAG;CACpC,IAAI,SAAS,MAAM,MAAM,IAAI,MAAM,6CAA6C;CAChF,OAAO;EAAE;EAAM;EAAM,WAAW;CAAK;AACvC;;;;;;AAOA,SAAgB,gBAAgB,MAA0B;CACxD,MAAM,OAAO,KAAK;CAClB,MAAM,OAAO,KAAK,YAAY,SAAS,IAAI,GAAG,KAAK,MAAM,KAAK,gBAAgB;CAC9E,OAAO,KAAK,OAAO,SAAS,IAAI,GAAG,KAAK,MAAM,KAAK,WAAW;AAChE;;;;;AAMA,SAAgB,YAAY,MAAsC;CAChE,OAAO,KAAK,WAAW,iBAAiB,KAAK,cAAc,KAAA,IAAY,KAAK,YAAY,KAAA;AAC1F;;;;;;;;;;;;AAaA,SAAgB,UAAU,MAAkB,IAAgB,KAAa,QAAuB;CAC9F,IAAI,OAAO,eAAe;EACxB,OAAO,KAAK;EACZ,OAAO,KAAK;CACd,OAAO,IAAI,WAAW,KAAA,GAAW;EAC/B,KAAK,YAAY;EACjB,KAAK,YAAY;CACnB;AACF;;;;;;;;AASA,SAAgB,yBAAyB,MAA4B;CACnE,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAmB,CAAC;CAC1B,MAAM,QAAQ,QAAiB;EAC7B,IAAI,OAAO,QAAQ,UAAU;GAC3B,MAAM,UAAU,IAAI,KAAK;GACzB,IAAI,QAAQ,SAAS,KAAK,CAAC,KAAK,IAAI,OAAO,GAAG;IAC5C,KAAK,IAAI,OAAO;IAChB,OAAO,KAAK,OAAO;GACrB;EACF;CACF;CAEA,IAAI,MAAM,QAAQ,KAAK,UAAU;OAC1B,MAAM,MAAM,KAAK,YACpB,IAAI,OAAO,QAAQ,OAAO,OAAO,UAC/B,KAAM,GAA+B,SAAS;CAAA;CAIpD,OAAO;AACT;;;;;;;;AASA,SAAgB,eAAe,KAAyB;CACtD,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,MAAM,IAAI,MAAM,mDAAmD;CAErE,MAAM,EAAE,UAAU,OAAO,oBAAoB;CAC7C,IAAI,OAAO,aAAa,YAAY,OAAO,UAAU,UACnD,MAAM,IAAI,MAAM,mDAAmD;CAErE,MAAM,IAAI,SAAS,KAAK;CACxB,MAAM,IAAI,MAAM,KAAK;CACrB,IAAI,EAAE,WAAW,KAAK,EAAE,WAAW,GACjC,MAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,MAAM,OAAO,oBAAoB,YAAY,gBAAgB,KAAK,CAAC,CAAC,SAAS,IAAI,gBAAgB,KAAK,IAAI,KAAA;CAChH,OAAO;EAAE,UAAU;EAAG,OAAO;EAAG,GAAI,QAAQ,KAAA,IAAY,EAAE,iBAAiB,IAAI,IAAI,CAAC;CAAG;AACzF;;;;;;AAiBA,SAAgB,uBAAuB,KAAqB;CAC1D,MAAM,IAAI,IAAI,KAAK;CACnB,IAAI,EAAE,WAAW,KAAK,EAAE,SAAA,KACtB,MAAM,IAAI,MAAM,+CAAiE;CAEnF,OAAO;AACT;;;;;;AAOA,SAAgB,mBAAmB,OAA2C;CAC5E,MAAM,QAAQ,MAAM,KAAI,UAAS;EAAE,IAAI,mBAAmB;EAAG,MAAM,uBAAuB,IAAI;EAAG,SAAS;CAAM,EAAE;CAClH,IAAI,MAAM,SAAA,IACR,MAAM,IAAI,MAAM,qCAAyD;CAE3E,OAAO;AACT;;;;;;;;AASA,SAAgB,mBAAmB,KAA+B;CAChE,IAAI,CAAC,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,4BAA4B;CACrE,IAAI,IAAI,SAAA,IACN,MAAM,IAAI,MAAM,qCAAyD;CAE3E,OAAO,IAAI,KAAK,UAAyB;EACvC,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,MAAM,IAAI,MAAM,kCAAkC;EACnG,MAAM,IAAI;EACV,MAAM,OAAO,uBAAuB,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO,EAAE;EAC5E,MAAM,KAAK,OAAO,EAAE,OAAO,YAAY,EAAE,GAAG,KAAK,CAAC,CAAC,SAAS,IAAI,EAAE,GAAG,KAAK,IAAI,mBAAmB;EACjG,MAAM,UAAU,EAAE,YAAY;EAC9B,MAAM,YAAY,OAAO,EAAE,cAAc,WAAW,EAAE,UAAU,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,IAAI,KAAA;EACvF,MAAM,YAAY,OAAO,EAAE,cAAc,YAAY,OAAO,SAAS,EAAE,SAAS,IAAI,EAAE,YAAY,KAAA;EAClG,MAAM,OAAO,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,KAAK,CAAC,CAAC,SAAS,IAAI,EAAE,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,IAAI,KAAA;EACpG,IAAI,CAAC,SAAS,OAAO;GAAE;GAAI;GAAM,SAAS;EAAM;EAChD,OAAO;GACL;GACA;GACA,SAAS;GACT,GAAI,cAAc,KAAA,KAAa,UAAU,SAAS,IAAI,EAAE,UAAU,IAAI,CAAC;GACvE,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;GAC/C,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACvC;CACF,CAAC;AACH;;AAGA,SAAgB,kBAAkB,MAAsE;CACtG,MAAM,QAAQ,KAAK,aAAa,CAAC;CACjC,OAAO;EAAE,MAAM,MAAM,QAAO,MAAK,EAAE,OAAO,CAAC,CAAC;EAAQ,OAAO,MAAM;CAAO;AAC1E;;AAGA,MAAM,mBAAmB;CAAE,cAAc;CAAI,QAAQ;CAAI,WAAW;AAAG;;AAGvE,MAAM,mBAAmB;;AAGzB,SAAS,oBAAoB,KAAc,OAAgD;CACzF,IAAI,QAAQ,KAAA,GAAW,OAAO,CAAC;CAC/B,IAAI,CAAC,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,UAAU,MAAM,6BAA6B;CACtF,MAAM,MAAM,IAAI,KAAI,UAAS;EAC3B,IAAI,OAAO,UAAU,UAAU,MAAM,IAAI,MAAM,UAAU,MAAM,6BAA6B;EAC5F,MAAM,IAAI,MAAM,KAAK;EACrB,IAAI,EAAE,WAAW,KAAK,EAAE,SAAS,kBAC/B,MAAM,IAAI,MAAM,UAAU,MAAM,sBAAsB,iBAAiB,YAAY;EAErF,OAAO;CACT,CAAC;CACD,IAAI,IAAI,SAAS,iBAAiB,QAChC,MAAM,IAAI,MAAM,UAAU,MAAM,oBAAoB,iBAAiB,OAAO,SAAS;CAEvF,OAAO;AACT;;;;;;AAOA,SAAgB,yBAAyB,KAA+B;CACtE,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,MAAM,IAAI,MAAM,0BAA0B;CACvF,MAAM,IAAI;CACV,MAAM,UAAU,OAAO,EAAE,YAAY,WAAW,EAAE,QAAQ,KAAK,IAAI;CACnE,IAAI,QAAQ,WAAW,KAAK,QAAQ,SAAS,KAC3C,MAAM,IAAI,MAAM,2CAA2C;CAE7D,MAAM,OAAO,OAAO,EAAE,SAAS,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,GAAI,IAAI;CACzE,OAAO;EACL;EACA,cAAc,oBAAoB,EAAE,cAAc,cAAc;EAChE,QAAQ,oBAAoB,EAAE,QAAQ,QAAQ;EAC9C,WAAW,oBAAoB,EAAE,WAAW,WAAW;EACvD;CACF;AACF;;AAmBA,SAAS,MAAM,KAA8B,KAAa,UAA0B;CAClF,MAAM,IAAI,IAAI;CACd,OAAO,OAAO,MAAM,WAAW,IAAI;AACrC;;AAGA,SAAS,MAAM,KAA8B,KAAa,UAA0B;CAClF,MAAM,IAAI,IAAI;CACd,OAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAC3D;;AAGA,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,qBAAqB,KAAkD;CACrF,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO,KAAA;CACpD,MAAM,MAA8B,CAAC;CACrC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAA8B,GAAG;EACzE,IAAI,OAAO,KAAK,GAAG,CAAC,CAAC,UAAA,GAA4B;EACjD,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,WAAW,KAAK,MAAM,SAAS,KAAK;EAClF,IAAI,QAAQ,MAAM,CAAC,mBAAmB,GAAG,GAAG;EAC5C,IAAI,OAAO,MAAM,KAAK;CACxB;CACA,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,SAAS,IAAI,MAAM,KAAA;AAC7C;;;;;;AAOA,SAAgB,sBAAsB,KAAiD;CACrF,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO,KAAA;CACpD,MAAM,IAAI;CACV,IAAI,OAAO,EAAE,SAAS,YAAY,CAAC,mBAAmB,EAAE,IAAI,GAAG,OAAO,KAAA;CACtE,IAAI,OAAO,EAAE,WAAW,YAAY,EAAE,OAAO,WAAW,KAAK,EAAE,OAAO,SAAS,KAAK,OAAO,KAAA;CAC3F,IAAI,OAAO,EAAE,iBAAiB,YAAY,EAAE,aAAa,WAAW,KAAK,EAAE,aAAa,SAAS,KAAM,OAAO,KAAA;CAC9G,MAAM,UAAU,MAAM,QAAQ,EAAE,OAAO,IACnC,EAAE,QAAQ,QAAQ,MAChB,OAAO,MAAM,YAAY,MAAM,QAAQ,OAAQ,EAAiB,SAAS,YACpE,OAAQ,EAAiB,YAAY,QAAQ,CAAC,CAAC,MAAM,GAAG,iBAAiB,IAChF,KAAA;CACJ,MAAM,aAAa,MAAM,QAAQ,EAAE,UAAU,IACzC,EAAE,WAAW,QAAQ,MAAmB,OAAO,MAAM,QAAQ,CAAC,CAAC,MAAM,GAAG,gBAAgB,IACxF,KAAA;CACJ,OAAO;EACL,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,cAAc,EAAE;EAChB,GAAI,OAAO,EAAE,eAAe,WAAW,EAAE,YAAY,EAAE,WAAW,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC;EACrF,GAAI,OAAO,EAAE,eAAe,WAAW,EAAE,YAAY,EAAE,WAAW,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC;EACrF,GAAI,YAAY,KAAA,KAAa,QAAQ,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;EACjE,GAAI,OAAO,EAAE,iBAAiB,YAAY,OAAO,SAAS,EAAE,YAAY,IAAI,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;EAChH,GAAI,eAAe,KAAA,KAAa,WAAW,SAAS,IAAI,EAAE,WAAW,IAAI,CAAC;EAC1E,GAAI,OAAO,EAAE,oBAAoB,YAAY,OAAO,SAAS,EAAE,eAAe,IAAI,EAAE,iBAAiB,EAAE,gBAAgB,IAAI,CAAC;EAC5H,GAAI,OAAO,EAAE,aAAa,WAAW,EAAE,UAAU,EAAE,SAAS,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC;EAC/E,GAAI,OAAO,EAAE,iBAAiB,YAAY,OAAO,SAAS,EAAE,YAAY,IAAI,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;CAClH;AACF;;;;;;;;;;AAWA,SAAgB,qBAAqB,KAAc,KAA6E;CAC9H,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAgB;CACzF,MAAM,IAAI;CACV,MAAM,KAAK,OAAO,EAAE,OAAO,WAAW,EAAE,GAAG,KAAK,IAAI;CACpD,MAAM,QAAQ,YAAmD;EAAE,IAAI;EAAO;CAAO;CAIrF,IAAI,CAAC,cAAc,EAAE,GAAG,OAAO,KAAK,kEAAkE;CACtG,IAAI;EACF,MAAM,YAAY,mBAChB,OAAO,EAAE,cAAc,YAAY,EAAE,cAAc,OAAO,EAAE,YAAgD,CAAC,GAC7G,GACF;EACA,MAAM,WAA4B,CAAC;EACnC,IAAI,MAAM,QAAQ,EAAE,QAAQ,GAC1B,KAAK,MAAM,KAAK,EAAE,UAAU;GAC1B,IAAI,OAAO,MAAM,YAAY,MAAM,MAAM,OAAO,KAAK,uBAAuB;GAC5E,MAAM,KAAK;GACX,MAAM,OAAO,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO;GACrD,IAAI,KAAK,KAAK,CAAC,CAAC,WAAW,KAAK,KAAK,SAAS,KAAM,OAAO,KAAK,sBAAsB;GACtF,SAAS,KAAK;IACZ,IAAI,OAAO,GAAG,OAAO,YAAY,GAAG,GAAG,SAAS,IAAI,GAAG,KAAK,aAAa;IACzE;IACA,SAAS,MAAM,IAAI,WAAW,CAAC;IAC/B,WAAW,MAAM,IAAI,aAAa,GAAG;IACrC,GAAI,OAAO,GAAG,aAAa,WAAW,EAAE,UAAU,GAAG,SAAS,IAAI,CAAC;IACnE,GAAI,OAAO,GAAG,cAAc,YAAY,sBAAsB,KAAK,GAAG,SAAS,KAAK,GAAG,UAAU,UAAU,MACvG;KACE,WAAW,GAAG;KACd,GAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,iBAAiB,QAAQ,CAAC,MAAM,QAAQ,GAAG,YAAY,IACjG,EAAE,cAAc,OAAO,YAAY,OAAO,QAAQ,GAAG,YAAY,CAAC,CAAC,QAAQ,CAAC,KAAK,WAAW,IAAI,UAAU,OAAO,OAAO,UAAU,YAAY,MAAM,UAAU,GAAI,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,EAA4B,IAC5M,CAAC;KACL,GAAI,MAAM,QAAQ,GAAG,UAAU,IAC3B,EAAE,YAAY,GAAG,WAAW,QAAQ,QAAiC,OAAO,QAAQ,YAAY,QAAQ,QACnG,OAAO,IAAI,SAAS,aAAa,IAAI,SAAS,MAAM,mBAAmB,IAAI,IAAI,MAC/E;MAAC;MAAU;MAAQ;KAAQ,CAAC,CAAC,SAAS,IAAI,OAAO,MAChD,IAAI,UAAU,KAAA,KAAa,OAAO,IAAI,UAAU,SAAS,CAAC,CAC/D,MAAM,GAAA,CAAmB,CAAC,CAAC,KAAI,SAAQ;MAAE,MAAM,IAAI;MAAM,SAAS,IAAI;MAAS,GAAI,IAAI,UAAU,KAAA,IAAY,EAAE,OAAO,IAAI,MAAM,MAAM,GAAG,GAAI,EAAE,IAAI,CAAC;KAAG,EAAE,EAAE,IAC5J,CAAC;IACP,IACA,CAAC;GACP,CAAC;EACH;OACK,OAAO,KAAK,2BAA2B;EAC9C,MAAM,aAAgC,CAAC;EACvC,IAAI,MAAM,QAAQ,EAAE,UAAU,GAC5B,KAAK,MAAM,KAAK,EAAE,YAAY;GAC5B,IAAI,OAAO,MAAM,YAAY,MAAM,MAAM,OAAO,KAAK,yBAAyB;GAC9E,MAAM,KAAK;GACX,MAAM,UAAU,GAAG,YAAY,cAAc,cAAc;GAC3D,MAAM,aAAa,GAAG;GACtB,IAAI,eAAe,aAAa,eAAe,eAAe,eAAe,YAAY,eAAe,aACtG,OAAO,KAAK,2BAA2B;GAIzC,MAAM,UAAU,eAAe,YAAY,WAAoB;GAC/D,WAAW,KAAK;IACd,IAAI,OAAO,GAAG,OAAO,YAAY,GAAG,GAAG,SAAS,IAAI,GAAG,KAAK,eAAe;IAC3E,GAAI,OAAO,GAAG,cAAc,WAAW,EAAE,WAAW,GAAG,UAAU,IAAI,CAAC;IACtE;IACA,GAAI,OAAO,GAAG,cAAc,WAAW,EAAE,WAAW,GAAG,UAAU,IAAI,CAAC;IACtE,GAAI,OAAO,GAAG,YAAY,WAAW,EAAE,SAAS,GAAG,QAAQ,IAAI,CAAC;IAChE;IACA,GAAI,eAAe,YAAY,EAAE,OAAO,iFAAiF,IAAK,OAAO,GAAG,UAAU,WAAW,EAAE,OAAO,GAAG,MAAM,IAAI,CAAC;IACpL,GAAI,OAAO,GAAG,cAAc,aAAa,GAAG,cAAc,cAAc,GAAG,cAAc,UAAU,EAAE,WAAW,GAAG,UAAU,IAAI,CAAC;IAClI,GAAI,OAAO,GAAG,kBAAkB,WAAW,EAAE,eAAe,GAAG,cAAc,IAAI,CAAC;IAClF,GAAI,OAAO,GAAG,WAAW,WAAW,EAAE,QAAQ,GAAG,OAAO,IAAI,CAAC;IAC7D,GAAI,OAAO,GAAG,iBAAiB,WAAW,EAAE,cAAc,GAAG,aAAa,IAAI,CAAC;IAC/E,GAAI,OAAO,GAAG,eAAe,WAAW,EAAE,YAAY,GAAG,WAAW,IAAI,CAAC;IACzE,GAAI,OAAO,GAAG,eAAe,WAAW,EAAE,YAAY,GAAG,WAAW,IAAI,CAAC;IACzE,GAAI,MAAM,QAAQ,GAAG,OAAO,IAAI,EAAE,SAAS,GAAG,QAAQ,QAAQ,MAC5D,OAAO,MAAM,YAAY,MAAM,QAAQ,OAAQ,EAAiB,SAAS,YAAY,OAAQ,EAAiB,YAAY,QAAQ,EAAE,IAAI,CAAC;IAC3I,GAAI,OAAO,GAAG,iBAAiB,WAAW,EAAE,cAAc,GAAG,aAAa,IAAI,CAAC;IAC/E,GAAI,MAAM,QAAQ,GAAG,UAAU,IAAI,EAAE,YAAY,GAAG,WAAW,QAAQ,MAAmB,OAAO,MAAM,QAAQ,EAAE,IAAI,CAAC;IACtH,GAAI,OAAO,GAAG,oBAAoB,WAAW,EAAE,iBAAiB,GAAG,gBAAgB,IAAI,CAAC;IACxF,GAAI,OAAO,GAAG,aAAa,WAAW,EAAE,UAAU,GAAG,SAAS,IAAI,CAAC;IACnE,GAAI,OAAO,GAAG,iBAAiB,WAAW,EAAE,cAAc,GAAG,aAAa,IAAI,CAAC;IAC/E,GAAI,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE,OAAO,GAAG,MAAM,MAAM,GAAA,CAAmB,CAAC,CAAC,IAAI,qBAAqB,CAAC,CAAC,QAAQ,MAAkC,MAAM,KAAA,CAAS,EAAE,IAAI,CAAC;IACtK,GAAI,OAAO,GAAG,WAAW,YAAY,GAAG,WAAW,OAAO,EAAE,QAAQ,yBAAyB,GAAG,MAAM,EAAE,IAAI,CAAC;GAC/G,CAAC;EACH;OACK,OAAO,KAAK,6BAA6B;EAChD,MAAM,SAAS,SAAS,MAAM,GAAG,UAAU,MAAM,CAAC;EAClD,MAAM,WAAW,MAAuB,OAAO,MAAM,YAAY,MAAM,QAAS,EAAY,SAAS,WAAW,OAAQ,EAA8B,cAAc,WAChK;GAAE,MAAM;GAAS,WAAY,EAA4B;EAAU,IACnE,EAAE,MAAM,OAAO;EACnB,MAAM,cAAc,qBAAqB,EAAE,QAAQ;EACnD,MAAM,OAAmB;GACvB;GACA,OAAO,eAAe,MAAM,GAAG,SAAS,EAAE,CAAC;GAC3C,aAAa,MAAM,GAAG,eAAe,EAAE,CAAC,CAAC,KAAK;GAC9C,QAAQ,gBAAgB,MAAM,GAAG,UAAU,EAAE,CAAC;GAC9C,aAAa,MAAM,GAAG,eAAe,EAAE;GACvC,SAAS,UAAU,MAAM,GAAG,WAAW,QAAQ,CAAC;GAChD;GACA,SAAS,EAAE,YAAY;GACvB;GACA,GAAI,OAAO,EAAE,UAAU,YAAY,EAAE,UAAU,OAAO,EAAE,OAAO,eAAe,EAAE,KAAK,EAAE,IAAI,CAAC;GAC5F,GAAI,OAAO,EAAE,cAAc,aAAa,EAAE,cAAc,cAAc,EAAE,cAAc,UAAU,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;GAC9H,GAAI,OAAO,EAAE,aAAa,YAAY,EAAE,SAAS,KAAK,CAAC,CAAC,SAAS,IAAI,EAAE,UAAU,EAAE,SAAS,KAAK,EAAE,IAAI,CAAC;GACxG,GAAI,OAAO,EAAE,eAAe,WAAW,EAAE,YAAY,aAAa,EAAE,UAAU,EAAE,IAAI,CAAC;GACrF,GAAI,MAAM,QAAQ,EAAE,SAAS,IAAI,EAAE,WAAW,mBAAmB,EAAE,SAAS,EAAE,IAAI,CAAC;GACnF,GAAI,OAAO,EAAE,WAAW,WAAW,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;GAC3D,GAAI,gBAAgB,KAAA,IAAY,EAAE,UAAU,YAAY,IAAI,CAAC;GAC7D,GAAI,WAAW,iBAAiB,OAAO,EAAE,cAAc,WAAW,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;GAChG,GAAI,WAAW,iBAAiB,OAAO,EAAE,cAAc,WAAW,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;GAChG,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;GACvD,WAAW,MAAM,GAAG,aAAa,GAAG;GACpC,WAAW,MAAM,GAAG,aAAa,GAAG;GACpC,WAAW,QAAQ,EAAE,SAAS;GAC9B,WAAW,QAAQ,EAAE,SAAS;GAC9B;GACA;GACA,GAAI,OAAO,EAAE,qBAAqB,WAAW,EAAE,kBAAkB,EAAE,iBAAiB,IAAI,CAAC;GACzF,GAAI,OAAO,EAAE,cAAc,WAAW,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;EACtE;EACA,IAAI,KAAK,YAAY,WAAW,GAAG,OAAO,KAAK,qBAAqB;EACpE,OAAO;GAAE,IAAI;GAAM;EAAK;CAC1B,SAAS,OAAO;EACd,OAAO,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;CACpE;AACF;;;;;;;;AASA,SAAgB,sBAAsB,KAAuB;CAC3D,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;CACpD,MAAM,IAAI;CACV,OAAO,OAAO,EAAE,OAAO,YAAY,cAAc,EAAE,EAAE,KAChD,OAAO,EAAE,UAAU,YAAY,EAAE,MAAM,SAAS,KAChD,OAAO,EAAE,gBAAgB,YAAY,EAAE,YAAY,SAAS,KAC5D,aAAa,SAAS,EAAE,MAAoB,KAC5C,OAAO,EAAE,YAAY,YAAY,OAAO,SAAS,EAAE,OAAO,KAAK,EAAE,WAAW,KAC5E,MAAM,QAAQ,EAAE,QAAQ,KAAK,MAAM,QAAQ,EAAE,UAAU,KACvD,OAAO,EAAE,cAAc,YAAY,EAAE,cAAc,QAClD,EAAE,UAAiC,SAAS,KAAA;AACpD;;;;;;;;;;AAWA,SAAgB,qBAAqB,KAAc,UAA+B,KAAyB;CACzG,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,MAAM,IAAI,MAAM,gBAAgB;CAC7E,MAAM,IAAI;CACV,IAAI,EAAE,kBAAA,GACJ,MAAM,IAAI,MAAM,sBAAsB,OAAO,EAAE,aAAa,EAAE,SAAgC;CAEhG,IAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,GAAG,MAAM,IAAI,MAAM,kBAAkB;CAC/D,MAAM,OAAmB;EAAE,QAAQ,CAAC;EAAG,WAAW,CAAC;EAAG,SAAS,CAAC;EAAG,GAAI,EAAE,aAAa,KAAA,IAAY,EAAE,UAAU,gBAAgB,EAAE,QAAQ,EAAE,IAAI,CAAC;CAAG;CAClJ,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,SAAS,EAAE,OAAO;EAC3B,MAAM,KAAK,OAAQ,OAA4B,OAAO,WAAY,MAAyB,KAAK,KAAA;EAChG,MAAM,SAAS,qBAAqB,OAAO,GAAG;EAC9C,IAAI,CAAC,OAAO,IAAI;GACd,KAAK,QAAQ,KAAK;IAAE,GAAI,OAAO,KAAA,IAAY,EAAE,GAAG,IAAI,CAAC;IAAI,QAAQ,OAAO;GAAO,CAAC;GAChF;EACF;EACA,IAAI,KAAK,IAAI,OAAO,KAAK,EAAE,GAAG;GAC5B,KAAK,QAAQ,KAAK;IAAE,IAAI,OAAO,KAAK;IAAI,QAAQ;GAAW,CAAC;GAC5D;EACF;EACA,KAAK,IAAI,OAAO,KAAK,EAAE;EACvB,IAAI,SAAS,IAAI,OAAO,KAAK,EAAE,GAAG,KAAK,UAAU,KAAK,OAAO,IAAI;OAC5D,KAAK,OAAO,KAAK,OAAO,IAAI;CACnC;CACA,OAAO;AACT;;;;;AA8BA,SAAgB,UAAU,MAA+B;CACvD,MAAM,OAAO,KAAK,WAAW,SAAS,IAAI,KAAK,WAAW,KAAK,WAAW,SAAS,KAAK,KAAA;CACxF,MAAM,YAAY,KAAK,cAAc,KAAA,KAAa,KAAK,UAAU,SAAS,IAAI,kBAAkB,IAAI,IAAI,KAAA;CACxG,OAAO;EACL,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,aAAa,KAAK;EAClB,SAAS,KAAK;EACd,QAAQ,KAAK;EACb,SAAS,KAAK;EACd,eAAe,KAAK,UAAU;EAC9B,WAAW,KAAK,UAAU;EAC1B,OAAO,KAAK;EACZ,YAAY,KAAK;EACjB,SAAS,KAAK;EACd,YAAY,YAAY,IAAI;EAC5B,cAAc,KAAK,SAAS;EAC5B,sBAAsB,MAAM;EAC5B,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;EAC/C,SAAS,KAAK,cAAc,KAAA;CAC9B;AACF"}
1
+ {"version":3,"file":"protocol.js","names":[],"sources":["../../src/shared/protocol.ts"],"sourcesContent":["/**\n * Task domain model, state machine, urgency classes, and cron math — the\n * framework-free core shared verbatim by the host half (tools, store, routes,\n * scheduler) and, from P2 on, the browser half (board view).\n *\n * Everything here is a pure function over plain data: no imports beyond the\n * standard library, no I/O, no globals. Tests drive it directly.\n *\n * @module dsh-taskboard/shared/protocol\n */\n\n// ---------------------------------------------------------------------------\n// Status vocabulary\n// ---------------------------------------------------------------------------\n\n/**\n * Task lifecycle states. Main board columns render `backlog → todo →\n * in_progress → in_review → done`; `canceled` and `archived` are secondary\n * states collected under an \"other tasks\" tab. `blocked` is NOT a status —\n * it is a horizontal marker any non-terminal state may carry.\n */\nexport type TaskStatus =\n | 'backlog'\n | 'todo'\n | 'in_progress'\n | 'in_review'\n | 'done'\n | 'canceled'\n | 'archived'\n\n/** Statuses shown as the five main board columns, in order. */\nexport const MAIN_STATUSES: readonly TaskStatus[] = [\n 'backlog',\n 'todo',\n 'in_progress',\n 'in_review',\n 'done',\n]\n\n/** Statuses collected under the secondary tab. */\nexport const SECONDARY_STATUSES: readonly TaskStatus[] = ['canceled', 'archived']\n\n/** Every valid status, main first. */\nexport const ALL_STATUSES: readonly TaskStatus[] = [...MAIN_STATUSES, ...SECONDARY_STATUSES]\n\n/**\n * Legal forward/sideways transitions. Anything not listed is rejected with\n * `invalid_transition`. `archived` is terminal.\n */\nconst TRANSITIONS: Readonly<Record<TaskStatus, readonly TaskStatus[]>> = {\n backlog: ['todo', 'canceled'],\n todo: ['in_progress', 'backlog', 'canceled'],\n in_progress: ['in_review', 'todo', 'canceled'],\n in_review: ['in_progress', 'todo', 'done', 'canceled'],\n done: ['archived', 'todo'],\n canceled: ['archived', 'todo'],\n archived: [],\n}\n\n/**\n * Whether a status move is legal per the state machine.\n * @param from - current status.\n * @param to - requested status.\n * @returns true when the transition is allowed.\n */\nexport function canTransition(from: TaskStatus, to: TaskStatus): boolean {\n return TRANSITIONS[from].includes(to)\n}\n\n/**\n * The claim move: the one transition that transfers ownership of a task to\n * the calling session. Guarded by the project (workspace) boundary in the\n * tool layer.\n */\nexport function isClaim(from: TaskStatus, to: TaskStatus): boolean {\n return from === 'todo' && to === 'in_progress'\n}\n\n/** Statuses a `done` move may depart from (user confirmation only). */\nexport function canCompleteFrom(from: TaskStatus): boolean {\n return from === 'in_review'\n}\n\n// ---------------------------------------------------------------------------\n// Urgency\n// ---------------------------------------------------------------------------\n\n/** Urgency classes with fixed UI colors. */\nexport type Urgency = 'urgent' | 'normal' | 'relaxed'\n\n/** All valid urgency values. */\nexport const URGENCIES: readonly Urgency[] = ['urgent', 'normal', 'relaxed']\n\n/** CSS color token per urgency: red / purple / blue. */\nexport const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {\n urgent: '#e5484d',\n normal: '#8e4ec6',\n relaxed: '#3e63dd',\n}\n\n// ---------------------------------------------------------------------------\n// Execution\n// ---------------------------------------------------------------------------\n\n/**\n * Per-task code isolation mode (0.3.0).\n * - `worktree`: each execution runs in a fresh `git worktree` on a dedicated\n * task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.\n * - `none`: run in the workspace directory as before, zero git interaction.\n * Omitted = {@link DEFAULT_ISOLATION}; since 0.5.0 creation materializes the\n * board default onto the record (看板设置 → 执行隔离), and non-git projects\n * still auto-degrade at run time (the execution record carries an\n * `isolationNote` explaining why).\n */\nexport type IsolationMode = 'worktree' | 'none'\n\n/**\n * Factory-default isolation (0.5.0): 原目录执行. Applies when neither the\n * task record nor the board setting (`BoardSettings.defaultIsolation`)\n * says otherwise. Before 0.5.0 the implicit default was 'worktree'.\n */\nexport const DEFAULT_ISOLATION: IsolationMode = 'none'\n\n/** Validate an isolation value. */\nexport function asIsolation(raw: string): IsolationMode {\n if (raw !== 'worktree' && raw !== 'none') {\n throw new Error(\"isolation must be 'worktree' or 'none'\")\n }\n return raw\n}\n\n/** Resolve a task's effective isolation (omitted → the factory default). */\nexport function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {\n return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation\n}\n\n/**\n * Execution permission preset (0.5.5). Matches DSH permission presets:\n * - 'workspace-write': 可写入工作区 (factory default)\n * - 'read-only': 仅可查看\n * - 'danger-full-access': 完全权限\n */\nexport type PermissionMode = 'workspace-write' | 'read-only' | 'danger-full-access'\n\n/** Factory default permission preset (0.5.5). */\nexport const DEFAULT_PERMISSION: PermissionMode = 'workspace-write'\n\n/** Canonical list of supported permission modes. */\nexport const ALL_PERMISSIONS: readonly PermissionMode[] = ['workspace-write', 'read-only', 'danger-full-access']\n\n/** Validate and normalize a permission string into a valid {@link PermissionMode}. */\nexport function asPermission(raw: unknown): PermissionMode {\n if (typeof raw !== 'string') return DEFAULT_PERMISSION\n const normalized = raw.trim()\n if (normalized === 'workspace-write' || normalized === 'workspaceWrite') return 'workspace-write'\n if (normalized === 'read-only' || normalized === 'readOnly') return 'read-only'\n if (normalized === 'danger-full-access' || normalized === 'fullAccess') return 'danger-full-access'\n throw new Error(\"permission must be 'workspace-write', 'read-only', or 'danger-full-access'\")\n}\n\n/**\n * Board-level settings persisted with the ledger (0.5.0). Only fields the\n * user explicitly set are present; absent fields follow factory defaults.\n */\nexport type BoardSettings = {\n /** Default code isolation applied when a NEW task is created without an explicit choice. */\n defaultIsolation?: IsolationMode\n /** Automatically capture external workspace sessions into the taskboard (default: false). */\n syncExternalSessions?: boolean\n /** Default permission preset applied when a NEW task is created without an explicit choice (0.5.5, default: 'workspace-write'). */\n defaultPermission?: PermissionMode\n}\n\n/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */\nexport function asBoardSettings(raw: unknown): BoardSettings {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('board settings must be an object')\n }\n const e = raw as Record<string, unknown>\n const out: BoardSettings = {}\n if (e.defaultIsolation !== undefined) {\n if (typeof e.defaultIsolation !== 'string') {\n throw new Error(\"defaultIsolation must be 'worktree' or 'none'\")\n }\n out.defaultIsolation = asIsolation(e.defaultIsolation)\n }\n if (e.syncExternalSessions !== undefined) {\n if (typeof e.syncExternalSessions !== 'boolean') {\n throw new Error('syncExternalSessions must be a boolean')\n }\n out.syncExternalSessions = e.syncExternalSessions\n }\n if (e.defaultPermission !== undefined) {\n out.defaultPermission = asPermission(e.defaultPermission)\n }\n return out\n}\n\n/** The effective default isolation for NEW tasks (board setting → factory default). */\nexport function defaultIsolationOf(settings?: BoardSettings): IsolationMode {\n return settings?.defaultIsolation ?? DEFAULT_ISOLATION\n}\n\n/** The effective external session sync switch (board setting → factory default false). */\nexport function defaultSyncExternalSessionsOf(settings?: BoardSettings): boolean {\n return settings?.syncExternalSessions ?? false\n}\n\n/** The effective default permission preset for NEW tasks (board setting → factory default 'workspace-write'). */\nexport function defaultPermissionOf(settings?: BoardSettings): PermissionMode {\n return settings?.defaultPermission ?? DEFAULT_PERMISSION\n}\n\n/** How a task may run. */\nexport type ExecutionMode = 'claim' | 'scheduled'\n\n/**\n * Per-task execution configuration. `claim` tasks wait for an in-project\n * session to claim them; `scheduled` tasks run on the host cron scheduler.\n */\nexport interface ExecutionConfig {\n mode: ExecutionMode\n /**\n * Five-field cron expression (minute hour day month weekday). Present on\n * PERIODIC scheduled tasks (定期执行): the scheduler refires the task each\n * time it comes due while the card sits in todo.\n */\n cron?: string\n /**\n * One-shot trigger time (epoch ms). Present on ONE-SHOT scheduled tasks\n * (定时执行): the scheduler fires the task once when due and consumes the\n * field. Mutually exclusive with {@link cron}.\n */\n runAt?: number\n /** Next due time (epoch ms); maintained by the host scheduler. */\n nextRunAt?: number\n /** Last time the scheduler triggered this task (epoch ms). */\n lastTriggeredAt?: number\n}\n\n/**\n * Parse a five-field cron expression. Supported field syntax: star, star/step\n * (`* / n` without spaces), a single number, an `a-b` range, and comma lists\n * of those. Day-of-week accepts both 0 and 7 as Sunday (normalized to 0).\n *\n * @param expr - the expression to parse.\n * @returns the match sets per field, or null when invalid.\n */\nexport function parseCron(expr: string): CronMatch | null {\n const fields = expr.trim().split(/\\s+/)\n if (fields.length !== 5) return null\n const ranges: ReadonlyArray<readonly [number, number]> = [\n [0, 59],\n [0, 23],\n [1, 31],\n [1, 12],\n [0, 7],\n ]\n const sets: Array<Set<number>> = []\n for (let i = 0; i < 5; i++) {\n const [min, max] = ranges[i]!\n const set = new Set<number>()\n if (!parseCronField(fields[i]!, min, max, set)) return null\n sets.push(set)\n }\n const weekdays = new Set<number>()\n for (const day of sets[4]!) weekdays.add(day === 7 ? 0 : day)\n return { minutes: sets[0]!, hours: sets[1]!, days: sets[2]!, months: sets[3]!, weekdays }\n}\n\n/** Parsed cron field match sets. */\nexport type CronMatch = {\n minutes: ReadonlySet<number>\n hours: ReadonlySet<number>\n days: ReadonlySet<number>\n months: ReadonlySet<number>\n weekdays: ReadonlySet<number>\n}\n\n/** Parse one cron field into a match set; false on any syntax error. */\nfunction parseCronField(field: string, min: number, max: number, out: Set<number>): boolean {\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10)\n if (!Number.isInteger(step) || step < 1) return false\n let lo: number\n let hi: number\n if (range === undefined || range === '') return false\n if (range === '*') {\n lo = min\n hi = max\n } else if (range.includes('-')) {\n const [a, b] = range.split('-')\n lo = Number.parseInt(a ?? '', 10)\n hi = Number.parseInt(b ?? '', 10)\n if (!Number.isInteger(lo) || !Number.isInteger(hi)) return false\n } else {\n lo = Number.parseInt(range, 10)\n if (!Number.isInteger(lo)) return false\n hi = stepRaw === undefined ? lo : max\n }\n if (lo < min || hi > max || lo > hi) return false\n for (let v = lo; v <= hi; v += step) out.add(v)\n }\n return out.size > 0\n}\n\n/**\n * The next time at or after `from` matching the cron sets (local time),\n * or null when no match exists within four years (e.g. Feb 30).\n * @param match - parsed cron sets.\n * @param from - epoch ms start point (inclusive match candidate).\n * @returns the next match's epoch ms, or null.\n */\nexport function nextCronTime(match: CronMatch, from: number): number | null {\n // Walk minute by minute from the next whole minute, capped at ~4 years.\n const start = new Date(from)\n start.setSeconds(0, 0)\n start.setMinutes(start.getMinutes() + 1)\n const cap = from + 4 * 366 * 24 * 60 * 60 * 1000\n let t = start.getTime()\n while (t <= cap) {\n const d = new Date(t)\n if (\n match.months.has(d.getMonth() + 1)\n && match.days.has(d.getDate())\n && match.weekdays.has(d.getDay())\n && match.hours.has(d.getHours())\n && match.minutes.has(d.getMinutes())\n ) {\n return t\n }\n t += 60_000\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Records\n// ---------------------------------------------------------------------------\n\n/** Who performed a write. */\nexport type Actor =\n | { kind: 'user' }\n | { kind: 'agent'; sessionId: string }\n | { kind: 'system' }\n\n/** Structured row of a multi-repo merge system comment (0.6.4). */\nexport type SystemCommentRow = {\n /** Repo path relative to the workspace ('' = the workspace root repo). */\n repo: string\n outcome: 'merged' | 'noop' | 'failed'\n /** Failure reason (verbatim) when outcome = 'failed'. */\n error?: string\n}\n\n/** A progress/report comment on a task. */\nexport type CommentRecord = {\n id: string\n /** Comment body (plain text; UI renders as pre-wrapped). */\n body: string\n /** Optimistic-concurrency version of this comment. */\n version: number\n createdAt: number\n /** The session that wrote this comment; absent for user-written ones. */\n threadId?: string\n /**\n * i18n key of a host-generated system message (0.6.4). The GUI localizes it\n * at render time; `body` stays a zh fallback for agent tools / CSV / raw\n * JSON views.\n */\n systemKey?: string\n /** Flat {name} interpolation params for the system message. */\n systemParams?: Record<string, string>\n /** Structured per-repo rows for the multi-repo merge summary (0.6.4). */\n systemRows?: SystemCommentRow[]\n}\n\n/** One commit produced by an isolated execution (hash + subject). */\nexport type CommitInfo = { hash: string; subject: string }\n\n// ---------------------------------------------------------------------------\n// Multi-repo mirror isolation (0.6.3)\n// ---------------------------------------------------------------------------\n\n/**\n * Max repos ONE task mirror may cover (0.6.3): the workspace root repo plus\n * its parallel nested repos. A workspace discovering more degrades the whole\n * worktree run to the original directory (plan §4.1 — predictable, and never\n * a half-built mirror).\n */\nexport const MAX_MIRROR_REPOS = 8\n\n/**\n * Whether `rel` is a legal repo-path key (0.6.3): `''` = the workspace root\n * repo; otherwise a relative forward-slash path with no traversal/absolute\n * shape and no dot segments. These keys ride into filesystem joins\n * (`<workspace>/<rel>`) and ledger maps — the same R4 paranoia as task ids.\n */\nexport function isValidRelRepoPath(rel: string): boolean {\n if (rel === '') return true\n if (rel.length === 0 || rel.length > 300) return false\n if (/^[A-Za-z]:[\\\\/]/.test(rel) || rel.startsWith('\\\\\\\\') || rel.startsWith('/')) return false\n const parts = rel.split('/')\n return parts.every(p =>\n p.length > 0 && p !== '.' && p !== '..' && !p.startsWith('.') && !/[\\\\:*?\"<>|]/.test(p))\n}\n\n/**\n * Per-repo isolation + evidence facts for one execution (0.6.3 multi-repo\n * mirror). Present only when the mirror covers MORE than the workspace root\n * repo alone (or has skipped repos) — a plain single-repo run keeps the\n * legacy flat fields above, untouched, so old ledgers/clients stay blind to\n * this. The root repo (when mirrored) ALSO dual-writes the flat fields.\n */\nexport type ExecutionRepoEvidence = {\n /** Repo path relative to the workspace ('' = the workspace root repo). */\n repo: string\n /** The task branch checked out in this repo's worktree. */\n branch: string\n /** Absolute path of the repo's worktree inside the task mirror. */\n worktreePath: string\n /** HEAD of the task branch before the execution started. */\n baseCommit?: string\n /** HEAD at settlement. */\n headCommit?: string\n /** Commits between baseCommit and headCommit (capped, newest first). */\n commits?: CommitInfo[]\n commitsTotal?: number\n /** Uncommitted changes at settlement (`status --porcelain` lines, capped). */\n dirtyFiles?: string[]\n dirtyFilesTotal?: number\n diffStat?: string\n changedFiles?: number\n}\n\n/**\n * The structured execution report an agent submits at handoff (0.4.0).\n * Commits/dirty/diff facts are host-collected git evidence — the report\n * covers the BUSINESS side the host cannot see.\n */\nexport type ExecutionReport = {\n /** What was done (1..2000 chars, required). */\n summary: string\n /** Files the agent changed (paths, ≤50 × 300 chars). */\n changedFiles: string[]\n /** How the work was self-verified (≤50 × 300 chars). */\n checks: string[]\n /** Produced artifacts worth reviewing (≤30 × 300 chars). */\n artifacts: string[]\n /** Known remaining risks / follow-ups (≤2000 chars, '' allowed). */\n risk: string\n}\n\n/** One Definition-of-Done checklist item (0.4.0). */\nexport type ChecklistItem = {\n id: string\n /** What must be true for acceptance (1..200 chars). */\n text: string\n checked: boolean\n /** Who checked it: an agent session id, or 'user' for GUI toggles. */\n checkedBy?: string\n /** When it was checked (epoch ms). */\n checkedAt?: number\n /** Evidence note attached when checking (≤400 chars). */\n note?: string\n}\n\n/** One execution attempt of a task. */\nexport type ExecutionRecord = {\n id: string\n /** The session this execution ran in; set once the session is really started. */\n sessionId?: string\n /** Effective configuration of a scheduled session; used only for compatible reuse. */\n sessionReuseKey?: string\n /** Trigger: manual button or the host scheduler. */\n trigger: 'manual' | 'scheduled'\n startedAt?: number\n endedAt?: number\n outcome: 'running' | 'succeeded' | 'failed' | 'cancelled'\n error?: string\n /** Code isolation actually used (`none` also covers degraded worktree runs). */\n isolation?: IsolationMode\n /** Why worktree isolation degraded to running in the original directory. */\n isolationNote?: string\n /** The task branch this execution worked on (worktree runs only). */\n branch?: string\n /** Absolute path of the dedicated worktree (worktree runs only). */\n worktreePath?: string\n /** HEAD of the task branch before the execution started. */\n baseCommit?: string\n /** HEAD at settlement. */\n headCommit?: string\n /** Commits between baseCommit and headCommit (hash + subject; capped at 50, newest first). */\n commits?: CommitInfo[]\n /** Total commits before the evidence cap (equals commits.length when under it). */\n commitsTotal?: number\n /** Uncommitted changes present at settlement (`git status --porcelain` lines; capped at 100). */\n dirtyFiles?: string[]\n /** Total uncommitted lines before the evidence cap. */\n dirtyFilesTotal?: number\n /** Aggregate diff stat between baseCommit and headCommit. */\n diffStat?: string\n /** How many files differ between baseCommit and headCommit. */\n changedFiles?: number\n /** Per-repo facts of a multi-repo mirror run (0.6.3; absent on single-repo runs). */\n repos?: ExecutionRepoEvidence[]\n /** The agent's structured report, submitted via taskboard_execution_report. */\n report?: ExecutionReport\n}\n\n/** The per-model override a task may carry; absent = session default model. */\nexport type TaskModel = {\n provider: string\n model: string\n /** Thinking intensity / reasoning effort (optional; e.g. 'low', 'medium', 'high', 'none'). */\n reasoningEffort?: string\n}\n\n/** One task on the board. */\nexport type TaskRecord = {\n id: string\n title: string\n description: string\n /** Extra execution prompt; the execution session receives title+description+prompt. */\n prompt: string\n /** Owning project: a DSH workspace id. */\n workspaceId: string\n urgency: Urgency\n status: TaskStatus\n /** Horizontal marker: work cannot continue right now (any non-terminal status). */\n blocked: boolean\n execution: ExecutionConfig\n model?: TaskModel\n /** Code isolation for executions (omitted = the worktree default; see {@link IsolationMode}). */\n isolation?: IsolationMode\n /**\n * The agent preset execution sessions are composed from (omitted = the\n * deployment default preset). Recorded on the session header and mounted\n * via the presets service at creation — this is what hands the session its\n * tool set. Editable any time (each run composes fresh).\n */\n presetId?: string\n /**\n * Execution permission preset (0.5.5; see {@link PermissionMode}).\n */\n permission?: PermissionMode\n /**\n * Definition-of-Done acceptance checklist (0.4.0). Agents may append items\n * and check/uncheck them (with evidence); the GUI may edit the whole list.\n * Unchecked items highlight at review time; done stays user-only.\n */\n checklist?: ChecklistItem[]\n /**\n * The task branch fixed at the FIRST worktree creation (`task/<标题>+<taskId>`).\n * Renaming the task afterwards never changes it (history preservation).\n * Multi-repo mirror runs (0.6.3): this stays the WORKSPACE ROOT repo's\n * branch; every nested repo pins its own under {@link branches}.\n */\n branch?: string\n /**\n * Per-repo task branches of a multi-repo mirror task (0.6.3): repo path\n * relative to the workspace (never `''` — the root uses {@link branch})\n * → branch name. Pinned at the repo's FIRST successful worktree creation,\n * same rename-proof semantics as {@link branch}.\n */\n branches?: Record<string, string>\n /**\n * The session currently holding the in-progress claim (explicit claim or a\n * live execution). Present only while `status === 'in_progress'`: any move\n * out of in_progress releases it. `updatedBy` is audit-only — user edits no\n * longer erase the holder.\n */\n claimedBy?: string\n /** When the current holder claimed the task (epoch ms). */\n claimedAt?: number\n version: number\n createdAt: number\n updatedAt: number\n createdBy: Actor\n updatedBy: Actor\n comments: CommentRecord[]\n executions: ExecutionRecord[]\n /** How many older execution records were pruned by the retention cap. */\n executionsPruned?: number\n /**\n * Id of the task this card continues (0.7.x periodic execution): when a\n * PERIODIC scheduled task succeeds, the finished card moves to in_review\n * and a fresh todo card carrying the cron is minted to keep the cycle\n * going. Absent on originally created tasks.\n */\n spawnedFrom?: string\n /** Soft-delete marker set by agent `taskboard_delete`; user confirms the purge. */\n trashedAt?: number\n}\n\n/** Retention cap: how many execution records each task keeps (oldest pruned). */\nexport const MAX_EXECUTIONS = 20\n\n/**\n * Enforce the execution-record retention cap on one task (in place): keep the\n * newest {@link MAX_EXECUTIONS} records, count the dropped ones in\n * `executionsPruned`. Running records are always the newest, never dropped.\n * @param task - the task to prune.\n */\nexport function pruneExecutions(task: TaskRecord): void {\n if (task.executions.length <= MAX_EXECUTIONS) return\n const dropped = task.executions.length - MAX_EXECUTIONS\n task.executions = task.executions.slice(-MAX_EXECUTIONS)\n task.executionsPruned = (task.executionsPruned ?? 0) + dropped\n}\n\n/**\n * Mint the successor card of a PERIODIC scheduled task that just succeeded\n * (0.7.x): the finished card goes to in_review for acceptance while this\n * fresh todo card carries the cron onward, keeping the cycle alive. The\n * next run is recomputed from `now` (no compensating catch-up burst).\n * Pure: the caller pushes the returned record into the ledger.\n * @param source - the finished periodic task (still carrying its cron).\n * @param prevExecutionId - id of the execution that just succeeded.\n * @param now - current epoch ms.\n * @returns the successor task record.\n */\nexport function spawnNextCycle(source: TaskRecord, prevExecutionId: string | undefined, now: number): TaskRecord {\n const cron = source.execution.cron\n if (cron === undefined) throw new Error('spawnNextCycle: source task has no cron')\n const match = parseCron(cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) throw new Error('spawnNextCycle: cron has no upcoming match within 4 years')\n return {\n id: newTaskId(),\n title: source.title,\n description: source.description,\n prompt: source.prompt,\n workspaceId: source.workspaceId,\n urgency: source.urgency,\n status: 'todo',\n blocked: false,\n execution: { mode: 'scheduled', cron, nextRunAt: next },\n ...(source.model !== undefined ? { model: structuredClone(source.model) } : {}),\n ...(source.isolation !== undefined ? { isolation: source.isolation } : {}),\n ...(source.presetId !== undefined ? { presetId: source.presetId } : {}),\n ...(source.permission !== undefined ? { permission: source.permission } : {}),\n ...(source.checklist !== undefined\n ? { checklist: source.checklist.map(item => ({ ...item, checked: false, checkedBy: undefined, checkedAt: undefined, note: undefined })) }\n : {}),\n ...(source.branch !== undefined ? { branch: source.branch } : {}),\n ...(source.branches !== undefined ? { branches: { ...source.branches } } : {}),\n spawnedFrom: source.id,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'system' },\n updatedBy: { kind: 'system' },\n comments: [{\n id: newCommentId(),\n body: normalizeBody(`[系统] 定期任务上一轮执行完毕(执行 ${prevExecutionId ?? '未知'}),本卡承接定时继续下一轮;上一轮成果见 ${source.id} 的待验收。`),\n systemKey: 'sys.spawnedFrom',\n systemParams: { sourceId: source.id, executionId: prevExecutionId ?? '' },\n version: 1,\n createdAt: now,\n }],\n executions: [],\n }\n}\n\n/** The whole durable ledger. */\nexport type TaskLedger = {\n schemaVersion: number\n /** Global monotonic revision; every mutation bumps it. */\n revision: number\n tasks: TaskRecord[]\n /** Board-level settings (0.5.0); absent on ledgers never touched by 设置. */\n settings?: BoardSettings\n}\n\n/** Current ledger format version. */\nexport const LEDGER_SCHEMA_VERSION = 1\n\n/** An empty ledger. */\nexport function emptyLedger(): TaskLedger {\n return { schemaVersion: LEDGER_SCHEMA_VERSION, revision: 0, tasks: [] }\n}\n\n// ---------------------------------------------------------------------------\n// ids\n// ---------------------------------------------------------------------------\n\n/** Random base36 suffix. */\nfunction suffix(): string {\n return Math.random().toString(36).slice(2, 8)\n}\n\n/**\n * Legal task id charset (R4): `t-<base36>-<base36>` from {@link newTaskId},\n * and the ONLY shape accepted from the outside (import) or used to build\n * filesystem paths (worktree dirs). Ids ride into `join(ws, '.dsh-worktrees',\n * id)` — a lax charset here is an arbitrary-directory delete primitive.\n */\nexport function isValidTaskId(id: string): boolean {\n return /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/.test(id)\n}\n\n/** Mint a task id. */\nexport function newTaskId(): string {\n return `t-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint a comment id. */\nexport function newCommentId(): string {\n return `c-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint a checklist item id. */\nexport function newChecklistItemId(): string {\n return `k-${Date.now().toString(36)}-${suffix()}`\n}\n\n/** Mint an execution id. */\nexport function newExecutionId(): string {\n return `e-${Date.now().toString(36)}-${suffix()}`\n}\n\n// ---------------------------------------------------------------------------\n// validation helpers (input shaping for tools and routes)\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and normalize a title: trimmed, 1..200 chars.\n * @param raw - the raw input.\n * @returns the normalized title.\n * @throws when empty or too long.\n */\nexport function normalizeTitle(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > 200) {\n throw new Error('title must be 1..200 characters')\n }\n return t\n}\n\n/**\n * Validate a task prompt: trimmed, at most 8000 chars; empty becomes ''.\n * @param raw - the raw input.\n */\nexport function normalizePrompt(raw: string | undefined): string {\n const t = (raw ?? '').trim()\n if (t.length > 8000) throw new Error('prompt must be at most 8000 characters')\n return t\n}\n\n/**\n * Validate and normalize a comment body: trimmed, 1..4000 chars.\n * @param raw - the raw input.\n */\nexport function normalizeBody(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > 4000) {\n throw new Error('comment body must be 1..4000 characters')\n }\n return t\n}\n\n/**\n * Validate an urgency value.\n * @param raw - the raw input.\n */\nexport function asUrgency(raw: string): Urgency {\n if (!URGENCIES.includes(raw as Urgency)) {\n throw new Error(`urgency must be one of: ${URGENCIES.join(', ')}`)\n }\n return raw as Urgency\n}\n\n/**\n * Validate a status value.\n * @param raw - the raw input.\n */\nexport function asStatus(raw: string): TaskStatus {\n if (!ALL_STATUSES.includes(raw as TaskStatus)) {\n throw new Error(`status must be one of: ${ALL_STATUSES.join(', ')}`)\n }\n return raw as TaskStatus\n}\n\n/**\n * Parse a raw runAt input: epoch ms number or ISO date string → epoch ms.\n * @param raw - untyped runAt value.\n * @returns the epoch ms, or undefined when absent.\n */\nfunction normalizeRunAt(raw: unknown): number | undefined {\n if (raw === undefined || raw === null) return undefined\n if (typeof raw === 'number' && Number.isFinite(raw)) return Math.trunc(raw)\n if (typeof raw === 'string') {\n const t = Date.parse(raw)\n if (Number.isNaN(t)) throw new Error('execution.runAt is not a valid time (epoch ms or ISO string)')\n return t\n }\n throw new Error('execution.runAt must be an epoch ms number or an ISO date string')\n}\n\n/**\n * Validate an execution config request from raw tool/route input.\n * `scheduled` requires either a valid cron (periodic, 定期执行) or a runAt\n * instant (one-shot, 定时执行); the two are mutually exclusive. A cron's\n * first `nextRunAt` is computed from `now`.\n * @param raw - raw execution input ({@link ExecutionConfig} fields, untyped).\n * @param now - current epoch ms.\n * @param opts - `allowPastRunAt` lets the import path keep a historical\n * one-shot instant instead of rejecting it.\n * @returns the normalized config.\n */\nexport function normalizeExecution(\n raw: { mode?: string; cron?: string; runAt?: unknown },\n now: number,\n opts?: { allowPastRunAt?: boolean },\n): ExecutionConfig {\n const mode = raw.mode ?? 'claim'\n if (mode !== 'claim' && mode !== 'scheduled') {\n throw new Error(\"execution.mode must be 'claim' or 'scheduled'\")\n }\n if (mode === 'claim') return { mode }\n const runAt = normalizeRunAt(raw.runAt)\n const cron = (raw.cron ?? '').trim()\n if (cron.length > 0 && runAt !== undefined) {\n throw new Error('execution: cron and runAt are mutually exclusive (periodic vs one-shot)')\n }\n if (runAt !== undefined) {\n if (!opts?.allowPastRunAt && runAt <= now) throw new Error('execution.runAt must be in the future')\n return { mode, runAt, nextRunAt: runAt }\n }\n const match = parseCron(cron)\n if (match === null) throw new Error('execution.cron is not a valid 5-field cron expression')\n const next = nextCronTime(match, now)\n if (next === null) throw new Error('execution.cron never matches within 4 years')\n return { mode, cron, nextRunAt: next }\n}\n\n/**\n * The effective prompt of a task: title+description, with the explicit\n * prompt appended when set — title+description+prompt.\n * @param task - the task.\n */\nexport function effectivePrompt(task: TaskRecord): string {\n const head = task.title\n const body = task.description.length > 0 ? `${head}\\n\\n${task.description}` : head\n return task.prompt.length > 0 ? `${body}\\n\\n${task.prompt}` : body\n}\n\n/**\n * Whether the task is currently claimed by a session (running state).\n * @param task - the task.\n */\nexport function isClaimedBy(task: TaskRecord): string | undefined {\n return task.status === 'in_progress' && task.claimedBy !== undefined ? task.claimedBy : undefined\n}\n\n/**\n * Maintain the explicit claim fields around a status change: entering\n * in_progress under a session records the holder (an execution-start or an\n * agent claim); every move out of in_progress releases the claim (handoff,\n * give-back, cancel). A user-driven move into in_progress records no holder —\n * no session works on it yet.\n * @param task - the task being written (mutated in place).\n * @param to - the target status.\n * @param now - current epoch ms.\n * @param holder - the session id claiming the task, when applicable.\n */\nexport function syncClaim(task: TaskRecord, to: TaskStatus, now: number, holder?: string): void {\n if (to !== 'in_progress') {\n delete task.claimedBy\n delete task.claimedAt\n } else if (holder !== undefined) {\n task.claimedBy = holder\n task.claimedAt = now\n }\n}\n\n/**\n * Collect unique execution session IDs associated with a task:\n * - executions with a non-empty `sessionId`\n * Creator and claim sessions may serve other tasks and are never included.\n * @param task - the task record to inspect.\n * @returns an array of distinct session IDs in stable discovery order.\n */\nexport function taskAssociatedSessionIds(task: TaskRecord): string[] {\n const seen = new Set<string>()\n const result: string[] = []\n const push = (raw: unknown) => {\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (trimmed.length > 0 && !seen.has(trimmed)) {\n seen.add(trimmed)\n result.push(trimmed)\n }\n }\n }\n\n if (Array.isArray(task.executions)) {\n for (const ex of task.executions) {\n if (ex !== null && typeof ex === 'object') {\n push((ex as { sessionId?: unknown }).sessionId)\n }\n }\n }\n return result\n}\n\n/**\n * Validate and normalize a pinned model: `{ provider, model, reasoningEffort? }`,\n * provider and model must be non-empty trimmed strings.\n * @param raw - the raw input.\n * @returns the normalized model.\n * @throws when the shape or the fields are invalid.\n */\nexport function normalizeModel(raw: unknown): TaskModel {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('model must be { provider: string, model: string }')\n }\n const { provider, model, reasoningEffort } = raw as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }\n if (typeof provider !== 'string' || typeof model !== 'string') {\n throw new Error('model must be { provider: string, model: string }')\n }\n const p = provider.trim()\n const m = model.trim()\n if (p.length === 0 || m.length === 0) {\n throw new Error('model.provider and model.model must be non-empty strings')\n }\n const eff = typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0 ? reasoningEffort.trim() : undefined\n return { provider: p, model: m, ...(eff !== undefined ? { reasoningEffort: eff } : {}) }\n}\n\n// ---------------------------------------------------------------------------\n// checklist + report validation (0.4.0)\n// ---------------------------------------------------------------------------\n\n/** Checklist size cap per task. */\nexport const MAX_CHECKLIST_ITEMS = 30\n\n/** Checklist item text cap (chars). */\nexport const MAX_CHECKLIST_TEXT = 200\n\n/**\n * Validate and normalize one checklist text line: trimmed, 1..200 chars.\n * @param raw - the raw text.\n * @throws when empty or too long.\n */\nexport function normalizeChecklistText(raw: string): string {\n const t = raw.trim()\n if (t.length === 0 || t.length > MAX_CHECKLIST_TEXT) {\n throw new Error(`checklist item text must be 1..${MAX_CHECKLIST_TEXT} characters`)\n }\n return t\n}\n\n/**\n * Build a fresh unchecked checklist from plain text lines (create route /\n * templates / tool adds).\n * @param texts - the item texts (validated individually).\n */\nexport function checklistFromTexts(texts: readonly string[]): ChecklistItem[] {\n const items = texts.map(text => ({ id: newChecklistItemId(), text: normalizeChecklistText(text), checked: false }))\n if (items.length > MAX_CHECKLIST_ITEMS) {\n throw new Error(`checklist may hold at most ${MAX_CHECKLIST_ITEMS} items`)\n }\n return items\n}\n\n/**\n * Validate and normalize a full checklist array (GUI update route, import):\n * missing ids are minted, text is checked, checked flags must be booleans,\n * checkedBy/checkedAt are kept only on checked items.\n * @param raw - untyped array from the wire.\n * @throws with a readable reason on any invalid entry.\n */\nexport function normalizeChecklist(raw: unknown): ChecklistItem[] {\n if (!Array.isArray(raw)) throw new Error('checklist must be an array')\n if (raw.length > MAX_CHECKLIST_ITEMS) {\n throw new Error(`checklist may hold at most ${MAX_CHECKLIST_ITEMS} items`)\n }\n return raw.map((entry): ChecklistItem => {\n if (typeof entry !== 'object' || entry === null) throw new Error('checklist item must be an object')\n const e = entry as Record<string, unknown>\n const text = normalizeChecklistText(typeof e.text === 'string' ? e.text : '')\n const id = typeof e.id === 'string' && e.id.trim().length > 0 ? e.id.trim() : newChecklistItemId()\n const checked = e.checked === true\n const checkedBy = typeof e.checkedBy === 'string' ? e.checkedBy.trim().slice(0, 100) : undefined\n const checkedAt = typeof e.checkedAt === 'number' && Number.isFinite(e.checkedAt) ? e.checkedAt : undefined\n const note = typeof e.note === 'string' && e.note.trim().length > 0 ? e.note.trim().slice(0, 400) : undefined\n if (!checked) return { id, text, checked: false }\n return {\n id,\n text,\n checked: true,\n ...(checkedBy !== undefined && checkedBy.length > 0 ? { checkedBy } : {}),\n ...(checkedAt !== undefined ? { checkedAt } : {}),\n ...(note !== undefined ? { note } : {}),\n }\n })\n}\n\n/** Checklist progress: how many items are checked (absent checklist → 0/0). */\nexport function checklistProgress(task: Pick<TaskRecord, 'checklist'>): { done: number; total: number } {\n const items = task.checklist ?? []\n return { done: items.filter(i => i.checked).length, total: items.length }\n}\n\n/** Report string-list caps. */\nconst REPORT_LIST_CAPS = { changedFiles: 50, checks: 50, artifacts: 30 } as const\n\n/** Per-entry cap for report lists (chars). */\nconst REPORT_ENTRY_MAX = 300\n\n/** Validate one report string list: strings trimmed 1..300 chars. */\nfunction normalizeReportList(raw: unknown, field: keyof typeof REPORT_LIST_CAPS): string[] {\n if (raw === undefined) return []\n if (!Array.isArray(raw)) throw new Error(`report.${field} must be an array of strings`)\n const out = raw.map(entry => {\n if (typeof entry !== 'string') throw new Error(`report.${field} must be an array of strings`)\n const t = entry.trim()\n if (t.length === 0 || t.length > REPORT_ENTRY_MAX) {\n throw new Error(`report.${field} entries must be 1..${REPORT_ENTRY_MAX} characters`)\n }\n return t\n })\n if (out.length > REPORT_LIST_CAPS[field]) {\n throw new Error(`report.${field} may hold at most ${REPORT_LIST_CAPS[field]} entries`)\n }\n return out\n}\n\n/**\n * Validate and normalize a structured execution report.\n * @param raw - untyped tool/route input.\n * @throws with a readable reason on any invalid field.\n */\nexport function normalizeExecutionReport(raw: unknown): ExecutionReport {\n if (typeof raw !== 'object' || raw === null) throw new Error('report must be an object')\n const e = raw as Record<string, unknown>\n const summary = typeof e.summary === 'string' ? e.summary.trim() : ''\n if (summary.length === 0 || summary.length > 2000) {\n throw new Error('report.summary must be 1..2000 characters')\n }\n const risk = typeof e.risk === 'string' ? e.risk.trim().slice(0, 2000) : ''\n return {\n summary,\n changedFiles: normalizeReportList(e.changedFiles, 'changedFiles'),\n checks: normalizeReportList(e.checks, 'checks'),\n artifacts: normalizeReportList(e.artifacts, 'artifacts'),\n risk,\n }\n}\n\n// ---------------------------------------------------------------------------\n// ledger import validation (0.4.0)\n// ---------------------------------------------------------------------------\n\n/** Result classifying every task in an import file against the live ledger. */\nexport type ImportPlan = {\n /** Structurally valid tasks whose ids are new (merge adds them). */\n create: TaskRecord[]\n /** Structurally valid tasks whose ids already exist (merge replaces them). */\n overwrite: TaskRecord[]\n /** Invalid entries with a human-readable reason (never imported). */\n invalid: Array<{ id?: string; reason: string }>\n /** The file's board settings (0.5.0); replace-mode swaps them, merge keeps the live ones. */\n settings?: BoardSettings\n}\n\n/** One unknown-value read helper: string fields with defaults. */\nfunction strOr(raw: Record<string, unknown>, key: string, fallback: string): string {\n const v = raw[key]\n return typeof v === 'string' ? v : fallback\n}\n\n/** One unknown-value read helper: finite numbers with defaults. */\nfunction numOr(raw: Record<string, unknown>, key: string, fallback: number): number {\n const v = raw[key]\n return typeof v === 'number' && Number.isFinite(v) ? v : fallback\n}\n\n/** Evidence caps for imported per-repo facts (aligns the host's collect caps). */\nconst IMPORT_COMMIT_CAP = 50\nconst IMPORT_DIRTY_CAP = 100\n\n/**\n * Sanitize an imported per-task branches map (0.6.3): legal repo keys only,\n * non-empty branch strings, capped at {@link MAX_MIRROR_REPOS} entries.\n * @returns undefined when nothing legal remains.\n */\nexport function normalizeBranchesMap(raw: unknown): Record<string, string> | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (Object.keys(out).length >= MAX_MIRROR_REPOS) break\n if (typeof value !== 'string' || value.trim().length === 0 || value.length > 200) continue\n if (key === '' || !isValidRelRepoPath(key)) continue\n out[key] = value.trim()\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Sanitize ONE imported per-repo evidence entry (0.6.3): rebuilds the record\n * field by field, capping evidence arrays like the host's own collection.\n * @returns undefined for a structurally illegal entry (dropped, not fatal).\n */\nexport function normalizeRepoEvidence(raw: unknown): ExecutionRepoEvidence | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined\n const e = raw as Record<string, unknown>\n if (typeof e.repo !== 'string' || !isValidRelRepoPath(e.repo)) return undefined\n if (typeof e.branch !== 'string' || e.branch.length === 0 || e.branch.length > 200) return undefined\n if (typeof e.worktreePath !== 'string' || e.worktreePath.length === 0 || e.worktreePath.length > 1000) return undefined\n const commits = Array.isArray(e.commits)\n ? e.commits.filter((c): c is CommitInfo =>\n typeof c === 'object' && c !== null && typeof (c as CommitInfo).hash === 'string'\n && typeof (c as CommitInfo).subject === 'string').slice(0, IMPORT_COMMIT_CAP)\n : undefined\n const dirtyFiles = Array.isArray(e.dirtyFiles)\n ? e.dirtyFiles.filter((l): l is string => typeof l === 'string').slice(0, IMPORT_DIRTY_CAP)\n : undefined\n return {\n repo: e.repo,\n branch: e.branch,\n worktreePath: e.worktreePath,\n ...(typeof e.baseCommit === 'string' ? { baseCommit: e.baseCommit.slice(0, 100) } : {}),\n ...(typeof e.headCommit === 'string' ? { headCommit: e.headCommit.slice(0, 100) } : {}),\n ...(commits !== undefined && commits.length > 0 ? { commits } : {}),\n ...(typeof e.commitsTotal === 'number' && Number.isFinite(e.commitsTotal) ? { commitsTotal: e.commitsTotal } : {}),\n ...(dirtyFiles !== undefined && dirtyFiles.length > 0 ? { dirtyFiles } : {}),\n ...(typeof e.dirtyFilesTotal === 'number' && Number.isFinite(e.dirtyFilesTotal) ? { dirtyFilesTotal: e.dirtyFilesTotal } : {}),\n ...(typeof e.diffStat === 'string' ? { diffStat: e.diffStat.slice(0, 500) } : {}),\n ...(typeof e.changedFiles === 'number' && Number.isFinite(e.changedFiles) ? { changedFiles: e.changedFiles } : {}),\n }\n}\n\n/**\n * Validate ONE imported task record (pure): rebuilds it field by field with\n * the normal validators, minting missing ids and re-arming cron. Executions\n * left `running` by the exporting machine are marked failed — their\n * settlement watchers died there and can never settle here.\n * @param raw - the untyped record.\n * @param now - current epoch ms (defaults for timestamps).\n * @returns the rebuilt record, or a rejection reason.\n */\nexport function validateImportedTask(raw: unknown, now: number): { ok: true; task: TaskRecord } | { ok: false; reason: string } {\n if (typeof raw !== 'object' || raw === null) return { ok: false, reason: 'not an object' }\n const e = raw as Record<string, unknown>\n const id = typeof e.id === 'string' ? e.id.trim() : ''\n const fail = (reason: string): { ok: false; reason: string } => ({ ok: false, reason })\n // R4①: length alone let traversal-shaped ids (`../../x`, `..\\..\\x`) into\n // the ledger; the charset gate is the primary defense for every downstream\n // filesystem use of a task id.\n if (!isValidTaskId(id)) return fail('missing/invalid id (must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$)')\n try {\n const execution = normalizeExecution(\n typeof e.execution === 'object' && e.execution !== null ? e.execution as { mode?: string; cron?: string; runAt?: unknown } : {},\n now,\n { allowPastRunAt: true },\n )\n const comments: CommentRecord[] = []\n if (Array.isArray(e.comments)) {\n for (const c of e.comments) {\n if (typeof c !== 'object' || c === null) return fail('invalid comment entry')\n const ce = c as Record<string, unknown>\n const body = typeof ce.body === 'string' ? ce.body : ''\n if (body.trim().length === 0 || body.length > 4000) return fail('invalid comment body')\n comments.push({\n id: typeof ce.id === 'string' && ce.id.length > 0 ? ce.id : newCommentId(),\n body,\n version: numOr(ce, 'version', 1),\n createdAt: numOr(ce, 'createdAt', now),\n ...(typeof ce.threadId === 'string' ? { threadId: ce.threadId } : {}),\n ...(typeof ce.systemKey === 'string' && /^sys\\.[A-Za-z0-9]+$/.test(ce.systemKey) && ce.systemKey.length <= 100\n ? {\n systemKey: ce.systemKey,\n ...(typeof ce.systemParams === 'object' && ce.systemParams !== null && !Array.isArray(ce.systemParams)\n ? { systemParams: Object.fromEntries(Object.entries(ce.systemParams).filter(([key, value]) => key.length <= 100 && typeof value === 'string' && value.length <= 4000).slice(0, 20)) as Record<string, string> }\n : {}),\n ...(Array.isArray(ce.systemRows)\n ? { systemRows: ce.systemRows.filter((row): row is SystemCommentRow => typeof row === 'object' && row !== null\n && typeof row.repo === 'string' && (row.repo === '' || isValidRelRepoPath(row.repo))\n && ['merged', 'noop', 'failed'].includes(row.outcome)\n && (row.error === undefined || typeof row.error === 'string'))\n .slice(0, MAX_MIRROR_REPOS).map(row => ({ repo: row.repo, outcome: row.outcome, ...(row.error !== undefined ? { error: row.error.slice(0, 4000) } : {}) })) }\n : {}),\n }\n : {}),\n })\n }\n } else return fail('comments must be an array')\n const executions: ExecutionRecord[] = []\n if (Array.isArray(e.executions)) {\n for (const x of e.executions) {\n if (typeof x !== 'object' || x === null) return fail('invalid execution entry')\n const xe = x as Record<string, unknown>\n const trigger = xe.trigger === 'scheduled' ? 'scheduled' : 'manual'\n const outcomeRaw = xe.outcome\n if (outcomeRaw !== 'running' && outcomeRaw !== 'succeeded' && outcomeRaw !== 'failed' && outcomeRaw !== 'cancelled') {\n return fail('invalid execution outcome')\n }\n // A running execution from the exporting machine can never settle\n // here — import it as failed with the reason recorded.\n const outcome = outcomeRaw === 'running' ? 'failed' as const : outcomeRaw\n executions.push({\n id: typeof xe.id === 'string' && xe.id.length > 0 ? xe.id : newExecutionId(),\n ...(typeof xe.sessionId === 'string' ? { sessionId: xe.sessionId } : {}),\n trigger,\n ...(typeof xe.startedAt === 'number' ? { startedAt: xe.startedAt } : {}),\n ...(typeof xe.endedAt === 'number' ? { endedAt: xe.endedAt } : {}),\n outcome,\n ...(outcomeRaw === 'running' ? { error: 'imported while still running (settlement watcher died with the exporting host)' } : (typeof xe.error === 'string' ? { error: xe.error } : {})),\n ...(typeof xe.isolation === 'string' && (xe.isolation === 'worktree' || xe.isolation === 'none') ? { isolation: xe.isolation } : {}),\n ...(typeof xe.isolationNote === 'string' ? { isolationNote: xe.isolationNote } : {}),\n ...(typeof xe.branch === 'string' ? { branch: xe.branch } : {}),\n ...(typeof xe.worktreePath === 'string' ? { worktreePath: xe.worktreePath } : {}),\n ...(typeof xe.baseCommit === 'string' ? { baseCommit: xe.baseCommit } : {}),\n ...(typeof xe.headCommit === 'string' ? { headCommit: xe.headCommit } : {}),\n ...(Array.isArray(xe.commits) ? { commits: xe.commits.filter((c): c is CommitInfo =>\n typeof c === 'object' && c !== null && typeof (c as CommitInfo).hash === 'string' && typeof (c as CommitInfo).subject === 'string') } : {}),\n ...(typeof xe.commitsTotal === 'number' ? { commitsTotal: xe.commitsTotal } : {}),\n ...(Array.isArray(xe.dirtyFiles) ? { dirtyFiles: xe.dirtyFiles.filter((l): l is string => typeof l === 'string') } : {}),\n ...(typeof xe.dirtyFilesTotal === 'number' ? { dirtyFilesTotal: xe.dirtyFilesTotal } : {}),\n ...(typeof xe.diffStat === 'string' ? { diffStat: xe.diffStat } : {}),\n ...(typeof xe.changedFiles === 'number' ? { changedFiles: xe.changedFiles } : {}),\n ...(Array.isArray(xe.repos) ? { repos: xe.repos.slice(0, MAX_MIRROR_REPOS).map(normalizeRepoEvidence).filter((r): r is ExecutionRepoEvidence => r !== undefined) } : {}),\n ...(typeof xe.report === 'object' && xe.report !== null ? { report: normalizeExecutionReport(xe.report) } : {}),\n })\n }\n } else return fail('executions must be an array')\n const status = asStatus(strOr(e, 'status', 'todo'))\n const actorOf = (v: unknown): Actor => (typeof v === 'object' && v !== null && (v as Actor).kind === 'agent' && typeof (v as { sessionId?: unknown }).sessionId === 'string'\n ? { kind: 'agent', sessionId: (v as { sessionId: string }).sessionId }\n : { kind: 'user' })\n const branchesMap = normalizeBranchesMap(e.branches)\n const task: TaskRecord = {\n id,\n title: normalizeTitle(strOr(e, 'title', '')),\n description: strOr(e, 'description', '').trim(),\n prompt: normalizePrompt(strOr(e, 'prompt', '')),\n workspaceId: strOr(e, 'workspaceId', ''),\n urgency: asUrgency(strOr(e, 'urgency', 'normal')),\n status,\n blocked: e.blocked === true,\n execution,\n ...(typeof e.model === 'object' && e.model !== null ? { model: normalizeModel(e.model) } : {}),\n ...(typeof e.isolation === 'string' && (e.isolation === 'worktree' || e.isolation === 'none') ? { isolation: e.isolation } : {}),\n ...(typeof e.presetId === 'string' && e.presetId.trim().length > 0 ? { presetId: e.presetId.trim() } : {}),\n ...(typeof e.permission === 'string' ? { permission: asPermission(e.permission) } : {}),\n ...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),\n ...(typeof e.branch === 'string' ? { branch: e.branch } : {}),\n ...(branchesMap !== undefined ? { branches: branchesMap } : {}),\n ...(status === 'in_progress' && typeof e.claimedBy === 'string' ? { claimedBy: e.claimedBy } : {}),\n ...(status === 'in_progress' && typeof e.claimedAt === 'number' ? { claimedAt: e.claimedAt } : {}),\n version: Math.max(1, Math.trunc(numOr(e, 'version', 1))),\n createdAt: numOr(e, 'createdAt', now),\n updatedAt: numOr(e, 'updatedAt', now),\n createdBy: actorOf(e.createdBy),\n updatedBy: actorOf(e.updatedBy),\n comments,\n executions,\n ...(typeof e.executionsPruned === 'number' ? { executionsPruned: e.executionsPruned } : {}),\n ...(typeof e.trashedAt === 'number' ? { trashedAt: e.trashedAt } : {}),\n }\n if (task.workspaceId.length === 0) return fail('missing workspaceId')\n return { ok: true, task }\n } catch (error) {\n return fail(error instanceof Error ? error.message : String(error))\n }\n}\n\n/**\n * Minimal structural check for ONE ledger record at load time (S11): unlike\n * {@link validateImportedTask} this REBUILDS NOTHING (cron state, ids and\n * timestamps must survive a load untouched) — it only rejects entries whose\n * shape would break downstream consumers, including the R4 id charset.\n * @param raw - the untyped record.\n */\nexport function isPlausibleTaskRecord(raw: unknown): boolean {\n if (typeof raw !== 'object' || raw === null) return false\n const t = raw as Record<string, unknown>\n return typeof t.id === 'string' && isValidTaskId(t.id)\n && typeof t.title === 'string' && t.title.length > 0\n && typeof t.workspaceId === 'string' && t.workspaceId.length > 0\n && ALL_STATUSES.includes(t.status as TaskStatus)\n && typeof t.version === 'number' && Number.isFinite(t.version) && t.version >= 1\n && Array.isArray(t.comments) && Array.isArray(t.executions)\n && typeof t.execution === 'object' && t.execution !== null\n && (t.execution as { mode?: unknown }).mode !== undefined\n}\n\n/**\n * Validate a whole imported ledger and classify its tasks against the live\n * one (pure). Duplicate ids INSIDE the file are invalid (first wins, later\n * copies reported); schemaVersion must match {@link LEDGER_SCHEMA_VERSION}.\n * @param raw - the parsed import file.\n * @param knownIds - live ledger task ids.\n * @param now - current epoch ms.\n * @throws when the file is not a ledger or the schemaVersion is unsupported.\n */\nexport function validateLedgerImport(raw: unknown, knownIds: ReadonlySet<string>, now: number): ImportPlan {\n if (typeof raw !== 'object' || raw === null) throw new Error('导入文件不是 JSON 对象')\n const e = raw as Record<string, unknown>\n if (e.schemaVersion !== LEDGER_SCHEMA_VERSION) {\n throw new Error(`不支持的 schemaVersion ${String(e.schemaVersion)}(当前支持 ${LEDGER_SCHEMA_VERSION})`)\n }\n if (!Array.isArray(e.tasks)) throw new Error('导入文件的 tasks 不是数组')\n const plan: ImportPlan = { create: [], overwrite: [], invalid: [], ...(e.settings !== undefined ? { settings: asBoardSettings(e.settings) } : {}) }\n const seen = new Set<string>()\n for (const entry of e.tasks) {\n const id = typeof (entry as { id?: unknown })?.id === 'string' ? (entry as { id: string }).id : undefined\n const result = validateImportedTask(entry, now)\n if (!result.ok) {\n plan.invalid.push({ ...(id !== undefined ? { id } : {}), reason: result.reason })\n continue\n }\n if (seen.has(result.task.id)) {\n plan.invalid.push({ id: result.task.id, reason: '文件内重复 id' })\n continue\n }\n seen.add(result.task.id)\n if (knownIds.has(result.task.id)) plan.overwrite.push(result.task)\n else plan.create.push(result.task)\n }\n return plan\n}\n\n/**\n * Compact list-projection of a task (token-friendly for `taskboard_list`).\n * @param task - the task.\n */\nexport type TaskSummary = {\n id: string\n title: string\n workspaceId: string\n urgency: Urgency\n status: TaskStatus\n blocked: boolean\n executionMode: ExecutionMode\n nextRunAt?: number\n model?: TaskModel\n permission?: PermissionMode\n version: number\n claimOwner?: string\n commentCount: number\n lastExecutionOutcome?: ExecutionRecord['outcome']\n /** Checklist progress (present only when the task has a checklist). */\n checklist?: { done: number; total: number }\n trashed: boolean\n}\n\n/**\n * Build the compact summary of a task.\n * @param task - the task.\n */\nexport function summarize(task: TaskRecord): TaskSummary {\n const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : undefined\n const checklist = task.checklist !== undefined && task.checklist.length > 0 ? checklistProgress(task) : undefined\n return {\n id: task.id,\n title: task.title,\n workspaceId: task.workspaceId,\n urgency: task.urgency,\n status: task.status,\n blocked: task.blocked,\n executionMode: task.execution.mode,\n nextRunAt: task.execution.nextRunAt,\n model: task.model,\n permission: task.permission,\n version: task.version,\n claimOwner: isClaimedBy(task),\n commentCount: task.comments.length,\n lastExecutionOutcome: last?.outcome,\n ...(checklist !== undefined ? { checklist } : {}),\n trashed: task.trashedAt !== undefined,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-taskboard",
3
3
  "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.7.2",
4
+ "version": "0.7.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -178,8 +178,20 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
178
178
  const [prompt, setPrompt] = useState(task?.prompt ?? prefill?.prompt ?? '')
179
179
  const [workspaceId, setWorkspaceId] = useState(task?.workspaceId ?? state.filters.workspaceId ?? state.workspaces[0]?.id ?? '')
180
180
  const [urgency, setUrgency] = useState<Urgency>(task?.urgency ?? (prefill?.urgency === 'urgent' || prefill?.urgency === 'relaxed' ? prefill.urgency : 'normal'))
181
- const [mode, setMode] = useState<'claim' | 'scheduled'>(task?.execution.mode === 'scheduled' || prefill?.execution?.mode === 'scheduled' ? 'scheduled' : 'claim')
181
+ const initExec = task?.execution ?? prefill?.execution
182
+ const [mode, setMode] = useState<'claim' | 'once' | 'periodic'>(
183
+ initExec?.mode === 'scheduled' ? (initExec.cron !== undefined ? 'periodic' : 'once') : 'claim',
184
+ )
182
185
  const [cron, setCron] = useState(task?.execution.cron ?? prefill?.execution?.cron ?? '0 9 * * *')
186
+ // One-shot trigger (定时执行): datetime-local string; '' = unset.
187
+ const initRunAt = initExec?.mode === 'scheduled' && initExec.runAt !== undefined ? initExec.runAt : undefined
188
+ const [runAt, setRunAt] = useState(() => {
189
+ if (initRunAt === undefined) return ''
190
+ const d = new Date(initRunAt)
191
+ if (Number.isNaN(d.getTime())) return ''
192
+ const pad = (n: number): string => String(n).padStart(2, '0')
193
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
194
+ })
183
195
  const [catalog, setCatalog] = useState<CatalogModel[]>([])
184
196
 
185
197
  // Model & reasoning effort selection:
@@ -245,10 +257,19 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
245
257
  }, [controller, editing, task?.presetId, initialPreset])
246
258
 
247
259
  // Live cron validation + next-run preview (same math as the host).
248
- const cronMatch = mode === 'scheduled' ? parseCron(cron.trim()) : null
260
+ const cronMatch = mode === 'periodic' ? parseCron(cron.trim()) : null
249
261
  const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null
250
- const cronBad = mode === 'scheduled' && (cronMatch === null || nextRun === null)
251
- const valid = title.trim().length > 0 && workspaceId !== '' && !cronBad
262
+ const cronBad = mode === 'periodic' && (cronMatch === null || nextRun === null)
263
+ const runAtMs = mode === 'once' && runAt !== '' ? new Date(runAt).getTime() : NaN
264
+ const runAtBad = mode === 'once' && (runAt === '' || Number.isNaN(runAtMs) || runAtMs <= Date.now())
265
+ const valid = title.trim().length > 0 && workspaceId !== '' && !cronBad && !runAtBad
266
+
267
+ /** Execution payload for submit: claim | periodic (cron) | one-shot (runAt ISO). */
268
+ const executionPayload = (): { mode: 'claim' | 'scheduled'; cron?: string; runAt?: string } => {
269
+ if (mode === 'periodic') return { mode: 'scheduled', cron: cron.trim() }
270
+ if (mode === 'once') return { mode: 'scheduled', runAt: new Date(runAt).toISOString() }
271
+ return { mode: 'claim' }
272
+ }
252
273
 
253
274
  // A task already in progress cannot be run again (host rejects it).
254
275
  const runBlocked = editing && task.status === 'in_progress'
@@ -309,7 +330,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
309
330
  prompt,
310
331
  urgency,
311
332
  workspaceId,
312
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
333
+ execution: executionPayload(),
313
334
  // '' in edit mode clears the pinned model back to the default.
314
335
  model: picked ?? null,
315
336
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
@@ -324,7 +345,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
324
345
  urgency,
325
346
  description: description.length > 0 ? description : undefined,
326
347
  prompt: prompt.length > 0 ? prompt : undefined,
327
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
348
+ execution: executionPayload(),
328
349
  model: picked,
329
350
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
330
351
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
@@ -351,7 +372,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
351
372
  prompt,
352
373
  urgency,
353
374
  workspaceId,
354
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
375
+ execution: executionPayload(),
355
376
  model: picked ?? null,
356
377
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
357
378
  presetId: presetOut ?? null,
@@ -366,7 +387,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
366
387
  urgency,
367
388
  description: description.length > 0 ? description : undefined,
368
389
  prompt: prompt.length > 0 ? prompt : undefined,
369
- execution: mode === 'scheduled' ? { mode, cron: cron.trim() } : { mode },
390
+ execution: executionPayload(),
370
391
  model: picked,
371
392
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
372
393
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
@@ -379,9 +400,15 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
379
400
  }
380
401
 
381
402
  const hint = !valid
382
- ? (title.trim().length === 0 ? t('form.hint.needTitle') : workspaceId === '' ? t('form.hint.needProject') : t('form.hint.cronBad'))
383
- : mode === 'scheduled' && nextRun !== null
403
+ ? (title.trim().length === 0
404
+ ? t('form.hint.needTitle')
405
+ : workspaceId === ''
406
+ ? t('form.hint.needProject')
407
+ : runAtBad ? t('form.hint.runAtBad') : t('form.hint.cronBad'))
408
+ : mode === 'periodic' && nextRun !== null
384
409
  ? t('form.hint.nextRun', { time: fmtTime(nextRun) })
410
+ : mode === 'once' && !Number.isNaN(runAtMs)
411
+ ? t('form.hint.onceAt', { time: fmtTime(runAtMs) })
385
412
  : editing
386
413
  ? t('form.hint.saveVersion', { v: task.version, next: task.version + 1 })
387
414
  : t('form.hint.createClaim')
@@ -516,19 +543,35 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
516
543
  </Field>
517
544
 
518
545
  <Field label={t('form.field.mode')} full>
519
- <div className="dsh-atb-mode-picker">
546
+ <div className="dsh-atb-mode-picker" data-exec="true">
520
547
  <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'claim'} onClick={() => setMode('claim')}>
521
548
  <span className="dsh-atb-mode-name">{t('form.mode.claim')}</span>
522
549
  <span className="dsh-atb-mode-hint">{t('form.mode.claimHint')}</span>
523
550
  </button>
524
- <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'scheduled'} onClick={() => setMode('scheduled')}>
525
- <span className="dsh-atb-mode-name">{t('form.mode.scheduled')}</span>
526
- <span className="dsh-atb-mode-hint">{t('form.mode.scheduledHint')}</span>
551
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'once'} onClick={() => setMode('once')}>
552
+ <span className="dsh-atb-mode-name">{t('form.mode.once')}</span>
553
+ <span className="dsh-atb-mode-hint">{t('form.mode.onceHint')}</span>
554
+ </button>
555
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'periodic'} onClick={() => setMode('periodic')}>
556
+ <span className="dsh-atb-mode-name">{t('form.mode.periodic')}</span>
557
+ <span className="dsh-atb-mode-hint">{t('form.mode.periodicHint')}</span>
527
558
  </button>
528
559
  </div>
529
560
  </Field>
530
561
 
531
- {mode === 'scheduled' && (
562
+ {mode === 'once' && (
563
+ <Field label={t('form.field.runAt')} required full>
564
+ <input
565
+ type="datetime-local"
566
+ className={runAtBad ? 'dsh-atb-input-bad' : undefined}
567
+ value={runAt}
568
+ onChange={e => setRunAt(e.target.value)}
569
+ spellCheck={false}
570
+ />
571
+ </Field>
572
+ )}
573
+
574
+ {mode === 'periodic' && (
532
575
  <Field label={t('form.field.cron')} required full>
533
576
  <input
534
577
  className={cronBad ? 'dsh-atb-input-bad' : undefined}
@@ -653,7 +696,7 @@ interface TaskRecordLike {
653
696
  prompt: string
654
697
  workspaceId: string
655
698
  urgency: Urgency
656
- execution: { mode: 'claim' | 'scheduled'; cron?: string }
699
+ execution: { mode: 'claim' | 'scheduled'; cron?: string; runAt?: number }
657
700
  model?: { provider: string; model: string; reasoningEffort?: string }
658
701
  isolation?: IsolationMode
659
702
  presetId?: string
@@ -693,7 +693,9 @@ export class BoardController {
693
693
  prompt: task.prompt.length > 0 ? task.prompt : undefined,
694
694
  execution: task.execution.mode === 'scheduled' && task.execution.cron !== undefined
695
695
  ? { mode: 'scheduled', cron: task.execution.cron }
696
- : { mode: 'claim' },
696
+ : task.execution.mode === 'scheduled' && task.execution.runAt !== undefined && task.execution.runAt > Date.now()
697
+ ? { mode: 'scheduled', runAt: new Date(task.execution.runAt).toISOString() }
698
+ : { mode: 'claim' },
697
699
  model: task.model,
698
700
  isolation: task.isolation,
699
701
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
@@ -766,7 +768,9 @@ export class BoardController {
766
768
  urgency: task.urgency,
767
769
  execution: task.execution.mode === 'scheduled' && task.execution.cron !== undefined
768
770
  ? { mode: 'scheduled', cron: task.execution.cron }
769
- : { mode: 'claim' },
771
+ : task.execution.mode === 'scheduled' && task.execution.runAt !== undefined
772
+ ? { mode: 'scheduled', runAt: new Date(task.execution.runAt).toISOString() }
773
+ : { mode: 'claim' },
770
774
  model: task.model,
771
775
  isolation: task.isolation,
772
776
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
@@ -280,6 +280,11 @@ export const en: TaskboardDict = {
280
280
  'form.field.mode': 'Execution mode',
281
281
  'form.mode.claim': '🤝 Claim-based',
282
282
  'form.mode.claimHint': 'Sessions in the project claim it',
283
+ 'form.mode.once': '⏰ One-shot',
284
+ 'form.mode.onceHint': 'Runs once at a set time',
285
+ 'form.mode.periodic': '🔁 Periodic',
286
+ 'form.mode.periodicHint': 'Repeats on a cron schedule',
287
+ 'form.field.runAt': 'Trigger time',
283
288
  'form.mode.scheduled': '⏰ Scheduled',
284
289
  'form.mode.scheduledHint': 'Starts automatically on schedule',
285
290
  'form.field.cron': 'Cron expression',
@@ -314,7 +319,9 @@ export const en: TaskboardDict = {
314
319
  'form.hint.needTitle': 'Enter a title',
315
320
  'form.hint.needProject': 'Select a project',
316
321
  'form.hint.cronBad': 'Invalid cron expression (min hour day month weekday)',
322
+ 'form.hint.runAtBad': 'Pick a future trigger time',
317
323
  'form.hint.nextRun': 'Next run {time}',
324
+ 'form.hint.onceAt': 'Fires once at {time}',
318
325
  'form.hint.saveVersion': 'On save: v{v} → v{next}',
319
326
  'form.hint.createClaim': 'After creation, sessions in the project can claim and run it',
320
327
  'form.action.runBlockedTitle': 'The task is running; cannot start another',
@@ -488,6 +495,9 @@ export const en: TaskboardDict = {
488
495
  'sys.sessionError': '[System] Session execution error: {error}; the task was returned to todo.',
489
496
  'sys.sessionDone': '[System] The session finished; automatically moved to in review.',
490
497
  'sys.cronDead': '[System] The cron expression {cron} has no trigger time within 4 years; scheduling disabled — fix the cron and re-enable.',
498
+ 'sys.periodicHandoff': '[System] Periodic run finished; the schedule now continues on the new todo card {nextTaskId}. Review this card, then accept.',
499
+ 'sys.spawnedFrom': '[System] The previous periodic run finished (execution {executionId}); this card carries the schedule onward. See {sourceId} for the finished round.',
500
+ 'sys.runAtMissed': '[System] The one-shot trigger time was missed (host was down); it will not re-fire. Run manually or reschedule.',
491
501
  'sys.mergeSingle': '[System] Branch {branch} merged into the main worktree (--no-ff).',
492
502
  'sys.mergeMulti': '[System] Branches merged per repo (--no-ff): {summary}',
493
503
  }