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.
Files changed (50) hide show
  1. package/README.md +25 -1
  2. package/lib/client.js +242 -174
  3. package/lib/host/execution.js +80 -33
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +49 -5
  6. package/lib/host/git.js.map +1 -1
  7. package/lib/host/routes.js +180 -109
  8. package/lib/host/routes.js.map +1 -1
  9. package/lib/host/scheduler.js +50 -28
  10. package/lib/host/scheduler.js.map +1 -1
  11. package/lib/host/sdk.js +7 -2
  12. package/lib/host/sdk.js.map +1 -1
  13. package/lib/host/store.js +41 -8
  14. package/lib/host/store.js.map +1 -1
  15. package/lib/host/templates.js +10 -3
  16. package/lib/host/templates.js.map +1 -1
  17. package/lib/host/tools.js +124 -93
  18. package/lib/host/tools.js.map +1 -1
  19. package/lib/index.js +3 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/shared/api.js.map +1 -1
  22. package/lib/shared/protocol.js +23 -2
  23. package/lib/shared/protocol.js.map +1 -1
  24. package/package.json +3 -2
  25. package/src/client/api.ts +19 -9
  26. package/src/client/board/ImportModal.tsx +1 -1
  27. package/src/client/board/TaskBoard.tsx +7 -38
  28. package/src/client/board/TaskCard.tsx +3 -5
  29. package/src/client/board/TaskDetail.tsx +30 -21
  30. package/src/client/board/TaskFormModal.tsx +30 -23
  31. package/src/client/board/format.ts +26 -0
  32. package/src/client/board/labels.ts +44 -0
  33. package/src/client/board-mount.tsx +9 -6
  34. package/src/client/controller.ts +60 -13
  35. package/src/client/index.ts +7 -5
  36. package/src/client/sidebar-entry.ts +16 -5
  37. package/src/client/styles.ts +5 -3
  38. package/src/host/execution.ts +90 -16
  39. package/src/host/git.ts +39 -10
  40. package/src/host/routes.ts +227 -126
  41. package/src/host/scheduler.ts +62 -36
  42. package/src/host/sdk.ts +12 -1
  43. package/src/host/store.ts +53 -7
  44. package/src/host/templates.ts +12 -3
  45. package/src/host/tools.ts +180 -123
  46. package/src/index.ts +10 -1
  47. package/src/shared/api.ts +1 -1
  48. package/src/shared/protocol.ts +35 -1
  49. package/src/shared/version.ts +1 -1
  50. package/src/client/board/NewTaskModal.tsx +0 -8
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../src/host/routes.ts"],"sourcesContent":["/**\n * /dsh-taskboard routes on the shared DSH webserver: a JSON API for the\n * GUI's human operations (create/update/move/comment/delete — actor `user`,\n * the done move IS allowed here) plus an SSE stream mirroring every\n * committed ledger mutation.\n *\n * All domain validation goes through the shared protocol pure functions; the\n * route layer only maps transport to envelope.\n *\n * @module dsh-taskboard/host/routes\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { readdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the webServer Context merge (ctx.webServer).\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport {\n asBoardSettings,\n asIsolation,\n asStatus,\n asUrgency,\n canTransition,\n checklistFromTexts,\n defaultIsolationOf,\n newCommentId,\n newTaskId,\n normalizeBody,\n normalizeChecklist,\n normalizeExecution,\n normalizeModel,\n normalizePrompt,\n normalizeTitle,\n summarize,\n syncClaim,\n validateLedgerImport,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { WORKTREE_DIR, worktreePathOf, type GitFace } from './git.ts'\nimport type { TaskTemplate } from '../shared/api.ts'\nimport type { TemplateStore } from './templates.ts'\nimport { ROUTE_PREFIX, SSE_PATH, type ApiFail, type ApiResult } from '../shared/api.ts'\nimport type { TaskStore } from './store.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Heartbeat cadence for the SSE stream. */\nconst HEARTBEAT_MS = 20_000\n\n/** How long a workspace git-detection result stays cached (fail-soft). */\nconst GIT_DETECT_TTL_MS = 60_000\n\n/** The workspaces face routes need (same narrow shape as tools). */\nexport type RoutesWorkspaceFace = WorkspaceFace\n\n/** Options. */\nexport interface TaskboardRoutesOptions {\n store: TaskStore\n workspaces: RoutesWorkspaceFace\n now: () => number\n /** Manual-run hook (the execution service); absent → 501. Options carry `reuseWorktree` (续跑). */\n run?: (taskId: string, options?: { reuseWorktree?: boolean }) => Promise<{ ok: true; executionId: string; sessionId: string } | { ok: false; error: string }>\n /** Cancel hook (the execution service); absent → 501. */\n cancel?: (taskId: string) => Promise<{ ok: true; executionId: string } | { ok: false; error: string }>\n /**\n * Registered model provider routes (from the host llm runtime), for\n * advisory validation of pinned models; undefined = runtime unavailable.\n */\n modelProviders?: () => string[] | undefined\n /** Git face for worktree actions + workspace git detection; absent → 501 on git actions. */\n git?: GitFace\n /** Task-template store (0.4.0); absent → 501 on template actions. */\n templates?: TemplateStore\n}\n\n/** Validate a template's task spec (routes-side, unknown → invalid_input). */\nfunction normalizeTemplateSpec(raw: unknown): TaskTemplate['task'] {\n if (typeof raw !== 'object' || raw === null) throw new Error('Error: invalid_input: task must be an object')\n const e = raw as Record<string, unknown>\n const spec: TaskTemplate['task'] = {}\n const str = (key: string): string | undefined => {\n const v = e[key]\n if (v === undefined) return undefined\n if (typeof v !== 'string') throw new Error(`Error: invalid_input: task.${key} must be a string`)\n return v\n }\n const title = str('title')\n const description = str('description')\n const prompt = str('prompt')\n const urgency = str('urgency')\n const isolation = str('isolation')\n const presetId = str('presetId')\n if (title !== undefined) spec.title = normalizeTitle(title)\n if (description !== undefined) spec.description = description\n if (prompt !== undefined) spec.prompt = normalizePrompt(prompt)\n if (urgency !== undefined) spec.urgency = asUrgency(urgency)\n if (isolation !== undefined) spec.isolation = asIsolation(isolation)\n if (presetId !== undefined && presetId.trim().length > 0) spec.presetId = presetId.trim()\n if (e.execution !== undefined) {\n spec.execution = normalizeExecution(e.execution as { mode?: string; cron?: string }, Date.now())\n }\n if (e.model !== undefined) spec.model = normalizeModel(e.model)\n if (e.checklist !== undefined) {\n if (!Array.isArray(e.checklist) || e.checklist.some(c => typeof c !== 'string')) {\n throw new Error('Error: invalid_input: task.checklist must be an array of strings')\n }\n checklistFromTexts(e.checklist as string[]) // validates count + texts\n spec.checklist = e.checklist as string[]\n }\n return spec\n}\n\n/** Validate a pinned model: structural check always, provider route when known. */\nfunction checkModel(raw: unknown, modelProviders?: () => string[] | undefined): TaskModel {\n const model = normalizeModel(raw)\n const providers = modelProviders?.()\n if (providers !== undefined && !providers.includes(model.provider)) {\n throw new Error(`Error: invalid_input: model provider \"${model.provider}\" has no registered route (available: ${providers.join(', ')})`)\n }\n return model\n}\n\n/** JSON-envelope writer. */\nfunction json(res: ServerResponse, payload: ApiResult<unknown>, status = 200): void {\n const body = JSON.stringify(payload)\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(body)\n}\n\n/** Domain failure → envelope + HTTP status. */\nfunction fail(code: ApiFail['error']['code'], message: string): { res: ApiFail; status: number } {\n const status = code === 'invalid_input' || code === 'invalid_transition' ? 400\n : code === 'not_found' ? 404\n : code === 'version_conflict' ? 409\n : code === 'forbidden' ? 403\n : 500\n return { res: { ok: false, error: { code, message } }, status }\n}\n\n/** Read one JSON body (null on parse failure). */\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n if (chunks.length === 0) return {}\n try {\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : null\n } catch {\n return null\n }\n}\n\n/** String field accessor (null when absent/not a string). */\nfunction str(body: Record<string, unknown>, key: string): string | null {\n const v = body[key]\n return typeof v === 'string' ? v : null\n}\n\n/** Number field accessor (undefined when absent; null when present but not a number). */\nfunction num(body: Record<string, unknown>, key: string): number | undefined | null {\n const v = body[key]\n if (v === undefined) return undefined\n return typeof v === 'number' && Number.isFinite(v) ? v : null\n}\n\n/** Normalize an agent preset id: trimmed, non-empty; empty string → undefined. */\nfunction normalizePresetId(raw: string | null): string | undefined {\n const t = (raw ?? '').trim()\n return t.length === 0 ? undefined : t\n}\n\n/** Map a thrown domain error to the envelope. */\nfunction toFail(error: unknown): { res: ApiFail; status: number } {\n const message = error instanceof Error ? error.message : String(error)\n const code = message.startsWith('Error: ') ? message.slice(7).split(':')[0] : undefined\n const known: ApiFail['error']['code'][] = ['invalid_input', 'not_found', 'version_conflict', 'invalid_transition', 'forbidden', 'internal']\n if (code !== undefined && (known as string[]).includes(code)) {\n return fail(code as ApiFail['error']['code'], message.slice(7 + code.length + 2))\n }\n if (code === 'workspace_mismatch') return fail('forbidden', message.slice(7 + code.length + 2))\n return fail('invalid_input', message)\n}\n\n/**\n * Register the taskboard routes.\n * @param ctx - context carrying the webServer service.\n * @param options - store + workspaces + clock.\n * @returns the disposer.\n */\nexport function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOptions): () => void {\n const { store, workspaces } = options\n const subscribers = new Set<ServerResponse>()\n let heartbeat: NodeJS.Timeout | undefined\n\n const broadcast = (change: { revision: number; kind: string; tasks: readonly TaskRecord[] }): void => {\n const frame = `event: change\\ndata: ${JSON.stringify({ revision: change.revision, kind: change.kind, tasks: change.tasks.map(summarize) })}\\n\\n`\n for (const res of subscribers) res.write(frame)\n }\n store.subscribe(broadcast)\n\n // Workspace git detection, TTL-cached and fail-soft (false on any error):\n // feeds the create-form isolation toggle and the diagnostics panel.\n const gitCache = new Map<string, { value: boolean; at: number }>()\n const gitHinted = new Set<string>()\n\n /** Whether <root>/.gitignore (missing file counts as missing) ignores our worktree dir. */\n const gitignoreMissing = async (path: string): Promise<boolean> => {\n try {\n const { readFile } = await import('node:fs/promises')\n const ignore = await readFile(join(path, '.gitignore'), 'utf8')\n return !ignore.split('\\n').some(l => {\n const t = l.trim().replace(/\\/+$/, '')\n return t === WORKTREE_DIR || t === `/${WORKTREE_DIR}`\n })\n } catch {\n return true // no .gitignore at all (or unreadable) → suggest creating one\n }\n }\n\n const gitAvailable = async (path: string): Promise<boolean> => {\n if (options.git === undefined) return false\n const hit = gitCache.get(path)\n if (hit !== undefined && options.now() - hit.at < GIT_DETECT_TTL_MS) return hit.value\n let value = false\n try {\n value = await options.git.detect(path)\n } catch { /* fail-soft → false */ }\n gitCache.set(path, { value, at: options.now() })\n // gitignore 建议 (plan §3.2): suggest (never write) ignoring our\n // worktree directory, once per workspace per host run.\n if (value && !gitHinted.has(path)) {\n gitHinted.add(path)\n if (await gitignoreMissing(path)) {\n console.info(`[dsh-taskboard] 建议在 ${path}/.gitignore 加入一行 ${WORKTREE_DIR}/ 以隐藏任务 worktree 目录(不会自动修改)`)\n }\n }\n return value\n }\n\n /** List orphan worktree dirs: entries under <ws>/.dsh-worktrees owned by no ledger task. */\n const listOrphanWorktrees = async (): Promise<Array<{ workspaceId: string; workspacePath: string; taskId: string; path: string }>> => {\n const orphans: Array<{ workspaceId: string; workspacePath: string; taskId: string; path: string }> = []\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n for (const ws of workspaces.list()) {\n let entries: string[] = []\n try {\n const dirents = await readdir(join(ws.path, WORKTREE_DIR), { withFileTypes: true })\n entries = dirents.filter(e => e.isDirectory()).map(e => e.name)\n } catch { /* no worktrees dir → nothing to do */ }\n for (const taskId of entries) {\n if (!known.has(taskId)) orphans.push({ workspaceId: ws.id, workspacePath: ws.path, taskId, path: worktreePathOf(ws.path, taskId) })\n }\n }\n return orphans\n }\n\n /** Git-enabled workspaces whose .gitignore does not cover the worktree dir. */\n const listGitignoreSuggestions = async (): Promise<Array<{ workspaceId: string; workspacePath: string }>> => {\n const suggestions: Array<{ workspaceId: string; workspacePath: string }> = []\n for (const ws of workspaces.list()) {\n if (!(await gitAvailable(ws.path))) continue\n if (await gitignoreMissing(ws.path)) suggestions.push({ workspaceId: ws.id, workspacePath: ws.path })\n }\n return suggestions\n }\n\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const url = new URL(req.url ?? '/', 'http://x')\n const pathname = url.pathname\n\n // ---------------------------------------------------------------- GET\n if (req.method === 'GET') {\n if (pathname === `${ROUTE_PREFIX}/state`) {\n await store.load()\n json(res, { ok: true, value: store.snapshot() })\n return\n }\n if (pathname === `${ROUTE_PREFIX}/workspaces`) {\n const list = workspaces.list()\n const flags = await Promise.all(list.map(ws => gitAvailable(ws.path)))\n json(res, {\n ok: true,\n value: list.map((ws, i) => ({ ...ws, sessionCount: 0, gitAvailable: flags[i] })),\n })\n return\n }\n if (pathname === `${ROUTE_PREFIX}/diagnostics`) {\n const ledger = store.snapshot()\n let staleRunning = 0\n for (const t of ledger.tasks) {\n for (const e of t.executions) if (e.outcome === 'running') staleRunning += 1\n }\n json(res, {\n ok: true,\n value: {\n revision: ledger.revision,\n tasks: ledger.tasks.length,\n staleRunning,\n orphanWorktrees: await listOrphanWorktrees(),\n gitIgnoreSuggestions: await listGitignoreSuggestions(),\n },\n })\n return\n }\n // Diff viewer (0.4.0): read-only git show/diff for one execution's\n // commit or changed path. Prefers the live worktree (uncommitted\n // view), falls back to the main repo.\n const diffMatch = pathname.match(new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)/diff$`))\n if (diffMatch !== null) {\n try {\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n const task = store.get(diffMatch[1]!)\n if (task === undefined) throw new Error('Error: not_found: no such task')\n const execution = task.executions.find(e => e.id === url.searchParams.get('execution'))\n if (execution === undefined) throw new Error('Error: not_found: no such execution')\n const commit = url.searchParams.get('commit')\n const filePath = url.searchParams.get('path')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n const cwd = execution.worktreePath ?? ws.path\n let result = commit !== null\n ? await options.git.showCommit(cwd, commit)\n : filePath !== null ? await options.git.showPathDiff(cwd, filePath, execution.baseCommit) : undefined\n // Fallback: the worktree may be gone — commits and committed\n // ranges still resolve in the main repo.\n if (result === undefined && execution.worktreePath !== undefined && cwd !== ws.path) {\n result = commit !== null\n ? await options.git.showCommit(ws.path, commit)\n : filePath !== null && execution.baseCommit !== undefined\n ? await options.git.showPathDiff(ws.path, filePath, execution.baseCommit)\n : undefined\n }\n if (result === undefined) {\n throw new Error('Error: invalid_input: 无法获取 diff(git 报错、对象不存在,或仅存于已删除的 worktree 且无基线)')\n }\n json(res, { ok: true, value: { diff: result.text, truncated: result.truncated } })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // Templates listing (0.4.0).\n if (pathname === `${ROUTE_PREFIX}/templates`) {\n if (options.templates === undefined) {\n const f = fail('invalid_input', 'template store unavailable')\n json(res, f.res, 501)\n return\n }\n json(res, { ok: true, value: { templates: await options.templates.list() } })\n return\n }\n\n // Board settings (0.5.0): absent fields follow factory defaults.\n if (pathname === `${ROUTE_PREFIX}/settings`) {\n await store.load()\n json(res, { ok: true, value: store.snapshot().settings ?? {} })\n return\n }\n\n const taskMatch = pathname.match(new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)$`))\n if (taskMatch !== null) {\n const task = store.get(taskMatch[1]!)\n if (task === undefined) { const f = fail('not_found', 'no such task'); json(res, f.res, f.status); return }\n json(res, { ok: true, value: task })\n return\n }\n res.writeHead(404)\n res.end()\n return\n }\n\n if (req.method !== 'POST') {\n res.writeHead(405)\n res.end()\n return\n }\n // CSRF fence: cross-site simple requests cannot set application/json.\n const contentType = req.headers['content-type'] ?? ''\n if (!contentType.toLowerCase().startsWith('application/json')) {\n const f = fail('invalid_input', 'content-type must be application/json')\n json(res, f.res, 415)\n return\n }\n const body = await readBody(req)\n if (body === null) {\n const f = fail('invalid_input', 'body is not a JSON object')\n json(res, f.res, 400)\n return\n }\n\n // ------------------------------------------------- POST /tasks (create)\n if (pathname === `${ROUTE_PREFIX}/tasks`) {\n try {\n const title = normalizeTitle(str(body, 'title') ?? '')\n const workspaceId = str(body, 'workspaceId') ?? ''\n if (workspaces.get(workspaceId) === undefined) throw new Error('Error: not_found: unknown workspace')\n const urgency = asUrgency(str(body, 'urgency') ?? '')\n const status = str(body, 'status') === null ? 'todo' as const : asStatus(str(body, 'status')!)\n const execution = normalizeExecution((body.execution as { mode?: string; cron?: string } | undefined) ?? {}, options.now())\n const model = body.model === undefined ? undefined : checkModel(body.model, options.modelProviders)\n const isolationRaw = str(body, 'isolation')\n // 0.5.0: an omitted isolation is MATERIALIZED from the board\n // setting (看板设置) at creation, so later setting changes never\n // rewrite existing tasks.\n const isolation = isolationRaw === null ? defaultIsolationOf(store.snapshot().settings) : asIsolation(isolationRaw)\n const presetId = normalizePresetId(str(body, 'presetId'))\n let checklist: TaskRecord['checklist'] = undefined\n if (body.checklist !== undefined) {\n if (!Array.isArray(body.checklist) || body.checklist.some(c => typeof c !== 'string')) {\n throw new Error('Error: invalid_input: checklist must be an array of strings')\n }\n const texts = (body.checklist as string[]).map(c => c.trim()).filter(c => c.length > 0)\n if (texts.length > 0) checklist = checklistFromTexts(texts)\n }\n const now = options.now()\n const task: TaskRecord = {\n id: newTaskId(),\n title,\n description: (str(body, 'description') ?? '').trim(),\n prompt: normalizePrompt(str(body, 'prompt') ?? undefined),\n workspaceId,\n urgency,\n status,\n blocked: false,\n execution,\n model,\n isolation,\n ...(presetId !== undefined ? { presetId } : {}),\n ...(checklist !== undefined ? { checklist } : {}),\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'user' },\n updatedBy: { kind: 'user' },\n comments: [],\n executions: [],\n }\n await store.mutate('task-created', ledger => {\n ledger.tasks.push(task)\n return [task]\n })\n json(res, { ok: true, value: summarize(task) }, 201)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------- POST /tasks/:id/{action}\n // (\\w+ after the id would not match hyphenated actions like\n // worktree-remove, hence the explicit class.)\n const actionMatch = pathname.match(new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)/([\\\\w-]+)$`))\n if (actionMatch !== null) {\n const id = actionMatch[1]!\n const action = actionMatch[2]!\n try {\n const task = store.get(id)\n if (task === undefined) throw new Error('Error: not_found: no such task')\n if (action === 'update') {\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n const next = structuredClone(task)\n const title = str(body, 'title')\n if (title !== null) next.title = normalizeTitle(title)\n const description = str(body, 'description')\n if (description !== null) next.description = description.trim()\n const prompt = str(body, 'prompt')\n if (prompt !== null) next.prompt = normalizePrompt(prompt)\n const urgency = str(body, 'urgency')\n if (urgency !== null) next.urgency = asUrgency(urgency)\n // GUI-only rebind to another project; validated against the workspace registry.\n const workspaceId = str(body, 'workspaceId')\n if (workspaceId !== null) {\n if (workspaces.get(workspaceId) === undefined) throw new Error('Error: not_found: unknown workspace')\n next.workspaceId = workspaceId\n }\n if (typeof body.blocked === 'boolean') next.blocked = body.blocked\n // The GUI (task owner surface) may edit model/execution; null clears the model.\n if (body.execution !== undefined) next.execution = normalizeExecution(body.execution as { mode?: string; cron?: string }, options.now())\n if (body.model === null) next.model = undefined\n else if (body.model !== undefined) next.model = checkModel(body.model, options.modelProviders)\n // Isolation may change only before the first execution (分支与基线\n // 取决于该选择 — plan §3.1: 执行开始后锁定).\n const isolationRaw = str(body, 'isolation')\n if (isolationRaw !== null) {\n if (task.executions.length > 0 || task.status === 'in_progress') {\n throw new Error('Error: invalid_input: isolation 已锁定(任务已有执行记录),不可修改')\n }\n next.isolation = asIsolation(isolationRaw)\n }\n // Preset may change any time: each run composes fresh.\n if (body.presetId === null) delete next.presetId\n else if (body.presetId !== undefined) next.presetId = normalizePresetId(str(body, 'presetId'))!\n // Checklist (0.4.0): the GUI replaces the whole list; null clears.\n if (body.checklist === null) delete next.checklist\n else if (body.checklist !== undefined) {\n const items = normalizeChecklist(body.checklist)\n if (items.length > 0) next.checklist = items\n else delete next.checklist\n }\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n await store.mutate('task-updated', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next) })\n return\n }\n if (action === 'move') {\n const ifVersion = num(body, 'ifVersion')\n const status = str(body, 'status') ?? ''\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n const to = asStatus(status)\n if (!canTransition(task.status, to)) throw new Error(`Error: invalid_transition: illegal transition ${task.status} → ${to}`)\n const next = structuredClone(task)\n next.status = to\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n if (task.status === 'todo' && to === 'in_progress') next.blocked = false\n // A user move records no holder; leaving in_progress releases any hold.\n syncClaim(next, to, options.now())\n await store.mutate('task-moved', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next) })\n return\n }\n if (action === 'reject') {\n // Card quick-reject: back to todo + optional user comment in one\n // atomic mutation (a failed move never strands an orphan comment).\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n if (!canTransition(task.status, 'todo')) throw new Error(`Error: invalid_transition: illegal transition ${task.status} → todo`)\n const next = structuredClone(task)\n next.status = 'todo'\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n syncClaim(next, 'todo', options.now())\n const commentText = str(body, 'body') ?? ''\n if (commentText.trim().length > 0) {\n next.comments.push({ id: newCommentId(), body: normalizeBody(commentText), version: 1, createdAt: options.now() })\n }\n await store.mutate('task-moved', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next) })\n return\n }\n if (action === 'comment') {\n const bodyText = str(body, 'body') ?? ''\n const comment = { id: newCommentId(), body: normalizeBody(bodyText), version: 1, createdAt: options.now() }\n const next = structuredClone(task)\n next.comments.push(comment)\n next.version = task.version + 1\n next.updatedAt = options.now()\n await store.mutate('comment-added', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: comment }, 201)\n return\n }\n if (action === 'delete') {\n const purge = body.purge === true\n if (purge) {\n if (task.trashedAt === undefined) throw new Error('Error: invalid_input: purge requires a trashed task (soft-delete first)')\n // Worktree safety before purge (plan §3.3, 0.3.1): refuse while\n // uncommitted work remains; otherwise clean the worktree and\n // the task branch along with the ledger entry.\n if (options.git !== undefined) {\n const ws = workspaces.get(task.workspaceId)\n if (ws !== undefined) {\n const path = worktreePathOf(ws.path, id)\n try {\n await options.git.removeWorktree(ws.path, path)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (message.includes('未提交修改')) {\n throw new Error(`Error: invalid_input: ${message};请先处理这些改动(提交、续跑或手动保存)再物理清除任务`)\n }\n if (/not a working tree|not a working-tree/i.test(message)) {\n // An unregistered leftover dir: plain fs removal.\n await rm(path, { recursive: true, force: true })\n } else {\n throw new Error(`Error: invalid_input: ${message}`)\n }\n }\n if (task.branch !== undefined) {\n try {\n await options.git.deleteBranch(ws.path, task.branch)\n } catch { /* best effort: the branch may outlive the task */ }\n }\n }\n }\n await store.mutate('task-deleted', ledger => {\n ledger.tasks = ledger.tasks.filter(t => t.id !== id)\n return []\n })\n json(res, { ok: true, value: { purged: true } })\n return\n }\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n const next = structuredClone(task)\n next.trashedAt = options.now()\n next.version = task.version + 1\n await store.mutate('task-deleted', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: { trashed: true } })\n return\n }\n if (action === 'run') {\n if (options.run === undefined) {\n const f = fail('invalid_input', 'execution service unavailable')\n json(res, f.res, 501)\n return\n }\n // `reuse: true` = 续跑: keep a live worktree/branch as-is instead\n // of resetting to a fresh baseline (0.3.1).\n const runOptions = body.reuse === true ? { reuseWorktree: true } : undefined\n const result = await options.run(id, runOptions)\n if (result.ok) json(res, { ok: true, value: result }, 202)\n else {\n const f = fail('invalid_input', result.error)\n json(res, f.res, f.status)\n }\n return\n }\n if (action === 'cancel') {\n if (options.cancel === undefined) {\n const f = fail('invalid_input', 'execution service unavailable')\n json(res, f.res, 501)\n return\n }\n const result = await options.cancel(id)\n if (result.ok) json(res, { ok: true, value: { cancelled: true, executionId: result.executionId } }, 202)\n else {\n const f = fail('invalid_input', result.error)\n json(res, f.res, f.status)\n }\n return\n }\n if (action === 'merge') {\n // ⇥ 合并 (detail page, user-only): merge the task branch into the\n // main worktree with --no-ff; conflicts are reported verbatim.\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n if (task.branch === undefined) throw new Error('Error: invalid_input: 该任务还没有 worktree 分支(未隔离执行过)')\n if (task.status === 'in_progress') throw new Error('Error: invalid_input: 任务执行中,不能合并')\n if (task.executions.some(e => e.outcome === 'running')) throw new Error('Error: invalid_input: 任务执行中,不能合并')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n // No-op detection (0.3.1): a branch with no commits over HEAD\n // merges as \"already up to date\" — report that instead of landing\n // a bogus 已合并 comment.\n let noop = false\n try {\n noop = await options.git.isAncestor(ws.path, task.branch)\n } catch { /* fail-soft: proceed to the real merge */ }\n if (noop) {\n json(res, { ok: true, value: { merged: false, noop: true, branch: task.branch } })\n return\n }\n try {\n await options.git.merge(ws.path, task.branch)\n } catch (error) {\n throw new Error(`Error: invalid_input: ${error instanceof Error ? error.message : String(error)}`)\n }\n const mergedComment = { id: newCommentId(), body: normalizeBody(`[系统] 分支 ${task.branch} 已合并到主工作区(--no-ff)。`), version: 1, createdAt: options.now() }\n const next = structuredClone(task)\n next.comments.push(mergedComment)\n next.version = task.version + 1\n next.updatedAt = options.now()\n await store.mutate('comment-added', ledger => {\n const i = ledger.tasks.findIndex(t => t.id === id)\n ledger.tasks[i] = next\n return [next]\n })\n json(res, { ok: true, value: { merged: true, branch: task.branch } })\n return\n }\n if (action === 'worktree-remove') {\n // 🗑 删除 worktree (detail page): refuses uncommitted changes;\n // optionally deletes the task branch after the worktree is gone.\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n if (task.executions.some(e => e.outcome === 'running')) throw new Error('Error: invalid_input: 任务执行中,不能删除 worktree')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n const path = worktreePathOf(ws.path, id)\n try {\n await options.git.removeWorktree(ws.path, path)\n } catch (error) {\n throw new Error(`Error: invalid_input: ${error instanceof Error ? error.message : String(error)}`)\n }\n let branchDeleted = false\n let branchError: string | undefined\n if (body.deleteBranch === true && task.branch !== undefined) {\n try {\n await options.git.deleteBranch(ws.path, task.branch)\n branchDeleted = true\n } catch (error) {\n branchError = error instanceof Error ? error.message : String(error)\n }\n }\n json(res, { ok: true, value: { removed: true, branchDeleted, ...(branchError !== undefined ? { branchError } : {}) } })\n return\n }\n const f = fail('not_found', `unknown action ${action}`)\n json(res, f.res, f.status)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // -------------------------------------- POST /worktree-cleanup (⚙ 诊断)\n if (pathname === `${ROUTE_PREFIX}/worktree-cleanup`) {\n try {\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n const workspaceId = str(body, 'workspaceId') ?? ''\n const taskId = str(body, 'taskId') ?? ''\n const ws = workspaces.get(workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n // Only dirs owned by NO ledger task may be cleaned here; live tasks\n // remove their worktree from the detail page.\n if (store.get(taskId) !== undefined) throw new Error('Error: invalid_input: 任务仍在看板中,请从任务详情页删除其 worktree')\n const path = worktreePathOf(ws.path, taskId)\n try {\n await options.git.removeWorktree(ws.path, path)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // An unregistered leftover (git no longer knows this worktree):\n // fall back to direct fs removal — the dir lives inside the\n // plugin's own .dsh-worktrees scope.\n if (/not a working tree|not a working-tree/i.test(message)) {\n await rm(path, { recursive: true, force: true })\n } else {\n throw new Error(`Error: invalid_input: ${message}`)\n }\n }\n json(res, { ok: true, value: { cleaned: true, path } })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ---------------------------------------------- POST /import/preview\n // (0.4.0) Dry-run: classify every task in the uploaded ledger file\n // against the live one; nothing is written.\n if (pathname === `${ROUTE_PREFIX}/import/preview`) {\n try {\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n const plan = validateLedgerImport(body, known, options.now())\n json(res, {\n ok: true,\n value: {\n plan: {\n create: plan.create.map(t => ({ id: t.id, title: t.title, status: t.status })),\n overwrite: plan.overwrite.map(t => ({ id: t.id, title: t.title, status: t.status })),\n invalid: plan.invalid,\n },\n },\n })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------------------ POST /import\n // (0.4.0) Commit an import. mode=merge upserts (create + overwrite by\n // id); mode=replace swaps the WHOLE ledger (invalid entries dropped)\n // after writing a timestamped backup of the current one.\n if (pathname === `${ROUTE_PREFIX}/import`) {\n try {\n const mode = str(body, 'mode') === 'replace' ? 'replace' as const : 'merge' as const\n const raw = body.ledger\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n const plan = validateLedgerImport(raw, known, options.now())\n const imported = [...plan.create, ...plan.overwrite]\n if (mode === 'replace' && imported.length === 0) {\n throw new Error('Error: invalid_input: 导入文件没有可导入的任务,已拒绝整册替换')\n }\n let backupFile: string | undefined\n if (mode === 'replace' && store.snapshot().tasks.length > 0) {\n backupFile = await store.backup()\n }\n let replacedTotal: number | undefined\n await store.mutate('task-created', ledger => {\n if (mode === 'replace') {\n replacedTotal = ledger.tasks.length\n ledger.tasks = structuredClone(imported)\n // Replace is a whole-ledger swap (0.5.0): board settings ride\n // along when the file carries them; merge keeps the live ones.\n if (plan.settings !== undefined) ledger.settings = structuredClone(plan.settings)\n else delete ledger.settings\n return ledger.tasks\n }\n const byId = new Map(ledger.tasks.map(t => [t.id, t]))\n for (const task of imported) byId.set(task.id, structuredClone(task))\n ledger.tasks = [...byId.values()]\n return structuredClone(imported)\n })\n json(res, {\n ok: true,\n value: {\n mode,\n created: plan.create.length,\n overwritten: plan.overwrite.length,\n ...(mode === 'replace' ? { replacedTotal } : {}),\n ...(backupFile !== undefined ? { backupFile } : {}),\n },\n })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------- POST /templates (+delete)\n if (pathname === `${ROUTE_PREFIX}/templates` || pathname === `${ROUTE_PREFIX}/templates/delete`) {\n try {\n if (options.templates === undefined) {\n const f = fail('invalid_input', 'template store unavailable')\n json(res, f.res, 501)\n return\n }\n if (pathname.endsWith('/delete')) {\n const id = str(body, 'id') ?? ''\n if (id.length === 0) throw new Error('Error: invalid_input: id required')\n const deleted = await options.templates.remove(id)\n json(res, { ok: true, value: { deleted } })\n return\n }\n const name = str(body, 'name') ?? ''\n if (name.trim().length === 0) throw new Error('Error: invalid_input: name required')\n const template = await options.templates.upsert({\n id: str(body, 'id') ?? undefined,\n name,\n task: normalizeTemplateSpec(body.task),\n })\n json(res, { ok: true, value: template }, 201)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------ POST /settings/update\n // (0.5.0) Whole-object replace semantics: omitted fields fall back to\n // their factory defaults. Affects only tasks created AFTER the change.\n if (pathname === `${ROUTE_PREFIX}/settings/update`) {\n try {\n const next = asBoardSettings(body)\n await store.mutate('settings-updated', ledger => {\n ledger.settings = next\n return []\n })\n json(res, { ok: true, value: next })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n res.writeHead(404)\n res.end()\n } catch (error) {\n const f = fail('internal', error instanceof Error ? error.message : String(error))\n json(res, f.res, f.status)\n }\n }\n\n const sse = (req: IncomingMessage, res: ServerResponse): void => {\n res.writeHead(200, {\n 'content-type': 'text/event-stream; charset=utf-8',\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n })\n res.write('retry: 2000\\n\\n')\n // Baseline frame: the client reconciles by revision and refetches state on gaps.\n res.write(`event: hello\\ndata: ${JSON.stringify({ revision: store.snapshot().revision })}\\n\\n`)\n subscribers.add(res)\n if (heartbeat === undefined) {\n heartbeat = setInterval(() => {\n for (const current of subscribers) current.write(': ping\\n\\n')\n }, HEARTBEAT_MS)\n }\n req.on('close', () => {\n subscribers.delete(res)\n if (subscribers.size === 0 && heartbeat !== undefined) {\n clearInterval(heartbeat)\n heartbeat = undefined\n }\n })\n }\n\n const disposers = [\n ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }),\n ctx.webServer.register({ kind: 'exact', path: SSE_PATH, handler: sse }),\n ]\n return () => {\n for (const dispose of disposers) dispose()\n if (heartbeat !== undefined) clearInterval(heartbeat)\n for (const res of subscribers) res.end()\n subscribers.clear()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../src/host/routes.ts"],"sourcesContent":["/**\n * /dsh-taskboard routes on the shared DSH webserver: a JSON API for the\n * GUI's human operations (create/update/move/comment/delete — actor `user`,\n * the done move IS allowed here) plus an SSE stream mirroring every\n * committed ledger mutation.\n *\n * All domain validation goes through the shared protocol pure functions; the\n * route layer only maps transport to envelope.\n *\n * @module dsh-taskboard/host/routes\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { readdir, rm } from 'node:fs/promises'\nimport { join, resolve, sep } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the webServer Context merge (ctx.webServer).\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport {\n asBoardSettings,\n asIsolation,\n asStatus,\n asUrgency,\n canTransition,\n checklistFromTexts,\n defaultIsolationOf,\n newCommentId,\n newTaskId,\n normalizeBody,\n normalizeChecklist,\n normalizeExecution,\n normalizeModel,\n normalizePrompt,\n normalizeTitle,\n summarize,\n syncClaim,\n validateLedgerImport,\n type TaskLedger,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { WORKTREE_DIR, worktreePathOf, type GitFace } from './git.ts'\nimport type { TaskTemplate } from '../shared/api.ts'\nimport type { TemplateStore } from './templates.ts'\nimport { ROUTE_PREFIX, SSE_PATH, type ApiFail, type ApiResult } from '../shared/api.ts'\nimport type { TaskStore } from './store.ts'\nimport { ERR, ToolError } from './tools.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Heartbeat cadence for the SSE stream. */\nconst HEARTBEAT_MS = 20_000\n\n/** Max accepted JSON body bytes (S8: unbounded buffering is a local OOM vector). */\nconst MAX_BODY_BYTES = 5 * 1024 * 1024\n\n/** Route shapes (T2: compiled once at module load, not on every request). */\nconst TASK_DIFF_RE = new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)/diff$`)\nconst TASK_RE = new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)$`)\nconst TASK_ACTION_RE = new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)/([\\\\w-]+)$`)\n\n/** How long a workspace git-detection result stays cached (fail-soft). */\nconst GIT_DETECT_TTL_MS = 60_000\n\n/** The workspaces face routes need (same narrow shape as tools). */\nexport type RoutesWorkspaceFace = WorkspaceFace\n\n/** Options. */\nexport interface TaskboardRoutesOptions {\n store: TaskStore\n workspaces: RoutesWorkspaceFace\n now: () => number\n /** Manual-run hook (the execution service); absent → 501. Options carry `reuseWorktree` (续跑). */\n run?: (taskId: string, options?: { reuseWorktree?: boolean }) => Promise<{ ok: true; executionId: string; sessionId: string } | { ok: false; error: string }>\n /** Cancel hook (the execution service); absent → 501. */\n cancel?: (taskId: string) => Promise<{ ok: true; executionId: string } | { ok: false; error: string }>\n /**\n * Registered model provider routes (from the host llm runtime), for\n * advisory validation of pinned models; undefined = runtime unavailable.\n */\n modelProviders?: () => string[] | undefined\n /** Git face for worktree actions + workspace git detection; absent → 501 on git actions. */\n git?: GitFace\n /** Task-template store (0.4.0); absent → 501 on template actions. */\n templates?: TemplateStore\n}\n\n/** Validate a template's task spec (routes-side, unknown → invalid_input). */\nfunction normalizeTemplateSpec(raw: unknown, now: number): TaskTemplate['task'] {\n if (typeof raw !== 'object' || raw === null) throw new Error('Error: invalid_input: task must be an object')\n const e = raw as Record<string, unknown>\n const spec: TaskTemplate['task'] = {}\n const str = (key: string): string | undefined => {\n const v = e[key]\n if (v === undefined) return undefined\n if (typeof v !== 'string') throw new Error(`Error: invalid_input: task.${key} must be a string`)\n return v\n }\n const title = str('title')\n const description = str('description')\n const prompt = str('prompt')\n const urgency = str('urgency')\n const isolation = str('isolation')\n const presetId = str('presetId')\n if (title !== undefined) spec.title = normalizeTitle(title)\n if (description !== undefined) spec.description = description\n if (prompt !== undefined) spec.prompt = normalizePrompt(prompt)\n if (urgency !== undefined) spec.urgency = asUrgency(urgency)\n if (isolation !== undefined) spec.isolation = asIsolation(isolation)\n if (presetId !== undefined && presetId.trim().length > 0) spec.presetId = presetId.trim()\n if (e.execution !== undefined) {\n spec.execution = normalizeExecution(e.execution as { mode?: string; cron?: string }, now)\n }\n if (e.model !== undefined) spec.model = normalizeModel(e.model)\n if (e.checklist !== undefined) {\n if (!Array.isArray(e.checklist) || e.checklist.some(c => typeof c !== 'string')) {\n throw new Error('Error: invalid_input: task.checklist must be an array of strings')\n }\n checklistFromTexts(e.checklist as string[]) // validates count + texts\n spec.checklist = e.checklist as string[]\n }\n return spec\n}\n\n/** Validate a pinned model: structural check always, provider route when known. */\nfunction checkModel(raw: unknown, modelProviders?: () => string[] | undefined): TaskModel {\n const model = normalizeModel(raw)\n const providers = modelProviders?.()\n if (providers !== undefined && !providers.includes(model.provider)) {\n throw new Error(`Error: invalid_input: model provider \"${model.provider}\" has no registered route (available: ${providers.join(', ')})`)\n }\n return model\n}\n\n/** JSON-envelope writer. */\nfunction json(res: ServerResponse, payload: ApiResult<unknown>, status = 200): void {\n const body = JSON.stringify(payload)\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(body)\n}\n\n/** Domain failure → envelope + HTTP status. */\nfunction fail(code: ApiFail['error']['code'], message: string): { res: ApiFail; status: number } {\n const status = code === 'invalid_input' || code === 'invalid_transition' ? 400\n : code === 'not_found' ? 404\n : code === 'version_conflict' ? 409\n : code === 'forbidden' ? 403\n : 500\n return { res: { ok: false, error: { code, message } }, status }\n}\n\n/**\n * Read one JSON body (null on parse failure). S8: rejects bodies over\n * MAX_BODY_BYTES by throwing — the local, unauthenticated HTTP surface must\n * not be an unbounded memory sink.\n */\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n const chunks: Buffer[] = []\n let total = 0\n for await (const chunk of req) {\n total += (chunk as Buffer).length\n if (total > MAX_BODY_BYTES) throw new Error('body too large')\n chunks.push(chunk as Buffer)\n }\n if (chunks.length === 0) return {}\n try {\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n return typeof parsed === 'object' && parsed !== null ? parsed as Record<string, unknown> : null\n } catch {\n return null\n }\n}\n\n/** String field accessor (null when absent/not a string). */\nfunction str(body: Record<string, unknown>, key: string): string | null {\n const v = body[key]\n return typeof v === 'string' ? v : null\n}\n\n/** Number field accessor (undefined when absent; null when present but not a number). */\nfunction num(body: Record<string, unknown>, key: string): number | undefined | null {\n const v = body[key]\n if (v === undefined) return undefined\n return typeof v === 'number' && Number.isFinite(v) ? v : null\n}\n\n/** Find a live task INSIDE a mutator (R1: guards run on the fresh draft). */\nfunction liveTaskAt(ledger: TaskLedger, id: string): { index: number; task: TaskRecord } {\n const index = ledger.tasks.findIndex(t => t.id === id)\n if (index < 0 || ledger.tasks[index]!.trashedAt !== undefined) throw new Error('Error: not_found: no such task')\n return { index, task: ledger.tasks[index]! }\n}\n\n/** Normalize an agent preset id: trimmed, non-empty; empty string → undefined. */\nfunction normalizePresetId(raw: string | null): string | undefined {\n const t = (raw ?? '').trim()\n return t.length === 0 ? undefined : t\n}\n\n/** Map a thrown domain error to the envelope. */\nfunction toFail(error: unknown): { res: ApiFail; status: number } {\n const message = error instanceof Error ? error.message : String(error)\n // Structured path first (review P2): ToolError carries its code — no need\n // to parse the 'Error: <code>: …' prefix it also renders into the message.\n if (error instanceof ToolError) {\n const mapped = error.code === ERR.workspaceMismatch ? 'forbidden' : error.code\n const known: ApiFail['error']['code'][] = ['invalid_input', 'not_found', 'version_conflict', 'invalid_transition', 'forbidden', 'internal']\n if ((known as string[]).includes(mapped)) {\n return fail(mapped as ApiFail['error']['code'], message.slice(7 + error.code.length + 2))\n }\n }\n const code = message.startsWith('Error: ') ? message.slice(7).split(':')[0] : undefined\n const known2: ApiFail['error']['code'][] = ['invalid_input', 'not_found', 'version_conflict', 'invalid_transition', 'forbidden', 'internal']\n if (code !== undefined && (known2 as string[]).includes(code)) {\n return fail(code as ApiFail['error']['code'], message.slice(7 + code.length + 2))\n }\n if (code === 'workspace_mismatch') return fail('forbidden', message.slice(7 + code.length + 2))\n return fail('invalid_input', message)\n}\n\n/**\n * Register the taskboard routes.\n * @param ctx - context carrying the webServer service.\n * @param options - store + workspaces + clock.\n * @returns the disposer.\n */\nexport function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOptions): () => void {\n const { store, workspaces } = options\n const subscribers = new Set<ServerResponse>()\n let heartbeat: NodeJS.Timeout | undefined\n\n /** R4③: a cleanup/purge target must resolve INSIDE <ws>/.dsh-worktrees — string joining alone is never trusted with an rm. */\n const insideWorktreeScope = (wsPath: string, target: string): boolean => {\n const scope = resolve(wsPath, WORKTREE_DIR)\n const resolved = resolve(target)\n return resolved === scope || resolved.startsWith(scope + sep)\n }\n\n const broadcast = (change: { revision: number; kind: string; tasks: readonly TaskRecord[] }): void => {\n const frame = `event: change\\ndata: ${JSON.stringify({ revision: change.revision, kind: change.kind, tasks: change.tasks.map(summarize) })}\\n\\n`\n for (const res of subscribers) res.write(frame)\n }\n const unsubscribeBroadcast = store.subscribe(broadcast)\n\n // Workspace git detection, TTL-cached and fail-soft (false on any error):\n // feeds the create-form isolation toggle and the diagnostics panel.\n const gitCache = new Map<string, { value: boolean; at: number }>()\n const gitHinted = new Set<string>()\n\n /** Whether <root>/.gitignore (missing file counts as missing) ignores our worktree dir. */\n const gitignoreMissing = async (path: string): Promise<boolean> => {\n try {\n const { readFile } = await import('node:fs/promises')\n const ignore = await readFile(join(path, '.gitignore'), 'utf8')\n return !ignore.split('\\n').some(l => {\n const t = l.trim().replace(/\\/+$/, '')\n return t === WORKTREE_DIR || t === `/${WORKTREE_DIR}`\n })\n } catch {\n return true // no .gitignore at all (or unreadable) → suggest creating one\n }\n }\n\n const gitAvailable = async (path: string): Promise<boolean> => {\n if (options.git === undefined) return false\n const hit = gitCache.get(path)\n if (hit !== undefined && options.now() - hit.at < GIT_DETECT_TTL_MS) return hit.value\n let value = false\n try {\n value = await options.git.detect(path)\n } catch { /* fail-soft → false */ }\n gitCache.set(path, { value, at: options.now() })\n // gitignore 建议 (plan §3.2): suggest (never write) ignoring our\n // worktree directory, once per workspace per host run.\n if (value && !gitHinted.has(path)) {\n gitHinted.add(path)\n if (await gitignoreMissing(path)) {\n console.info(`[dsh-taskboard] 建议在 ${path}/.gitignore 加入一行 ${WORKTREE_DIR}/ 以隐藏任务 worktree 目录(不会自动修改)`)\n }\n }\n return value\n }\n\n /** List orphan worktree dirs: entries under <ws>/.dsh-worktrees owned by no ledger task. */\n const listOrphanWorktrees = async (): Promise<Array<{ workspaceId: string; workspacePath: string; taskId: string; path: string }>> => {\n const orphans: Array<{ workspaceId: string; workspacePath: string; taskId: string; path: string }> = []\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n for (const ws of workspaces.list()) {\n let entries: string[] = []\n try {\n const dirents = await readdir(join(ws.path, WORKTREE_DIR), { withFileTypes: true })\n entries = dirents.filter(e => e.isDirectory()).map(e => e.name)\n } catch { /* no worktrees dir → nothing to do */ }\n for (const taskId of entries) {\n if (!known.has(taskId)) orphans.push({ workspaceId: ws.id, workspacePath: ws.path, taskId, path: worktreePathOf(ws.path, taskId) })\n }\n }\n return orphans\n }\n\n /** Git-enabled workspaces whose .gitignore does not cover the worktree dir. */\n const listGitignoreSuggestions = async (): Promise<Array<{ workspaceId: string; workspacePath: string }>> => {\n const suggestions: Array<{ workspaceId: string; workspacePath: string }> = []\n for (const ws of workspaces.list()) {\n if (!(await gitAvailable(ws.path))) continue\n if (await gitignoreMissing(ws.path)) suggestions.push({ workspaceId: ws.id, workspacePath: ws.path })\n }\n return suggestions\n }\n\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const url = new URL(req.url ?? '/', 'http://x')\n const pathname = url.pathname\n\n // ---------------------------------------------------------------- GET\n if (req.method === 'GET') {\n if (pathname === `${ROUTE_PREFIX}/state`) {\n await store.load()\n json(res, { ok: true, value: store.snapshot() })\n return\n }\n if (pathname === `${ROUTE_PREFIX}/workspaces`) {\n const list = workspaces.list()\n const flags = await Promise.all(list.map(ws => gitAvailable(ws.path)))\n json(res, {\n ok: true,\n value: list.map((ws, i) => ({ ...ws, sessionCount: 0, gitAvailable: flags[i] })),\n })\n return\n }\n if (pathname === `${ROUTE_PREFIX}/diagnostics`) {\n const ledger = store.snapshot()\n let staleRunning = 0\n for (const t of ledger.tasks) {\n for (const e of t.executions) if (e.outcome === 'running') staleRunning += 1\n }\n json(res, {\n ok: true,\n value: {\n revision: ledger.revision,\n tasks: ledger.tasks.length,\n staleRunning,\n orphanWorktrees: await listOrphanWorktrees(),\n gitIgnoreSuggestions: await listGitignoreSuggestions(),\n },\n })\n return\n }\n // Diff viewer (0.4.0): read-only git show/diff for one execution's\n // commit or changed path. Prefers the live worktree (uncommitted\n // view), falls back to the main repo.\n const diffMatch = pathname.match(TASK_DIFF_RE)\n if (diffMatch !== null) {\n try {\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n const task = store.get(diffMatch[1]!)\n if (task === undefined) throw new Error('Error: not_found: no such task')\n const execution = task.executions.find(e => e.id === url.searchParams.get('execution'))\n if (execution === undefined) throw new Error('Error: not_found: no such execution')\n const commit = url.searchParams.get('commit')\n const filePath = url.searchParams.get('path')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n const cwd = execution.worktreePath ?? ws.path\n let result = commit !== null\n ? await options.git.showCommit(cwd, commit)\n : filePath !== null ? await options.git.showPathDiff(cwd, filePath, execution.baseCommit) : undefined\n // Fallback: the worktree may be gone — commits and committed\n // ranges still resolve in the main repo.\n if (result === undefined && execution.worktreePath !== undefined && cwd !== ws.path) {\n result = commit !== null\n ? await options.git.showCommit(ws.path, commit)\n : filePath !== null && execution.baseCommit !== undefined\n ? await options.git.showPathDiff(ws.path, filePath, execution.baseCommit)\n : undefined\n }\n if (result === undefined) {\n throw new Error('Error: invalid_input: 无法获取 diff(git 报错、对象不存在,或仅存于已删除的 worktree 且无基线)')\n }\n json(res, { ok: true, value: { diff: result.text, truncated: result.truncated } })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // Templates listing (0.4.0).\n if (pathname === `${ROUTE_PREFIX}/templates`) {\n if (options.templates === undefined) {\n const f = fail('invalid_input', 'template store unavailable')\n json(res, f.res, 501)\n return\n }\n json(res, { ok: true, value: { templates: await options.templates.list() } })\n return\n }\n\n // Board settings (0.5.0): absent fields follow factory defaults.\n if (pathname === `${ROUTE_PREFIX}/settings`) {\n await store.load()\n json(res, { ok: true, value: store.snapshot().settings ?? {} })\n return\n }\n\n const taskMatch = pathname.match(TASK_RE)\n if (taskMatch !== null) {\n const task = store.get(taskMatch[1]!)\n if (task === undefined) { const f = fail('not_found', 'no such task'); json(res, f.res, f.status); return }\n json(res, { ok: true, value: task })\n return\n }\n res.writeHead(404)\n res.end()\n return\n }\n\n if (req.method !== 'POST') {\n res.writeHead(405, { allow: 'GET, POST' })\n res.end()\n return\n }\n // CSRF fence: cross-site simple requests cannot set application/json.\n const contentType = req.headers['content-type'] ?? ''\n if (!contentType.toLowerCase().startsWith('application/json')) {\n const f = fail('invalid_input', 'content-type must be application/json')\n json(res, f.res, 415)\n return\n }\n let body: Record<string, unknown> | null\n try {\n body = await readBody(req)\n } catch {\n const f = fail('invalid_input', `request body exceeds ${MAX_BODY_BYTES} bytes`)\n json(res, f.res, 413)\n return\n }\n if (body === null) {\n const f = fail('invalid_input', 'body is not a JSON object')\n json(res, f.res, 400)\n return\n }\n\n // ------------------------------------------------- POST /tasks (create)\n if (pathname === `${ROUTE_PREFIX}/tasks`) {\n try {\n const title = normalizeTitle(str(body, 'title') ?? '')\n const workspaceId = str(body, 'workspaceId') ?? ''\n if (workspaces.get(workspaceId) === undefined) throw new Error('Error: not_found: unknown workspace')\n const urgency = asUrgency(str(body, 'urgency') ?? '')\n const status = str(body, 'status') === null ? 'todo' as const : asStatus(str(body, 'status')!)\n if (status !== 'backlog' && status !== 'todo') {\n throw new Error('Error: invalid_transition: a new task must start as backlog or todo')\n }\n const execution = normalizeExecution((body.execution as { mode?: string; cron?: string } | undefined) ?? {}, options.now())\n const model = body.model === undefined ? undefined : checkModel(body.model, options.modelProviders)\n const isolationRaw = str(body, 'isolation')\n // 0.5.0: an omitted isolation is MATERIALIZED from the board\n // setting (看板设置) at creation, so later setting changes never\n // rewrite existing tasks.\n const isolation = isolationRaw === null ? defaultIsolationOf(store.snapshot().settings) : asIsolation(isolationRaw)\n const presetId = normalizePresetId(str(body, 'presetId'))\n let checklist: TaskRecord['checklist'] = undefined\n if (body.checklist !== undefined) {\n if (!Array.isArray(body.checklist) || body.checklist.some(c => typeof c !== 'string')) {\n throw new Error('Error: invalid_input: checklist must be an array of strings')\n }\n const texts = (body.checklist as string[]).map(c => c.trim()).filter(c => c.length > 0)\n if (texts.length > 0) checklist = checklistFromTexts(texts)\n }\n const now = options.now()\n const task: TaskRecord = {\n id: newTaskId(),\n title,\n description: (str(body, 'description') ?? '').trim(),\n prompt: normalizePrompt(str(body, 'prompt') ?? undefined),\n workspaceId,\n urgency,\n status,\n blocked: false,\n execution,\n model,\n isolation,\n ...(presetId !== undefined ? { presetId } : {}),\n ...(checklist !== undefined ? { checklist } : {}),\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'user' },\n updatedBy: { kind: 'user' },\n comments: [],\n executions: [],\n }\n await store.mutate('task-created', ledger => {\n ledger.tasks.push(task)\n return [task]\n })\n json(res, { ok: true, value: summarize(task) }, 201)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------- POST /tasks/:id/{action}\n // (\\w+ after the id would not match hyphenated actions like\n // worktree-remove, hence the explicit class in the hoisted pattern.)\n const actionMatch = pathname.match(TASK_ACTION_RE)\n if (actionMatch !== null) {\n const id = actionMatch[1]!\n const action = actionMatch[2]!\n try {\n const task = store.get(id)\n if (task === undefined) throw new Error('Error: not_found: no such task')\n if (action === 'update') {\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n // R1: version guard + write inside the mutation, on the fresh draft.\n let next: TaskRecord | undefined\n await store.mutate('task-updated', ledger => {\n const { index, task } = liveTaskAt(ledger, id)\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n if (task.status === 'archived') throw new Error('Error: invalid_transition: archived tasks are immutable')\n next = structuredClone(task)\n const title = str(body, 'title')\n if (title !== null) next.title = normalizeTitle(title)\n const description = str(body, 'description')\n if (description !== null) next.description = description.trim()\n const prompt = str(body, 'prompt')\n if (prompt !== null) next.prompt = normalizePrompt(prompt)\n const urgency = str(body, 'urgency')\n if (urgency !== null) next.urgency = asUrgency(urgency)\n // GUI-only rebind to another project; validated against the workspace registry.\n const workspaceId = str(body, 'workspaceId')\n if (workspaceId !== null) {\n if (workspaces.get(workspaceId) === undefined) throw new Error('Error: not_found: unknown workspace')\n next.workspaceId = workspaceId\n }\n if (typeof body.blocked === 'boolean') next.blocked = body.blocked\n // The GUI (task owner surface) may edit model/execution; null clears the model.\n if (body.execution !== undefined) next.execution = normalizeExecution(body.execution as { mode?: string; cron?: string }, options.now())\n if (body.model === null) next.model = undefined\n else if (body.model !== undefined) next.model = checkModel(body.model, options.modelProviders)\n // Isolation may change only before the first execution (分支与基线\n // 取决于该选择 — plan §3.1: 执行开始后锁定).\n const isolationRaw = str(body, 'isolation')\n if (isolationRaw !== null) {\n if (task.executions.length > 0 || task.status === 'in_progress') {\n throw new Error('Error: invalid_input: isolation 已锁定(任务已有执行记录),不可修改')\n }\n next.isolation = asIsolation(isolationRaw)\n }\n // Preset may change any time: each run composes fresh.\n if (body.presetId === null) delete next.presetId\n else if (body.presetId !== undefined) next.presetId = normalizePresetId(str(body, 'presetId'))!\n // Checklist (0.4.0): the GUI replaces the whole list; null clears.\n if (body.checklist === null) delete next.checklist\n else if (body.checklist !== undefined) {\n const items = normalizeChecklist(body.checklist)\n if (items.length > 0) next.checklist = items\n else delete next.checklist\n }\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next!) })\n return\n }\n if (action === 'move') {\n const ifVersion = num(body, 'ifVersion')\n const status = str(body, 'status') ?? ''\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n const to = asStatus(status)\n let next: TaskRecord | undefined\n await store.mutate('task-moved', ledger => {\n const { index, task } = liveTaskAt(ledger, id)\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n if (!canTransition(task.status, to)) throw new Error(`Error: invalid_transition: illegal transition ${task.status} → ${to}`)\n next = structuredClone(task)\n next.status = to\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n if (task.status === 'todo' && to === 'in_progress') next.blocked = false\n // A user move records no holder; leaving in_progress releases any hold.\n syncClaim(next, to, options.now())\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next!) })\n return\n }\n if (action === 'reject') {\n // Card quick-reject: back to todo + optional user comment in one\n // atomic mutation (a failed move never strands an orphan comment).\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n const commentText = str(body, 'body') ?? ''\n let next: TaskRecord | undefined\n await store.mutate('task-moved', ledger => {\n const { index, task } = liveTaskAt(ledger, id)\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n if (!canTransition(task.status, 'todo')) throw new Error(`Error: invalid_transition: illegal transition ${task.status} → todo`)\n next = structuredClone(task)\n next.status = 'todo'\n next.version = task.version + 1\n next.updatedAt = options.now()\n next.updatedBy = { kind: 'user' }\n syncClaim(next, 'todo', options.now())\n if (commentText.trim().length > 0) {\n next.comments.push({ id: newCommentId(), body: normalizeBody(commentText), version: 1, createdAt: options.now() })\n }\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: summarize(next!) })\n return\n }\n if (action === 'comment') {\n const bodyText = str(body, 'body') ?? ''\n const comment = { id: newCommentId(), body: normalizeBody(bodyText), version: 1, createdAt: options.now() }\n await store.mutate('comment-added', ledger => {\n const { index, task } = liveTaskAt(ledger, id)\n if (task.status === 'archived') throw new Error('Error: invalid_transition: archived tasks are immutable')\n const next = structuredClone(task)\n next.comments.push(comment)\n next.version = task.version + 1\n next.updatedAt = options.now()\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: comment }, 201)\n return\n }\n if (action === 'delete') {\n const purge = body.purge === true\n if (purge) {\n if (task.trashedAt === undefined) throw new Error('Error: invalid_input: purge requires a trashed task (soft-delete first)')\n // Worktree safety before purge (plan §3.3, 0.3.1): refuse while\n // uncommitted work remains; otherwise clean the worktree and\n // the task branch along with the ledger entry.\n if (options.git !== undefined) {\n const ws = workspaces.get(task.workspaceId)\n if (ws !== undefined) {\n const path = worktreePathOf(ws.path, id)\n if (!insideWorktreeScope(ws.path, path)) {\n throw new Error('Error: invalid_input: 非法的清除路径(不在任务工作目录范围内)')\n }\n try {\n // S3: 'unregistered' (an orphaned dir git forgot) is a\n // structured outcome, not a parsed stderr message.\n if (await options.git.removeWorktree(ws.path, path) === 'unregistered') {\n // An unregistered leftover dir: plain fs removal.\n await rm(path, { recursive: true, force: true })\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n // Structured classification (review P2): git tags its dirty\n // rejections with a code; the keyword stays as a fallback.\n const dirty = (error as { code?: string }).code === 'dirty-worktree' || message.includes('未提交修改')\n if (dirty) {\n throw new Error(`Error: invalid_input: ${message};请先处理这些改动(提交、续跑或手动保存)再物理清除任务`)\n }\n throw new Error(`Error: invalid_input: ${message}`)\n }\n if (task.branch !== undefined) {\n try {\n await options.git.deleteBranch(ws.path, task.branch)\n } catch { /* best effort: the branch may outlive the task */ }\n }\n }\n }\n await store.mutate('task-deleted', ledger => {\n ledger.tasks = ledger.tasks.filter(t => t.id !== id)\n return []\n })\n json(res, { ok: true, value: { purged: true } })\n return\n }\n const ifVersion = num(body, 'ifVersion')\n if (ifVersion === undefined || ifVersion === null) throw new Error('Error: version_conflict: ifVersion required')\n await store.mutate('task-deleted', ledger => {\n const { index, task } = liveTaskAt(ledger, id)\n if (ifVersion !== task.version) throw new Error(`Error: version_conflict: stale version ${ifVersion} (current ${task.version})`)\n // S5: a running execution keeps writing to the task — refuse the\n // soft-delete until it is cancelled or settled. T8: clear residue.\n if (task.executions.some(e => e.outcome === 'running')) {\n throw new Error('Error: invalid_input: 任务有正在运行的执行,请先取消或等它结束再删除')\n }\n const next = structuredClone(task)\n next.trashedAt = options.now()\n next.version = task.version + 1\n delete next.claimedBy\n delete next.claimedAt\n next.blocked = false\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: { trashed: true } })\n return\n }\n if (action === 'run') {\n if (options.run === undefined) {\n const f = fail('invalid_input', 'execution service unavailable')\n json(res, f.res, 501)\n return\n }\n // `reuse: true` = 续跑: keep a live worktree/branch as-is instead\n // of resetting to a fresh baseline (0.3.1).\n const runOptions = body.reuse === true ? { reuseWorktree: true } : undefined\n const result = await options.run(id, runOptions)\n if (result.ok) json(res, { ok: true, value: result }, 202)\n else {\n const f = fail('invalid_input', result.error)\n json(res, f.res, f.status)\n }\n return\n }\n if (action === 'cancel') {\n if (options.cancel === undefined) {\n const f = fail('invalid_input', 'execution service unavailable')\n json(res, f.res, 501)\n return\n }\n const result = await options.cancel(id)\n if (result.ok) json(res, { ok: true, value: { cancelled: true, executionId: result.executionId } }, 202)\n else {\n const f = fail('invalid_input', result.error)\n json(res, f.res, f.status)\n }\n return\n }\n if (action === 'merge') {\n // ⇥ 合并 (detail page, user-only): merge the task branch into the\n // main worktree with --no-ff; conflicts are reported verbatim.\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n if (task.branch === undefined) throw new Error('Error: invalid_input: 该任务还没有 worktree 分支(未隔离执行过)')\n if (task.status === 'in_progress') throw new Error('Error: invalid_input: 任务执行中,不能合并')\n if (task.executions.some(e => e.outcome === 'running')) throw new Error('Error: invalid_input: 任务执行中,不能合并')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n // No-op detection (0.3.1): a branch with no commits over HEAD\n // merges as \"already up to date\" — report that instead of landing\n // a bogus 已合并 comment.\n let noop = false\n try {\n noop = await options.git.isAncestor(ws.path, task.branch)\n } catch { /* fail-soft: proceed to the real merge */ }\n if (noop) {\n json(res, { ok: true, value: { merged: false, noop: true, branch: task.branch } })\n return\n }\n try {\n await options.git.merge(ws.path, task.branch)\n } catch (error) {\n throw new Error(`Error: invalid_input: ${error instanceof Error ? error.message : String(error)}`)\n }\n const mergedComment = { id: newCommentId(), body: normalizeBody(`[系统] 分支 ${task.branch} 已合并到主工作区(--no-ff)。`), version: 1, createdAt: options.now() }\n // R1: the git merge above is slow — re-find the FRESH task inside\n // the mutation so a concurrent comment is never overwritten.\n await store.mutate('comment-added', ledger => {\n const { index, task: fresh } = liveTaskAt(ledger, id)\n const next = structuredClone(fresh)\n next.comments.push(mergedComment)\n next.version = fresh.version + 1\n next.updatedAt = options.now()\n ledger.tasks[index] = next\n return [next]\n })\n json(res, { ok: true, value: { merged: true, branch: task.branch } })\n return\n }\n if (action === 'worktree-remove') {\n // 🗑 删除 worktree (detail page): refuses uncommitted changes;\n // optionally deletes the task branch after the worktree is gone.\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n if (task.executions.some(e => e.outcome === 'running')) throw new Error('Error: invalid_input: 任务执行中,不能删除 worktree')\n const ws = workspaces.get(task.workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n const path = worktreePathOf(ws.path, id)\n if (!insideWorktreeScope(ws.path, path)) {\n throw new Error('Error: invalid_input: 非法的清除路径(不在任务工作目录范围内)')\n }\n try {\n // S3: an unregistered leftover at the task's own path is\n // removed from the filesystem directly.\n if (await options.git.removeWorktree(ws.path, path) === 'unregistered') {\n await rm(path, { recursive: true, force: true })\n }\n } catch (error) {\n throw new Error(`Error: invalid_input: ${error instanceof Error ? error.message : String(error)}`)\n }\n let branchDeleted = false\n let branchError: string | undefined\n if (body.deleteBranch === true && task.branch !== undefined) {\n try {\n await options.git.deleteBranch(ws.path, task.branch)\n branchDeleted = true\n } catch (error) {\n branchError = error instanceof Error ? error.message : String(error)\n }\n }\n json(res, { ok: true, value: { removed: true, branchDeleted, ...(branchError !== undefined ? { branchError } : {}) } })\n return\n }\n const f = fail('not_found', `unknown action ${action}`)\n json(res, f.res, f.status)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // -------------------------------------- POST /worktree-cleanup (⚙ 诊断)\n if (pathname === `${ROUTE_PREFIX}/worktree-cleanup`) {\n try {\n if (options.git === undefined) {\n const f = fail('invalid_input', 'git integration unavailable')\n json(res, f.res, 501)\n return\n }\n const workspaceId = str(body, 'workspaceId') ?? ''\n const taskId = str(body, 'taskId') ?? ''\n const ws = workspaces.get(workspaceId)\n if (ws === undefined) throw new Error('Error: not_found: unknown workspace')\n // Only dirs owned by NO ledger task may be cleaned here; live tasks\n // remove their worktree from the detail page.\n if (store.get(taskId) !== undefined) throw new Error('Error: invalid_input: 任务仍在看板中,请从任务详情页删除其 worktree')\n // worktreePathOf throws on an illegal id charset (R4②); the resolved\n // target must additionally stay inside the plugin's own worktree\n // scope — the taskId is fully attacker-controlled body input (R4③).\n const path = worktreePathOf(ws.path, taskId)\n if (!insideWorktreeScope(ws.path, path)) {\n throw new Error('Error: invalid_input: 非法的清除路径(不在任务工作目录范围内)')\n }\n try {\n // S3: 'unregistered' = git no longer knows this worktree; remove\n // the leftover dir directly (scope-verified above).\n if (await options.git.removeWorktree(ws.path, path) === 'unregistered') {\n await rm(path, { recursive: true, force: true })\n }\n } catch (error) {\n throw new Error(`Error: invalid_input: ${error instanceof Error ? error.message : String(error)}`)\n }\n json(res, { ok: true, value: { cleaned: true, path } })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ---------------------------------------------- POST /import/preview\n // (0.4.0) Dry-run: classify every task in the uploaded ledger file\n // against the live one; nothing is written.\n if (pathname === `${ROUTE_PREFIX}/import/preview`) {\n try {\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n const plan = validateLedgerImport(body, known, options.now())\n json(res, {\n ok: true,\n value: {\n plan: {\n create: plan.create.map(t => ({ id: t.id, title: t.title, status: t.status })),\n overwrite: plan.overwrite.map(t => ({ id: t.id, title: t.title, status: t.status })),\n invalid: plan.invalid,\n },\n },\n })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------------------ POST /import\n // (0.4.0) Commit an import. mode=merge upserts (create + overwrite by\n // id); mode=replace swaps the WHOLE ledger (invalid entries dropped)\n // after writing a timestamped backup of the current one.\n if (pathname === `${ROUTE_PREFIX}/import`) {\n try {\n const mode = str(body, 'mode') === 'replace' ? 'replace' as const : 'merge' as const\n const raw = body.ledger\n const known = new Set(store.snapshot().tasks.map(t => t.id))\n const plan = validateLedgerImport(raw, known, options.now())\n const imported = [...plan.create, ...plan.overwrite]\n if (mode === 'replace' && imported.length === 0) {\n throw new Error('Error: invalid_input: 导入文件没有可导入的任务,已拒绝整册替换')\n }\n let backupFile: string | undefined\n if (mode === 'replace' && store.snapshot().tasks.length > 0) {\n backupFile = await store.backup()\n }\n let replacedTotal: number | undefined\n await store.mutate('ledger-replaced', ledger => {\n if (mode === 'replace') {\n // S6: a whole-ledger swap must not strand live executions — the\n // running sessions keep working with no task to settle into.\n if (ledger.tasks.some(t => t.executions.some(e => e.outcome === 'running'))) {\n throw new Error('Error: invalid_input: 有任务正在执行,不能整册替换(请先取消或等待结束)')\n }\n replacedTotal = ledger.tasks.length\n ledger.tasks = structuredClone(imported)\n // Replace is a whole-ledger swap (0.5.0): board settings ride\n // along when the file carries them; merge keeps the live ones.\n if (plan.settings !== undefined) ledger.settings = structuredClone(plan.settings)\n else delete ledger.settings\n return ledger.tasks\n }\n const byId = new Map(ledger.tasks.map(t => [t.id, t]))\n for (const task of imported) {\n // S6: merging over a task whose execution is live would orphan\n // that run the same way — refuse the overwrite.\n const existing = byId.get(task.id)\n if (existing !== undefined && existing.executions.some(e => e.outcome === 'running')) {\n throw new Error(`Error: invalid_input: 任务 ${task.id} 正在执行,不能被导入覆盖`)\n }\n byId.set(task.id, structuredClone(task))\n }\n ledger.tasks = [...byId.values()]\n return structuredClone(imported)\n })\n json(res, {\n ok: true,\n value: {\n mode,\n created: plan.create.length,\n overwritten: plan.overwrite.length,\n ...(mode === 'replace' ? { replacedTotal } : {}),\n ...(backupFile !== undefined ? { backupFile } : {}),\n },\n })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------- POST /templates (+delete)\n if (pathname === `${ROUTE_PREFIX}/templates` || pathname === `${ROUTE_PREFIX}/templates/delete`) {\n try {\n if (options.templates === undefined) {\n const f = fail('invalid_input', 'template store unavailable')\n json(res, f.res, 501)\n return\n }\n if (pathname.endsWith('/delete')) {\n const id = str(body, 'id') ?? ''\n if (id.length === 0) throw new Error('Error: invalid_input: id required')\n const deleted = await options.templates.remove(id)\n json(res, { ok: true, value: { deleted } })\n return\n }\n const name = str(body, 'name') ?? ''\n if (name.trim().length === 0) throw new Error('Error: invalid_input: name required')\n const template = await options.templates.upsert({\n id: str(body, 'id') ?? undefined,\n name,\n task: normalizeTemplateSpec(body.task, options.now()),\n })\n json(res, { ok: true, value: template }, 201)\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n // ------------------------------------------ POST /settings/update\n // (0.5.0) Whole-object replace semantics: omitted fields fall back to\n // their factory defaults. Affects only tasks created AFTER the change.\n if (pathname === `${ROUTE_PREFIX}/settings/update`) {\n try {\n const next = asBoardSettings(body)\n await store.mutate('settings-updated', ledger => {\n ledger.settings = next\n return []\n })\n json(res, { ok: true, value: next })\n } catch (error) {\n const f = toFail(error)\n json(res, f.res, f.status)\n }\n return\n }\n\n res.writeHead(404)\n res.end()\n } catch (error) {\n const f = fail('internal', error instanceof Error ? error.message : String(error))\n json(res, f.res, f.status)\n }\n }\n\n const sse = (req: IncomingMessage, res: ServerResponse): void => {\n res.writeHead(200, {\n 'content-type': 'text/event-stream; charset=utf-8',\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n })\n res.write('retry: 2000\\n\\n')\n // Baseline frame: the client reconciles by revision and refetches state on gaps.\n res.write(`event: hello\\ndata: ${JSON.stringify({ revision: store.snapshot().revision })}\\n\\n`)\n subscribers.add(res)\n // S2: a socket that dies between 'close' detection and the next write\n // would emit 'error' on the response — unhandled, that escalates to an\n // uncaughtException. Drop the subscriber instead.\n res.on('error', () => { subscribers.delete(res) })\n if (heartbeat === undefined) {\n heartbeat = setInterval(() => {\n for (const current of subscribers) current.write(': ping\\n\\n')\n }, HEARTBEAT_MS)\n }\n req.on('close', () => {\n subscribers.delete(res)\n if (subscribers.size === 0 && heartbeat !== undefined) {\n clearInterval(heartbeat)\n heartbeat = undefined\n }\n })\n }\n\n const disposers = [\n ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }),\n ctx.webServer.register({ kind: 'exact', path: SSE_PATH, handler: sse }),\n ]\n return () => {\n unsubscribeBroadcast()\n for (const dispose of disposers) dispose()\n if (heartbeat !== undefined) clearInterval(heartbeat)\n for (const res of subscribers) res.end()\n subscribers.clear()\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { nextCronTime, parseCron } from "../shared/protocol.js";
1
+ import { newCommentId, nextCronTime, normalizeBody, parseCron } from "../shared/protocol.js";
2
2
  import "./execution.js";
3
3
  //#region src/host/scheduler.ts
4
4
  /**
@@ -15,6 +15,16 @@ import "./execution.js";
15
15
  const TICK_MS = 6e4;
16
16
  /** A due window older than this is skipped (missed while the host was down). */
17
17
  const SKIP_AFTER_MS = 5 * 6e4;
18
+ const DEFAULT_TIMERS = {
19
+ setInterval: (fn, ms) => setInterval(fn, ms),
20
+ clearInterval: (handle) => {
21
+ clearInterval(handle);
22
+ },
23
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
24
+ clearTimeout: (handle) => {
25
+ clearTimeout(handle);
26
+ }
27
+ };
18
28
  /**
19
29
  * The cron scheduler.
20
30
  */
@@ -22,31 +32,36 @@ var SchedulerService = class {
22
32
  deps;
23
33
  handle;
24
34
  catchup;
35
+ timers = DEFAULT_TIMERS;
25
36
  /** @param deps - store + execution + clock. */
26
37
  constructor(deps) {
27
38
  this.deps = deps;
28
39
  }
29
40
  /** Start ticking. */
30
41
  start() {
31
- const timers = this.deps.timers ?? {
32
- setInterval: (fn, ms) => setInterval(fn, ms),
33
- clearInterval: (handle) => clearInterval(handle)
42
+ this.timers = this.deps.timers === void 0 ? DEFAULT_TIMERS : {
43
+ ...DEFAULT_TIMERS,
44
+ ...this.deps.timers
34
45
  };
35
- this.handle = timers.setInterval(() => {
36
- this.tick();
46
+ this.handle = this.timers.setInterval(() => {
47
+ this.tick().catch((error) => {
48
+ console.error("[dsh-taskboard] scheduler tick failed:", error);
49
+ });
37
50
  }, TICK_MS);
38
- this.catchup = setTimeout(() => {
39
- this.tick();
51
+ this.catchup = this.timers.setTimeout(() => {
52
+ this.tick().catch((error) => {
53
+ console.error("[dsh-taskboard] scheduler tick failed:", error);
54
+ });
40
55
  }, 3e3);
41
56
  }
42
57
  /** Stop ticking. */
43
58
  dispose() {
44
59
  if (this.catchup !== void 0) {
45
- clearTimeout(this.catchup);
60
+ this.timers.clearTimeout(this.catchup);
46
61
  this.catchup = void 0;
47
62
  }
48
63
  if (this.handle === void 0) return;
49
- (this.deps.timers ?? { clearInterval: (h) => clearInterval(h) }).clearInterval(this.handle);
64
+ this.timers.clearInterval(this.handle);
50
65
  this.handle = void 0;
51
66
  }
52
67
  /** One scheduler pass (exported for tests). */
@@ -54,42 +69,49 @@ var SchedulerService = class {
54
69
  await this.deps.store.load();
55
70
  const now = this.deps.now();
56
71
  const ledger = this.deps.store.snapshot();
57
- const atCapacity = this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? 3);
58
72
  for (const task of ledger.tasks) {
59
73
  if (task.execution.mode !== "scheduled" || task.execution.cron === void 0) continue;
60
74
  if (task.execution.nextRunAt === void 0) continue;
61
75
  if (task.status === "in_progress" || task.trashedAt !== void 0) continue;
62
76
  if (task.execution.nextRunAt > now) continue;
63
- if (atCapacity) continue;
77
+ if (this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? 3)) continue;
64
78
  const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS;
65
- await this.advance(task.id, now);
79
+ await this.advanceAndMark(task.id, now, missed ? void 0 : task.execution.nextRunAt);
66
80
  if (missed) continue;
67
- const lastTriggeredAt = task.execution.nextRunAt;
68
- await this.markTriggered(task.id, lastTriggeredAt);
69
81
  await this.deps.execution.run(task.id, "scheduled").catch((error) => {
70
82
  console.error("[dsh-taskboard] scheduled run failed:", error);
71
83
  });
72
84
  }
73
85
  }
74
- /** Recompute and persist the next run for one scheduled task. */
75
- async advance(taskId, now) {
86
+ /**
87
+ * Recompute the next run and record the trigger instant for one scheduled
88
+ * task, in one serial-queue mutation. S12: a cron that can no longer match
89
+ * anything within the 4-year scan window (only reachable through a
90
+ * hand-edited ledger — every normal entry point validates) would otherwise
91
+ * leave nextRunAt in the past and spin a full ~2M-iteration scan every
92
+ * tick; it is cleared with a system comment instead of dying silently.
93
+ */
94
+ async advanceAndMark(taskId, now, triggeredAt) {
76
95
  await this.deps.store.mutate("task-updated", (ledger) => {
77
96
  const task = ledger.tasks.find((t) => t.id === taskId);
78
97
  if (task === void 0 || task.execution.cron === void 0) return void 0;
98
+ if (task.status === "in_progress" || task.trashedAt !== void 0) return void 0;
79
99
  const match = parseCron(task.execution.cron);
80
100
  const next = match === null ? void 0 : nextCronTime(match, now) ?? void 0;
81
- if (next === void 0) return void 0;
101
+ if (next === void 0) {
102
+ const deadCron = task.execution.cron;
103
+ task.execution.cron = void 0;
104
+ task.execution.nextRunAt = void 0;
105
+ task.comments.push({
106
+ id: newCommentId(),
107
+ body: normalizeBody(`[系统] 定时表达式 ${deadCron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。`),
108
+ version: 1,
109
+ createdAt: now
110
+ });
111
+ return [task];
112
+ }
82
113
  task.execution.nextRunAt = next;
83
- return [task];
84
- });
85
- }
86
- /** Record the trigger instant on the task. */
87
- async markTriggered(taskId, at) {
88
- if (at === void 0) return;
89
- await this.deps.store.mutate("task-updated", (ledger) => {
90
- const task = ledger.tasks.find((t) => t.id === taskId);
91
- if (task === void 0) return void 0;
92
- task.execution.lastTriggeredAt = at;
114
+ if (triggeredAt !== void 0) task.execution.lastTriggeredAt = triggeredAt;
93
115
  return [task];
94
116
  });
95
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { nextCronTime, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport { DEFAULT_MAX_CONCURRENT, type ExecutionService } from './execution.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Tick cadence. */\nconst TICK_MS = 60_000\n\n/** A due window older than this is skipped (missed while the host was down). */\nconst SKIP_AFTER_MS = 5 * 60_000\n\n/** Everything the scheduler needs. */\nexport interface SchedulerDeps {\n store: TaskStore\n execution: Pick<ExecutionService, 'run' | 'inFlight'>\n now: () => number\n /** Max concurrently running executions (default 3; must match the execution service). */\n maxConcurrent?: number\n /** Timer face (injectable for tests). */\n timers?: {\n setInterval(fn: () => void, ms: number): unknown\n clearInterval(handle: unknown): void\n }\n}\n\n/**\n * The cron scheduler.\n */\nexport class SchedulerService {\n private handle: unknown\n private catchup: ReturnType<typeof setTimeout> | undefined\n\n /** @param deps - store + execution + clock. */\n constructor(private readonly deps: SchedulerDeps) {}\n\n /** Start ticking. */\n start(): void {\n const timers = this.deps.timers ?? {\n setInterval: (fn: () => void, ms: number) => setInterval(fn, ms),\n clearInterval: (handle: unknown) => clearInterval(handle as ReturnType<typeof setInterval>),\n }\n this.handle = timers.setInterval(() => { void this.tick() }, TICK_MS)\n // Catch up promptly on host restart: run one tick soon after start. The\n // handle is cleared on dispose so a torn-down scheduler never fires.\n this.catchup = setTimeout(() => { void this.tick() }, 3_000)\n }\n\n /** Stop ticking. */\n dispose(): void {\n if (this.catchup !== undefined) {\n clearTimeout(this.catchup)\n this.catchup = undefined\n }\n if (this.handle === undefined) return\n const timers = this.deps.timers ?? { clearInterval: (h: unknown) => clearInterval(h as ReturnType<typeof setInterval>) }\n timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** One scheduler pass (exported for tests). */\n async tick(): Promise<void> {\n // Load once before reading: snapshot() does not trigger a load, and the\n // scheduler may be the first consumer after a host restart (otherwise it\n // would tick over an empty ledger until something else loads it).\n await this.deps.store.load()\n const now = this.deps.now()\n const ledger: TaskLedger = this.deps.store.snapshot()\n const atCapacity = this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT)\n for (const task of ledger.tasks) {\n if (task.execution.mode !== 'scheduled' || task.execution.cron === undefined) continue\n if (task.execution.nextRunAt === undefined) continue\n if (task.status === 'in_progress' || task.trashedAt !== undefined) continue\n if (task.execution.nextRunAt > now) continue\n // At the concurrency cap: leave nextRunAt in the past and retry next\n // tick — advancing here would silently burn this window.\n if (atCapacity) continue\n const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS\n\n // Advance the schedule FIRST (idempotent under re-ticks), then run\n // unless the window was missed entirely.\n await this.advance(task.id, now)\n if (missed) continue\n const lastTriggeredAt = task.execution.nextRunAt\n await this.markTriggered(task.id, lastTriggeredAt)\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n }\n }\n\n /** Recompute and persist the next run for one scheduled task. */\n private async advance(taskId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.cron === undefined) return undefined\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) return undefined\n task.execution.nextRunAt = next\n return [task]\n })\n }\n\n /** Record the trigger instant on the task. */\n private async markTriggered(taskId: string, at: number | undefined): Promise<void> {\n if (at === undefined) return\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined) return undefined\n task.execution.lastTriggeredAt = at\n return [task]\n })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,UAAU;;AAGhB,MAAM,gBAAgB,IAAI;;;;AAmB1B,IAAa,mBAAb,MAA8B;CAKC;CAJ7B;CACA;;CAGA,YAAY,MAAsC;EAArB,KAAA,OAAA;CAAsB;;CAGnD,QAAc;EACZ,MAAM,SAAS,KAAK,KAAK,UAAU;GACjC,cAAc,IAAgB,OAAe,YAAY,IAAI,EAAE;GAC/D,gBAAgB,WAAoB,cAAc,MAAwC;EAC5F;EACA,KAAK,SAAS,OAAO,kBAAkB;GAAE,KAAU,KAAK;EAAE,GAAG,OAAO;EAGpE,KAAK,UAAU,iBAAiB;GAAE,KAAU,KAAK;EAAE,GAAG,GAAK;CAC7D;;CAGA,UAAgB;EACd,IAAI,KAAK,YAAY,KAAA,GAAW;GAC9B,aAAa,KAAK,OAAO;GACzB,KAAK,UAAU,KAAA;EACjB;EACA,IAAI,KAAK,WAAW,KAAA,GAAW;EAE/B,CADe,KAAK,KAAK,UAAU,EAAE,gBAAgB,MAAe,cAAc,CAAmC,EAAE,EAAA,CAChH,cAAc,KAAK,MAAM;EAChC,KAAK,SAAS,KAAA;CAChB;;CAGA,MAAM,OAAsB;EAI1B,MAAM,KAAK,KAAK,MAAM,KAAK;EAC3B,MAAM,MAAM,KAAK,KAAK,IAAI;EAC1B,MAAM,SAAqB,KAAK,KAAK,MAAM,SAAS;EACpD,MAAM,aAAa,KAAK,KAAK,UAAU,SAAS,MAAM,KAAK,KAAK,iBAAA;EAChE,KAAK,MAAM,QAAQ,OAAO,OAAO;GAC/B,IAAI,KAAK,UAAU,SAAS,eAAe,KAAK,UAAU,SAAS,KAAA,GAAW;GAC9E,IAAI,KAAK,UAAU,cAAc,KAAA,GAAW;GAC5C,IAAI,KAAK,WAAW,iBAAiB,KAAK,cAAc,KAAA,GAAW;GACnE,IAAI,KAAK,UAAU,YAAY,KAAK;GAGpC,IAAI,YAAY;GAChB,MAAM,SAAS,MAAM,KAAK,UAAU,YAAY;GAIhD,MAAM,KAAK,QAAQ,KAAK,IAAI,GAAG;GAC/B,IAAI,QAAQ;GACZ,MAAM,kBAAkB,KAAK,UAAU;GACvC,MAAM,KAAK,cAAc,KAAK,IAAI,eAAe;GACjD,MAAM,KAAK,KAAK,UAAU,IAAI,KAAK,IAAI,WAAW,CAAC,CAAC,OAAM,UAAS;IACjE,QAAQ,MAAM,yCAAyC,KAAK;GAC9D,CAAC;EACH;CACF;;CAGA,MAAc,QAAQ,QAAgB,KAA4B;EAChE,MAAM,KAAK,KAAK,MAAM,OAAO,iBAAiB,WAAW;GACvD,MAAM,OAAO,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACnD,IAAI,SAAS,KAAA,KAAa,KAAK,UAAU,SAAS,KAAA,GAAW,OAAO,KAAA;GACpE,MAAM,QAAQ,UAAU,KAAK,UAAU,IAAI;GAC3C,MAAM,OAAO,UAAU,OAAO,KAAA,IAAY,aAAa,OAAO,GAAG,KAAK,KAAA;GACtE,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,KAAK,UAAU,YAAY;GAC3B,OAAO,CAAC,IAAI;EACd,CAAC;CACH;;CAGA,MAAc,cAAc,QAAgB,IAAuC;EACjF,IAAI,OAAO,KAAA,GAAW;EACtB,MAAM,KAAK,KAAK,MAAM,OAAO,iBAAiB,WAAW;GACvD,MAAM,OAAO,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACnD,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,KAAK,UAAU,kBAAkB;GACjC,OAAO,CAAC,IAAI;EACd,CAAC;CACH;AACF"}
1
+ {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { newCommentId, nextCronTime, normalizeBody, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport { DEFAULT_MAX_CONCURRENT, type ExecutionService } from './execution.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Tick cadence. */\nconst TICK_MS = 60_000\n\n/** A due window older than this is skipped (missed while the host was down). */\nconst SKIP_AFTER_MS = 5 * 60_000\n\n/** Everything the scheduler needs. */\nexport interface SchedulerDeps {\n store: TaskStore\n execution: Pick<ExecutionService, 'run' | 'inFlight'>\n now: () => number\n /** Max concurrently running executions (default 3; must match the execution service). */\n maxConcurrent?: number\n /** Timer face (injectable for tests). The timeout pair is optional so\n * older injections keep working; gaps fall back to the globals. */\n timers?: {\n setInterval(fn: () => void, ms: number): unknown\n clearInterval(handle: unknown): void\n setTimeout?(fn: () => void, ms: number): unknown\n clearTimeout?(handle: unknown): void\n }\n}\n\ntype SchedulerTimers = NonNullable<SchedulerDeps['timers']>\n\nconst DEFAULT_TIMERS: Required<SchedulerTimers> = {\n setInterval: (fn: () => void, ms: number): unknown => setInterval(fn, ms),\n clearInterval: (handle: unknown): void => { clearInterval(handle as Parameters<typeof clearInterval>[0]) },\n setTimeout: (fn: () => void, ms: number): unknown => setTimeout(fn, ms),\n clearTimeout: (handle: unknown): void => { clearTimeout(handle as Parameters<typeof clearTimeout>[0]) },\n}\n\n/**\n * The cron scheduler.\n */\nexport class SchedulerService {\n private handle: unknown\n private catchup: unknown\n private timers: Required<SchedulerTimers> = DEFAULT_TIMERS\n\n /** @param deps - store + execution + clock. */\n constructor(private readonly deps: SchedulerDeps) {}\n\n /** Start ticking. */\n start(): void {\n // Fill optional timer slots from the globals so a legacy injection that\n // only carries the interval pair still works end to end.\n this.timers = this.deps.timers === undefined ? DEFAULT_TIMERS : { ...DEFAULT_TIMERS, ...this.deps.timers }\n // A tick rejection (disk error inside a mutation) must never surface as\n // an unhandled rejection log it and keep the schedule alive.\n this.handle = this.timers.setInterval(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, TICK_MS)\n // Catch up promptly on host restart: run one tick soon after start. The\n // handles are cleared on dispose so a torn-down scheduler never fires.\n this.catchup = this.timers.setTimeout(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, 3_000)\n }\n\n /** Stop ticking. */\n dispose(): void {\n if (this.catchup !== undefined) {\n this.timers.clearTimeout(this.catchup)\n this.catchup = undefined\n }\n if (this.handle === undefined) return\n this.timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** One scheduler pass (exported for tests). */\n async tick(): Promise<void> {\n // Load once before reading: snapshot() does not trigger a load, and the\n // scheduler may be the first consumer after a host restart (otherwise it\n // would tick over an empty ledger until something else loads it).\n await this.deps.store.load()\n const now = this.deps.now()\n const ledger: TaskLedger = this.deps.store.snapshot()\n for (const task of ledger.tasks) {\n if (task.execution.mode !== 'scheduled' || task.execution.cron === undefined) continue\n if (task.execution.nextRunAt === undefined) continue\n if (task.status === 'in_progress' || task.trashedAt !== undefined) continue\n if (task.execution.nextRunAt > now) continue\n // At the concurrency cap (S4: checked FRESH per task — runs register\n // only after agent creation, so a once-per-tick snapshot under-counted\n // the startup window): leave nextRunAt in the past and retry next tick\n // — advancing here would silently burn this window.\n if (this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT)) continue\n const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS\n\n // Advance the schedule AND record the trigger in ONE mutation (S13:\n // one revision bump, one broadcast, and the two writes can no longer\n // straddle a status change), then run unless the window was missed.\n await this.advanceAndMark(task.id, now, missed ? undefined : task.execution.nextRunAt)\n if (missed) continue\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n }\n }\n\n /**\n * Recompute the next run and record the trigger instant for one scheduled\n * task, in one serial-queue mutation. S12: a cron that can no longer match\n * anything within the 4-year scan window (only reachable through a\n * hand-edited ledger — every normal entry point validates) would otherwise\n * leave nextRunAt in the past and spin a full ~2M-iteration scan every\n * tick; it is cleared with a system comment instead of dying silently.\n */\n private async advanceAndMark(taskId: string, now: number, triggeredAt: number | undefined): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.cron === undefined) return undefined\n if (task.status === 'in_progress' || task.trashedAt !== undefined) return undefined\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) {\n const deadCron = task.execution.cron\n task.execution.cron = undefined\n task.execution.nextRunAt = undefined\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 定时表达式 ${deadCron} 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。`),\n version: 1,\n createdAt: now,\n })\n return [task]\n }\n task.execution.nextRunAt = next\n if (triggeredAt !== undefined) task.execution.lastTriggeredAt = triggeredAt\n return [task]\n })\n }\n}\n"],"mappings":"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"}
package/lib/host/sdk.js CHANGED
@@ -1,5 +1,5 @@
1
- import { homedir } from "node:os";
2
1
  import { join, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
3
  //#region src/host/sdk.ts
4
4
  /**
5
5
  * Self-contained replacements for the three @deepseek-ai runtime imports the
@@ -107,7 +107,12 @@ function validateValue(schema, value, path) {
107
107
  });
108
108
  return violations;
109
109
  }
110
- return matchesScalarType(value, schema.type) ? [] : [`${path} must be ${schema.type}`];
110
+ if (!matchesScalarType(value, schema.type)) return [`${path} must be ${schema.type}`];
111
+ const enumValues = schema.enum;
112
+ if (enumValues !== void 0 && !enumValues.some((v) => v === value)) return [`${path} must be one of ${enumValues.map(String).join(", ")}`];
113
+ const constValue = schema.const;
114
+ if (constValue !== void 0 && constValue !== value) return [`${path} must be ${String(constValue)}`];
115
+ return [];
111
116
  }
112
117
  /**
113
118
  * Define a first-party tool: compile the parameter spec, pre-validate