dsh-taskboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +169 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +2085 -0
  5. package/lib/host/execution.js +189 -0
  6. package/lib/host/execution.js.map +1 -0
  7. package/lib/host/protocol-text.js +37 -0
  8. package/lib/host/protocol-text.js.map +1 -0
  9. package/lib/host/routes.js +369 -0
  10. package/lib/host/routes.js.map +1 -0
  11. package/lib/host/scheduler.js +91 -0
  12. package/lib/host/scheduler.js.map +1 -0
  13. package/lib/host/sdk.js +145 -0
  14. package/lib/host/sdk.js.map +1 -0
  15. package/lib/host/store.js +112 -0
  16. package/lib/host/store.js.map +1 -0
  17. package/lib/host/tools.js +620 -0
  18. package/lib/host/tools.js.map +1 -0
  19. package/lib/index.js +91 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/invariant.js +22 -0
  22. package/lib/invariant.js.map +1 -0
  23. package/lib/shared/api.js +9 -0
  24. package/lib/shared/api.js.map +1 -0
  25. package/lib/shared/protocol.js +279 -0
  26. package/lib/shared/protocol.js.map +1 -0
  27. package/package.json +74 -0
  28. package/src/client/api.ts +90 -0
  29. package/src/client/board/NewTaskModal.tsx +8 -0
  30. package/src/client/board/TaskBoard.tsx +184 -0
  31. package/src/client/board/TaskCard.tsx +61 -0
  32. package/src/client/board/TaskDetail.tsx +210 -0
  33. package/src/client/board/TaskFormModal.tsx +257 -0
  34. package/src/client/board-mount.tsx +92 -0
  35. package/src/client/controller.ts +241 -0
  36. package/src/client/index.ts +87 -0
  37. package/src/client/sidebar-entry.ts +165 -0
  38. package/src/client/styles.ts +391 -0
  39. package/src/host/execution.ts +244 -0
  40. package/src/host/protocol-text.ts +37 -0
  41. package/src/host/routes.ts +387 -0
  42. package/src/host/scheduler.ts +107 -0
  43. package/src/host/sdk.ts +200 -0
  44. package/src/host/store.ts +139 -0
  45. package/src/host/tools.ts +631 -0
  46. package/src/index.ts +124 -0
  47. package/src/invariant.ts +22 -0
  48. package/src/shared/api.ts +98 -0
  49. package/src/shared/protocol.ts +475 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","names":["v"],"sources":["../../src/host/tools.ts"],"sourcesContent":["/**\n * The eight `taskboard_*` agent tools. All writes require a calling agent\n * session (ownership audit), carry optimistic-version checks, and enforce\n * the protocol gates in CODE, not in prompt text:\n *\n * - `move → done` is rejected for agent callers (user confirmation only)\n * - `move todo → in_progress` requires the calling session's workspace to\n * match the task's project (claim boundary)\n * - taking over a task held by another session is rejected\n * - `delete` for agent callers only sets the soft-delete marker\n *\n * OUTPUT CONTRACT (lesson: registry `createSuccessResult` renders\n * `output.render(args, value)` into `result.content`, and the loop feeds\n * exactly that content to the model — the raw JSON `value` never reaches\n * the model): render() IS the model-facing tool result. Every render must\n * carry the complete facts an agent needs to act (ids, versions, statuses);\n * a \"terse UI summary\" here starves the agent.\n *\n * @module dsh-taskboard/host/tools\n */\nimport type { WorkspaceRegistry } from '@deepseek-ai/dsh-workspace'\nimport { defineTool } from './sdk.ts'\nimport {\n asStatus,\n asUrgency,\n canTransition,\n effectivePrompt,\n isClaim,\n newCommentId,\n newTaskId,\n normalizeBody,\n normalizeExecution,\n normalizePrompt,\n normalizeTitle,\n summarize,\n type Actor,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Render side: one compact task line (id/status/version are load-bearing). */\nfunction taskLine(t: {\n id: string\n title: string\n status: string\n urgency: string\n version: number\n workspaceId: string\n blocked: boolean\n executionMode: string\n commentCount?: number\n lastExecutionOutcome?: string\n trashed?: boolean\n}): string {\n const parts = [\n `- ${t.id} [${t.status}] v${t.version} · ${t.urgency} · 项目 ${t.workspaceId}`,\n `「${t.title}」`,\n ]\n if (t.blocked) parts.push('·受阻')\n if (t.executionMode === 'scheduled') parts.push('·定时')\n if (t.commentCount !== undefined && t.commentCount > 0) parts.push(`·评论${t.commentCount}`)\n if (t.lastExecutionOutcome !== undefined) parts.push(`·上次执行${t.lastExecutionOutcome}`)\n if (t.trashed === true) parts.push('·已删')\n return parts.join(' ')\n}\n\n/** Render side: the full task detail block (everything an executor needs). */\nfunction taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {\n const lines: string[] = [\n `任务 ${t.id} 「${t.title}」`,\n `状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? ' · 受阻' : ''}`,\n `执行方式: ${t.execution.mode}${t.execution.cron !== undefined ? ` cron=${t.execution.cron}` : ''}`,\n ]\n if (t.execution.nextRunAt !== undefined) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`)\n if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`)\n lines.push(`描述: ${t.description.length > 0 ? t.description : '(无)'}`)\n lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`)\n if (t.comments.length > 0) {\n lines.push(`评论 (${t.comments.length}):`)\n for (const c of t.comments) {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n lines.push(` - [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`)\n }\n } else {\n lines.push('评论: 无')\n }\n if (t.executions.length > 0) {\n lines.push(`执行记录 (${t.executions.length}):`)\n for (const e of t.executions) {\n const at = e.startedAt !== undefined ? new Date(e.startedAt).toISOString() : '?'\n const err = e.error !== undefined ? ` 错误: ${e.error}` : ''\n lines.push(` - [${e.trigger} ${at}] ${e.outcome}${err}`)\n }\n } else {\n lines.push('执行记录: 无')\n }\n const updatedBy = t.updatedBy.kind === 'agent' ? `agent ${String(t.updatedBy.sessionId).slice(0, 24)}` : 'user'\n lines.push(`更新: ${new Date(t.updatedAt).toISOString()} 由 ${updatedBy}`)\n return lines.join('\\n')\n}\n\n/** Stable error codes surfaced at the head of tool error messages. */\nexport const ERR = {\n notFound: 'not_found',\n versionConflict: 'version_conflict',\n workspaceMismatch: 'workspace_mismatch',\n invalidTransition: 'invalid_transition',\n forbidden: 'forbidden',\n requiresAgent: 'unauthorized_actor',\n invalidInput: 'invalid_input',\n} as const\n\n/** Tool failure: an Error whose message starts with a stable code. */\nclass ToolError extends Error {\n constructor(readonly code: string, detail: string) {\n super(`Error: ${code}: ${detail}`)\n }\n}\n\n/** The workspace face the tools need (narrow for tests). */\nexport interface WorkspaceFace {\n /** Resolve the workspace owning a canonical cwd, if any. */\n resolveByPath(path: string): Promise<{ id: string } | undefined>\n /** Get a workspace by id. */\n get(id: string): { id: string; path: string; title: string } | undefined\n /** List all workspaces. */\n list(): Array<{ id: string; path: string; title: string }>\n}\n\n/** Adapt the real registry to the narrow face. */\nexport function workspaceFace(registry: WorkspaceRegistry): WorkspaceFace {\n // Explicit field mapping: Workspace entities expose path/title as prototype\n // getters, which JSON.stringify skips (own enumerable properties only).\n return {\n resolveByPath: async (path) => {\n const ws = await registry.resolveByPath(path as never)\n return ws === undefined ? undefined : { id: ws.id }\n },\n get: id => {\n const ws = registry.get(id as never)\n return ws === undefined ? undefined : { id: ws.id, path: ws.path, title: ws.title }\n },\n list: () => registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title })),\n }\n}\n\n/** Everything the tool set needs. */\nexport interface ToolDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n /** Current epoch ms (injectable for tests). */\n now: () => number\n}\n\n/** Resolve the calling agent's actor and session id. */\nfunction caller(exec: ToolRunContext): { actor: Actor & { kind: 'agent' }; sessionId: string } {\n if (!exec.agent) throw new ToolError(ERR.requiresAgent, 'taskboard tools require a calling agent session')\n const sessionId = exec.agent.id\n return { actor: { kind: 'agent', sessionId }, sessionId }\n}\n\n/** The calling session's workspace id (undefined when unaffiliated). */\nasync function callerWorkspace(deps: ToolDeps, exec: ToolRunContext): Promise<string | undefined> {\n const cwd = exec.agent?.session.header.cwd\n if (typeof cwd !== 'string' || cwd.length === 0) return undefined\n const ws = await deps.workspaces.resolveByPath(cwd)\n return ws?.id\n}\n\n/** Guard: version match. */\nfunction versionGuard(task: TaskRecord, ifVersion: number | undefined): void {\n if (ifVersion === undefined) {\n throw new ToolError(ERR.versionConflict, 'this write requires ifVersion; read the task first')\n }\n if (ifVersion !== task.version) {\n throw new ToolError(ERR.versionConflict, `stale version ${ifVersion} (current ${task.version}); re-read the task and retry once`)\n }\n}\n\n/** Re-throw with a stable code; non-ToolErrors become invalid_input. */\nfunction fail(error: unknown): never {\n if (error instanceof ToolError) throw error\n const message = error instanceof Error ? error.message : String(error)\n throw new ToolError(ERR.invalidInput, message)\n}\n\n/** Loose json output schema shared by every taskboard tool. */\nconst JSON_OUT = { type: 'json' } as const\n\n/** Deep-JSON a value for a json-rooted tool output (spread results lose implicit index signatures). */\nfunction json<T>(value: T): Record<string, unknown> {\n return JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n}\n\n/** The exec context face the tools read (agent identity + session cwd). */\nexport interface ToolRunContext {\n agent?: { id: string; session: { header: { cwd?: string } } }\n}\n\n/** Registry-like context face (tests stub this). */\nexport interface ToolContextFace {\n tools: { register(tool: { name: string }): unknown }\n}\n\n/**\n * Register all eight tools.\n * @param ctx - a context exposing `tools.register`.\n * @param deps - store + workspaces + clock.\n * @returns dispose functions, one per tool.\n */\nexport function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Array<() => void> {\n const disposers: Array<() => void> = []\n const { store, workspaces } = deps\n\n // Env-gated tool-call tracing (ATB_TRACE=1) — evidence for protocol E2E.\n const register = (tool: { name: string; execute?: unknown }) => {\n if (process.env.ATB_TRACE === '1' && typeof tool.execute === 'function') {\n const orig = tool.execute as (args: unknown, exec: unknown) => Promise<unknown>\n tool.execute = async (args: unknown, exec: unknown) => {\n console.error(`[atb ▶] ${tool.name}`, JSON.stringify(args).slice(0, 300))\n try {\n const result = await orig(args, exec)\n console.error(`[atb ✓] ${tool.name}`, JSON.stringify(result).slice(0, 300))\n return result\n } catch (error) {\n console.error(`[atb ✗] ${tool.name}`, String(error).slice(0, 400))\n throw error\n }\n }\n }\n return ctx.tools.register(tool as { name: string })\n }\n\n // ------------------------------------------------------------------ list\n disposers.push(register(defineTool({\n name: 'taskboard_list',\n description:\n 'List task-board tasks. Filter by project (workspaceId), status, or urgency. '\n + 'Returns compact summaries (id, title, status, urgency, version, claim owner). '\n + 'Check this before starting work to find claimable todo tasks in your project.',\n parameters: {\n workspaceId: { type: 'string', description: 'Filter by project (DSH workspace id).' },\n status: { type: 'string', description: 'Filter by exact status (backlog/todo/in_progress/in_review/done/canceled/archived).' },\n urgency: { type: 'string', description: 'Filter by urgency (urgent/normal/relaxed).' },\n includeTrashed: { type: 'boolean', description: 'Include soft-deleted tasks (default false).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { revision?: number; tasks?: Array<Record<string, unknown>> }\n const tasks = v.tasks ?? []\n const head = `任务 ${tasks.length} 条(台账 rev ${v.revision ?? '?'})`\n if (tasks.length === 0) return [{ type: 'text', text: `${head}:无匹配任务。` }]\n return [{ type: 'text', text: [head, ...tasks.map(t => taskLine(t as never))].join('\\n') }]\n },\n },\n async execute(args) {\n try {\n const a = args as { workspaceId?: string; status?: string; urgency?: string; includeTrashed?: boolean }\n const tasks = store.snapshot().tasks.filter(t =>\n (a.workspaceId === undefined || t.workspaceId === a.workspaceId)\n && (a.status === undefined || t.status === a.status)\n && (a.urgency === undefined || t.urgency === a.urgency)\n && (a.includeTrashed === true || t.trashedAt === undefined))\n return json({ revision: store.snapshot().revision, tasks: tasks.map(summarize) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------- get\n disposers.push(register(defineTool({\n name: 'taskboard_get',\n description:\n 'Read one task in full: description, prompt, project, urgency, status, comments, executions, version. '\n + 'Read this (and the comments) BEFORE claiming or starting work on a task.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id from the board.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: TaskRecord & { effectivePrompt?: string } }\n return [{ type: 'text', text: v.task === undefined ? '任务不存在。' : taskDetail(v.task) }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const { id } = args\n const task = store.get(id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${id}`)\n return json({ task: { ...task, effectivePrompt: effectivePrompt(task) } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- create\n disposers.push(register(defineTool({\n name: 'taskboard_create',\n description:\n 'Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). '\n + 'Optional: description, prompt (sent to a fresh session on execution), status (default todo), '\n + 'execution mode (claim|scheduled + cron), model {provider, model} to pin executions to a model. '\n + 'Do not track trivial requests as tasks.',\n parameters: {\n title: { type: 'string', required: true, description: 'Short imperative line (1..200 chars).' },\n workspaceId: { type: 'string', required: true, description: 'Project (DSH workspace id) this task belongs to.' },\n urgency: { type: 'string', required: true, description: 'urgent (red) | normal (purple) | relaxed (blue).' },\n description: { type: 'string', description: 'What the task involves (plain text).' },\n prompt: { type: 'string', description: 'Prompt sent to a fresh session when executed; default = title+description.' },\n status: { type: 'string', description: 'Initial status; default todo. backlog = not approved for execution.' },\n execution: {\n type: 'object',\n additionalProperties: false,\n description: 'Execution config: { mode: \"claim\" } (default) or { mode: \"scheduled\", cron: \"m h dom mon dow\" }.',\n properties: {\n mode: { type: 'string', description: 'claim | scheduled.' },\n cron: { type: 'string', description: 'Five-field cron expression (scheduled only).' },\n },\n },\n model: {\n type: 'object',\n additionalProperties: false,\n description: 'Pin executions to one configured model: { provider, model }. Omit to use the default model.',\n properties: {\n provider: { type: 'string', description: 'Provider route id.' },\n model: { type: 'string', description: 'Provider-owned model id.' },\n },\n },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '创建失败。' : `已创建任务 ${t.id} [${t.status}] v${t.version}。写入前先 taskboard_get 读取。` }]\n },\n },\n async execute(args: {\n title: string\n workspaceId: string\n urgency: string\n status?: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: { provider?: string; model?: string }\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const title = normalizeTitle(args.title)\n if (workspaces.get(args.workspaceId) === undefined) {\n throw new ToolError(ERR.notFound, `unknown workspaceId ${args.workspaceId}`)\n }\n const urgency = asUrgency(args.urgency)\n const status = args.status === undefined ? 'todo' as const : asStatus(args.status)\n if (status === 'done' || status === 'archived') {\n throw new ToolError(ERR.invalidTransition, 'a new task cannot start as done/archived')\n }\n const execution = normalizeExecution(args.execution ?? {}, deps.now())\n if (args.model !== undefined && (typeof args.model.provider !== 'string' || typeof args.model.model !== 'string')) {\n throw new ToolError(ERR.invalidInput, 'model must be { provider: string, model: string }')\n }\n const now = deps.now()\n const task: TaskRecord = {\n id: newTaskId(),\n title,\n description: (args.description ?? '').trim(),\n prompt: normalizePrompt(args.prompt),\n workspaceId: args.workspaceId,\n urgency,\n status,\n blocked: false,\n execution,\n model: args.model as TaskModel | undefined,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: actor,\n updatedBy: actor,\n comments: [],\n executions: [],\n }\n await store.mutate('task-created', ledger => {\n ledger.tasks.push(task)\n return [task]\n })\n return json({ task: summarize(task) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- update\n disposers.push(register(defineTool({\n name: 'taskboard_update',\n description:\n 'Update a task\\'s title/description/prompt/urgency/blocked. Requires ifVersion (read first). '\n + 'The model and execution config are read-only through this tool (they belong to the task owner/user).',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'The task version you read; the write fails on mismatch.' },\n title: { type: 'string', description: 'New title.' },\n description: { type: 'string', description: 'New description.' },\n prompt: { type: 'string', description: 'New execution prompt.' },\n urgency: { type: 'string', description: 'urgent | normal | relaxed.' },\n blocked: { type: 'boolean', description: 'Blocked marker (work cannot continue right now).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '更新失败。' : `已更新任务 ${t.id},当前 v${t.version} [${t.status}]。` }]\n },\n },\n async execute(args: {\n id: string\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n if (task.status === 'archived') throw new ToolError(ERR.invalidTransition, 'archived tasks are immutable')\n const next: TaskRecord = structuredClone(task)\n if (args.title !== undefined) next.title = normalizeTitle(args.title)\n if (args.description !== undefined) next.description = args.description.trim()\n if (args.prompt !== undefined) next.prompt = normalizePrompt(args.prompt)\n if (args.urgency !== undefined) next.urgency = asUrgency(args.urgency)\n if (args.blocked !== undefined) next.blocked = args.blocked\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n await store.mutate('task-updated', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ------------------------------------------------------------------ move\n disposers.push(register(defineTool({\n name: 'taskboard_move',\n description:\n 'Move a task between statuses (requires ifVersion). Claim = todo→in_progress (only a session '\n + 'inside the task\\'s project may claim; never take over a task held by another session). '\n + 'After implementing and self-verifying: comment, then in_progress→in_review. '\n + 'You can NEVER move a task to done — that requires explicit user confirmation.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n status: { type: 'string', required: true, description: 'Target status.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read; fails on mismatch.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { task?: { id?: string; status?: string; version?: number } }\n const t = v.task\n return [{ type: 'text', text: t === undefined ? '移动失败。' : `任务 ${t.id} 已移到 ${t.status},当前 v${t.version}。` }]\n },\n },\n async execute(args: { id: string; status: string; ifVersion: number }, exec: unknown) {\n try {\n const { actor } = caller(exec as ToolRunContext)\n const to = asStatus(args.status)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n\n // Code-level gate: agents never complete a task.\n if (to === 'done') {\n throw new ToolError(ERR.forbidden, 'moving a task to done requires explicit user confirmation (GUI); agents cannot do it')\n }\n if (!canTransition(task.status, to)) {\n throw new ToolError(ERR.invalidTransition, `illegal transition ${task.status} → ${to}`)\n }\n // Exclusive hold: while a task is in_progress under an agent, no other\n // session may move it at all (that would be a takeover).\n if (task.status === 'in_progress' && task.updatedBy.kind === 'agent' && task.updatedBy.sessionId !== actor.sessionId) {\n throw new ToolError(ERR.forbidden, `task is held by session ${task.updatedBy.sessionId}; never take over another session's claim`)\n }\n // Claim boundary: the calling session must belong to the task's project.\n if (isClaim(task.status, to)) {\n const wsId = await callerWorkspace(deps, exec as ToolRunContext)\n if (wsId !== task.workspaceId) {\n throw new ToolError(ERR.workspaceMismatch, 'only a session inside this task\\'s project may claim it')\n }\n }\n const next: TaskRecord = structuredClone(task)\n next.status = to\n next.version = task.version + 1\n next.updatedAt = deps.now()\n next.updatedBy = actor\n if (isClaim(task.status, to)) next.blocked = false\n await store.mutate('task-moved', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ task: summarize(next) })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ----------------------------------------------------------- comment_add\n disposers.push(register(defineTool({\n name: 'taskboard_comment_add',\n description:\n 'Append a progress/report comment to a task. When handing off to review, the comment should cover: '\n + 'what changed, how it was verified, outcome, and remaining risks.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n body: { type: 'string', required: true, description: 'Comment text (1..4000 chars).' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comment?: { id?: string }; task?: { id?: string; version?: number; status?: string } }\n const c = v.comment\n const t = v.task\n if (c === undefined || t === undefined) return [{ type: 'text', text: '评论失败。' }]\n // The comment bumped the version — echo it so the agent can chain the\n // next write (e.g. move → in_review) WITHOUT re-reading.\n return [{\n type: 'text',\n text: `评论 ${c.id} 已添加;任务 ${t.id} 当前 v${t.version} [${t.status}](后续写操作用此版本号).`,\n }]\n },\n },\n async execute(args: { id: string; body: string }, exec: unknown) {\n try {\n const { sessionId } = caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n const comment = {\n id: newCommentId(),\n body: normalizeBody(args.body),\n version: 1,\n createdAt: deps.now(),\n threadId: sessionId,\n }\n const next: TaskRecord = structuredClone(task)\n next.comments.push(comment)\n next.version = task.version + 1\n next.updatedAt = deps.now()\n await store.mutate('comment-added', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return json({ comment, task: { id: next.id, version: next.version, status: next.status } })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // -------------------------------------------------------------- comments\n disposers.push(register(defineTool({\n name: 'taskboard_comments',\n description: 'List a task\\'s comments, oldest first. Read them before deciding to start work.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { comments?: unknown[] }\n const list = v.comments as Array<{ body: string; createdAt: number; threadId?: string }> | undefined\n if (list === undefined || list.length === 0) return [{ type: 'text', text: '无评论。' }]\n const lines = list.map(c => {\n const who = c.threadId !== undefined ? `agent ${String(c.threadId).slice(0, 24)}` : 'user'\n return `- [${who} ${new Date(c.createdAt).toISOString()}] ${c.body}`\n })\n return [{ type: 'text', text: `评论 ${list.length} 条:\\n${lines.join('\\n')}` }]\n },\n },\n async execute(args: { id: string }) {\n try {\n const task = store.get(args.id)\n if (task === undefined || task.trashedAt !== undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n return json({ comments: task.comments })\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n // ---------------------------------------------------------------- delete\n disposers.push(register(defineTool({\n name: 'taskboard_delete',\n description:\n 'Soft-delete a task (marks it trashed; the user confirms the purge in the GUI). '\n + 'Requires ifVersion. Prefer canceled/archived over delete unless the task was a mistake.',\n parameters: {\n id: { type: 'string', required: true, description: 'Task id.' },\n ifVersion: { type: 'number', required: true, description: 'Task version you read.' },\n },\n output: {\n schema: JSON_OUT,\n render: (_args, value) => {\n const v = value as { trashed?: boolean }\n return [{ type: 'text', text: v.trashed === true ? '任务已标记删除(等待用户在 GUI 清除)。' : '删除失败。' }]\n },\n },\n async execute(args: { id: string; ifVersion: number }, exec: unknown) {\n try {\n caller(exec as ToolRunContext)\n const task = store.get(args.id)\n if (task === undefined) throw new ToolError(ERR.notFound, `no task ${args.id}`)\n versionGuard(task, args.ifVersion)\n const next: TaskRecord = structuredClone(task)\n next.trashedAt = deps.now()\n next.version = task.version + 1\n await store.mutate('task-deleted', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === args.id)\n ledger.tasks[i] = next\n return [next]\n })\n return { trashed: true }\n } catch (error) { fail(error) }\n },\n })) as () => void)\n\n return disposers\n}\n"],"mappings":";;;;AA0CA,SAAS,SAAS,GAYP;CACT,MAAM,QAAQ,CACZ,KAAK,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ,KAAK,EAAE,QAAQ,QAAQ,EAAE,eAC/D,IAAI,EAAE,MAAM,EACd;CACA,IAAI,EAAE,SAAS,MAAM,KAAK,KAAK;CAC/B,IAAI,EAAE,kBAAkB,aAAa,MAAM,KAAK,KAAK;CACrD,IAAI,EAAE,iBAAiB,KAAA,KAAa,EAAE,eAAe,GAAG,MAAM,KAAK,MAAM,EAAE,cAAc;CACzF,IAAI,EAAE,yBAAyB,KAAA,GAAW,MAAM,KAAK,QAAQ,EAAE,sBAAsB;CACrF,IAAI,EAAE,YAAY,MAAM,MAAM,KAAK,KAAK;CACxC,OAAO,MAAM,KAAK,GAAG;AACvB;;AAGA,SAAS,WAAW,GAAsD;CACxE,MAAM,QAAkB;EACtB,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM;EACvB,OAAO,EAAE,OAAO,KAAK,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAAS,EAAE,cAAc,EAAE,UAAU,UAAU;EACnG,SAAS,EAAE,UAAU,OAAO,EAAE,UAAU,SAAS,KAAA,IAAY,SAAS,EAAE,UAAU,SAAS;CAC7F;CACA,IAAI,EAAE,UAAU,cAAc,KAAA,GAAW,MAAM,KAAK,SAAS,IAAI,KAAK,EAAE,UAAU,SAAS,CAAC,CAAC,YAAY,GAAG;CAC5G,IAAI,EAAE,UAAU,KAAA,GAAW,MAAM,KAAK,SAAS,EAAE,MAAM,SAAS,GAAG,EAAE,MAAM,OAAO;CAClF,MAAM,KAAK,OAAO,EAAE,YAAY,SAAS,IAAI,EAAE,cAAc,OAAO;CACpE,MAAM,KAAK,cAAc,EAAE,mBAAmB,gBAAgB,CAAC,GAAG;CAClE,IAAI,EAAE,SAAS,SAAS,GAAG;EACzB,MAAM,KAAK,OAAO,EAAE,SAAS,OAAO,GAAG;EACvC,KAAK,MAAM,KAAK,EAAE,UAAU;GAC1B,MAAM,MAAM,EAAE,aAAa,KAAA,IAAY,SAAS,OAAO,EAAE,QAAQ,CAAC,CAAC,MAAM,GAAG,EAAE,MAAM;GACpF,MAAM,KAAK,QAAQ,IAAI,GAAG,IAAI,KAAK,EAAE,SAAS,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,MAAM;EAC5E;CACF,OACE,MAAM,KAAK,OAAO;CAEpB,IAAI,EAAE,WAAW,SAAS,GAAG;EAC3B,MAAM,KAAK,SAAS,EAAE,WAAW,OAAO,GAAG;EAC3C,KAAK,MAAM,KAAK,EAAE,YAAY;GAC5B,MAAM,KAAK,EAAE,cAAc,KAAA,IAAY,IAAI,KAAK,EAAE,SAAS,CAAC,CAAC,YAAY,IAAI;GAC7E,MAAM,MAAM,EAAE,UAAU,KAAA,IAAY,QAAQ,EAAE,UAAU;GACxD,MAAM,KAAK,QAAQ,EAAE,QAAQ,GAAG,GAAG,IAAI,EAAE,UAAU,KAAK;EAC1D;CACF,OACE,MAAM,KAAK,SAAS;CAEtB,MAAM,YAAY,EAAE,UAAU,SAAS,UAAU,SAAS,OAAO,EAAE,UAAU,SAAS,CAAC,CAAC,MAAM,GAAG,EAAE,MAAM;CACzG,MAAM,KAAK,OAAO,IAAI,KAAK,EAAE,SAAS,CAAC,CAAC,YAAY,EAAE,KAAK,WAAW;CACtE,OAAO,MAAM,KAAK,IAAI;AACxB;;AAGA,MAAa,MAAM;CACjB,UAAU;CACV,iBAAiB;CACjB,mBAAmB;CACnB,mBAAmB;CACnB,WAAW;CACX,eAAe;CACf,cAAc;AAChB;;AAGA,IAAM,YAAN,cAAwB,MAAM;CACP;CAArB,YAAY,MAAuB,QAAgB;EACjD,MAAM,UAAU,KAAK,IAAI,QAAQ;EADd,KAAA,OAAA;CAErB;AACF;;AAaA,SAAgB,cAAc,UAA4C;CAGxE,OAAO;EACL,eAAe,OAAO,SAAS;GAC7B,MAAM,KAAK,MAAM,SAAS,cAAc,IAAa;GACrD,OAAO,OAAO,KAAA,IAAY,KAAA,IAAY,EAAE,IAAI,GAAG,GAAG;EACpD;EACA,MAAK,OAAM;GACT,MAAM,KAAK,SAAS,IAAI,EAAW;GACnC,OAAO,OAAO,KAAA,IAAY,KAAA,IAAY;IAAE,IAAI,GAAG;IAAI,MAAM,GAAG;IAAM,OAAO,GAAG;GAAM;EACpF;EACA,YAAY,SAAS,KAAK,CAAC,CAAC,KAAI,QAAO;GAAE,IAAI,GAAG;GAAI,MAAM,GAAG;GAAM,OAAO,GAAG;EAAM,EAAE;CACvF;AACF;;AAWA,SAAS,OAAO,MAA+E;CAC7F,IAAI,CAAC,KAAK,OAAO,MAAM,IAAI,UAAU,IAAI,eAAe,iDAAiD;CACzG,MAAM,YAAY,KAAK,MAAM;CAC7B,OAAO;EAAE,OAAO;GAAE,MAAM;GAAS;EAAU;EAAG;CAAU;AAC1D;;AAGA,eAAe,gBAAgB,MAAgB,MAAmD;CAChG,MAAM,MAAM,KAAK,OAAO,QAAQ,OAAO;CACvC,IAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG,OAAO,KAAA;CAExD,QAAO,MADU,KAAK,WAAW,cAAc,GAAG,EAAA,EACvC;AACb;;AAGA,SAAS,aAAa,MAAkB,WAAqC;CAC3E,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,UAAU,IAAI,iBAAiB,oDAAoD;CAE/F,IAAI,cAAc,KAAK,SACrB,MAAM,IAAI,UAAU,IAAI,iBAAiB,iBAAiB,UAAU,YAAY,KAAK,QAAQ,mCAAmC;AAEpI;;AAGA,SAAS,KAAK,OAAuB;CACnC,IAAI,iBAAiB,WAAW,MAAM;CACtC,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACrE,MAAM,IAAI,UAAU,IAAI,cAAc,OAAO;AAC/C;;AAGA,MAAM,WAAW,EAAE,MAAM,OAAO;;AAGhC,SAAS,KAAQ,OAAmC;CAClD,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;;;;;AAkBA,SAAgB,uBAAuB,KAAsB,MAAmC;CAC9F,MAAM,YAA+B,CAAC;CACtC,MAAM,EAAE,OAAO,eAAe;CAG9B,MAAM,YAAY,SAA8C;EAC9D,IAAI,QAAQ,IAAI,cAAc,OAAO,OAAO,KAAK,YAAY,YAAY;GACvE,MAAM,OAAO,KAAK;GAClB,KAAK,UAAU,OAAO,MAAe,SAAkB;IACrD,QAAQ,MAAM,WAAW,KAAK,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC;IACxE,IAAI;KACF,MAAM,SAAS,MAAM,KAAK,MAAM,IAAI;KACpC,QAAQ,MAAM,WAAW,KAAK,QAAQ,KAAK,UAAU,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC;KAC1E,OAAO;IACT,SAAS,OAAO;KACd,QAAQ,MAAM,WAAW,KAAK,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC;KACjE,MAAM;IACR;GACF;EACF;EACA,OAAO,IAAI,MAAM,SAAS,IAAwB;CACpD;CAGA,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAGF,YAAY;GACV,aAAa;IAAE,MAAM;IAAU,aAAa;GAAwC;GACpF,QAAQ;IAAE,MAAM;IAAU,aAAa;GAAsF;GAC7H,SAAS;IAAE,MAAM;IAAU,aAAa;GAA6C;GACrF,gBAAgB;IAAE,MAAM;IAAW,aAAa;GAA8C;EAChG;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IACxB,MAAM,IAAI;IACV,MAAM,QAAQ,EAAE,SAAS,CAAC;IAC1B,MAAM,OAAO,MAAM,MAAM,OAAO,YAAY,EAAE,YAAY,IAAI;IAC9D,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,GAAG,KAAK;IAAS,CAAC;IACxE,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,CAAC,MAAM,GAAG,MAAM,KAAI,MAAK,SAAS,CAAU,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;IAAE,CAAC;GAC5F;EACF;EACA,MAAM,QAAQ,MAAM;GAClB,IAAI;IACF,MAAM,IAAI;IACV,MAAM,QAAQ,MAAM,SAAS,CAAC,CAAC,MAAM,QAAO,OACzC,EAAE,gBAAgB,KAAA,KAAa,EAAE,gBAAgB,EAAE,iBAChD,EAAE,WAAW,KAAA,KAAa,EAAE,WAAW,EAAE,YACzC,EAAE,YAAY,KAAA,KAAa,EAAE,YAAY,EAAE,aAC3C,EAAE,mBAAmB,QAAQ,EAAE,cAAc,KAAA,EAAU;IAC7D,OAAO,KAAK;KAAE,UAAU,MAAM,SAAS,CAAC,CAAC;KAAU,OAAO,MAAM,IAAI,SAAS;IAAE,CAAC;GAClF,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAEF,YAAY,EACV,IAAI;GAAE,MAAM;GAAU,UAAU;GAAM,aAAa;EAA0B,EAC/E;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IACxB,MAAM,IAAI;IACV,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,EAAE,SAAS,KAAA,IAAY,WAAW,WAAW,EAAE,IAAI;IAAE,CAAC;GACtF;EACF;EACA,MAAM,QAAQ,MAAsB;GAClC,IAAI;IACF,MAAM,EAAE,OAAO;IACf,MAAM,OAAO,MAAM,IAAI,EAAE;IACzB,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,IAAI;IACzG,OAAO,KAAK,EAAE,MAAM;KAAE,GAAG;KAAM,iBAAiB,gBAAgB,IAAI;IAAE,EAAE,CAAC;GAC3E,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAIF,YAAY;GACV,OAAO;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAwC;GAC9F,aAAa;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAmD;GAC/G,SAAS;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAmD;GAC3G,aAAa;IAAE,MAAM;IAAU,aAAa;GAAuC;GACnF,QAAQ;IAAE,MAAM;IAAU,aAAa;GAA6E;GACpH,QAAQ;IAAE,MAAM;IAAU,aAAa;GAAsE;GAC7G,WAAW;IACT,MAAM;IACN,sBAAsB;IACtB,aAAa;IACb,YAAY;KACV,MAAM;MAAE,MAAM;MAAU,aAAa;KAAqB;KAC1D,MAAM;MAAE,MAAM;MAAU,aAAa;KAA+C;IACtF;GACF;GACA,OAAO;IACL,MAAM;IACN,sBAAsB;IACtB,aAAa;IACb,YAAY;KACV,UAAU;MAAE,MAAM;MAAU,aAAa;KAAqB;KAC9D,OAAO;MAAE,MAAM;MAAU,aAAa;KAA2B;IACnE;GACF;EACF;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IAExB,MAAM,IAAIA,MAAE;IACZ,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,MAAM,KAAA,IAAY,UAAU,SAAS,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK,EAAE,QAAQ;IAAyB,CAAC;GAChI;EACF;EACA,MAAM,QAAQ,MASX,MAAe;GAChB,IAAI;IACF,MAAM,EAAE,UAAU,OAAO,IAAsB;IAC/C,MAAM,QAAQ,eAAe,KAAK,KAAK;IACvC,IAAI,WAAW,IAAI,KAAK,WAAW,MAAM,KAAA,GACvC,MAAM,IAAI,UAAU,IAAI,UAAU,uBAAuB,KAAK,aAAa;IAE7E,MAAM,UAAU,UAAU,KAAK,OAAO;IACtC,MAAM,SAAS,KAAK,WAAW,KAAA,IAAY,SAAkB,SAAS,KAAK,MAAM;IACjF,IAAI,WAAW,UAAU,WAAW,YAClC,MAAM,IAAI,UAAU,IAAI,mBAAmB,0CAA0C;IAEvF,MAAM,YAAY,mBAAmB,KAAK,aAAa,CAAC,GAAG,KAAK,IAAI,CAAC;IACrE,IAAI,KAAK,UAAU,KAAA,MAAc,OAAO,KAAK,MAAM,aAAa,YAAY,OAAO,KAAK,MAAM,UAAU,WACtG,MAAM,IAAI,UAAU,IAAI,cAAc,mDAAmD;IAE3F,MAAM,MAAM,KAAK,IAAI;IACrB,MAAM,OAAmB;KACvB,IAAI,UAAU;KACd;KACA,cAAc,KAAK,eAAe,GAAA,CAAI,KAAK;KAC3C,QAAQ,gBAAgB,KAAK,MAAM;KACnC,aAAa,KAAK;KAClB;KACA;KACA,SAAS;KACT;KACA,OAAO,KAAK;KACZ,SAAS;KACT,WAAW;KACX,WAAW;KACX,WAAW;KACX,WAAW;KACX,UAAU,CAAC;KACX,YAAY,CAAC;IACf;IACA,MAAM,MAAM,OAAO,iBAAgB,WAAU;KAC3C,OAAO,MAAM,KAAK,IAAI;KACtB,OAAO,CAAC,IAAI;IACd,CAAC;IACD,OAAO,KAAK,EAAE,MAAM,UAAU,IAAI,EAAE,CAAC;GACvC,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAEF,YAAY;GACV,IAAI;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAW;GAC9D,WAAW;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAA0D;GACpH,OAAO;IAAE,MAAM;IAAU,aAAa;GAAa;GACnD,aAAa;IAAE,MAAM;IAAU,aAAa;GAAmB;GAC/D,QAAQ;IAAE,MAAM;IAAU,aAAa;GAAwB;GAC/D,SAAS;IAAE,MAAM;IAAU,aAAa;GAA6B;GACrE,SAAS;IAAE,MAAM;IAAW,aAAa;GAAmD;EAC9F;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IAExB,MAAM,IAAIA,MAAE;IACZ,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,MAAM,KAAA,IAAY,UAAU,SAAS,EAAE,GAAG,OAAO,EAAE,QAAQ,IAAI,EAAE,OAAO;IAAI,CAAC;GAC7G;EACF;EACA,MAAM,QAAQ,MAQX,MAAe;GAChB,IAAI;IACF,MAAM,EAAE,UAAU,OAAO,IAAsB;IAC/C,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE;IAC9B,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,KAAK,IAAI;IAC9G,aAAa,MAAM,KAAK,SAAS;IACjC,IAAI,KAAK,WAAW,YAAY,MAAM,IAAI,UAAU,IAAI,mBAAmB,8BAA8B;IACzG,MAAM,OAAmB,gBAAgB,IAAI;IAC7C,IAAI,KAAK,UAAU,KAAA,GAAW,KAAK,QAAQ,eAAe,KAAK,KAAK;IACpE,IAAI,KAAK,gBAAgB,KAAA,GAAW,KAAK,cAAc,KAAK,YAAY,KAAK;IAC7E,IAAI,KAAK,WAAW,KAAA,GAAW,KAAK,SAAS,gBAAgB,KAAK,MAAM;IACxE,IAAI,KAAK,YAAY,KAAA,GAAW,KAAK,UAAU,UAAU,KAAK,OAAO;IACrE,IAAI,KAAK,YAAY,KAAA,GAAW,KAAK,UAAU,KAAK;IACpD,KAAK,UAAU,KAAK,UAAU;IAC9B,KAAK,YAAY,KAAK,IAAI;IAC1B,KAAK,YAAY;IACjB,MAAM,MAAM,OAAO,iBAAgB,WAAU;KAC3C,MAAM,IAAI,OAAO,MAAM,WAAU,MAAK,EAAE,OAAO,KAAK,EAAE;KACtD,OAAO,MAAM,KAAK;KAClB,OAAO,CAAC,IAAI;IACd,CAAC;IACD,OAAO,KAAK,EAAE,MAAM,UAAU,IAAI,EAAE,CAAC;GACvC,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAIF,YAAY;GACV,IAAI;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAW;GAC9D,QAAQ;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAiB;GACxE,WAAW;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAA4C;EACxG;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IAExB,MAAM,IAAIA,MAAE;IACZ,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,MAAM,KAAA,IAAY,UAAU,MAAM,EAAE,GAAG,OAAO,EAAE,OAAO,OAAO,EAAE,QAAQ;IAAG,CAAC;GAC5G;EACF;EACA,MAAM,QAAQ,MAAyD,MAAe;GACpF,IAAI;IACF,MAAM,EAAE,UAAU,OAAO,IAAsB;IAC/C,MAAM,KAAK,SAAS,KAAK,MAAM;IAC/B,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE;IAC9B,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,KAAK,IAAI;IAC9G,aAAa,MAAM,KAAK,SAAS;IAGjC,IAAI,OAAO,QACT,MAAM,IAAI,UAAU,IAAI,WAAW,sFAAsF;IAE3H,IAAI,CAAC,cAAc,KAAK,QAAQ,EAAE,GAChC,MAAM,IAAI,UAAU,IAAI,mBAAmB,sBAAsB,KAAK,OAAO,KAAK,IAAI;IAIxF,IAAI,KAAK,WAAW,iBAAiB,KAAK,UAAU,SAAS,WAAW,KAAK,UAAU,cAAc,MAAM,WACzG,MAAM,IAAI,UAAU,IAAI,WAAW,2BAA2B,KAAK,UAAU,UAAU,0CAA0C;IAGnI,IAAI,QAAQ,KAAK,QAAQ,EAAE;SAErB,MADe,gBAAgB,MAAM,IAAsB,MAClD,KAAK,aAChB,MAAM,IAAI,UAAU,IAAI,mBAAmB,wDAAyD;IAAA;IAGxG,MAAM,OAAmB,gBAAgB,IAAI;IAC7C,KAAK,SAAS;IACd,KAAK,UAAU,KAAK,UAAU;IAC9B,KAAK,YAAY,KAAK,IAAI;IAC1B,KAAK,YAAY;IACjB,IAAI,QAAQ,KAAK,QAAQ,EAAE,GAAG,KAAK,UAAU;IAC7C,MAAM,MAAM,OAAO,eAAc,WAAU;KACzC,MAAM,IAAI,OAAO,MAAM,WAAU,MAAK,EAAE,OAAO,KAAK,EAAE;KACtD,OAAO,MAAM,KAAK;KAClB,OAAO,CAAC,IAAI;IACd,CAAC;IACD,OAAO,KAAK,EAAE,MAAM,UAAU,IAAI,EAAE,CAAC;GACvC,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAEF,YAAY;GACV,IAAI;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAW;GAC9D,MAAM;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAgC;EACvF;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IACxB,MAAM,IAAI;IACV,MAAM,IAAI,EAAE;IACZ,MAAM,IAAI,EAAE;IACZ,IAAI,MAAM,KAAA,KAAa,MAAM,KAAA,GAAW,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM;IAAQ,CAAC;IAG/E,OAAO,CAAC;KACN,MAAM;KACN,MAAM,MAAM,EAAE,GAAG,UAAU,EAAE,GAAG,OAAO,EAAE,QAAQ,IAAI,EAAE,OAAO;IAChE,CAAC;GACH;EACF;EACA,MAAM,QAAQ,MAAoC,MAAe;GAC/D,IAAI;IACF,MAAM,EAAE,cAAc,OAAO,IAAsB;IACnD,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE;IAC9B,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,KAAK,IAAI;IAC9G,MAAM,UAAU;KACd,IAAI,aAAa;KACjB,MAAM,cAAc,KAAK,IAAI;KAC7B,SAAS;KACT,WAAW,KAAK,IAAI;KACpB,UAAU;IACZ;IACA,MAAM,OAAmB,gBAAgB,IAAI;IAC7C,KAAK,SAAS,KAAK,OAAO;IAC1B,KAAK,UAAU,KAAK,UAAU;IAC9B,KAAK,YAAY,KAAK,IAAI;IAC1B,MAAM,MAAM,OAAO,kBAAiB,WAAU;KAC5C,MAAM,IAAI,OAAO,MAAM,WAAU,MAAK,EAAE,OAAO,KAAK,EAAE;KACtD,OAAO,MAAM,KAAK;KAClB,OAAO,CAAC,IAAI;IACd,CAAC;IACD,OAAO,KAAK;KAAE;KAAS,MAAM;MAAE,IAAI,KAAK;MAAI,SAAS,KAAK;MAAS,QAAQ,KAAK;KAAO;IAAE,CAAC;GAC5F,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aAAa;EACb,YAAY,EACV,IAAI;GAAE,MAAM;GAAU,UAAU;GAAM,aAAa;EAAW,EAChE;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IAExB,MAAM,OAAOA,MAAE;IACf,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,GAAG,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM;IAAO,CAAC;IACnF,MAAM,QAAQ,KAAK,KAAI,MAAK;KAE1B,OAAO,MADK,EAAE,aAAa,KAAA,IAAY,SAAS,OAAO,EAAE,QAAQ,CAAC,CAAC,MAAM,GAAG,EAAE,MAAM,OACnE,GAAG,IAAI,KAAK,EAAE,SAAS,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE;IAChE,CAAC;IACD,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAM,MAAM,KAAK,OAAO,OAAO,MAAM,KAAK,IAAI;IAAI,CAAC;GAC7E;EACF;EACA,MAAM,QAAQ,MAAsB;GAClC,IAAI;IACF,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE;IAC9B,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,KAAK,IAAI;IAC9G,OAAO,KAAK,EAAE,UAAU,KAAK,SAAS,CAAC;GACzC,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAGjB,UAAU,KAAK,SAAS,WAAW;EACjC,MAAM;EACN,aACE;EAEF,YAAY;GACV,IAAI;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAW;GAC9D,WAAW;IAAE,MAAM;IAAU,UAAU;IAAM,aAAa;GAAyB;EACrF;EACA,QAAQ;GACN,QAAQ;GACR,SAAS,OAAO,UAAU;IAExB,OAAO,CAAC;KAAE,MAAM;KAAQ,MAAMA,MAAE,YAAY,OAAO,2BAA2B;IAAQ,CAAC;GACzF;EACF;EACA,MAAM,QAAQ,MAAyC,MAAe;GACpE,IAAI;IACF,OAAO,IAAsB;IAC7B,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE;IAC9B,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,UAAU,IAAI,UAAU,WAAW,KAAK,IAAI;IAC9E,aAAa,MAAM,KAAK,SAAS;IACjC,MAAM,OAAmB,gBAAgB,IAAI;IAC7C,KAAK,YAAY,KAAK,IAAI;IAC1B,KAAK,UAAU,KAAK,UAAU;IAC9B,MAAM,MAAM,OAAO,iBAAgB,WAAU;KAC3C,MAAM,IAAI,OAAO,MAAM,WAAU,MAAK,EAAE,OAAO,KAAK,EAAE;KACtD,OAAO,MAAM,KAAK;KAClB,OAAO,CAAC,IAAI;IACd,CAAC;IACD,OAAO,EAAE,SAAS,KAAK;GACzB,SAAS,OAAO;IAAE,KAAK,KAAK;GAAE;EAChC;CACF,CAAC,CAAC,CAAe;CAEjB,OAAO;AACT"}
package/lib/index.js ADDED
@@ -0,0 +1,91 @@
1
+ import { PROTOCOL_SECTION_NAME, TASKBOARD_PROTOCOL } from "./host/protocol-text.js";
2
+ import { dshHomePath } from "./host/sdk.js";
3
+ import { ExecutionService } from "./host/execution.js";
4
+ import { registerTaskboardRoutes } from "./host/routes.js";
5
+ import { SchedulerService } from "./host/scheduler.js";
6
+ import { TaskStore } from "./host/store.js";
7
+ import { registerTaskboardTools, workspaceFace } from "./host/tools.js";
8
+ //#region src/index.ts
9
+ /** Ledger file name under the DSH home. */
10
+ const LEDGER_FILE = "dsh-taskboard.json";
11
+ /** Cordis plugin name. */
12
+ const name = "dsh-taskboard";
13
+ /** Required host services (tool registry + prompt assembly). */
14
+ const inject = ["tools", "systemPrompt"];
15
+ /**
16
+ * Mount the host half.
17
+ * @param ctx - the plugin context (tools + systemPrompt injected).
18
+ */
19
+ function apply(ctx) {
20
+ const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) });
21
+ const now = () => Date.now();
22
+ const disposeSection = ctx.systemPrompt.section({
23
+ name: PROTOCOL_SECTION_NAME,
24
+ order: 180,
25
+ text: TASKBOARD_PROTOCOL
26
+ });
27
+ ctx.effect(() => disposeSection, "dsh-taskboard: protocol section");
28
+ ctx.inject(["workspaceRegistry"], (wsCtx) => {
29
+ const disposers = [];
30
+ disposers.push(...registerTaskboardTools(wsCtx, {
31
+ store,
32
+ workspaces: workspaceFace(wsCtx.workspaceRegistry),
33
+ now
34
+ }));
35
+ const events = { onSessionEvent: (listener) => wsCtx.on("session/event", (session, event) => {
36
+ listener(session.id, event);
37
+ }) };
38
+ wsCtx.inject(["agents"], (agentCtx) => {
39
+ const execution = new ExecutionService({
40
+ store,
41
+ agents: { create: (options) => agentCtx.agents.create(options) },
42
+ workspaces: {
43
+ get: (id) => workspaceFace(wsCtx.workspaceRegistry).get(id),
44
+ attach: async (workspaceId, sessionId) => {
45
+ const ws = wsCtx.workspaceRegistry.get(workspaceId);
46
+ if (ws !== void 0) await ws.attachSession(sessionId);
47
+ }
48
+ },
49
+ events,
50
+ now,
51
+ defaultModel: () => {
52
+ try {
53
+ const selection = agentCtx.get("agentDefaultModel");
54
+ const read = selection?.currentSelection;
55
+ return read === void 0 ? void 0 : read.call(selection);
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ });
61
+ let disposeRoutes;
62
+ agentCtx.inject(["webServer"], (webCtx) => {
63
+ disposeRoutes = registerTaskboardRoutes(webCtx, {
64
+ store,
65
+ workspaces: workspaceFace(wsCtx.workspaceRegistry),
66
+ now,
67
+ run: (taskId) => execution.run(taskId, "manual")
68
+ });
69
+ return () => disposeRoutes?.();
70
+ });
71
+ const scheduler = new SchedulerService({
72
+ store,
73
+ execution,
74
+ now
75
+ });
76
+ scheduler.start();
77
+ disposers.push(() => scheduler.dispose());
78
+ return () => {
79
+ disposeRoutes?.();
80
+ for (const dispose of disposers.splice(0)) dispose();
81
+ };
82
+ });
83
+ return () => {
84
+ for (const dispose of disposers.splice(0)) dispose();
85
+ };
86
+ });
87
+ }
88
+ //#endregion
89
+ export { LEDGER_FILE, apply, inject, name };
90
+
91
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host loader entry for dsh-taskboard.\n *\n * Wiring: the ledger store (one JSON file under the DSH home), the eight\n * `taskboard_*` agent tools, the agent workflow-protocol system-prompt\n * section, the /taskboard JSON+SSE routes (when a webServer is served),\n * the host execution service (fresh in-project sessions, pinned models), and\n * the host-side cron scheduler for scheduled tasks.\n *\n * Export shape follows the dsh-tool-todo lesson: a function/namespace plugin —\n * `name` / `inject` / `apply`, NO default export.\n *\n * @module dsh-taskboard\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only module imports: they load the cordis Context augmentations\n// (ctx.tools / ctx.systemPrompt / ctx.agents) and vanish at compile time —\n// the built host half keeps ZERO runtime @deepseek-ai imports.\nimport type {} from '@deepseek-ai/dsh-tools'\nimport type {} from '@deepseek-ai/dsh-system-prompt'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'\nimport { ExecutionService, type EventsFace } from './host/execution.ts'\nimport { registerTaskboardRoutes } from './host/routes.ts'\nimport { SchedulerService } from './host/scheduler.ts'\nimport { dshHomePath } from './host/sdk.ts'\nimport { TaskStore } from './host/store.ts'\nimport { registerTaskboardTools, workspaceFace } from './host/tools.ts'\n\n/** Ledger file name under the DSH home. */\nexport const LEDGER_FILE = 'dsh-taskboard.json'\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard'\n\n/** Required host services (tool registry + prompt assembly). */\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Mount the host half.\n * @param ctx - the plugin context (tools + systemPrompt injected).\n */\nexport function apply(ctx: Context): void {\n const store = new TaskStore({ file: dshHomePath(LEDGER_FILE) })\n const now = () => Date.now()\n\n // Agent workflow protocol (claim discipline, retry rules, done-gate).\n const disposeSection = ctx.systemPrompt.section({\n name: PROTOCOL_SECTION_NAME,\n order: PROTOCOL_SECTION_ORDER,\n text: TASKBOARD_PROTOCOL,\n })\n ctx.effect(() => disposeSection, 'dsh-taskboard: protocol section')\n\n // Tools, routes, execution, and the scheduler all come up with the\n // workspace registry (claim boundary + project execution need it).\n ctx.inject(['workspaceRegistry'], (wsCtx: Context) => {\n const disposers: Array<() => void> = []\n disposers.push(...registerTaskboardTools(wsCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n }))\n\n // Settlement listener over the session event bus.\n const events: EventsFace = {\n onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {\n listener(session.id, event as { type: string; data?: unknown })\n }),\n }\n\n wsCtx.inject(['agents'], (agentCtx: Context) => {\n const execution = new ExecutionService({\n store,\n agents: {\n create: (options): Promise<never> => agentCtx.agents.create(options as never) as Promise<never>,\n },\n workspaces: {\n get: id => workspaceFace(wsCtx.workspaceRegistry).get(id),\n attach: async (workspaceId, sessionId) => {\n const ws = wsCtx.workspaceRegistry.get(workspaceId as never)\n if (ws !== undefined) await ws.attachSession(sessionId as never)\n },\n },\n events,\n now,\n defaultModel: () => {\n try {\n const selection = agentCtx.get('agentDefaultModel') as { currentSelection?: () => { provider: string; model: string } | undefined } | undefined\n const read = selection?.currentSelection\n return read === undefined ? undefined : read.call(selection)\n } catch { return undefined }\n },\n })\n\n // /dsh-taskboard routes (the run action reaches the execution service).\n let disposeRoutes: (() => void) | undefined\n agentCtx.inject(['webServer'], (webCtx: Context) => {\n disposeRoutes = registerTaskboardRoutes(webCtx, {\n store,\n workspaces: workspaceFace(wsCtx.workspaceRegistry),\n now,\n run: (taskId: string) => execution.run(taskId, 'manual'),\n })\n return () => disposeRoutes?.()\n })\n\n // Host-side cron scheduler: due scheduled tasks execute even with no\n // browser open.\n const scheduler = new SchedulerService({ store, execution, now })\n scheduler.start()\n disposers.push(() => scheduler.dispose())\n\n return () => {\n disposeRoutes?.()\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n\n return () => {\n for (const dispose of disposers.splice(0)) dispose()\n }\n })\n}\n"],"mappings":";;;;;;;;;AA8BA,MAAa,cAAc;;AAG3B,MAAa,OAAO;;AAGpB,MAAa,SAAS,CAAC,SAAS,cAAc;;;;;AAM9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,WAAW,EAAE,CAAC;CAC9D,MAAM,YAAY,KAAK,IAAI;CAG3B,MAAM,iBAAiB,IAAI,aAAa,QAAQ;EAC9C,MAAM;EACN,OAAA;EACA,MAAM;CACR,CAAC;CACD,IAAI,aAAa,gBAAgB,iCAAiC;CAIlE,IAAI,OAAO,CAAC,mBAAmB,IAAI,UAAmB;EACpD,MAAM,YAA+B,CAAC;EACtC,UAAU,KAAK,GAAG,uBAAuB,OAAO;GAC9C;GACA,YAAY,cAAc,MAAM,iBAAiB;GACjD;EACF,CAAC,CAAC;EAGF,MAAM,SAAqB,EACzB,iBAAiB,aAAa,MAAM,GAAG,kBAAkB,SAAS,UAAU;GAC1E,SAAS,QAAQ,IAAI,KAAyC;EAChE,CAAC,EACH;EAEA,MAAM,OAAO,CAAC,QAAQ,IAAI,aAAsB;GAC9C,MAAM,YAAY,IAAI,iBAAiB;IACrC;IACA,QAAQ,EACN,SAAS,YAA4B,SAAS,OAAO,OAAO,OAAgB,EAC9E;IACA,YAAY;KACV,MAAK,OAAM,cAAc,MAAM,iBAAiB,CAAC,CAAC,IAAI,EAAE;KACxD,QAAQ,OAAO,aAAa,cAAc;MACxC,MAAM,KAAK,MAAM,kBAAkB,IAAI,WAAoB;MAC3D,IAAI,OAAO,KAAA,GAAW,MAAM,GAAG,cAAc,SAAkB;KACjE;IACF;IACA;IACA;IACA,oBAAoB;KAClB,IAAI;MACF,MAAM,YAAY,SAAS,IAAI,mBAAmB;MAClD,MAAM,OAAO,WAAW;MACxB,OAAO,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,KAAK,SAAS;KAC7D,QAAQ;MAAE;KAAiB;IAC7B;GACF,CAAC;GAGD,IAAI;GACJ,SAAS,OAAO,CAAC,WAAW,IAAI,WAAoB;IAClD,gBAAgB,wBAAwB,QAAQ;KAC9C;KACA,YAAY,cAAc,MAAM,iBAAiB;KACjD;KACA,MAAM,WAAmB,UAAU,IAAI,QAAQ,QAAQ;IACzD,CAAC;IACD,aAAa,gBAAgB;GAC/B,CAAC;GAID,MAAM,YAAY,IAAI,iBAAiB;IAAE;IAAO;IAAW;GAAI,CAAC;GAChE,UAAU,MAAM;GAChB,UAAU,WAAW,UAAU,QAAQ,CAAC;GAExC,aAAa;IACX,gBAAgB;IAChB,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACrD;EACF,CAAC;EAED,aAAa;GACX,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;EACrD;CACF,CAAC;AACH"}
@@ -0,0 +1,22 @@
1
+ //#region src/invariant.ts
2
+ /**
3
+ * Invariant companion for dsh-taskboard.
4
+ *
5
+ * DSH convention (see dsh-base / dsh-workspace): packages may ship an
6
+ * `./invariant` export — a minimal companion plugin that reserves package
7
+ * ownership in compositions that load invariants without the full plugin.
8
+ * No behavior in P0; the real plugin carries everything.
9
+ */
10
+ /** Cordis plugin name. */
11
+ const name = "dsh-taskboard-invariant";
12
+ /** No services required. */
13
+ const inject = [];
14
+ /**
15
+ * Register the invariant companion (no-op in P0).
16
+ * @param _ctx - the plugin context.
17
+ */
18
+ function apply(_ctx) {}
19
+ //#endregion
20
+ export { apply, inject, name };
21
+
22
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariant.js","names":[],"sources":["../src/invariant.ts"],"sourcesContent":["/**\n * Invariant companion for dsh-taskboard.\n *\n * DSH convention (see dsh-base / dsh-workspace): packages may ship an\n * `./invariant` export — a minimal companion plugin that reserves package\n * ownership in compositions that load invariants without the full plugin.\n * No behavior in P0; the real plugin carries everything.\n */\n\n/** Cordis plugin name. */\nexport const name = 'dsh-taskboard-invariant'\n\n/** No services required. */\nexport const inject: string[] = []\n\n/**\n * Register the invariant companion (no-op in P0).\n * @param _ctx - the plugin context.\n */\nexport function apply(_ctx: unknown): void {\n /* nothing to reserve yet */\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAa,OAAO;;AAGpB,MAAa,SAAmB,CAAC;;;;;AAMjC,SAAgB,MAAM,MAAqB,CAE3C"}
@@ -0,0 +1,9 @@
1
+ //#region src/shared/api.ts
2
+ /** Route prefix on the shared DSH webserver (same origin as the GUI). */
3
+ const ROUTE_PREFIX = "/dsh-taskboard";
4
+ /** SSE stream path (exact route; longest-prefix wins keep it disjoint). */
5
+ const SSE_PATH = "/dsh-taskboard/events";
6
+ //#endregion
7
+ export { ROUTE_PREFIX, SSE_PATH };
8
+
9
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","names":[],"sources":["../../src/shared/api.ts"],"sourcesContent":["/**\n * Wire contract for the /taskboard host routes: the JSON envelope,\n * request/response shapes, and SSE event payloads shared by the host routes\n * and the browser client.\n *\n * @module dsh-taskboard/shared/api\n */\nimport type { TaskLedger, TaskRecord, TaskSummary } from './protocol.ts'\n\nexport type { TaskRecord }\n\n/** Route prefix on the shared DSH webserver (same origin as the GUI). */\nexport const ROUTE_PREFIX = '/dsh-taskboard'\n\n/** SSE stream path (exact route; longest-prefix wins keep it disjoint). */\nexport const SSE_PATH = '/dsh-taskboard/events'\n\n/** Stable error codes (mirror the tool-level codes plus HTTP mapping). */\nexport type ApiErrorCode =\n | 'invalid_input'\n | 'not_found'\n | 'version_conflict'\n | 'invalid_transition'\n | 'forbidden'\n | 'internal'\n\n/** Success envelope. */\nexport type ApiOk<T> = { ok: true; value: T }\n\n/** Failure envelope. */\nexport type ApiFail = { ok: false; error: { code: ApiErrorCode; message: string } }\n\n/** The envelope either way. */\nexport type ApiResult<T> = ApiOk<T> | ApiFail\n\n// ---------------------------------------------------------------------------\n// payloads\n// ---------------------------------------------------------------------------\n\n/** Full-state response (the reconnect baseline after an SSE gap). */\nexport type StateResponse = TaskLedger\n\n/** Workspace listing for the UI pickers. */\nexport type WorkspaceView = { id: string; path: string; title: string; sessionCount: number }\n\n/** Create-task request body (actor is always the GUI user). */\nexport type CreateTaskBody = {\n title: string\n workspaceId: string\n urgency: string\n description?: string\n prompt?: string\n execution?: { mode?: string; cron?: string }\n model?: { provider: string; model: string }\n}\n\n/** Update-task request body (ifVersion mandatory). */\nexport type UpdateTaskBody = {\n ifVersion: number\n title?: string\n description?: string\n prompt?: string\n urgency?: string\n blocked?: boolean\n /** Rebind the task to another project (GUI owner surface only). */\n workspaceId?: string\n execution?: { mode?: string; cron?: string }\n model?: { provider: string; model: string } | null\n}\n\n/** Move-task request body (ifVersion mandatory; the user MAY move to done). */\nexport type MoveTaskBody = { ifVersion: number; status: string }\n\n/** Comment request body. */\nexport type CommentBody = { body: string }\n\n/** Delete request body (purge=true physically removes a trashed task). */\nexport type DeleteTaskBody = { ifVersion?: number; purge?: boolean }\n\n/** Run request body (P3). */\nexport type RunTaskBody = Record<string, never>\n\n/** One task (full record) response. */\nexport type TaskResponse = TaskRecord\n\n/** Summary response used by list-ish endpoints. */\nexport type SummaryResponse = { tasks: TaskSummary[] }\n\n// ---------------------------------------------------------------------------\n// SSE\n// ---------------------------------------------------------------------------\n\n/** Change frame pushed on every committed ledger mutation. */\nexport type ChangeEvent = {\n revision: number\n kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded'\n tasks: TaskSummary[]\n}\n"],"mappings":";;AAYA,MAAa,eAAe;;AAG5B,MAAa,WAAW"}
@@ -0,0 +1,279 @@
1
+ //#region src/shared/protocol.ts
2
+ /** Statuses shown as the five main board columns, in order. */
3
+ const MAIN_STATUSES = [
4
+ "backlog",
5
+ "todo",
6
+ "in_progress",
7
+ "in_review",
8
+ "done"
9
+ ];
10
+ /** Statuses collected under the secondary tab. */
11
+ const SECONDARY_STATUSES = ["canceled", "archived"];
12
+ /** Every valid status, main first. */
13
+ const ALL_STATUSES = [...MAIN_STATUSES, ...SECONDARY_STATUSES];
14
+ /**
15
+ * Legal forward/sideways transitions. Anything not listed is rejected with
16
+ * `invalid_transition`. `archived` is terminal.
17
+ */
18
+ const TRANSITIONS = {
19
+ backlog: ["todo", "canceled"],
20
+ todo: [
21
+ "in_progress",
22
+ "backlog",
23
+ "canceled"
24
+ ],
25
+ in_progress: [
26
+ "in_review",
27
+ "todo",
28
+ "canceled"
29
+ ],
30
+ in_review: [
31
+ "in_progress",
32
+ "todo",
33
+ "done",
34
+ "canceled"
35
+ ],
36
+ done: ["archived"],
37
+ canceled: ["archived", "todo"],
38
+ archived: []
39
+ };
40
+ /**
41
+ * Whether a status move is legal per the state machine.
42
+ * @param from - current status.
43
+ * @param to - requested status.
44
+ * @returns true when the transition is allowed.
45
+ */
46
+ function canTransition(from, to) {
47
+ return TRANSITIONS[from].includes(to);
48
+ }
49
+ /**
50
+ * The claim move: the one transition that transfers ownership of a task to
51
+ * the calling session. Guarded by the project (workspace) boundary in the
52
+ * tool layer.
53
+ */
54
+ function isClaim(from, to) {
55
+ return from === "todo" && to === "in_progress";
56
+ }
57
+ /** All valid urgency values. */
58
+ const URGENCIES = [
59
+ "urgent",
60
+ "normal",
61
+ "relaxed"
62
+ ];
63
+ /**
64
+ * Parse a five-field cron expression. Supported field syntax: star, star/step
65
+ * (`* / n` without spaces), a single number, an `a-b` range, and comma lists
66
+ * of those. Day-of-week accepts both 0 and 7 as Sunday (normalized to 0).
67
+ *
68
+ * @param expr - the expression to parse.
69
+ * @returns the match sets per field, or null when invalid.
70
+ */
71
+ function parseCron(expr) {
72
+ const fields = expr.trim().split(/\s+/);
73
+ if (fields.length !== 5) return null;
74
+ const ranges = [
75
+ [0, 59],
76
+ [0, 23],
77
+ [1, 31],
78
+ [1, 12],
79
+ [0, 7]
80
+ ];
81
+ const sets = [];
82
+ for (let i = 0; i < 5; i++) {
83
+ const [min, max] = ranges[i];
84
+ const set = /* @__PURE__ */ new Set();
85
+ if (!parseCronField(fields[i], min, max, set)) return null;
86
+ sets.push(set);
87
+ }
88
+ const weekdays = /* @__PURE__ */ new Set();
89
+ for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
90
+ return {
91
+ minutes: sets[0],
92
+ hours: sets[1],
93
+ days: sets[2],
94
+ months: sets[3],
95
+ weekdays
96
+ };
97
+ }
98
+ /** Parse one cron field into a match set; false on any syntax error. */
99
+ function parseCronField(field, min, max, out) {
100
+ for (const part of field.split(",")) {
101
+ const [range, stepRaw] = part.split("/");
102
+ const step = stepRaw === void 0 ? 1 : Number.parseInt(stepRaw, 10);
103
+ if (!Number.isInteger(step) || step < 1) return false;
104
+ let lo;
105
+ let hi;
106
+ if (range === void 0 || range === "") return false;
107
+ if (range === "*") {
108
+ lo = min;
109
+ hi = max;
110
+ } else if (range.includes("-")) {
111
+ const [a, b] = range.split("-");
112
+ lo = Number.parseInt(a ?? "", 10);
113
+ hi = Number.parseInt(b ?? "", 10);
114
+ if (!Number.isInteger(lo) || !Number.isInteger(hi)) return false;
115
+ } else {
116
+ lo = Number.parseInt(range, 10);
117
+ if (!Number.isInteger(lo)) return false;
118
+ hi = stepRaw === void 0 ? lo : max;
119
+ }
120
+ if (lo < min || hi > max || lo > hi) return false;
121
+ for (let v = lo; v <= hi; v += step) out.add(v);
122
+ }
123
+ return out.size > 0;
124
+ }
125
+ /**
126
+ * The next time at or after `from` matching the cron sets (local time),
127
+ * or null when no match exists within four years (e.g. Feb 30).
128
+ * @param match - parsed cron sets.
129
+ * @param from - epoch ms start point (inclusive match candidate).
130
+ * @returns the next match's epoch ms, or null.
131
+ */
132
+ function nextCronTime(match, from) {
133
+ const start = new Date(from);
134
+ start.setSeconds(0, 0);
135
+ start.setMinutes(start.getMinutes() + 1);
136
+ const cap = from + 4 * 366 * 24 * 60 * 60 * 1e3;
137
+ let t = start.getTime();
138
+ while (t <= cap) {
139
+ const d = new Date(t);
140
+ if (match.months.has(d.getMonth() + 1) && match.days.has(d.getDate()) && match.weekdays.has(d.getDay()) && match.hours.has(d.getHours()) && match.minutes.has(d.getMinutes())) return t;
141
+ t += 6e4;
142
+ }
143
+ return null;
144
+ }
145
+ /** An empty ledger. */
146
+ function emptyLedger() {
147
+ return {
148
+ schemaVersion: 1,
149
+ revision: 0,
150
+ tasks: []
151
+ };
152
+ }
153
+ /** Random base36 suffix. */
154
+ function suffix() {
155
+ return Math.random().toString(36).slice(2, 8);
156
+ }
157
+ /** Mint a task id. */
158
+ function newTaskId() {
159
+ return `t-${Date.now().toString(36)}-${suffix()}`;
160
+ }
161
+ /** Mint a comment id. */
162
+ function newCommentId() {
163
+ return `c-${Date.now().toString(36)}-${suffix()}`;
164
+ }
165
+ /** Mint an execution id. */
166
+ function newExecutionId() {
167
+ return `e-${Date.now().toString(36)}-${suffix()}`;
168
+ }
169
+ /**
170
+ * Validate and normalize a title: trimmed, 1..200 chars.
171
+ * @param raw - the raw input.
172
+ * @returns the normalized title.
173
+ * @throws when empty or too long.
174
+ */
175
+ function normalizeTitle(raw) {
176
+ const t = raw.trim();
177
+ if (t.length === 0 || t.length > 200) throw new Error("title must be 1..200 characters");
178
+ return t;
179
+ }
180
+ /**
181
+ * Validate a task prompt: trimmed, at most 8000 chars; empty becomes ''.
182
+ * @param raw - the raw input.
183
+ */
184
+ function normalizePrompt(raw) {
185
+ const t = (raw ?? "").trim();
186
+ if (t.length > 8e3) throw new Error("prompt must be at most 8000 characters");
187
+ return t;
188
+ }
189
+ /**
190
+ * Validate and normalize a comment body: trimmed, 1..4000 chars.
191
+ * @param raw - the raw input.
192
+ */
193
+ function normalizeBody(raw) {
194
+ const t = raw.trim();
195
+ if (t.length === 0 || t.length > 4e3) throw new Error("comment body must be 1..4000 characters");
196
+ return t;
197
+ }
198
+ /**
199
+ * Validate an urgency value.
200
+ * @param raw - the raw input.
201
+ */
202
+ function asUrgency(raw) {
203
+ if (!URGENCIES.includes(raw)) throw new Error(`urgency must be one of: ${URGENCIES.join(", ")}`);
204
+ return raw;
205
+ }
206
+ /**
207
+ * Validate a status value.
208
+ * @param raw - the raw input.
209
+ */
210
+ function asStatus(raw) {
211
+ if (!ALL_STATUSES.includes(raw)) throw new Error(`status must be one of: ${ALL_STATUSES.join(", ")}`);
212
+ return raw;
213
+ }
214
+ /**
215
+ * Validate an execution config request from raw tool/route input.
216
+ * `scheduled` requires a valid cron; computes the first `nextRunAt` from
217
+ * `now`.
218
+ * @param raw - raw execution input ({@link ExecutionConfig} fields, untyped).
219
+ * @param now - current epoch ms.
220
+ * @returns the normalized config.
221
+ */
222
+ function normalizeExecution(raw, now) {
223
+ const mode = raw.mode ?? "claim";
224
+ if (mode !== "claim" && mode !== "scheduled") throw new Error("execution.mode must be 'claim' or 'scheduled'");
225
+ if (mode === "claim") return { mode };
226
+ const cron = (raw.cron ?? "").trim();
227
+ const match = parseCron(cron);
228
+ if (match === null) throw new Error("execution.cron is not a valid 5-field cron expression");
229
+ const next = nextCronTime(match, now);
230
+ if (next === null) throw new Error("execution.cron never matches within 4 years");
231
+ return {
232
+ mode,
233
+ cron,
234
+ nextRunAt: next
235
+ };
236
+ }
237
+ /**
238
+ * The effective prompt of a task: explicit prompt, or title+description.
239
+ * @param task - the task.
240
+ */
241
+ function effectivePrompt(task) {
242
+ if (task.prompt.length > 0) return task.prompt;
243
+ const head = task.title;
244
+ return task.description.length > 0 ? `${head}\n\n${task.description}` : head;
245
+ }
246
+ /**
247
+ * Whether the task is currently claimed by a session (running state).
248
+ * @param task - the task.
249
+ */
250
+ function isClaimedBy(task) {
251
+ return task.status === "in_progress" && task.updatedBy.kind === "agent" ? task.updatedBy.sessionId : void 0;
252
+ }
253
+ /**
254
+ * Build the compact summary of a task.
255
+ * @param task - the task.
256
+ */
257
+ function summarize(task) {
258
+ const last = task.executions.length > 0 ? task.executions[task.executions.length - 1] : void 0;
259
+ return {
260
+ id: task.id,
261
+ title: task.title,
262
+ workspaceId: task.workspaceId,
263
+ urgency: task.urgency,
264
+ status: task.status,
265
+ blocked: task.blocked,
266
+ executionMode: task.execution.mode,
267
+ nextRunAt: task.execution.nextRunAt,
268
+ model: task.model,
269
+ version: task.version,
270
+ claimOwner: isClaimedBy(task),
271
+ commentCount: task.comments.length,
272
+ lastExecutionOutcome: last?.outcome,
273
+ trashed: task.trashedAt !== void 0
274
+ };
275
+ }
276
+ //#endregion
277
+ export { ALL_STATUSES, MAIN_STATUSES, SECONDARY_STATUSES, URGENCIES, asStatus, asUrgency, canTransition, effectivePrompt, emptyLedger, isClaim, isClaimedBy, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeExecution, normalizePrompt, normalizeTitle, parseCron, summarize };
278
+
279
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +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/** 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 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}\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 version: number\n createdAt: number\n updatedAt: number\n createdBy: Actor\n updatedBy: Actor\n comments: CommentRecord[]\n executions: ExecutionRecord[]\n /** Soft-delete marker set by agent `taskboard_delete`; user confirms the purge. */\n trashedAt?: number\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}\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 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.updatedBy.kind === 'agent'\n ? task.updatedBy.sessionId\n : undefined\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 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 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 trashed: task.trashedAt !== undefined,\n }\n}\n"],"mappings":"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"}