dsh-taskboard 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/lib/client.js +242 -174
- package/lib/host/execution.js +80 -33
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +49 -5
- package/lib/host/git.js.map +1 -1
- package/lib/host/routes.js +180 -109
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +50 -28
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/sdk.js +7 -2
- package/lib/host/sdk.js.map +1 -1
- package/lib/host/store.js +41 -8
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +10 -3
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +124 -93
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +23 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/client/api.ts +19 -9
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/TaskBoard.tsx +7 -38
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +30 -23
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/board-mount.tsx +9 -6
- package/src/client/controller.ts +60 -13
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +16 -5
- package/src/client/styles.ts +5 -3
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +227 -126
- package/src/host/scheduler.ts +62 -36
- package/src/host/sdk.ts +12 -1
- package/src/host/store.ts +53 -7
- package/src/host/templates.ts +12 -3
- package/src/host/tools.ts +180 -123
- package/src/index.ts +10 -1
- package/src/shared/api.ts +1 -1
- package/src/shared/protocol.ts +35 -1
- package/src/shared/version.ts +1 -1
- package/src/client/board/NewTaskModal.tsx +0 -8
|
@@ -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 * 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}\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 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/** 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\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\n/** One commit produced by an isolated execution (hash + subject). */\nexport type CommitInfo = { hash: string; subject: string }\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 /** 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 /** 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}\n\n/** One task on the board. */\nexport type TaskRecord = {\n id: string\n title: string\n description: string\n /** The prompt sent to a fresh session on execution; falls back to title+description. */\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 * 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 */\n branch?: 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/** 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: explicit prompt, or title+description.\n * @param task - the task.\n */\nexport function effectivePrompt(task: TaskRecord): string {\n if (task.prompt.length > 0) return task.prompt\n const head = task.title\n return task.description.length > 0 ? `${head}\\n\\n${task.description}` : head\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 * Validate and normalize a pinned model: `{ provider, model }`, both\n * 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 } = raw as { provider?: unknown; model?: 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 return { provider: p, model: m }\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/**\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 if (id.length === 0 || id.length > 100) return fail('missing/invalid id')\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 })\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 ...(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 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 ...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),\n ...(typeof e.branch === 'string' ? { branch: e.branch } : {}),\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 * 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 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 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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"}
|
|
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 * 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}\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 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/** 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/** 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\n/** One commit produced by an isolated execution (hash + subject). */\nexport type CommitInfo = { hash: string; subject: string }\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 /** 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 /** 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}\n\n/** One task on the board. */\nexport type TaskRecord = {\n id: string\n title: string\n description: string\n /** The prompt sent to a fresh session on execution; falls back to title+description. */\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 * 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 */\n branch?: 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: explicit prompt, or title+description.\n * @param task - the task.\n */\nexport function effectivePrompt(task: TaskRecord): string {\n if (task.prompt.length > 0) return task.prompt\n const head = task.title\n return task.description.length > 0 ? `${head}\\n\\n${task.description}` : head\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 * Validate and normalize a pinned model: `{ provider, model }`, both\n * 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 } = raw as { provider?: unknown; model?: 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 return { provider: p, model: m }\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/**\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 })\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 ...(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 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 ...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),\n ...(typeof e.branch === 'string' ? { branch: e.branch } : {}),\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 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 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.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"agent"
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
|
-
"build": "npm run build:host && npm run build:client",
|
|
48
|
+
"build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
|
|
49
49
|
"build:host": "tsdown -c tsdown.host.config.ts",
|
|
50
50
|
"build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
|
|
51
51
|
"watch": "tsdown -c tsdown.host.config.ts --watch",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@types/react": "~18.3.1",
|
|
66
66
|
"@types/react-dom": "^18.3.7",
|
|
67
67
|
"jsdom": "^25.0.1",
|
|
68
|
+
"puppeteer-core": "^25.8.0",
|
|
68
69
|
"react": "^18.3.1",
|
|
69
70
|
"react-dom": "^18.3.1",
|
|
70
71
|
"tsdown": "0.22.2",
|
package/src/client/api.ts
CHANGED
|
@@ -39,11 +39,19 @@ async function unwrap<T>(pending: Response | Promise<Response>): Promise<T> {
|
|
|
39
39
|
return body.value
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Request timeout (S15: a hung fetch must never pin refreshInFlight forever). */
|
|
43
|
+
const TIMEOUT_MS = 10_000
|
|
44
|
+
|
|
45
|
+
async function get<T>(path: string): Promise<T> {
|
|
46
|
+
return unwrap<T>(fetch(path, { signal: AbortSignal.timeout(TIMEOUT_MS) }))
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
async function post<T>(path: string, body: unknown): Promise<T> {
|
|
43
50
|
const res = await fetch(path, {
|
|
44
51
|
method: 'POST',
|
|
45
52
|
headers: { 'content-type': 'application/json' },
|
|
46
53
|
body: JSON.stringify(body),
|
|
54
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
47
55
|
})
|
|
48
56
|
return unwrap<T>(res)
|
|
49
57
|
}
|
|
@@ -95,10 +103,10 @@ export interface TaskboardClient {
|
|
|
95
103
|
/** Build the client over fetch + EventSource. */
|
|
96
104
|
export function createClient(): TaskboardClient {
|
|
97
105
|
return {
|
|
98
|
-
state: () =>
|
|
99
|
-
workspaces: () =>
|
|
106
|
+
state: () => get<StateResponse>('/dsh-taskboard/state'),
|
|
107
|
+
workspaces: () => get<WorkspaceView[]>('/dsh-taskboard/workspaces'),
|
|
100
108
|
create: body => post('/dsh-taskboard/tasks', body),
|
|
101
|
-
get: id =>
|
|
109
|
+
get: id => get<TaskRecord>(`/dsh-taskboard/tasks/${encodeURIComponent(id)}`),
|
|
102
110
|
update: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/update`, body),
|
|
103
111
|
move: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/move`, body),
|
|
104
112
|
reject: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/reject`, body),
|
|
@@ -108,31 +116,33 @@ export function createClient(): TaskboardClient {
|
|
|
108
116
|
cancel: id => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/cancel`, {}),
|
|
109
117
|
mergeBranch: id => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/merge`, {}),
|
|
110
118
|
worktreeRemove: (id, body) => post(`/dsh-taskboard/tasks/${encodeURIComponent(id)}/worktree-remove`, body),
|
|
111
|
-
diagnostics: () =>
|
|
119
|
+
diagnostics: () => get<DiagnosticsResponse>('/dsh-taskboard/diagnostics'),
|
|
112
120
|
worktreeCleanup: (workspaceId, taskId) => post('/dsh-taskboard/worktree-cleanup', { workspaceId, taskId }),
|
|
113
121
|
diff: (taskId, query) => {
|
|
114
122
|
const params = new URLSearchParams({ execution: query.execution })
|
|
115
123
|
if (query.commit !== undefined) params.set('commit', query.commit)
|
|
116
124
|
if (query.path !== undefined) params.set('path', query.path)
|
|
117
|
-
return
|
|
125
|
+
return get<DiffResponse>(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`)
|
|
118
126
|
},
|
|
119
127
|
importPreview: file => post('/dsh-taskboard/import/preview', file),
|
|
120
128
|
importCommit: (mode, ledger) => post('/dsh-taskboard/import', { mode, ledger }),
|
|
121
|
-
templates: () =>
|
|
129
|
+
templates: () => get<TemplatesResponse>('/dsh-taskboard/templates'),
|
|
122
130
|
templateUpsert: body => post('/dsh-taskboard/templates', body),
|
|
123
131
|
templateDelete: id => post('/dsh-taskboard/templates/delete', { id }),
|
|
124
|
-
settings: () =>
|
|
132
|
+
settings: () => get<SettingsResponse>('/dsh-taskboard/settings'),
|
|
125
133
|
updateSettings: body => post('/dsh-taskboard/settings/update', body),
|
|
126
134
|
stream(onChange, onGap) {
|
|
127
135
|
const es = new EventSource('/dsh-taskboard/events')
|
|
128
136
|
let revision: number | undefined
|
|
129
137
|
const hello = (event: MessageEvent): void => {
|
|
130
|
-
|
|
138
|
+
let payload: { revision: number }
|
|
139
|
+
try { payload = JSON.parse(event.data) as { revision: number } } catch { return } // malformed frame → ignore
|
|
131
140
|
if (revision !== undefined && payload.revision !== revision) onGap()
|
|
132
141
|
revision = payload.revision
|
|
133
142
|
}
|
|
134
143
|
const change = (event: MessageEvent): void => {
|
|
135
|
-
|
|
144
|
+
let payload: ChangeEvent
|
|
145
|
+
try { payload = JSON.parse(event.data) as ChangeEvent } catch { return } // malformed frame → ignore
|
|
136
146
|
// A gap means we missed frames while disconnected: reconcile fully.
|
|
137
147
|
if (revision !== undefined && payload.revision !== revision + 1) onGap()
|
|
138
148
|
revision = payload.revision
|
|
@@ -168,7 +168,7 @@ export function ImportModal({ controller }: { controller: BoardController }) {
|
|
|
168
168
|
className="dsh-atb-btn"
|
|
169
169
|
data-primary="true"
|
|
170
170
|
data-danger={mode === 'replace' && confirmReplace ? 'true' : undefined}
|
|
171
|
-
disabled={plan === undefined || busy
|
|
171
|
+
disabled={plan === undefined || busy}
|
|
172
172
|
onClick={commit}
|
|
173
173
|
>
|
|
174
174
|
{mode === 'replace' && confirmReplace ? '确认整册替换' : '执行导入'}
|
|
@@ -9,6 +9,8 @@ import type { BoardController, ControllerState } from '../controller.ts'
|
|
|
9
9
|
import type { TaskRecord, TaskStatus, Urgency } from '../../shared/protocol.ts'
|
|
10
10
|
import { MAIN_STATUSES, canTransition } from '../../shared/protocol.ts'
|
|
11
11
|
import { PLUGIN_VERSION } from '../../shared/version.ts'
|
|
12
|
+
import { COLUMN_LABELS, URGENCY_LABEL } from './labels.ts'
|
|
13
|
+
import { fmtTime, isStaleClaim } from './format.ts'
|
|
12
14
|
import { DRAG_TYPE, TaskCard } from './TaskCard.tsx'
|
|
13
15
|
import { TaskDetail } from './TaskDetail.tsx'
|
|
14
16
|
import { TaskFormModal } from './TaskFormModal.tsx'
|
|
@@ -17,41 +19,6 @@ import { ImportModal } from './ImportModal.tsx'
|
|
|
17
19
|
import { TemplateManager } from './TemplateManager.tsx'
|
|
18
20
|
import { useAlert } from './AlertModal.tsx'
|
|
19
21
|
|
|
20
|
-
/** Column labels. */
|
|
21
|
-
const COLUMN_LABELS: Readonly<Record<TaskStatus, string>> = {
|
|
22
|
-
backlog: '待规划',
|
|
23
|
-
todo: '待办',
|
|
24
|
-
in_progress: '进行中',
|
|
25
|
-
in_review: '待验收',
|
|
26
|
-
done: '已完成',
|
|
27
|
-
canceled: '已取消',
|
|
28
|
-
archived: '已归档',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/** Urgency chip labels. */
|
|
33
|
-
const URGENCY_LABELS: Readonly<Record<Urgency, string>> = {
|
|
34
|
-
urgent: '紧急',
|
|
35
|
-
normal: '一般',
|
|
36
|
-
relaxed: '不急',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Format an epoch ms as a short local stamp. */
|
|
40
|
-
export function fmtTime(ms: number | undefined): string {
|
|
41
|
-
if (ms === undefined) return ''
|
|
42
|
-
const d = new Date(ms)
|
|
43
|
-
const pad = (n: number) => String(n).padStart(2, '0')
|
|
44
|
-
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** A claim idle for longer than this is highlighted as stale (ms). */
|
|
48
|
-
export const STALE_CLAIM_MS = 30 * 60_000
|
|
49
|
-
|
|
50
|
-
/** Whether the task's claim is stale (in_progress, held, idle too long). */
|
|
51
|
-
export function isStaleClaim(task: TaskRecord, now: number): boolean {
|
|
52
|
-
return task.status === 'in_progress' && task.claimedAt !== undefined && now - task.claimedAt > STALE_CLAIM_MS
|
|
53
|
-
}
|
|
54
|
-
|
|
55
22
|
/** Urgency sort rank (urgent first). */
|
|
56
23
|
const URGENCY_RANK: Record<Urgency, number> = { urgent: 0, normal: 1, relaxed: 2 }
|
|
57
24
|
|
|
@@ -126,7 +93,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
126
93
|
title={t.task.description !== undefined && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name}
|
|
127
94
|
onClick={() => { closeMenu(); controller.newFromTemplate(t.task) }}
|
|
128
95
|
>
|
|
129
|
-
{t.name}
|
|
96
|
+
{t.name}
|
|
130
97
|
</button>
|
|
131
98
|
))}
|
|
132
99
|
<div className="dsh-atb-newmenu-sep" />
|
|
@@ -172,7 +139,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
172
139
|
onClick={() => controller.toggleUrgency(u)}
|
|
173
140
|
>
|
|
174
141
|
<span className="dsh-atb-dot" data-urgency={u} />
|
|
175
|
-
{
|
|
142
|
+
{URGENCY_LABEL[u]}
|
|
176
143
|
</button>
|
|
177
144
|
))}
|
|
178
145
|
<button type="button" className="dsh-atb-btn" onClick={() => controller.toggleSecondary()}>
|
|
@@ -284,7 +251,9 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
284
251
|
|
|
285
252
|
{selected !== undefined && (
|
|
286
253
|
<div className="dsh-atb-detailpanel">
|
|
287
|
-
|
|
254
|
+
{/* key: remount per task — without it, confirm states and comment
|
|
255
|
+
drafts leak across task switches (review P0). */}
|
|
256
|
+
<TaskDetail key={selected.id} task={selected} controller={controller} now={now} />
|
|
288
257
|
</div>
|
|
289
258
|
)}
|
|
290
259
|
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
import { useState } from 'react'
|
|
15
15
|
import type { BoardController } from '../controller.ts'
|
|
16
16
|
import type { TaskRecord } from '../../shared/protocol.ts'
|
|
17
|
-
import { fmtTime, isStaleClaim } from './
|
|
18
|
-
|
|
19
|
-
const URGENCY_LABEL: Record<TaskRecord['urgency'], string> = { urgent: '紧急', normal: '一般', relaxed: '不急' }
|
|
20
|
-
const OUTCOME_LABEL: Record<string, string> = { running: '执行中', succeeded: '成功', failed: '失败', cancelled: '已取消' }
|
|
17
|
+
import { fmtTime, isStaleClaim } from './format.ts'
|
|
18
|
+
import { OUTCOME_LABEL, URGENCY_LABEL } from './labels.ts'
|
|
21
19
|
|
|
22
20
|
/** dataTransfer type carrying the dragged task id. */
|
|
23
21
|
export const DRAG_TYPE = 'application/x-dsh-atb-task'
|
|
@@ -57,7 +55,7 @@ export function TaskCard({ task, controller, draggable = false, now, onAlert }:
|
|
|
57
55
|
// Block drag if a session is still executing this task
|
|
58
56
|
if (running !== undefined) {
|
|
59
57
|
e.preventDefault()
|
|
60
|
-
const msg =
|
|
58
|
+
const msg = `该任务正由会话执行中(${task.title}),不能拖动`
|
|
61
59
|
if (onAlert !== undefined) onAlert(msg)
|
|
62
60
|
else alert(msg)
|
|
63
61
|
return
|
|
@@ -12,7 +12,8 @@ import type { BoardController } from '../controller.ts'
|
|
|
12
12
|
import type { ExecutionRecord, TaskRecord } from '../../shared/protocol.ts'
|
|
13
13
|
import { canTransition, checklistProgress } from '../../shared/protocol.ts'
|
|
14
14
|
import { useAlert } from './AlertModal.tsx'
|
|
15
|
-
import { fmtTime, isStaleClaim } from './
|
|
15
|
+
import { fmtTime, isStaleClaim } from './format.ts'
|
|
16
|
+
import { MOVE_LABEL, OUTCOME_LABEL, STATUS_LABEL, URGENCY_LABEL } from './labels.ts'
|
|
16
17
|
|
|
17
18
|
/** Statuses a user may move this task to, per the state machine. */
|
|
18
19
|
function moveTargets(task: TaskRecord): TaskRecord['status'][] {
|
|
@@ -20,14 +21,6 @@ function moveTargets(task: TaskRecord): TaskRecord['status'][] {
|
|
|
20
21
|
return all.filter(to => canTransition(task.status, to))
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const MOVE_LABEL: Record<string, string> = {
|
|
24
|
-
backlog: '待规划', todo: '待办', in_progress: '进行中', in_review: '待验收',
|
|
25
|
-
done: '完成', canceled: '取消', archived: '归档',
|
|
26
|
-
}
|
|
27
|
-
const STATUS_LABEL: Record<string, string> = { ...MOVE_LABEL }
|
|
28
|
-
const URGENCY_LABEL: Record<string, string> = { urgent: '紧急', normal: '一般', relaxed: '不急' }
|
|
29
|
-
const OUTCOME_LABEL: Record<string, string> = { running: '执行中', succeeded: '成功', failed: '失败', cancelled: '已取消' }
|
|
30
|
-
|
|
31
24
|
/** Compact session-id display (execution sessions carry the taskboard infix). */
|
|
32
25
|
function shortId(id: string | undefined): string {
|
|
33
26
|
if (id === undefined) return ''
|
|
@@ -374,6 +367,10 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
374
367
|
const [confirmDone, setConfirmDone] = useState(false)
|
|
375
368
|
const [confirmPurge, setConfirmPurge] = useState(false)
|
|
376
369
|
const [confirmCancel, setConfirmCancel] = useState(false)
|
|
370
|
+
// Top action buttons (duplicate / save-as-template / run / reuse-run)
|
|
371
|
+
// share one in-flight guard: a double click used to fire duplicate runs or
|
|
372
|
+
// copies while the first round-trip was still pending (review P0).
|
|
373
|
+
const [actionBusy, setActionBusy] = useState(false)
|
|
377
374
|
const { alert: showAlert, el: alertEl } = useAlert()
|
|
378
375
|
const ws = controller.getSnapshot().workspaces.find(w => w.id === task.workspaceId)
|
|
379
376
|
const canRun = task.status !== 'in_progress' && task.status !== 'done' && task.status !== 'archived'
|
|
@@ -382,6 +379,13 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
382
379
|
const stale = now !== undefined && isStaleClaim(task, now)
|
|
383
380
|
const unchecked = (task.checklist ?? []).filter(i => !i.checked).length
|
|
384
381
|
|
|
382
|
+
/** Fire one top action under the shared busy guard; re-enable on settle. */
|
|
383
|
+
const runAction = (action: () => Promise<unknown>): void => {
|
|
384
|
+
if (actionBusy) return
|
|
385
|
+
setActionBusy(true)
|
|
386
|
+
void action().catch(() => undefined).finally(() => setActionBusy(false))
|
|
387
|
+
}
|
|
388
|
+
|
|
385
389
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
386
390
|
const jumpToSession = (sessionId: string): void => {
|
|
387
391
|
void controller.openSession(sessionId).then(result => {
|
|
@@ -426,7 +430,7 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
426
430
|
<Chip>v{task.version}</Chip>
|
|
427
431
|
</div>
|
|
428
432
|
<div className="dsh-atb-detail-sub">
|
|
429
|
-
更新 {fmtTime(task.updatedAt)} · 最近操作 {task.updatedBy.kind === 'agent' ? `🤖 ${shortId(task.updatedBy.sessionId)}` : '👤 用户'}
|
|
433
|
+
更新 {fmtTime(task.updatedAt)} · 最近操作 {task.updatedBy.kind === 'agent' ? `🤖 ${shortId(task.updatedBy.sessionId)}` : task.updatedBy.kind === 'system' ? '⚙️ 系统' : '👤 用户'}
|
|
430
434
|
</div>
|
|
431
435
|
</div>
|
|
432
436
|
<div className="dsh-atb-detail-topbtns">
|
|
@@ -435,7 +439,8 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
435
439
|
type="button"
|
|
436
440
|
className="dsh-atb-detail-edit"
|
|
437
441
|
title="复制此任务的全部配置为一张新卡(待办列)"
|
|
438
|
-
|
|
442
|
+
disabled={actionBusy}
|
|
443
|
+
onClick={() => runAction(() => controller.duplicate(task))}
|
|
439
444
|
>
|
|
440
445
|
⧉ 复制
|
|
441
446
|
</button>
|
|
@@ -443,11 +448,11 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
443
448
|
type="button"
|
|
444
449
|
className="dsh-atb-detail-edit"
|
|
445
450
|
title="把此任务的配置(含清单)保存为模板,新建任务时可用"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}}
|
|
451
|
+
disabled={actionBusy}
|
|
452
|
+
onClick={() => runAction(async () => {
|
|
453
|
+
const ok = await controller.saveAsTemplate(task)
|
|
454
|
+
if (ok) showAlert('已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)')
|
|
455
|
+
})}
|
|
451
456
|
>
|
|
452
457
|
⌗ 存为模板
|
|
453
458
|
</button>
|
|
@@ -456,7 +461,8 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
456
461
|
type="button"
|
|
457
462
|
className="dsh-atb-detail-run"
|
|
458
463
|
title="续跑:保留现有 worktree 与分支(上次的改动和提交都在原处),在其上继续执行;默认「立即执行」会重置为全新基线"
|
|
459
|
-
|
|
464
|
+
disabled={actionBusy}
|
|
465
|
+
onClick={() => runAction(() => controller.run(task.id, true))}
|
|
460
466
|
>
|
|
461
467
|
↻ 续跑
|
|
462
468
|
</button>
|
|
@@ -466,7 +472,8 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
466
472
|
type="button"
|
|
467
473
|
className="dsh-atb-detail-run"
|
|
468
474
|
title={task.model !== undefined ? `新会话执行(${task.model.model})` : '新会话执行(默认模型)'}
|
|
469
|
-
|
|
475
|
+
disabled={actionBusy}
|
|
476
|
+
onClick={() => runAction(() => controller.run(task.id))}
|
|
470
477
|
>
|
|
471
478
|
▶ 立即执行
|
|
472
479
|
</button>
|
|
@@ -578,8 +585,8 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
578
585
|
onChange={e => setComment(e.target.value)}
|
|
579
586
|
onKeyDown={e => {
|
|
580
587
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && comment.trim().length > 0) {
|
|
581
|
-
|
|
582
|
-
setComment('')
|
|
588
|
+
// T13: keep the draft when the post fails (reject 表单同样保留).
|
|
589
|
+
void controller.comment(task.id, comment).then(ok => { if (ok) setComment('') })
|
|
583
590
|
}
|
|
584
591
|
}}
|
|
585
592
|
/>
|
|
@@ -587,7 +594,9 @@ export function TaskDetail({ task, controller, now }: { task: TaskRecord; contro
|
|
|
587
594
|
type="button"
|
|
588
595
|
className="dsh-atb-composer-send"
|
|
589
596
|
disabled={comment.trim().length === 0}
|
|
590
|
-
onClick={() => {
|
|
597
|
+
onClick={() => {
|
|
598
|
+
void controller.comment(task.id, comment).then(ok => { if (ok) setComment('') })
|
|
599
|
+
}}
|
|
591
600
|
>
|
|
592
601
|
发表
|
|
593
602
|
</button>
|